langchain-arcade 1.3.1__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,832 +0,0 @@
1
- import os
2
- import warnings
3
- from collections.abc import Iterator
4
- from typing import Any, Optional, Union
5
-
6
- from arcadepy import NOT_GIVEN, Arcade, AsyncArcade
7
- from arcadepy.types import ToolDefinition
8
- from arcadepy.types.shared import AuthorizationResponse
9
- from langchain_core.tools import StructuredTool
10
-
11
- from langchain_arcade._utilities import wrap_arcade_tool
12
-
13
- ClientType = Union[Arcade, AsyncArcade]
14
-
15
-
16
- class LangChainToolManager:
17
- """
18
- Base tool manager for LangChain framework.
19
- Provides a common interface for both synchronous and asynchronous tool managers.
20
-
21
- This class handles the storage and retrieval of tool definitions and provides
22
- common functionality used by both synchronous and asynchronous implementations.
23
- """
24
-
25
- def __init__(self) -> None:
26
- self._tools: dict[str, ToolDefinition] = {}
27
-
28
- @property
29
- def tools(self) -> list[str]:
30
- """
31
- Get the list of tools by name in the manager.
32
-
33
- Returns:
34
- A list of tool names (strings) currently stored in the manager.
35
- """
36
- return list(self._tools.keys())
37
-
38
- def __len__(self) -> int:
39
- """Return the number of tools in the manager."""
40
- return len(self._tools)
41
-
42
- def _get_client_config(self, **kwargs: Any) -> dict[str, Any]:
43
- """
44
- Get the client configurations from environment variables and kwargs.
45
-
46
- If api_key or base_url are in the kwargs, they will be used.
47
- Otherwise, the environment variables ARCADE_API_KEY and ARCADE_BASE_URL will be used.
48
- If both are provided, the kwargs will take precedence.
49
-
50
- Args:
51
- **kwargs: Keyword arguments that may contain api_key and base_url.
52
-
53
- Returns:
54
- A dictionary of client configuration parameters.
55
- """
56
- client_kwargs = {
57
- "api_key": kwargs.get("api_key", os.getenv("ARCADE_API_KEY")),
58
- }
59
- base_url = kwargs.get("base_url", os.getenv("ARCADE_BASE_URL"))
60
- if base_url:
61
- client_kwargs["base_url"] = base_url
62
- return client_kwargs
63
-
64
- def _get_tool_definition(self, tool_name: str) -> ToolDefinition:
65
- """
66
- Get a tool definition by name, raising an error if not found.
67
-
68
- Args:
69
- tool_name: The name of the tool to retrieve.
70
-
71
- Returns:
72
- The ToolDefinition for the specified tool.
73
-
74
- Raises:
75
- ValueError: If the tool is not found in the manager.
76
- """
77
- try:
78
- return self._tools[tool_name]
79
- except KeyError:
80
- raise ValueError(f"Tool '{tool_name}' not found in this manager instance")
81
-
82
- def __getitem__(self, tool_name: str) -> ToolDefinition:
83
- """
84
- Get a tool definition by name using dictionary-like access.
85
-
86
- Args:
87
- tool_name: The name of the tool to retrieve.
88
-
89
- Returns:
90
- The ToolDefinition for the specified tool.
91
-
92
- Raises:
93
- ValueError: If the tool is not found in the manager.
94
- """
95
- return self._get_tool_definition(tool_name)
96
-
97
- def requires_auth(self, tool_name: str) -> bool:
98
- """
99
- Check if a tool requires authorization.
100
-
101
- Args:
102
- tool_name: The name of the tool to check.
103
-
104
- Returns:
105
- True if the tool requires authorization, False otherwise.
106
- """
107
- tool_def = self._get_tool_definition(tool_name)
108
- if tool_def.requirements is None:
109
- return False
110
- return tool_def.requirements.authorization is not None
111
-
112
-
113
- class ToolManager(LangChainToolManager):
114
- """
115
- Synchronous Arcade tool manager for LangChain framework.
116
-
117
- This class wraps Arcade tools as LangChain StructuredTool objects for integration
118
- with synchronous operations.
119
-
120
- Example:
121
- >>> manager = ToolManager(api_key="your-api-key")
122
- >>> # Initialize with specific tools and toolkits
123
- >>> manager.init_tools(tools=["Search.SearchGoogle"], toolkits=["Weather"])
124
- >>> # Get tools as LangChain StructuredTools
125
- >>> langchain_tools = manager.to_langchain()
126
- >>> # Handle authorization for tools that require it
127
- >>> if manager.requires_auth("Search.SearchGoogle"):
128
- >>> auth_response = manager.authorize("Search.SearchGoogle", "user_123")
129
- >>> manager.wait_for_auth(auth_response.id)
130
- """
131
-
132
- def __init__(self, client: Optional[Arcade] = None, **kwargs: Any) -> None:
133
- """
134
- Initialize the ToolManager.
135
-
136
- Example:
137
- >>> manager = ToolManager(api_key="your-api-key")
138
- >>> # or with an existing client
139
- >>> client = Arcade(api_key="your-api-key")
140
- >>> manager = ToolManager(client=client)
141
-
142
- Args:
143
- client: Optional Arcade client instance. If not provided, one will be created.
144
- **kwargs: Additional keyword arguments to pass to the Arcade client if creating one.
145
- Common options include api_key and base_url.
146
- """
147
- super().__init__()
148
- if client is None:
149
- client_kwargs = self._get_client_config(**kwargs)
150
- client = Arcade(**client_kwargs)
151
- self._client = client
152
-
153
- @property
154
- def definitions(self) -> list[ToolDefinition]:
155
- """
156
- Get the list of tool definitions in the manager.
157
-
158
- Returns:
159
- A list of ToolDefinition objects currently stored in the manager.
160
- """
161
- return list(self._tools.values())
162
-
163
- def __iter__(self) -> Iterator[tuple[str, ToolDefinition]]:
164
- """
165
- Iterate over the tools in the manager as (name, definition) pairs.
166
-
167
- Returns:
168
- Iterator over (tool_name, tool_definition) tuples.
169
- """
170
- yield from self._tools.items()
171
-
172
- def to_langchain(
173
- self, use_interrupts: bool = True, use_underscores: bool = True
174
- ) -> list[StructuredTool]:
175
- """
176
- Get the tools in the manager as LangChain StructuredTool objects.
177
-
178
- Args:
179
- use_interrupts: Whether to use interrupts for the tool. This is useful
180
- for LangGraph workflows where you need to handle tool
181
- authorization through state transitions.
182
- use_underscores: Whether to use underscores for the tool name instead of periods.
183
- For example, "Search_SearchGoogle" vs "Search.SearchGoogle".
184
- Some model providers like OpenAI work better with underscores.
185
-
186
- Returns:
187
- List of StructuredTool instances ready to use with LangChain.
188
- """
189
- tool_map = _create_tool_map(self.definitions, use_underscores=use_underscores)
190
- return [
191
- wrap_arcade_tool(self._client, tool_name, definition, langgraph=use_interrupts)
192
- for tool_name, definition in tool_map.items()
193
- ]
194
-
195
- def init_tools(
196
- self,
197
- tools: Optional[list[str]] = None,
198
- toolkits: Optional[list[str]] = None,
199
- limit: Optional[int] = None,
200
- offset: Optional[int] = None,
201
- raise_on_empty: bool = True,
202
- ) -> list[StructuredTool]:
203
- """
204
- Initialize the tools in the manager and return them as LangChain tools.
205
-
206
- This will clear any existing tools in the manager and replace them with
207
- the new tools specified by the tools and toolkits parameters.
208
-
209
- Note: In version 2.0+, this method returns a list of StructuredTool objects.
210
- In earlier versions, it returned None.
211
-
212
- Example:
213
- >>> manager = ToolManager(api_key="your-api-key")
214
- >>> langchain_tools = manager.init_tools(tools=["Search.SearchGoogle"])
215
- >>> # Use these tools with a LangChain chain or agent
216
- >>> agent = Agent(tools=langchain_tools, llm=llm)
217
-
218
- Args:
219
- tools: Optional list of specific tool names to include (e.g., "Search.SearchGoogle").
220
- toolkits: Optional list of toolkit names to include all tools from (e.g., "Search").
221
- limit: Optional limit on the number of tools to retrieve per request.
222
- offset: Optional offset for paginated requests.
223
- raise_on_empty: Whether to raise an error if no tools or toolkits are provided.
224
-
225
- Returns:
226
- List of StructuredTool instances ready to use with LangChain.
227
-
228
- Raises:
229
- ValueError: If no tools or toolkits are provided and raise_on_empty is True.
230
- """
231
- tools_list = self._retrieve_tool_definitions(tools, toolkits, raise_on_empty, limit, offset)
232
- self._tools = _create_tool_map(tools_list)
233
- return self.to_langchain()
234
-
235
- def authorize(self, tool_name: str, user_id: str) -> AuthorizationResponse:
236
- """
237
- Authorize a user for a specific tool.
238
-
239
- Example:
240
- >>> manager = ToolManager(api_key="your-api-key")
241
- >>> manager.init_tools(tools=["Gmail.SendEmail"])
242
- >>> auth_response = manager.authorize("Gmail.SendEmail", "user_123")
243
- >>> # auth_response.auth_url contains the URL for the user to authorize
244
-
245
- Args:
246
- tool_name: The name of the tool to authorize.
247
- user_id: The user ID to authorize. This should be a unique identifier for the user.
248
-
249
- Returns:
250
- AuthorizationResponse containing authorization details, including the auth_url
251
- that should be presented to the user to complete authorization.
252
- """
253
- return self._client.tools.authorize(tool_name=tool_name, user_id=user_id)
254
-
255
- def is_authorized(self, authorization_id: str) -> bool:
256
- """
257
- Check if a tool authorization is complete.
258
-
259
- Example:
260
- >>> manager = ToolManager(api_key="your-api-key")
261
- >>> auth_response = manager.authorize("Gmail.SendEmail", "user_123")
262
- >>> # After user completes authorization
263
- >>> is_complete = manager.is_authorized(auth_response.id)
264
-
265
- Args:
266
- authorization_id: The authorization ID to check. This can be the full AuthorizationResponse
267
- object or just the ID string.
268
-
269
- Returns:
270
- True if the authorization is completed, False otherwise.
271
- """
272
- # Handle case where entire AuthorizationResponse object is passed
273
- if hasattr(authorization_id, "id"):
274
- authorization_id = authorization_id.id
275
-
276
- response = self._client.auth.status(id=authorization_id)
277
- if response:
278
- return response.status == "completed"
279
- return False
280
-
281
- def wait_for_auth(self, authorization_id: str) -> AuthorizationResponse:
282
- """
283
- Wait for a tool authorization to complete. This method blocks until
284
- the authorization is complete or fails.
285
-
286
- Example:
287
- >>> manager = ToolManager(api_key="your-api-key")
288
- >>> auth_response = manager.authorize("Gmail.SendEmail", "user_123")
289
- >>> # Share auth_response.auth_url with the user
290
- >>> # Wait for the user to complete authorization
291
- >>> completed_auth = manager.wait_for_auth(auth_response.id)
292
-
293
- Args:
294
- authorization_id: The authorization ID to wait for. This can be the full
295
- AuthorizationResponse object or just the ID string.
296
-
297
- Returns:
298
- AuthorizationResponse with the completed authorization details.
299
- """
300
- # Handle case where entire AuthorizationResponse object is passed
301
- if hasattr(authorization_id, "id"):
302
- authorization_id = authorization_id.id
303
-
304
- return self._client.auth.wait_for_completion(authorization_id)
305
-
306
- def _retrieve_tool_definitions(
307
- self,
308
- tools: Optional[list[str]] = None,
309
- toolkits: Optional[list[str]] = None,
310
- raise_on_empty: bool = True,
311
- limit: Optional[int] = None,
312
- offset: Optional[int] = None,
313
- ) -> list[ToolDefinition]:
314
- """
315
- Retrieve tool definitions from the Arcade client, accounting for pagination.
316
-
317
- Args:
318
- tools: Optional list of specific tool names to include.
319
- toolkits: Optional list of toolkit names to include all tools from.
320
- raise_on_empty: Whether to raise an error if no tools or toolkits are provided.
321
- limit: Optional limit on the number of tools to retrieve per request.
322
- offset: Optional offset for paginated requests.
323
-
324
- Returns:
325
- List of ToolDefinition instances.
326
-
327
- Raises:
328
- ValueError: If no tools or toolkits are provided and raise_on_empty is True.
329
- """
330
- all_tools: list[ToolDefinition] = []
331
-
332
- # If no specific tools or toolkits are requested, raise an error.
333
- if not tools and not toolkits:
334
- if raise_on_empty:
335
- raise ValueError("No tools or toolkits provided to retrieve tool definitions.")
336
- return []
337
-
338
- # Retrieve individual tools if specified
339
- if tools:
340
- for tool_id in tools:
341
- single_tool = self._client.tools.get(name=tool_id)
342
- all_tools.append(single_tool)
343
-
344
- # Retrieve tools from specified toolkits
345
- if toolkits:
346
- for tk in toolkits:
347
- # Convert None to NOT_GIVEN for Stainless client
348
- paginated_tools = self._client.tools.list(
349
- toolkit=tk,
350
- limit=limit if limit is not None else NOT_GIVEN,
351
- offset=offset if offset is not None else NOT_GIVEN,
352
- )
353
- all_tools.extend(paginated_tools)
354
-
355
- return all_tools
356
-
357
- def add_tool(self, tool_name: str) -> None:
358
- """
359
- Add a single tool to the manager by name.
360
-
361
- Unlike init_tools(), this method preserves existing tools in the manager
362
- and only adds the specified tool.
363
-
364
- Example:
365
- >>> manager = ToolManager(api_key="your-api-key")
366
- >>> manager.add_tool("Gmail.SendEmail")
367
- >>> manager.add_tool("Search.SearchGoogle")
368
- >>> # Get all tools including newly added ones
369
- >>> all_tools = manager.to_langchain()
370
-
371
- Args:
372
- tool_name: The fully qualified name of the tool to add (e.g., "Search.SearchGoogle")
373
-
374
- Raises:
375
- ValueError: If the tool cannot be found
376
- """
377
- tool = self._client.tools.get(name=tool_name)
378
- self._tools.update(_create_tool_map([tool]))
379
-
380
- def add_toolkit(
381
- self, toolkit_name: str, limit: Optional[int] = None, offset: Optional[int] = None
382
- ) -> None:
383
- """
384
- Add all tools from a specific toolkit to the manager.
385
-
386
- Unlike init_tools(), this method preserves existing tools in the manager
387
- and only adds the tools from the specified toolkit.
388
-
389
- Example:
390
- >>> manager = ToolManager(api_key="your-api-key")
391
- >>> manager.add_toolkit("Gmail")
392
- >>> manager.add_toolkit("Search")
393
- >>> # Get all tools including newly added ones
394
- >>> all_tools = manager.to_langchain()
395
-
396
- Args:
397
- toolkit_name: The name of the toolkit to add (e.g., "Search")
398
- limit: Optional limit on the number of tools to retrieve per request
399
- offset: Optional offset for paginated requests
400
-
401
- Raises:
402
- ValueError: If the toolkit cannot be found or has no tools
403
- """
404
- tools = self._client.tools.list(
405
- toolkit=toolkit_name,
406
- limit=NOT_GIVEN if limit is None else limit,
407
- offset=NOT_GIVEN if offset is None else offset,
408
- )
409
-
410
- for tool in tools:
411
- self._tools.update(_create_tool_map([tool]))
412
-
413
- def get_tools(
414
- self,
415
- tools: Optional[list[str]] = None,
416
- toolkits: Optional[list[str]] = None,
417
- langgraph: bool = True,
418
- ) -> list[StructuredTool]:
419
- """
420
- DEPRECATED: Return the tools in the manager as LangChain StructuredTool objects.
421
-
422
- This method is deprecated and will be removed in a future major version.
423
- Please use `init_tools()` to initialize tools and `to_langchain()` to convert them.
424
-
425
- Args:
426
- tools: Optional list of tool names to include.
427
- toolkits: Optional list of toolkits to include.
428
- langgraph: Whether to use LangGraph-specific behavior
429
- such as NodeInterrupts for auth.
430
-
431
- Returns:
432
- List of StructuredTool instances.
433
- """
434
- warnings.warn(
435
- "get_tools() is deprecated and will be removed in the next major version. "
436
- "Please use init_tools() to initialize tools and to_langchain() to convert them.",
437
- DeprecationWarning,
438
- stacklevel=2,
439
- )
440
-
441
- # Support existing usage pattern
442
- if tools or toolkits:
443
- self.init_tools(tools=tools, toolkits=toolkits)
444
-
445
- return self.to_langchain(use_interrupts=langgraph)
446
-
447
-
448
- class ArcadeToolManager(ToolManager):
449
- """
450
- Deprecated alias for ToolManager.
451
-
452
- ArcadeToolManager is deprecated and will be removed in the next major version.
453
- Please use ToolManager instead.
454
- """
455
-
456
- def __init__(self, *args: Any, **kwargs: Any) -> None:
457
- warnings.warn(
458
- "ArcadeToolManager is deprecated and will be removed in the next major version. "
459
- "Please use ToolManager instead.",
460
- DeprecationWarning,
461
- stacklevel=2,
462
- )
463
- super().__init__(*args, **kwargs)
464
-
465
-
466
- class AsyncToolManager(LangChainToolManager):
467
- """
468
- Async version of Arcade tool manager for LangChain framework.
469
-
470
- This class wraps Arcade tools as LangChain StructuredTool objects for integration
471
- with asynchronous operations.
472
-
473
- Example:
474
- >>> manager = AsyncToolManager(api_key="your-api-key")
475
- >>> # Initialize with specific tools and toolkits
476
- >>> await manager.init_tools(tools=["Search.SearchGoogle"], toolkits=["Weather"])
477
- >>> # Get tools as LangChain StructuredTools
478
- >>> langchain_tools = await manager.to_langchain()
479
- >>> # Handle authorization for tools that require it
480
- >>> if manager.requires_auth("Search.SearchGoogle"):
481
- >>> auth_response = await manager.authorize("Search.SearchGoogle", "user_123")
482
- >>> await manager.wait_for_auth(auth_response.id)
483
- """
484
-
485
- def __init__(
486
- self,
487
- client: Optional[AsyncArcade] = None,
488
- **kwargs: Any,
489
- ) -> None:
490
- """
491
- Initialize the AsyncToolManager.
492
-
493
- Example:
494
- >>> manager = AsyncToolManager(api_key="your-api-key")
495
- >>> # or with an existing client
496
- >>> client = AsyncArcade(api_key="your-api-key")
497
- >>> manager = AsyncToolManager(client=client)
498
-
499
- Args:
500
- client: Optional AsyncArcade client instance. If not provided, one will be created.
501
- **kwargs: Additional keyword arguments to pass to the AsyncArcade client if creating one.
502
- Common options include api_key and base_url.
503
- """
504
- super().__init__()
505
- if not client:
506
- client_kwargs = self._get_client_config(**kwargs)
507
- client = AsyncArcade(**client_kwargs)
508
- self._client = client
509
-
510
- @property
511
- def definitions(self) -> list[ToolDefinition]:
512
- """
513
- Get the list of tool definitions in the manager.
514
-
515
- Returns:
516
- A list of ToolDefinition objects currently stored in the manager.
517
- """
518
- return list(self._tools.values())
519
-
520
- def __iter__(self) -> Iterator[tuple[str, ToolDefinition]]:
521
- """
522
- Iterate over the tools in the manager as (name, definition) pairs.
523
-
524
- Returns:
525
- Iterator over (tool_name, tool_definition) tuples.
526
- """
527
- yield from self._tools.items()
528
-
529
- async def init_tools(
530
- self,
531
- tools: Optional[list[str]] = None,
532
- toolkits: Optional[list[str]] = None,
533
- limit: Optional[int] = None,
534
- offset: Optional[int] = None,
535
- raise_on_empty: bool = True,
536
- ) -> list[StructuredTool]:
537
- """
538
- Initialize the tools in the manager asynchronously and return them as LangChain tools.
539
-
540
- This will clear any existing tools in the manager and replace them with
541
- the new tools specified by the tools and toolkits parameters.
542
-
543
- Example:
544
- >>> manager = AsyncToolManager(api_key="your-api-key")
545
- >>> langchain_tools = await manager.init_tools(tools=["Search.SearchGoogle"])
546
- >>> # Use these tools with a LangChain chain or agent
547
- >>> agent = Agent(tools=langchain_tools, llm=llm)
548
-
549
- Args:
550
- tools: Optional list of specific tool names to include (e.g., "Search.SearchGoogle").
551
- toolkits: Optional list of toolkit names to include all tools from (e.g., "Search").
552
- limit: Optional limit on the number of tools to retrieve per request.
553
- offset: Optional offset for paginated requests.
554
- raise_on_empty: Whether to raise an error if no tools or toolkits are provided.
555
-
556
- Returns:
557
- List of StructuredTool instances ready to use with LangChain.
558
-
559
- Raises:
560
- ValueError: If no tools or toolkits are provided and raise_on_empty is True.
561
- """
562
- tools_list = await self._retrieve_tool_definitions(
563
- tools, toolkits, raise_on_empty, limit, offset
564
- )
565
- self._tools.update(_create_tool_map(tools_list))
566
- return await self.to_langchain()
567
-
568
- async def to_langchain(
569
- self, use_interrupts: bool = True, use_underscores: bool = True
570
- ) -> list[StructuredTool]:
571
- """
572
- Get the tools in the manager as LangChain StructuredTool objects asynchronously.
573
-
574
- Args:
575
- use_interrupts: Whether to use interrupts for the tool. This is useful
576
- for LangGraph workflows where you need to handle tool
577
- authorization through state transitions.
578
- use_underscores: Whether to use underscores for the tool name instead of periods.
579
- For example, "Search_SearchGoogle" vs "Search.SearchGoogle".
580
- Some model providers like OpenAI work better with underscores.
581
-
582
- Returns:
583
- List of StructuredTool instances ready to use with LangChain.
584
- """
585
- tool_map = _create_tool_map(self.definitions, use_underscores=use_underscores)
586
- return [
587
- wrap_arcade_tool(self._client, tool_name, definition, langgraph=use_interrupts)
588
- for tool_name, definition in tool_map.items()
589
- ]
590
-
591
- async def authorize(self, tool_name: str, user_id: str) -> AuthorizationResponse:
592
- """
593
- Authorize a user for a tool.
594
-
595
- Example:
596
- >>> manager = AsyncToolManager(api_key="your-api-key")
597
- >>> await manager.init_tools(tools=["Gmail.SendEmail"])
598
- >>> auth_response = await manager.authorize("Gmail.SendEmail", "user_123")
599
- >>> # auth_response.auth_url contains the URL for the user to authorize
600
-
601
- Args:
602
- tool_name: The name of the tool to authorize.
603
- user_id: The user ID to authorize. This should be a unique identifier for the user.
604
-
605
- Returns:
606
- AuthorizationResponse containing authorization details, including the auth_url
607
- that should be presented to the user to complete authorization.
608
- """
609
- return await self._client.tools.authorize(tool_name=tool_name, user_id=user_id)
610
-
611
- async def is_authorized(self, authorization_id: str) -> bool:
612
- """
613
- Check if a tool authorization is complete.
614
-
615
- Example:
616
- >>> manager = AsyncToolManager(api_key="your-api-key")
617
- >>> auth_response = await manager.authorize("Gmail.SendEmail", "user_123")
618
- >>> # After user completes authorization
619
- >>> is_complete = await manager.is_authorized(auth_response.id)
620
-
621
- Args:
622
- authorization_id: The authorization ID to check. This can be the full AuthorizationResponse
623
- object or just the ID string.
624
-
625
- Returns:
626
- True if the authorization is completed, False otherwise.
627
- """
628
- # Handle case where entire AuthorizationResponse object is passed
629
- if hasattr(authorization_id, "id"):
630
- authorization_id = authorization_id.id
631
-
632
- auth_status = await self._client.auth.status(id=authorization_id)
633
- return auth_status.status == "completed"
634
-
635
- async def wait_for_auth(self, authorization_id: str) -> AuthorizationResponse:
636
- """
637
- Wait for a tool authorization to complete. This method blocks until
638
- the authorization is complete or fails.
639
-
640
- Example:
641
- >>> manager = AsyncToolManager(api_key="your-api-key")
642
- >>> auth_response = await manager.authorize("Gmail.SendEmail", "user_123")
643
- >>> # Share auth_response.auth_url with the user
644
- >>> # Wait for the user to complete authorization
645
- >>> completed_auth = await manager.wait_for_auth(auth_response.id)
646
-
647
- Args:
648
- authorization_id: The authorization ID to wait for. This can be the full
649
- AuthorizationResponse object or just the ID string.
650
-
651
- Returns:
652
- AuthorizationResponse with the completed authorization details.
653
- """
654
- # Handle case where entire AuthorizationResponse object is passed
655
- if hasattr(authorization_id, "id"):
656
- authorization_id = authorization_id.id
657
-
658
- return await self._client.auth.wait_for_completion(authorization_id)
659
-
660
- async def _retrieve_tool_definitions(
661
- self,
662
- tools: Optional[list[str]] = None,
663
- toolkits: Optional[list[str]] = None,
664
- raise_on_empty: bool = True,
665
- limit: Optional[int] = None,
666
- offset: Optional[int] = None,
667
- ) -> list[ToolDefinition]:
668
- """
669
- Retrieve tool definitions asynchronously from the Arcade client, accounting for pagination.
670
-
671
- Args:
672
- tools: Optional list of specific tool names to include.
673
- toolkits: Optional list of toolkit names to include all tools from.
674
- raise_on_empty: Whether to raise an error if no tools or toolkits are provided.
675
- limit: Optional limit on the number of tools to retrieve per request.
676
- offset: Optional offset for paginated requests.
677
-
678
- Returns:
679
- List of ToolDefinition instances.
680
-
681
- Raises:
682
- ValueError: If no tools or toolkits are provided and raise_on_empty is True.
683
- """
684
- all_tools: list[ToolDefinition] = []
685
-
686
- # If no specific tools or toolkits are requested, raise an error.
687
- if not tools and not toolkits:
688
- if raise_on_empty:
689
- raise ValueError("No tools or toolkits provided to retrieve tool definitions.")
690
- return []
691
-
692
- # First, gather single tools if the user specifically requested them.
693
- if tools:
694
- for tool_id in tools:
695
- # ToolsResource.get(...) returns a single ToolDefinition.
696
- single_tool = await self._client.tools.get(name=tool_id)
697
- all_tools.append(single_tool)
698
-
699
- # Next, gather tool definitions from any requested toolkits.
700
- if toolkits:
701
- for tk in toolkits:
702
- # Convert None to NOT_GIVEN for Stainless client
703
- paginated_tools = await self._client.tools.list(
704
- toolkit=tk,
705
- limit=NOT_GIVEN if limit is None else limit,
706
- offset=NOT_GIVEN if offset is None else offset,
707
- )
708
- async for tool in paginated_tools:
709
- all_tools.append(tool)
710
-
711
- return all_tools
712
-
713
- async def add_tool(self, tool_name: str) -> None:
714
- """
715
- Add a single tool to the manager by name.
716
-
717
- Unlike init_tools(), this method preserves existing tools in the manager
718
- and only adds the specified tool.
719
-
720
- Example:
721
- >>> manager = AsyncToolManager(api_key="your-api-key")
722
- >>> await manager.add_tool("Gmail.SendEmail")
723
- >>> await manager.add_tool("Search.SearchGoogle")
724
- >>> # Get all tools including newly added ones
725
- >>> all_tools = await manager.to_langchain()
726
-
727
- Args:
728
- tool_name: The fully qualified name of the tool to add (e.g., "Search.SearchGoogle")
729
-
730
- Raises:
731
- ValueError: If the tool cannot be found
732
- """
733
- tool = await self._client.tools.get(name=tool_name)
734
- self._tools.update(_create_tool_map([tool]))
735
-
736
- async def add_toolkit(
737
- self, toolkit_name: str, limit: Optional[int] = None, offset: Optional[int] = None
738
- ) -> None:
739
- """
740
- Add all tools from a specific toolkit to the manager.
741
-
742
- Unlike init_tools(), this method preserves existing tools in the manager
743
- and only adds the tools from the specified toolkit.
744
-
745
- Example:
746
- >>> manager = AsyncToolManager(api_key="your-api-key")
747
- >>> await manager.add_toolkit("Gmail")
748
- >>> await manager.add_toolkit("Search")
749
- >>> # Get all tools including newly added ones
750
- >>> all_tools = await manager.to_langchain()
751
-
752
- Args:
753
- toolkit_name: The name of the toolkit to add (e.g., "Search")
754
- limit: Optional limit on the number of tools to retrieve per request
755
- offset: Optional offset for paginated requests
756
-
757
- Raises:
758
- ValueError: If the toolkit cannot be found or has no tools
759
- """
760
- paginated_tools = await self._client.tools.list(
761
- toolkit=toolkit_name,
762
- limit=NOT_GIVEN if limit is None else limit,
763
- offset=NOT_GIVEN if offset is None else offset,
764
- )
765
-
766
- async for tool in paginated_tools:
767
- self._tools.update(_create_tool_map([tool]))
768
-
769
- async def get_tools(
770
- self,
771
- tools: Optional[list[str]] = None,
772
- toolkits: Optional[list[str]] = None,
773
- langgraph: bool = True,
774
- ) -> list[StructuredTool]:
775
- """
776
- DEPRECATED: Return the tools in the manager as LangChain StructuredTool objects.
777
-
778
- This method is deprecated and will be removed in a future major version.
779
- Please use `init_tools()` to initialize tools and `to_langchain()` to convert them.
780
-
781
- Args:
782
- tools: Optional list of tool names to include.
783
- toolkits: Optional list of toolkits to include.
784
- langgraph: Whether to use LangGraph-specific behavior
785
- such as NodeInterrupts for auth.
786
-
787
- Returns:
788
- List of StructuredTool instances.
789
- """
790
- warnings.warn(
791
- "get_tools() is deprecated and will be removed in the next major version. "
792
- "Please use init_tools() to initialize tools and to_langchain() to convert them.",
793
- DeprecationWarning,
794
- stacklevel=2,
795
- )
796
-
797
- # Support existing usage pattern
798
- if tools or toolkits:
799
- return await self.init_tools(tools=tools, toolkits=toolkits)
800
- return []
801
-
802
-
803
- def _create_tool_map(
804
- tools: list[ToolDefinition],
805
- use_underscores: bool = True,
806
- ) -> dict[str, ToolDefinition]:
807
- """
808
- Build a dictionary that maps the "full_tool_name" to the tool definition.
809
-
810
- Args:
811
- tools: List of ToolDefinition objects to map.
812
- use_underscores: Whether to use underscores instead of periods in tool names.
813
- For example, "Search_SearchGoogle" vs "Search.SearchGoogle".
814
-
815
- Returns:
816
- Dictionary mapping tool names to tool definitions.
817
-
818
- Note:
819
- This is a temporary solution to support the naming convention of certain model providers
820
- like OpenAI, which work better with underscores in tool names.
821
- """
822
- tool_map: dict[str, ToolDefinition] = {}
823
- for tool in tools:
824
- # Ensure toolkit name and tool name are not None before creating the key
825
- toolkit_name = tool.toolkit.name if tool.toolkit and tool.toolkit.name else None
826
- if toolkit_name and tool.name:
827
- if use_underscores:
828
- tool_name = f"{toolkit_name}_{tool.name}"
829
- else:
830
- tool_name = f"{toolkit_name}.{tool.name}"
831
- tool_map[tool_name] = tool
832
- return tool_map