langchain-arcade 1.1.0__py3-none-any.whl → 1.3.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.
@@ -1,91 +1,414 @@
1
1
  import os
2
+ import warnings
2
3
  from collections.abc import Iterator
3
- from typing import Any, Optional
4
+ from typing import Any, Optional, Union
4
5
 
5
- from arcadepy import Arcade
6
+ from arcadepy import NOT_GIVEN, Arcade, AsyncArcade
6
7
  from arcadepy.types import ToolDefinition
7
8
  from arcadepy.types.shared import AuthorizationResponse
8
9
  from langchain_core.tools import StructuredTool
9
10
 
10
- from langchain_arcade._utilities import (
11
- wrap_arcade_tool,
12
- )
11
+ from langchain_arcade._utilities import wrap_arcade_tool
13
12
 
13
+ ClientType = Union[Arcade, AsyncArcade]
14
14
 
15
- class ArcadeToolManager:
15
+
16
+ class LangChainToolManager:
16
17
  """
17
- Arcade tool manager for LangChain framework.
18
+ Base tool manager for LangChain framework.
19
+ Provides a common interface for both synchronous and asynchronous tool managers.
18
20
 
19
- This class wraps Arcade tools as LangChain `StructuredTool`
20
- objects for integration.
21
+ This class handles the storage and retrieval of tool definitions and provides
22
+ common functionality used by both synchronous and asynchronous implementations.
21
23
  """
22
24
 
23
- def __init__(
24
- self,
25
- client: Optional[Arcade] = None,
26
- **kwargs: dict[str, Any],
27
- ) -> None:
28
- """Initialize the ArcadeToolManager.
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.
29
135
 
30
136
  Example:
31
- >>> manager = ArcadeToolManager(api_key="...")
32
- >>>
33
- >>> # retrieve a specific tool as a langchain tool
34
- >>> manager.get_tools(tools=["Search.SearchGoogle"])
35
- >>>
36
- >>> # retrieve all tools in a toolkit as langchain tools
37
- >>> manager.get_tools(toolkits=["Search"])
38
- >>>
39
- >>> # clear and initialize new tools in the manager
40
- >>> manager.init_tools(tools=["Search.SearchGoogle"], toolkits=["Search"])
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)
41
141
 
42
142
  Args:
43
- client: Optional Arcade client instance.
44
- **kwargs: Additional keyword arguments to pass to the Arcade client.
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.
45
146
  """
46
- if not client:
47
- api_key = kwargs.get("api_key", os.getenv("ARCADE_API_KEY"))
48
- base_url = kwargs.get("base_url", os.getenv("ARCADE_BASE_URL"))
49
- arcade_kwargs = {"api_key": api_key, **kwargs}
50
- if base_url:
51
- arcade_kwargs["base_url"] = base_url
52
-
53
- client = Arcade(**arcade_kwargs) # type: ignore[arg-type]
54
- self.client = client
55
- self._tools: dict[str, ToolDefinition] = {}
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
56
152
 
57
153
  @property
58
- def tools(self) -> list[str]:
59
- return list(self._tools.keys())
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())
60
162
 
61
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
+ """
62
170
  yield from self._tools.items()
63
171
 
64
- def __len__(self) -> int:
65
- return len(self._tools)
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.
66
177
 
67
- def __getitem__(self, tool_name: str) -> ToolDefinition:
68
- return self._tools[tool_name]
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
+ ]
69
194
 
70
195
  def init_tools(
71
196
  self,
72
197
  tools: Optional[list[str]] = None,
73
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
74
382
  ) -> None:
75
- """Initialize the tools in the manager.
383
+ """
384
+ Add all tools from a specific toolkit to the manager.
76
385
 
77
- This will clear any existing tools in the manager.
386
+ Unlike init_tools(), this method preserves existing tools in the manager
387
+ and only adds the tools from the specified toolkit.
78
388
 
79
389
  Example:
80
- >>> manager = ArcadeToolManager(api_key="...")
81
- >>> manager.init_tools(tools=["Search.SearchGoogle"])
82
- >>> manager.get_tools()
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()
83
395
 
84
396
  Args:
85
- tools: Optional list of tool names to include.
86
- toolkits: Optional list of toolkits to include.
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
87
403
  """
88
- self._tools = self._retrieve_tool_definitions(tools, toolkits)
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]))
89
412
 
90
413
  def get_tools(
91
414
  self,
@@ -93,20 +416,11 @@ class ArcadeToolManager:
93
416
  toolkits: Optional[list[str]] = None,
94
417
  langgraph: bool = True,
95
418
  ) -> list[StructuredTool]:
96
- """Return the tools in the manager as LangChain StructuredTool objects.
97
-
98
- Note: if tools/toolkits are provided, the manager will update it's
99
- internal tools using a dictionary update by tool name.
100
-
101
- If langgraph is True, the tools will be wrapped with LangGraph-specific
102
- behavior such as NodeInterrupts for auth.
103
- Note: Changed in 1.0.0 to default to True.
419
+ """
420
+ DEPRECATED: Return the tools in the manager as LangChain StructuredTool objects.
104
421
 
105
- Example:
106
- >>> manager = ArcadeToolManager(api_key="...")
107
- >>>
108
- >>> # retrieve a specific tool as a langchain tool
109
- >>> manager.get_tools(tools=["Search.SearchGoogle"])
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.
110
424
 
111
425
  Args:
112
426
  tools: Optional list of tool names to include.
@@ -117,103 +431,402 @@ class ArcadeToolManager:
117
431
  Returns:
118
432
  List of StructuredTool instances.
