langchain-arcade 0.1.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) 2024, 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,73 @@
1
+ Metadata-Version: 2.1
2
+ Name: langchain-arcade
3
+ Version: 0.1.0
4
+ Summary: An integration package connecting Arcade AI 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
+ Provides-Extra: langgraph
16
+ Requires-Dist: arcadepy (>=0.1.1,<0.2.0)
17
+ Requires-Dist: langchain-core (>=0.3.0,<0.4.0)
18
+ Requires-Dist: langgraph (>=0.2.32,<0.3.0) ; extra == "langgraph"
19
+ Project-URL: Repository, https://github.com/arcadeai/arcade-ai/tree/main/contrib/langchain
20
+ Description-Content-Type: text/markdown
21
+
22
+ <div align="center">
23
+
24
+ <img width="400px" src="https://docs-112.pages.dev/images/logo/arcade-ai-logo.png" />
25
+
26
+ ### LLM Tool Calling Platform
27
+
28
+ <p>
29
+ <img alt="GitHub Last Commit" src="https://img.shields.io/github/last-commit/arcadeai/arcade-ai" />
30
+ <img alt="GitHub Issues" src="https://img.shields.io/github/issues/arcadeai/arcade-ai" />
31
+ <img alt="GitHub Pull Requests" src="https://img.shields.io/github/issues-pr/arcadeai/arcade-ai" />
32
+ <img alt="GitHub License" src="https://img.shields.io/badge/License-MIT-yellow.svg" />
33
+ <img alt="Discord" src="https://img.shields.io/discord/1110910277110743103?label=Discord&logo=discord&logoColor=white&style=plastic&color=d7b023)](https://discord.gg/" />
34
+ </p>
35
+
36
+ ---
37
+
38
+ <p align="center">
39
+ <a href="https://docs-112.pages.dev" target="_blank">Docs</a> •
40
+ <a href="https://docs-112.pages.dev/guides" target="_blank">Guides</a> •
41
+ <a href="https://docs-112.pages.dev/integrations" target="_blank">Integrations</a> •
42
+ <a href="https://discord.com/invite/" target="_blank">Discord</a>
43
+
44
+ </p>
45
+
46
+ <br />
47
+
48
+ </div>
49
+
50
+ Arcade AI is the developer platform for building tools designed to be used with language models. With Arcade, developers can create, deploy, and easily integrate new tools with language models to enhance their capabilities.
51
+
52
+ ## Setup
53
+
54
+ Follow [these instructions](https://docs-112.pages.dev/home/quickstart) to Install Arcade AI and create an API key.
55
+
56
+ This example is using OpenAI, as the LLM provider. Ensure you have an [OpenAI API key](https://platform.openai.com/docs/quickstart).
57
+
58
+ Copy the `.env.example` file to `.env` and supply your API keys for `OPENAI_API_KEY` and `ARCADE_API_KEY`.
59
+
60
+ ## Usage with LangGraph API
61
+
62
+ ### Local testing with LangGraph Studio
63
+
64
+ For testing locally (e.g., currently supported only on MacOS), you can use the LangGraph Studio desktop application.
65
+
66
+ [Download LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download) and open this directory in the Studio application.
67
+
68
+ The `langgraph.json` file in this directory specifies the graph that will be loaded in Studio.
69
+
70
+ ### Deploying to LangGraph Cloud
71
+
72
+ Follow [these instructions](https://langchain-ai.github.io/langgraph/cloud/quick_start/#deploy-to-cloud) to deploy your graph to LangGraph Cloud.
73
+
@@ -0,0 +1,51 @@
1
+ <div align="center">
2
+
3
+ <img width="400px" src="https://docs-112.pages.dev/images/logo/arcade-ai-logo.png" />
4
+
5
+ ### LLM Tool Calling Platform
6
+
7
+ <p>
8
+ <img alt="GitHub Last Commit" src="https://img.shields.io/github/last-commit/arcadeai/arcade-ai" />
9
+ <img alt="GitHub Issues" src="https://img.shields.io/github/issues/arcadeai/arcade-ai" />
10
+ <img alt="GitHub Pull Requests" src="https://img.shields.io/github/issues-pr/arcadeai/arcade-ai" />
11
+ <img alt="GitHub License" src="https://img.shields.io/badge/License-MIT-yellow.svg" />
12
+ <img alt="Discord" src="https://img.shields.io/discord/1110910277110743103?label=Discord&logo=discord&logoColor=white&style=plastic&color=d7b023)](https://discord.gg/" />
13
+ </p>
14
+
15
+ ---
16
+
17
+ <p align="center">
18
+ <a href="https://docs-112.pages.dev" target="_blank">Docs</a> •
19
+ <a href="https://docs-112.pages.dev/guides" target="_blank">Guides</a> •
20
+ <a href="https://docs-112.pages.dev/integrations" target="_blank">Integrations</a> •
21
+ <a href="https://discord.com/invite/" target="_blank">Discord</a>
22
+
23
+ </p>
24
+
25
+ <br />
26
+
27
+ </div>
28
+
29
+ Arcade AI is the developer platform for building tools designed to be used with language models. With Arcade, developers can create, deploy, and easily integrate new tools with language models to enhance their capabilities.
30
+
31
+ ## Setup
32
+
33
+ Follow [these instructions](https://docs-112.pages.dev/home/quickstart) to Install Arcade AI and create an API key.
34
+
35
+ This example is using OpenAI, as the LLM provider. Ensure you have an [OpenAI API key](https://platform.openai.com/docs/quickstart).
36
+
37
+ Copy the `.env.example` file to `.env` and supply your API keys for `OPENAI_API_KEY` and `ARCADE_API_KEY`.
38
+
39
+ ## Usage with LangGraph API
40
+
41
+ ### Local testing with LangGraph Studio
42
+
43
+ For testing locally (e.g., currently supported only on MacOS), you can use the LangGraph Studio desktop application.
44
+
45
+ [Download LangGraph Studio](https://github.com/langchain-ai/langgraph-studio?tab=readme-ov-file#download) and open this directory in the Studio application.
46
+
47
+ The `langgraph.json` file in this directory specifies the graph that will be loaded in Studio.
48
+
49
+ ### Deploying to LangGraph Cloud
50
+
51
+ Follow [these instructions](https://langchain-ai.github.io/langgraph/cloud/quick_start/#deploy-to-cloud) to deploy your graph to LangGraph Cloud.
@@ -0,0 +1,3 @@
1
+ from .manager import ArcadeToolManager
2
+
3
+ __all__ = ["ArcadeToolManager"]
@@ -0,0 +1,182 @@
1
+ from typing import Any, Callable
2
+
3
+ from arcadepy import NOT_GIVEN, Arcade
4
+ from arcadepy.types.shared 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.inputs.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 = (
120
+ "Please use the following link to authorize: "
121
+ f"{auth_response.authorization_url}"
122
+ )
123
+ if langgraph:
124
+ raise NodeInterrupt(auth_message)
125
+ return {"error": auth_message}
126
+
127
+ # Execute the tool with provided inputs
128
+ execute_response = client.tools.execute(
129
+ tool_name=tool_name,
130
+ inputs=kwargs,
131
+ user_id=user_id if user_id is not None else NOT_GIVEN,
132
+ )
133
+
134
+ if execute_response.success:
135
+ return execute_response.output.value # type: ignore[union-attr]
136
+ error_message = str(execute_response.output.error) # type: ignore[union-attr]
137
+ if langgraph:
138
+ raise NodeInterrupt(error_message)
139
+ return {"error": error_message}
140
+
141
+ return tool_function
142
+
143
+
144
+ def wrap_arcade_tool(
145
+ client: Arcade,
146
+ tool_name: str,
147
+ tool_def: ToolDefinition,
148
+ langgraph: bool = False,
149
+ ) -> StructuredTool:
150
+ """Wrap an Arcade `ToolDefinition` as a LangChain `StructuredTool`.
151
+
152
+ Args:
153
+ client: The Arcade client instance.
154
+ tool_name: The name of the tool to wrap.
155
+ tool_def: The ToolDefinition object to wrap.
156
+ langgraph: Whether to enable LangGraph-specific behavior.
157
+
158
+ Returns:
159
+ A StructuredTool instance representing the Arcade tool.
160
+ """
161
+ description = tool_def.description or "No description provided."
162
+
163
+ # Create a Pydantic model for the tool's input arguments
164
+ args_schema = tool_definition_to_pydantic_model(tool_def)
165
+
166
+ # Create the action function
167
+ action_func = create_tool_function(
168
+ client=client,
169
+ tool_name=tool_name,
170
+ tool_def=tool_def,
171
+ args_schema=args_schema,
172
+ langgraph=langgraph,
173
+ )
174
+
175
+ # Create the StructuredTool instance
176
+ return StructuredTool.from_function(
177
+ func=action_func,
178
+ name=tool_name,
179
+ description=description,
180
+ args_schema=args_schema,
181
+ inject_kwargs={"user_id"},
182
+ )
@@ -0,0 +1,181 @@
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.shared import AuthorizationResponse, ToolDefinition
7
+ from langchain_core.tools import StructuredTool
8
+
9
+ from langchain_arcade._utilities import (
10
+ wrap_arcade_tool,
11
+ )
12
+
13
+
14
+ class ArcadeToolManager:
15
+ """
16
+ Arcade tool manager for LangChain framework.
17
+
18
+ This class wraps Arcade tools as LangChain `StructuredTool`
19
+ objects for integration.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ client: Optional[Arcade] = None,
25
+ **kwargs: dict[str, Any],
26
+ ) -> None:
27
+ """Initialize the ArcadeToolManager.
28
+
29
+ Example:
30
+ >>> manager = ArcadeToolManager(api_key="...")
31
+ >>>
32
+ >>> # retrieve a specific tool as a langchain tool
33
+ >>> manager.get_tools(tools=["Search.SearchGoogle"])
34
+ >>>
35
+ >>> # retrieve all tools in a toolkit as langchain tools
36
+ >>> manager.get_tools(toolkits=["Search"])
37
+ >>>
38
+ >>> # clear and initialize new tools in the manager
39
+ >>> manager.init_tools(tools=["Search.SearchGoogle"], toolkits=["Search"])
40
+
41
+ Args:
42
+ client: Optional Arcade client instance.
43
+ """
44
+ if not client:
45
+ api_key = kwargs.get("api_key", os.getenv("ARCADE_API_KEY", None))
46
+ client = Arcade(api_key=api_key) # type: ignore[arg-type]
47
+ self.client = client
48
+ self._tools: dict[str, ToolDefinition] = {}
49
+
50
+ @property
51
+ def tools(self) -> list[str]:
52
+ return list(self._tools.keys())
53
+
54
+ def __iter__(self) -> Iterator[tuple[str, ToolDefinition]]:
55
+ yield from self._tools.items()
56
+
57
+ def __len__(self) -> int:
58
+ return len(self._tools)
59
+
60
+ def __getitem__(self, tool_name: str) -> ToolDefinition:
61
+ return self._tools[tool_name]
62
+
63
+ def init_tools(
64
+ self,
65
+ tools: Optional[list[str]] = None,
66
+ toolkits: Optional[list[str]] = None,
67
+ ) -> None:
68
+ """Initialize the tools in the manager.
69
+
70
+ This will clear any existing tools in the manager.
71
+
72
+ Example:
73
+ >>> manager = ArcadeToolManager(api_key="...")
74
+ >>> manager.init_tools(tools=["Search.SearchGoogle"])
75
+ >>> manager.get_tools()
76
+
77
+ Args:
78
+ tools: Optional list of tool names to include.
79
+ toolkits: Optional list of toolkits to include.
80
+ """
81
+ self._tools = self._retrieve_tool_definitions(tools, toolkits)
82
+
83
+ def get_tools(
84
+ self,
85
+ tools: Optional[list[str]] = None,
86
+ toolkits: Optional[list[str]] = None,
87
+ langgraph: bool = False,
88
+ ) -> list[StructuredTool]:
89
+ """Return the tools in the manager as LangChain StructuredTool objects.
90
+
91
+ Note: if tools/toolkits are provided, the manager will update it's
92
+ internal tools using a dictionary update by tool name.
93
+
94
+ Example:
95
+ >>> manager = ArcadeToolManager(api_key="...")
96
+ >>>
97
+ >>> # retrieve a specific tool as a langchain tool
98
+ >>> manager.get_tools(tools=["Search.SearchGoogle"])
99
+
100
+ Args:
101
+ tools: Optional list of tool names to include.
102
+ toolkits: Optional list of toolkits to include.
103
+ langgraph: Whether to use LangGraph-specific behavior
104
+ such as NodeInterrupts for auth.
105
+
106
+ Returns:
107
+ List of StructuredTool instances.
108
+ """
109
+ # TODO account for versioning
110
+ if tools or toolkits:
111
+ new_tools = self._retrieve_tool_definitions(tools, toolkits)
112
+ self._tools.update(new_tools)
113
+
114
+ langchain_tools: list[StructuredTool] = []
115
+ for tool_name, definition in self:
116
+ lc_tool = wrap_arcade_tool(self.client, tool_name, definition, langgraph)
117
+ langchain_tools.append(lc_tool)
118
+ return langchain_tools
119
+
120
+ def authorize(self, tool_name: str, user_id: str) -> AuthorizationResponse:
121
+ """Authorize a user for a tool.
122
+
123
+ Example:
124
+ >>> manager = ArcadeToolManager(api_key="...")
125
+ >>> manager.authorize("X.PostTweet", "user_123")
126
+
127
+ Args:
128
+ tool_name: The name of the tool to authorize.
129
+ user_id: The user ID to authorize.
130
+
131
+ Returns:
132
+ AuthorizationResponse
133
+ """
134
+ return self.client.tools.authorize(tool_name=tool_name, user_id=user_id)
135
+
136
+ def is_authorized(self, authorization_id: str) -> bool:
137
+ """Check if a tool authorization is complete.
138
+
139
+ Example:
140
+ >>> manager = ArcadeToolManager(api_key="...")
141
+ >>> manager.init_tools(tools=["Searc"])
142
+ >>> manager.is_authorized("auth_123")
143
+ """
144
+ return self.client.auth.status(authorization_id=authorization_id).status == "completed"
145
+
146
+ def requires_auth(self, tool_name: str) -> bool:
147
+ """Check if a tool requires authorization."""
148
+
149
+ tool_def = self._get_tool_definition(tool_name)
150
+ if tool_def.requirements is None:
151
+ return False
152
+ return tool_def.requirements.authorization is not None
153
+
154
+ def _get_tool_definition(self, tool_name: str) -> ToolDefinition:
155
+ try:
156
+ return self._tools[tool_name]
157
+ except KeyError:
158
+ raise ValueError(f"Tool '{tool_name}' not found in this ArcadeToolManager instance")
159
+
160
+ def _retrieve_tool_definitions(
161
+ self, tools: Optional[list[str]] = None, toolkits: Optional[list[str]] = None
162
+ ) -> dict[str, ToolDefinition]:
163
+ all_tools: list[ToolDefinition] = []
164
+ if tools or toolkits:
165
+ if tools:
166
+ single_tools = [self.client.tools.get(tool_id=tool_id) for tool_id in tools]
167
+ all_tools.extend(single_tools)
168
+ if toolkits:
169
+ for tk in toolkits:
170
+ all_tools.extend(self.client.tools.list(toolkit=tk))
171
+ else:
172
+ # retrieve all tools
173
+ all_tools = self.client.tools.list().items
174
+
175
+ tool_definitions: dict[str, ToolDefinition] = {}
176
+
177
+ for tool in all_tools:
178
+ full_tool_name = f"{tool.toolkit.name}_{tool.name}"
179
+ tool_definitions[full_tool_name] = tool
180
+
181
+ return tool_definitions
File without changes
@@ -0,0 +1,49 @@
1
+ [tool.poetry]
2
+ name = "langchain-arcade"
3
+ version = "0.1.0"
4
+ description = "An integration package connecting Arcade AI 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
+ langchain-core = "^0.3.0"
13
+ arcadepy = "~0.1.1"
14
+ langgraph = {version = ">=0.2.32,<0.3.0", optional = true}
15
+
16
+ [tool.poetry.extras]
17
+ langgraph = ["langgraph"]
18
+
19
+ [tool.poetry.group.dev.dependencies]
20
+ pytest = "^8.1.2"
21
+ pytest-cov = "^4.0.0"
22
+ mypy = "^1.5.1"
23
+ pre-commit = "^3.4.0"
24
+ tox = "^4.11.1"
25
+ pytest-asyncio = "^0.23.7"
26
+
27
+
28
+ [tool.mypy]
29
+ files = ["langchain_arcade"]
30
+ python_version = "3.10"
31
+ disallow_untyped_defs = "True"
32
+ disallow_any_unimported = "True"
33
+ no_implicit_optional = "True"
34
+ check_untyped_defs = "True"
35
+ warn_return_any = "True"
36
+ warn_unused_ignores = "True"
37
+ show_error_codes = "True"
38
+ ignore_missing_imports = "True"
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
42
+
43
+
44
+ [tool.coverage.run]
45
+ branch = true
46
+ source = ["langchain_arcade"]
47
+
48
+ [tool.coverage.report]
49
+ skip_empty = true