langchain-arcade 1.1.0__tar.gz → 1.2.0__tar.gz

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.
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.1
2
+ Name: langchain-arcade
3
+ Version: 1.2.0
4
+ Summary: An integration package connecting Arcade and Langchain/LangGraph
5
+ Home-page: https://github.com/arcadeai/arcade-ai/tree/main/contrib/langchain
6
+ License: MIT
7
+ Author: Arcade
8
+ Author-email: dev@arcade.dev
9
+ Requires-Python: >=3.10,<4
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Dist: arcadepy (==1.1.*)
17
+ Requires-Dist: langgraph (>=0.2.67,<0.3.0)
18
+ Project-URL: Repository, https://github.com/arcadeai/arcade-ai/tree/main/contrib/langchain
19
+ Description-Content-Type: text/markdown
20
+
21
+ <h3 align="center">
22
+ <a name="readme-top"></a>
23
+ <img
24
+ src="https://docs.arcade.dev/images/logo/arcade-logo.png"
25
+ >
26
+ </h3>
27
+ <div align="center">
28
+ <h3>Arcade Langchain Integration</h3>
29
+ <a href="https://github.com/arcadeai/langchain-arcade/blob/main/LICENSE">
30
+ <img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License">
31
+ </a>
32
+ <a href="https://pepy.tech/project/langchain-arcade">
33
+ <img src="https://static.pepy.tech/badge/langchain-arcade" alt="Downloads">
34
+ <a href="https://pypi.org/project/langchain-arcade/">
35
+ <img src="https://img.shields.io/pypi/v/langchain-arcade.svg" alt="PyPI">
36
+ </a>
37
+ </a>
38
+
39
+ </div>
40
+
41
+ <p align="center">
42
+ <a href="https://docs.arcade.dev" target="_blank">Arcade Documentation</a> •
43
+ <a href="https://docs.arcade.dev/toolkits" target="_blank">Toolkits</a> •
44
+ <a href="https://github.com/ArcadeAI/arcade-py" target="_blank">Python Client</a> •
45
+ <a href="https://github.com/ArcadeAI/arcade-js" target="_blank">JavaScript Client</a>
46
+ </p>
47
+
48
+ ## Overview
49
+
50
+ `langchain-arcade` allows you to use Arcade tools in your LangChain and LangGraph applications. This integration provides a simple way to access Arcade's extensive toolkit ecosystem, including tools for search, email, document processing, and more.
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install langchain-arcade
56
+ ```
57
+
58
+ ## Basic Usage
59
+
60
+ ### 1. Initialize the Tool Manager
61
+
62
+ The `ToolManager` is the main entry point for working with Arcade tools in LangChain:
63
+
64
+ ```python
65
+ import os
66
+ from langchain_arcade import ToolManager
67
+
68
+ # Initialize with your API key
69
+ manager = ToolManager(api_key=os.environ["ARCADE_API_KEY"])
70
+
71
+ # Initialize with specific tools or toolkits
72
+ tools = manager.init_tools(
73
+ tools=["Web.ScrapeUrl"], # Individual tools
74
+ toolkits=["Search"] # All tools from a toolkit
75
+ )
76
+
77
+ # Convert to LangChain tools
78
+ langchain_tools = manager.to_langchain()
79
+ ```
80
+
81
+ ### 2. Use with LangGraph
82
+
83
+ Here's a simple example of using Arcade tools with LangGraph:
84
+
85
+ ```python
86
+ from langchain_openai import ChatOpenAI
87
+ from langgraph.checkpoint.memory import MemorySaver
88
+ from langgraph.prebuilt import create_react_agent
89
+
90
+ # Create a LangGraph agent
91
+ model = ChatOpenAI(model="gpt-4o")
92
+ memory = MemorySaver()
93
+ graph = create_react_agent(model, tools, checkpointer=memory)
94
+
95
+ config = {"configurable": {"thread_id": "1", "user_id": "user@example.com"}}
96
+ user_input = {"messages": [("user", "List my important emails")]}
97
+
98
+ for chunk in graph.stream(user_input, config, stream_mode="values"):
99
+ print(chunk["messages"][-1].content)
100
+ ```
101
+
102
+ ## Using Tools with Authorization in LangGraph
103
+
104
+ Many Arcade tools require user authorization. Here's how to handle it:
105
+
106
+ ### 1. Using with prebuilt agents
107
+
108
+ ```python
109
+ import os
110
+
111
+ from langchain_arcade import ToolManager
112
+ from langchain_openai import ChatOpenAI
113
+ from langgraph.prebuilt import create_react_agent
114
+
115
+ # Initialize tools
116
+ manager = ToolManager(api_key=os.environ["ARCADE_API_KEY"])
117
+ manager.init_tools(toolkits=["Github"])
118
+ tools = manager.to_langchain(use_interrupts=True)
119
+
120
+ # Create agent
121
+ model = ChatOpenAI(model="gpt-4o")
122
+ graph = create_react_agent(model, tools)
123
+
124
+ # Run the agent with the "user_id" field in the config
125
+ # IMPORTANT the "user_id" field is required for tools that require user authorization
126
+ config = {"configurable": {"user_id": "user@lgexample.com"}}
127
+ user_input = {"messages": [("user", "Star the arcadeai/arcade-ai repository on GitHub")]}
128
+
129
+ for chunk in graph.stream(user_input, config, debug=True):
130
+ if chunk.get("__interrupt__"):
131
+ # print the authorization url
132
+ print(chunk["__interrupt__"][0].value)
133
+ # visit the URL to authorize the tool
134
+ # once you have authorized the tool, you can run again and the agent will continue
135
+ elif chunk.get("agent"):
136
+ print(chunk["agent"]["messages"][-1].content)
137
+
138
+ # see the functional example for continuing the agent after authorization
139
+ # and for handling authorization errors gracefully
140
+
141
+ ```
142
+
143
+ See the Functional examples in the [examples directory](https://github.com/ArcadeAI/arcade-ai/tree/main/examples/langchain) that continue the agent after authorization and handle authorization errors gracefully.
144
+
145
+ ### Async Support
146
+
147
+ For asynchronous applications, use `AsyncToolManager`:
148
+
149
+ ```python
150
+ import asyncio
151
+ from langchain_arcade import AsyncToolManager
152
+
153
+ async def main():
154
+ manager = AsyncToolManager(api_key=os.environ["ARCADE_API_KEY"])
155
+ await manager.init_tools(toolkits=["Google"])
156
+ tools = await manager.to_langchain()
157
+
158
+ # Use tools with async LangChain/LangGraph components
159
+
160
+ asyncio.run(main())
161
+ ```
162
+
163
+ ## Tool Authorization Flow
164
+
165
+ Many Arcade tools require user authorization. This can be handled in many ways but the `ToolManager` provides a simple flow that can be used with prebuilt agents and also the functional API. The typical flow is:
166
+
167
+ 1. Attempt to use a tool that requires authorization
168
+ 2. Check the state for interrupts from the `NodeInterrupt` exception (or Command)
169
+ 3. Call `manager.authorize(tool_name, user_id)` to get an authorization URL
170
+ 4. Present the URL to the user
171
+ 5. Call `manager.wait_for_auth(auth_response.id)` to wait for completion
172
+ 6. Resume the agent execution
173
+
174
+ ## Available Toolkits
175
+
176
+ Arcade provides many toolkits including:
177
+
178
+ - `Search`: Google search, Bing search
179
+ - `Google`: Gmail, Google Drive, Google Calendar
180
+ - `Web`: Crawling, scraping, etc
181
+ - `Github`: Repository operations
182
+ - `Slack`: Sending messages to Slack
183
+ - `Linkedin`: Posting to Linkedin
184
+ - `X`: Posting and reading tweets on X
185
+ - And many more
186
+
187
+ For a complete list, see the [Arcade Toolkits documentation](https://docs.arcade.dev/toolkits).
188
+
189
+ ## More Examples
190
+
191
+ For more examples, see the [examples directory](https://github.com/ArcadeAI/arcade-ai/tree/main/examples/langchain).
192
+
@@ -0,0 +1,171 @@
1
+ <h3 align="center">
2
+ <a name="readme-top"></a>
3
+ <img
4
+ src="https://docs.arcade.dev/images/logo/arcade-logo.png"
5
+ >
6
+ </h3>
7
+ <div align="center">
8
+ <h3>Arcade Langchain Integration</h3>
9
+ <a href="https://github.com/arcadeai/langchain-arcade/blob/main/LICENSE">
10
+ <img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License">
11
+ </a>
12
+ <a href="https://pepy.tech/project/langchain-arcade">
13
+ <img src="https://static.pepy.tech/badge/langchain-arcade" alt="Downloads">
14
+ <a href="https://pypi.org/project/langchain-arcade/">
15
+ <img src="https://img.shields.io/pypi/v/langchain-arcade.svg" alt="PyPI">
16
+ </a>
17
+ </a>
18
+
19
+ </div>
20
+
21
+ <p align="center">
22
+ <a href="https://docs.arcade.dev" target="_blank">Arcade Documentation</a> •
23
+ <a href="https://docs.arcade.dev/toolkits" target="_blank">Toolkits</a> •
24
+ <a href="https://github.com/ArcadeAI/arcade-py" target="_blank">Python Client</a> •
25
+ <a href="https://github.com/ArcadeAI/arcade-js" target="_blank">JavaScript Client</a>
26
+ </p>
27
+
28
+ ## Overview
29
+
30
+ `langchain-arcade` allows you to use Arcade tools in your LangChain and LangGraph applications. This integration provides a simple way to access Arcade's extensive toolkit ecosystem, including tools for search, email, document processing, and more.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ pip install langchain-arcade
36
+ ```
37
+
38
+ ## Basic Usage
39
+
40
+ ### 1. Initialize the Tool Manager
41
+
42
+ The `ToolManager` is the main entry point for working with Arcade tools in LangChain:
43
+
44
+ ```python
45
+ import os
46
+ from langchain_arcade import ToolManager
47
+
48
+ # Initialize with your API key
49
+ manager = ToolManager(api_key=os.environ["ARCADE_API_KEY"])
50
+
51
+ # Initialize with specific tools or toolkits
52
+ tools = manager.init_tools(
53
+ tools=["Web.ScrapeUrl"], # Individual tools
54
+ toolkits=["Search"] # All tools from a toolkit
55
+ )
56
+
57
+ # Convert to LangChain tools
58
+ langchain_tools = manager.to_langchain()
59
+ ```
60
+
61
+ ### 2. Use with LangGraph
62
+
63
+ Here's a simple example of using Arcade tools with LangGraph:
64
+
65
+ ```python
66
+ from langchain_openai import ChatOpenAI
67
+ from langgraph.checkpoint.memory import MemorySaver
68
+ from langgraph.prebuilt import create_react_agent
69
+
70
+ # Create a LangGraph agent
71
+ model = ChatOpenAI(model="gpt-4o")
72
+ memory = MemorySaver()
73
+ graph = create_react_agent(model, tools, checkpointer=memory)
74
+
75
+ config = {"configurable": {"thread_id": "1", "user_id": "user@example.com"}}
76
+ user_input = {"messages": [("user", "List my important emails")]}
77
+
78
+ for chunk in graph.stream(user_input, config, stream_mode="values"):
79
+ print(chunk["messages"][-1].content)
80
+ ```
81
+
82
+ ## Using Tools with Authorization in LangGraph
83
+
84
+ Many Arcade tools require user authorization. Here's how to handle it:
85
+
86
+ ### 1. Using with prebuilt agents
87
+
88
+ ```python
89
+ import os
90
+
91
+ from langchain_arcade import ToolManager
92
+ from langchain_openai import ChatOpenAI
93
+ from langgraph.prebuilt import create_react_agent
94
+
95
+ # Initialize tools
96
+ manager = ToolManager(api_key=os.environ["ARCADE_API_KEY"])
97
+ manager.init_tools(toolkits=["Github"])
98
+ tools = manager.to_langchain(use_interrupts=True)
99
+
100
+ # Create agent
101
+ model = ChatOpenAI(model="gpt-4o")
102
+ graph = create_react_agent(model, tools)
103
+
104
+ # Run the agent with the "user_id" field in the config
105
+ # IMPORTANT the "user_id" field is required for tools that require user authorization
106
+ config = {"configurable": {"user_id": "user@lgexample.com"}}
107
+ user_input = {"messages": [("user", "Star the arcadeai/arcade-ai repository on GitHub")]}
108
+
109
+ for chunk in graph.stream(user_input, config, debug=True):
110
+ if chunk.get("__interrupt__"):
111
+ # print the authorization url
112
+ print(chunk["__interrupt__"][0].value)
113
+ # visit the URL to authorize the tool
114
+ # once you have authorized the tool, you can run again and the agent will continue
115
+ elif chunk.get("agent"):
116
+ print(chunk["agent"]["messages"][-1].content)
117
+
118
+ # see the functional example for continuing the agent after authorization
119
+ # and for handling authorization errors gracefully
120
+
121
+ ```
122
+
123
+ See the Functional examples in the [examples directory](https://github.com/ArcadeAI/arcade-ai/tree/main/examples/langchain) that continue the agent after authorization and handle authorization errors gracefully.
124
+
125
+ ### Async Support
126
+
127
+ For asynchronous applications, use `AsyncToolManager`:
128
+
129
+ ```python
130
+ import asyncio
131
+ from langchain_arcade import AsyncToolManager
132
+
133
+ async def main():
134
+ manager = AsyncToolManager(api_key=os.environ["ARCADE_API_KEY"])
135
+ await manager.init_tools(toolkits=["Google"])
136
+ tools = await manager.to_langchain()
137
+
138
+ # Use tools with async LangChain/LangGraph components
139
+
140
+ asyncio.run(main())
141
+ ```
142
+
143
+ ## Tool Authorization Flow
144
+
145
+ Many Arcade tools require user authorization. This can be handled in many ways but the `ToolManager` provides a simple flow that can be used with prebuilt agents and also the functional API. The typical flow is:
146
+
147
+ 1. Attempt to use a tool that requires authorization
148
+ 2. Check the state for interrupts from the `NodeInterrupt` exception (or Command)
149
+ 3. Call `manager.authorize(tool_name, user_id)` to get an authorization URL
150
+ 4. Present the URL to the user
151
+ 5. Call `manager.wait_for_auth(auth_response.id)` to wait for completion
152
+ 6. Resume the agent execution
153
+
154
+ ## Available Toolkits
155
+
156
+ Arcade provides many toolkits including:
157
+
158
+ - `Search`: Google search, Bing search
159
+ - `Google`: Gmail, Google Drive, Google Calendar
160
+ - `Web`: Crawling, scraping, etc
161
+ - `Github`: Repository operations
162
+ - `Slack`: Sending messages to Slack
163
+ - `Linkedin`: Posting to Linkedin
164
+ - `X`: Posting and reading tweets on X
165
+ - And many more
166
+
167
+ For a complete list, see the [Arcade Toolkits documentation](https://docs.arcade.dev/toolkits).
168
+
169
+ ## More Examples
170
+
171
+ For more examples, see the [examples directory](https://github.com/ArcadeAI/arcade-ai/tree/main/examples/langchain).
@@ -0,0 +1,7 @@
1
+ from .manager import ArcadeToolManager, AsyncToolManager, ToolManager
2
+
3
+ __all__ = [
4
+ "ToolManager",
5
+ "AsyncToolManager",
6
+ "ArcadeToolManager", # Deprecated
7
+ ]
@@ -1,7 +1,7 @@
1
- from typing import Any, Callable
1
+ from typing import Any, Callable, Union
2
2
 
3
- from arcadepy import NOT_GIVEN, Arcade
4
- from arcadepy.types import ToolDefinition
3
+ from arcadepy import NOT_GIVEN, Arcade, AsyncArcade
4
+ from arcadepy.types import ExecuteToolResponse, ToolDefinition
5
5
  from langchain_core.runnables import RunnableConfig
6
6
  from langchain_core.tools import StructuredTool
7
7
  from pydantic import BaseModel, Field, create_model
@@ -68,6 +68,51 @@ def tool_definition_to_pydantic_model(tool_def: ToolDefinition) -> type[BaseMode
68
68
  )
69
69
 
70
70
 
71
+ def process_tool_execution_response(
72
+ execute_response: ExecuteToolResponse, tool_name: str, langgraph: bool
73
+ ) -> Any:
74
+ """Process the response from tool execution and handle errors appropriately.
75
+
76
+ Args:
77
+ execute_response: The response from tool execution
78
+ tool_name: The name of the tool that was executed
79
+ langgraph: Whether LangGraph-specific behavior is enabled
80
+
81
+ Returns:
82
+ The output value on success, or error details on failure
83
+ """
84
+ if execute_response.success and execute_response.output is not None:
85
+ return execute_response.output.value
86
+
87
+ # Extract detailed error information
88
+ error_details = {
89
+ "error": "Unknown error occurred",
90
+ "tool": tool_name,
91
+ }
92
+
93
+ if execute_response.output is not None and execute_response.output.error is not None:
94
+ error = execute_response.output.error
95
+ error_message = str(error.message) if hasattr(error, "message") else "Unknown error"
96
+ error_details["error"] = error_message
97
+
98
+ # Add all non-None optional error fields to the details
99
+ if (
100
+ hasattr(error, "additional_prompt_content")
101
+ and error.additional_prompt_content is not None
102
+ ):
103
+ error_details["additional_prompt_content"] = error.additional_prompt_content
104
+ if hasattr(error, "can_retry") and error.can_retry is not None:
105
+ error_details["can_retry"] = str(error.can_retry)
106
+ if hasattr(error, "developer_message") and error.developer_message is not None:
107
+ error_details["developer_message"] = str(error.developer_message)
108
+ if hasattr(error, "retry_after_ms") and error.retry_after_ms is not None:
109
+ error_details["retry_after_ms"] = str(error.retry_after_ms)
110
+
111
+ if langgraph:
112
+ raise NodeInterrupt(error_details)
113
+ return error_details
114
+
115
+
71
116
  def create_tool_function(
72
117
  client: Arcade,
73
118
  tool_name: str,
@@ -128,18 +173,13 @@ def create_tool_function(
128
173
  user_id=user_id if user_id is not None else NOT_GIVEN,
129
174
  )
130
175
 
131
- if execute_response.success:
132
- return execute_response.output.value # type: ignore[union-attr]
133
- error_message = str(execute_response.output.error) # type: ignore[union-attr]
134
- if langgraph:
135
- raise NodeInterrupt(error_message)
136
- return {"error": error_message}
176
+ return process_tool_execution_response(execute_response, tool_name, langgraph)
137
177
 
138
178
  return tool_function
139
179
 
140
180
 
141
181
  def wrap_arcade_tool(
142
- client: Arcade,
182
+ client: Union[Arcade, AsyncArcade],
143
183
  tool_name: str,
144
184
  tool_def: ToolDefinition,
145
185
  langgraph: bool = False,
@@ -161,13 +201,23 @@ def wrap_arcade_tool(
161
201
  args_schema = tool_definition_to_pydantic_model(tool_def)
162
202
 
163
203
  # Create the action function
164
- action_func = create_tool_function(
165
- client=client,
166
- tool_name=tool_name,
167
- tool_def=tool_def,
168
- args_schema=args_schema,
169
- langgraph=langgraph,
170
- )
204
+ if isinstance(client, Arcade):
205
+ action_func = create_tool_function(
206
+ client=client,
207
+ tool_name=tool_name,
208
+ tool_def=tool_def,
209
+ args_schema=args_schema,
210
+ langgraph=langgraph,
211
+ )
212
+ else:
213
+ # Use async tool function for AsyncArcade client
214
+ action_func = create_async_tool_function(
215
+ client=client,
216
+ tool_name=tool_name,
217
+ tool_def=tool_def,
218
+ args_schema=args_schema,
219
+ langgraph=langgraph,
220
+ )
171
221
 
172
222
  # Create the StructuredTool instance
173
223
  return StructuredTool.from_function(
@@ -177,3 +227,68 @@ def wrap_arcade_tool(
177
227
  args_schema=args_schema,
178
228
  inject_kwargs={"user_id"},
179
229
  )
230
+
231
+
232
+ def create_async_tool_function(
233
+ client: AsyncArcade,
234
+ tool_name: str,
235
+ tool_def: ToolDefinition,
236
+ args_schema: type[BaseModel],
237
+ langgraph: bool = False,
238
+ ) -> Callable:
239
+ """Create an async callable function to execute an Arcade tool.
240
+
241
+ Args:
242
+ client: The AsyncArcade client instance.
243
+ tool_name: The name of the tool to wrap.
244
+ tool_def: The ToolDefinition of the tool to wrap.
245
+ args_schema: The Pydantic model representing the tool's arguments.
246
+ langgraph: Whether to enable LangGraph-specific behavior.
247
+
248
+ Returns:
249
+ An async callable function that executes the tool.
250
+ """
251
+ if langgraph and not LANGGRAPH_ENABLED:
252
+ raise ImportError("LangGraph is not installed. Please install it to use this feature.")
253
+
254
+ requires_authorization = (
255
+ tool_def.requirements is not None and tool_def.requirements.authorization is not None
256
+ )
257
+
258
+ async def tool_function(config: RunnableConfig, **kwargs: Any) -> Any:
259
+ """Run the Arcade tool with the given parameters.
260
+
261
+ Args:
262
+ config: RunnableConfig containing execution context.
263
+ **kwargs: Tool input arguments.
264
+
265
+ Returns:
266
+ The output from the tool execution.
267
+ """
268
+ user_id = config.get("configurable", {}).get("user_id") if config else None
269
+
270
+ if requires_authorization:
271
+ if user_id is None:
272
+ error_message = f"user_id is required to run {tool_name}"
273
+ if langgraph:
274
+ raise NodeInterrupt(error_message)
275
+ return {"error": error_message}
276
+
277
+ # Authorize the user for the tool
278
+ auth_response = await client.tools.authorize(tool_name=tool_name, user_id=user_id)
279
+ if auth_response.status != "completed":
280
+ auth_message = f"Please use the following link to authorize: {auth_response.url}"
281
+ if langgraph:
282
+ raise NodeInterrupt(auth_message)
283
+ return {"error": auth_message}
284
+
285
+ # Execute the tool with provided inputs
286
+ execute_response = await client.tools.execute(
287
+ tool_name=tool_name,
288
+ input=kwargs,
289
+ user_id=user_id if user_id is not None else NOT_GIVEN,
290
+ )
291
+
292
+ return process_tool_execution_response(execute_response, tool_name, langgraph)
293
+
294
+ return tool_function