119
433
  """
120
- # TODO account for versioning
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
121
442
  if tools or toolkits:
122
- new_tools = self._retrieve_tool_definitions(tools, toolkits)
123
- self._tools.update(new_tools)
124
- elif len(self) == 0:
125
- self.init_tools()
443
+ self.init_tools(tools=tools, toolkits=toolkits)
126
444
 
127
- langchain_tools: list[StructuredTool] = []
128
- for tool_name, definition in self:
129
- lc_tool = wrap_arcade_tool(self.client, tool_name, definition, langgraph)
130
- langchain_tools.append(lc_tool)
131
- return langchain_tools
445
+ return self.to_langchain(use_interrupts=langgraph)
132
446
 
133
- def authorize(self, tool_name: str, user_id: str) -> AuthorizationResponse:
134
- """Authorize a user for a tool.
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.
135
492
 
136
493
  Example:
137
- >>> manager = ArcadeToolManager(api_key="...")
138
- >>> manager.authorize("X.PostTweet", "user_123")
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
139
600
 
140
601
  Args:
141
602
  tool_name: The name of the tool to authorize.
142
- user_id: The user ID to authorize.
603
+ user_id: The user ID to authorize. This should be a unique identifier for the user.
143
604
 
144
605
  Returns:
145
- AuthorizationResponse
606
+ AuthorizationResponse containing authorization details, including the auth_url
607
+ that should be presented to the user to complete authorization.
146
608
  """
147
- return self.client.tools.authorize(tool_name=tool_name, user_id=user_id)
609
+ return await self._client.tools.authorize(tool_name=tool_name, user_id=user_id)
148
610
 
149
- def is_authorized(self, authorization_id: str) -> bool:
150
- """Check if a tool authorization is complete.
611
+ async def is_authorized(self, authorization_id: str) -> bool:
612
+ """
613
+ Check if a tool authorization is complete.
151
614
 
152
615
  Example:
153
- >>> manager = ArcadeToolManager(api_key="...")
154
- >>> manager.init_tools(toolkits=["Search"])
155
- >>> manager.is_authorized("auth_123")
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.
156
627
  """
157
- return self.client.auth.status(id=authorization_id).status == "completed"
628
+ # Handle case where entire AuthorizationResponse object is passed
629
+ if hasattr(authorization_id, "id"):
630
+ authorization_id = authorization_id.id
158
631
 
159
- def wait_for_auth(self, authorization_id: str) -> AuthorizationResponse:
160
- """Wait for a tool authorization to complete.
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.
161
639
 
162
640
  Example:
163
- >>> manager = ArcadeToolManager(api_key="...")
164
- >>> manager.init_tools(toolkits=["Google.ListEmails"])
165
- >>> response = manager.authorize("Google.ListEmails", "user_123")
166
- >>> manager.wait_for_auth(response)
167
- >>> # or
168
- >>> manager.wait_for_auth(response.id)
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.
169
653
  """
170
- return self.client.auth.wait_for_completion(authorization_id)
654
+ # Handle case where entire AuthorizationResponse object is passed
655
+ if hasattr(authorization_id, "id"):
656
+ authorization_id = authorization_id.id
171
657
 
172
- def requires_auth(self, tool_name: str) -> bool:
173
- """Check if a tool requires authorization."""
658
+ return await self._client.auth.wait_for_completion(authorization_id)
174
659
 
175
- tool_def = self._get_tool_definition(tool_name)
176
- if tool_def.requirements is None:
177
- return False
178
- return tool_def.requirements.authorization is not None
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.
179
670
 
180
- def _get_tool_definition(self, tool_name: str) -> ToolDefinition:
181
- try:
182
- return self._tools[tool_name]
183
- except KeyError:
184
- raise ValueError(f"Tool '{tool_name}' not found in this ArcadeToolManager instance")
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.
185
677
 
186
- def _retrieve_tool_definitions(
187
- self, tools: Optional[list[str]] = None, toolkits: Optional[list[str]] = None
188
- ) -> dict[str, ToolDefinition]:
189
- """Retrieve tool definitions from the Arcade client, accounting for pagination."""
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
+ """
190
684
  all_tools: list[ToolDefinition] = []
191
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
+
192
692
  # First, gather single tools if the user specifically requested them.
193
693
  if tools:
194
694
  for tool_id in tools:
195
695
  # ToolsResource.get(...) returns a single ToolDefinition.
196
- single_tool = self.client.tools.get(name=tool_id)
696
+ single_tool = await self._client.tools.get(name=tool_id)
197
697
  all_tools.append(single_tool)
198
698
 
199
699
  # Next, gather tool definitions from any requested toolkits.
200
700
  if toolkits:
201
701
  for tk in toolkits:
202
- # tools.list(...) returns a paginated response (SyncOffsetPage),
203
- # which has an __iter__ method that automatically iterates over all pages.
204
- paginated_tools = self.client.tools.list(toolkit=tk)
205
- all_tools.extend(paginated_tools)
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.
206
716
 
207
- # If no specific tools or toolkits were requested, retrieve *all* tools.
208
- if not tools and not toolkits:
209
- paginated_all_tools = self.client.tools.list()
210
- all_tools.extend(paginated_all_tools)
211
- # Build a dictionary that maps the "full_tool_name" to the tool definition.
212
- tool_definitions: dict[str, ToolDefinition] = {}
213
- for tool in all_tools:
214
- # For items returned by .list(), the 'toolkit' and 'name' attributes
215
- # should be present as plain fields on the object. (No need to do toolkit.name)
216
- full_tool_name = f"{tool.toolkit.name}_{tool.name}"
217
- tool_definitions[full_tool_name] = tool
218
-
219
- return tool_definitions
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