langchain-arcade 1.0.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025, Arcade AI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.1
2
+ Name: langchain-arcade
3
+ Version: 1.0.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 AI
8
+ Author-email: dev@arcade-ai.com
9
+ Requires-Python: >=3.10,<3.13
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
+ Requires-Dist: arcadepy (>=1.0.0,<2.0.0)
16
+ Requires-Dist: langgraph (>=0.2.67,<0.3.0)
17
+ Project-URL: Repository, https://github.com/arcadeai/arcade-ai/tree/main/contrib/langchain
18
+ Description-Content-Type: text/markdown
19
+
20
+ <h3 align="center">
21
+ <a name="readme-top"></a>
22
+ <img
23
+ src="https://docs.arcade-ai.com/images/logo/arcade-ai-logo.png"
24
+ >
25
+ </h3>
26
+ <div align="center">
27
+ <h3>LangChain Integration</h3>
28
+ <a href="https://github.com/arcadeai/arcade-ai/blob/main/LICENSE">
29
+ <img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License">
30
+ </a>
31
+ <a href="https://pepy.tech/project/langchain-arcade">
32
+ <img src="https://static.pepy.tech/badge/langchain-arcade" alt="Downloads">
33
+ </a>
34
+
35
+ </div>
36
+
37
+ <p align="center">
38
+ <a href="https://docs.arcade-ai.com" target="_blank">Docs</a> •
39
+ <a href="https://docs.arcade-ai.com/integrations" target="_blank">Integrations</a> •
40
+ <a href="https://github.com/ArcadeAI/cookbook" target="_blank">Cookbook</a> •
41
+ <a href="https://github.com/ArcadeAI/arcade-py" target="_blank">Python Client</a> •
42
+ <a href="https://github.com/ArcadeAI/arcade-js" target="_blank">JavaScript Client</a>
43
+ </p>
44
+
45
+ ## Overview
46
+
47
+ `langchain-arcade` allows you to use Arcade AI tools in your LangChain and LangGraph applications.
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install langchain-arcade
53
+ ```
54
+
55
+ ## Usage
56
+
57
+ See the [examples](https://github.com/ArcadeAI/arcade-ai/tree/main/examples/langchain) for usage examples
58
+
@@ -0,0 +1,38 @@
1
+ <h3 align="center">
2
+ <a name="readme-top"></a>
3
+ <img
4
+ src="https://docs.arcade-ai.com/images/logo/arcade-ai-logo.png"
5
+ >
6
+ </h3>
7
+ <div align="center">
8
+ <h3>LangChain Integration</h3>
9
+ <a href="https://github.com/arcadeai/arcade-ai/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>
15
+
16
+ </div>
17
+
18
+ <p align="center">
19
+ <a href="https://docs.arcade-ai.com" target="_blank">Docs</a> •
20
+ <a href="https://docs.arcade-ai.com/integrations" target="_blank">Integrations</a> •
21
+ <a href="https://github.com/ArcadeAI/cookbook" target="_blank">Cookbook</a> •
22
+ <a href="https://github.com/ArcadeAI/arcade-py" target="_blank">Python Client</a> •
23
+ <a href="https://github.com/ArcadeAI/arcade-js" target="_blank">JavaScript Client</a>
24
+ </p>
25
+
26
+ ## Overview
27
+
28
+ `langchain-arcade` allows you to use Arcade AI tools in your LangChain and LangGraph applications.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install langchain-arcade
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ See the [examples](https://github.com/ArcadeAI/arcade-ai/tree/main/examples/langchain) for usage examples
@@ -0,0 +1,3 @@
1
+ from .manager import ArcadeToolManager
2
+
3
+ __all__ = ["ArcadeToolManager"]
@@ -0,0 +1,179 @@
1
+ from typing import Any, Callable
2
+
3
+ from arcadepy import NOT_GIVEN, Arcade
4
+ from arcadepy.types import ToolDefinition
5
+ from langchain_core.runnables import RunnableConfig
6
+ from langchain_core.tools import StructuredTool
7
+ from pydantic import BaseModel, Field, create_model
8
+
9
+ # Check if LangGraph is enabled
10
+ LANGGRAPH_ENABLED = True
11
+ try:
12
+ from langgraph.errors import NodeInterrupt
13
+ except ImportError:
14
+ LANGGRAPH_ENABLED = False
15
+
16
+ # Mapping of Arcade value types to Python types
17
+ TYPE_MAPPING = {
18
+ "string": str,
19
+ "number": float,
20
+ "integer": int,
21
+ "boolean": bool,
22
+ "array": list,
23
+ "json": dict,
24
+ }
25
+
26
+
27
+ def get_python_type(val_type: str) -> Any:
28
+ """Map Arcade value types to Python types.
29
+
30
+ Args:
31
+ val_type: The value type as a string.
32
+
33
+ Returns:
34
+ Corresponding Python type.
35
+ """
36
+ _type = TYPE_MAPPING.get(val_type)
37
+ if _type is None:
38
+ raise ValueError(f"Invalid value type: {val_type}")
39
+ return _type
40
+
41
+
42
+ def tool_definition_to_pydantic_model(tool_def: ToolDefinition) -> type[BaseModel]:
43
+ """Convert a ToolDefinition's inputs into a Pydantic BaseModel.
44
+
45
+ Args:
46
+ tool_def: The ToolDefinition object to convert.
47
+
48
+ Returns:
49
+ A Pydantic BaseModel class representing the tool's input schema.
50
+ """
51
+ try:
52
+ fields: dict[str, Any] = {}
53
+ for param in tool_def.input.parameters or []:
54
+ param_type = get_python_type(param.value_schema.val_type)
55
+ if param_type == list and param.value_schema.inner_val_type: # noqa: E721
56
+ inner_type: type[Any] = get_python_type(param.value_schema.inner_val_type)
57
+ param_type = list[inner_type] # type: ignore[valid-type]
58
+ param_description = param.description or "No description provided."
59
+ default = ... if param.required else None
60
+ fields[param.name] = (
61
+ param_type,
62
+ Field(default=default, description=param_description),
63
+ )
64
+ return create_model(f"{tool_def.name}Args", **fields)
65
+ except ValueError as e:
66
+ raise ValueError(
67
+ f"Error converting {tool_def.name} parameters into pydantic model for langchain: {e}"
68
+ )
69
+
70
+
71
+ def create_tool_function(
72
+ client: Arcade,
73
+ tool_name: str,
74
+ tool_def: ToolDefinition,
75
+ args_schema: type[BaseModel],
76
+ langgraph: bool = False,
77
+ ) -> Callable:
78
+ """Create a callable function to execute an Arcade tool.
79
+
80
+ Args:
81
+ client: The Arcade client instance.
82
+ tool_name: The name of the tool to wrap.
83
+ tool_def: The ToolDefinition of the tool to wrap.
84
+ args_schema: The Pydantic model representing the tool's arguments.
85
+ langgraph: Whether to enable LangGraph-specific behavior.
86
+
87
+ Returns:
88
+ A callable function that executes the tool.
89
+ """
90
+ if langgraph and not LANGGRAPH_ENABLED:
91
+ raise ImportError("LangGraph is not installed. Please install it to use this feature.")
92
+
93
+ requires_authorization = (
94
+ tool_def.requirements is not None and tool_def.requirements.authorization is not None
95
+ )
96
+
97
+ def tool_function(config: RunnableConfig, **kwargs: Any) -> Any:
98
+ """Execute the Arcade tool with the given parameters.
99
+
100
+ Args:
101
+ config: RunnableConfig containing execution context.
102
+ **kwargs: Tool input arguments.
103
+
104
+ Returns:
105
+ The output from the tool execution.
106
+ """
107
+ user_id = config.get("configurable", {}).get("user_id") if config else None
108
+
109
+ if requires_authorization:
110
+ if user_id is None:
111
+ error_message = f"user_id is required to run {tool_name}"
112
+ if langgraph:
113
+ raise NodeInterrupt(error_message)
114
+ return {"error": error_message}
115
+
116
+ # Authorize the user for the tool
117
+ auth_response = client.tools.authorize(tool_name=tool_name, user_id=user_id)
118
+ if auth_response.status != "completed":
119
+ auth_message = f"Please use the following link to authorize: {auth_response.url}"
120
+ if langgraph:
121
+ raise NodeInterrupt(auth_message)
122
+ return {"error": auth_message}
123
+
124
+ # Execute the tool with provided inputs
125
+ execute_response = client.tools.execute(
126
+ tool_name=tool_name,
127
+ input=kwargs,
128
+ user_id=user_id if user_id is not None else NOT_GIVEN,
129
+ )
130
+
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}
137
+
138
+ return tool_function
139
+
140
+
141
+ def wrap_arcade_tool(
142
+ client: Arcade,
143
+ tool_name: str,
144
+ tool_def: ToolDefinition,
145
+ langgraph: bool = False,
146
+ ) -> StructuredTool:
147
+ """Wrap an Arcade `ToolDefinition` as a LangChain `StructuredTool`.
148
+
149
+ Args:
150
+ client: The Arcade client instance.
151
+ tool_name: The name of the tool to wrap.
152
+ tool_def: The ToolDefinition object to wrap.
153
+ langgraph: Whether to enable LangGraph-specific behavior.
154
+
155
+ Returns:
156
+ A StructuredTool instance representing the Arcade tool.
157
+ """
158
+ description = tool_def.description or "No description provided."
159
+
160
+ # Create a Pydantic model for the tool's input arguments
161
+ args_schema = tool_definition_to_pydantic_model(tool_def)
162
+
163
+ # 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
+ )
171
+
172
+ # Create the StructuredTool instance
173
+ return StructuredTool.from_function(
174
+ func=action_func,
175
+ name=tool_name,
176
+ description=description,
177
+ args_schema=args_schema,
178
+ inject_kwargs={"user_id"},
179
+ )
@@ -0,0 +1,219 @@
1
+ import os
2
+ from collections.abc import Iterator
3
+ from typing import Any, Optional
4
+
5
+ from arcadepy import Arcade
6
+ from arcadepy.types import ToolDefinition
7
+ from arcadepy.types.shared import AuthorizationResponse
8
+ from langchain_core.tools import StructuredTool
9
+
10
+ from langchain_arcade._utilities import (
11
+ wrap_arcade_tool,
12
+ )
13
+
14
+
15
+ class ArcadeToolManager:
16
+ """
17
+ Arcade tool manager for LangChain framework.
18
+
19
+ This class wraps Arcade tools as LangChain `StructuredTool`
20
+ objects for integration.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ client: Optional[Arcade] = None,
26
+ **kwargs: dict[str, Any],
27
+ ) -> None:
28
+ """Initialize the ArcadeToolManager.
29
+
30
+ 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"])
41
+
42
+ Args:
43
+ client: Optional Arcade client instance.
44
+ **kwargs: Additional keyword arguments to pass to the Arcade client.
45
+ """
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] = {}
56
+
57
+ @property
58
+ def tools(self) -> list[str]:
59
+ return list(self._tools.keys())
60
+
61
+ def __iter__(self) -> Iterator[tuple[str, ToolDefinition]]:
62
+ yield from self._tools.items()
63
+
64
+ def __len__(self) -> int:
65
+ return len(self._tools)
66
+
67
+ def __getitem__(self, tool_name: str) -> ToolDefinition:
68
+ return self._tools[tool_name]
69
+
70
+ def init_tools(
71
+ self,
72
+ tools: Optional[list[str]] = None,
73
+ toolkits: Optional[list[str]] = None,
74
+ ) -> None:
75
+ """Initialize the tools in the manager.
76
+
77
+ This will clear any existing tools in the manager.
78
+
79
+ Example:
80
+ >>> manager = ArcadeToolManager(api_key="...")
81
+ >>> manager.init_tools(tools=["Search.SearchGoogle"])
82
+ >>> manager.get_tools()
83
+
84
+ Args:
85
+ tools: Optional list of tool names to include.
86
+ toolkits: Optional list of toolkits to include.
87
+ """
88
+ self._tools = self._retrieve_tool_definitions(tools, toolkits)
89
+
90
+ def get_tools(
91
+ self,
92
+ tools: Optional[list[str]] = None,
93
+ toolkits: Optional[list[str]] = None,
94
+ langgraph: bool = True,
95
+ ) -> 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.
104
+
105
+ Example:
106
+ >>> manager = ArcadeToolManager(api_key="...")
107
+ >>>
108
+ >>> # retrieve a specific tool as a langchain tool
109
+ >>> manager.get_tools(tools=["Search.SearchGoogle"])
110
+
111
+ Args:
112
+ tools: Optional list of tool names to include.
113
+ toolkits: Optional list of toolkits to include.
114
+ langgraph: Whether to use LangGraph-specific behavior
115
+ such as NodeInterrupts for auth.
116
+
117
+ Returns:
118
+ List of StructuredTool instances.
119
+ """
120
+ # TODO account for versioning
121
+ 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()
126
+
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
132
+
133
+ def authorize(self, tool_name: str, user_id: str) -> AuthorizationResponse:
134
+ """Authorize a user for a tool.
135
+
136
+ Example:
137
+ >>> manager = ArcadeToolManager(api_key="...")
138
+ >>> manager.authorize("X.PostTweet", "user_123")
139
+
140
+ Args:
141
+ tool_name: The name of the tool to authorize.
142
+ user_id: The user ID to authorize.
143
+
144
+ Returns:
145
+ AuthorizationResponse
146
+ """
147
+ return self.client.tools.authorize(tool_name=tool_name, user_id=user_id)
148
+
149
+ def is_authorized(self, authorization_id: str) -> bool:
150
+ """Check if a tool authorization is complete.
151
+
152
+ Example:
153
+ >>> manager = ArcadeToolManager(api_key="...")
154
+ >>> manager.init_tools(toolkits=["Search"])
155
+ >>> manager.is_authorized("auth_123")
156
+ """
157
+ return self.client.auth.status(id=authorization_id).status == "completed"
158
+
159
+ def wait_for_auth(self, authorization_id: str) -> AuthorizationResponse:
160
+ """Wait for a tool authorization to complete.
161
+
162
+ 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)
169
+ """
170
+ return self.client.auth.wait_for_completion(authorization_id)
171
+
172
+ def requires_auth(self, tool_name: str) -> bool:
173
+ """Check if a tool requires authorization."""
174
+
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
179
+
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")
185
+
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."""
190
+ all_tools: list[ToolDefinition] = []
191
+
192
+ # First, gather single tools if the user specifically requested them.
193
+ if tools:
194
+ for tool_id in tools:
195
+ # ToolsResource.get(...) returns a single ToolDefinition.
196
+ single_tool = self.client.tools.get(name=tool_id)
197
+ all_tools.append(single_tool)
198
+
199
+ # Next, gather tool definitions from any requested toolkits.
200
+ if toolkits:
201
+ for tk in toolkits:
202
+ # tools.list(...) returns a paginated response (SyncOffsetPage),
203
+ # so we iterate over its items to accumulate tool definitions.
204
+ paginated_tools = self.client.tools.list(toolkit=tk)
205
+ all_tools.extend(paginated_tools.items)
206
+
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.items)
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
File without changes
@@ -0,0 +1,46 @@
1
+ [tool.poetry]
2
+ name = "langchain-arcade"
3
+ version = "1.0.0"
4
+ description = "An integration package connecting Arcade and LangChain/LangGraph"
5
+ authors = ["Arcade AI <dev@arcade-ai.com>"]
6
+ readme = "README.md"
7
+ repository = "https://github.com/arcadeai/arcade-ai/tree/main/contrib/langchain"
8
+ license = "MIT"
9
+
10
+ [tool.poetry.dependencies]
11
+ python = ">=3.10,<3.13"
12
+ arcadepy = "^1.0.0"
13
+ langgraph = ">=0.2.67,<0.3.0"
14
+
15
+
16
+ [tool.poetry.group.dev.dependencies]
17
+ pytest = "^8.1.2"
18
+ pytest-cov = "^4.0.0"
19
+ mypy = "^1.5.1"
20
+ pre-commit = "^3.4.0"
21
+ tox = "^4.11.1"
22
+ pytest-asyncio = "^0.23.7"
23
+
24
+
25
+ [tool.mypy]
26
+ files = ["langchain_arcade"]
27
+ python_version = "3.10"
28
+ disallow_untyped_defs = "True"
29
+ disallow_any_unimported = "True"
30
+ no_implicit_optional = "True"
31
+ check_untyped_defs = "True"
32
+ warn_return_any = "True"
33
+ warn_unused_ignores = "True"
34
+ show_error_codes = "True"
35
+ ignore_missing_imports = "True"
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
39
+
40
+
41
+ [tool.coverage.run]
42
+ branch = true
43
+ source = ["langchain_arcade"]
44
+
45
+ [tool.coverage.report]
46
+ skip_empty = true