**PR Description** This update bumps the integration’s version to `0.2.0` and brings several important changes to how `langchain-arcade` interfaces with Arcade tools: 1. **Updated Tool Definition Imports** • Replaces `arcadepy.types.shared.ToolDefinition` with `arcadepy.types.ToolGetResponse as ToolDefinition`. • The parameter extraction is now done via `tool_def.input.parameters` instead of the previous `tool_def.inputs.parameters`. 2. **Authorization Flow Adjustments** • Uses `auth_response.url` instead of `auth_response.authorization_url`. • The `authorize` and `is_authorized` methods now rely on the Arcade client’s updated arguments (`client.auth.status(id=authorization_id)`). 3. **Tool Execution Parameter Renaming** • The `execute` method now expects `input=kwargs` instead of `inputs=kwargs`, aligning with Arcade’s new API spec. 4. **Tool Retrieval Enhancements** • `_retrieve_tool_definitions` is revised to better handle pagination and tool listing (including when no tools/toolkits are explicitly provided). 5. **Version & Dependency Updates** • Increases `langchain-arcade` to `0.2.0`. • Switches `arcadepy` dependency to `~1.0.0rc1`. • Updates example requirements to consume `langchain-arcade[langgraph]>=0.2.0`. These changes may affect existing code that relies on older parameter names (`inputs.parameters` → `input.parameters`) and the renamed execute argument. Please ensure any integrations or custom usage of Arcade tools is updated accordingly.
179 lines
5.8 KiB
Python
179 lines
5.8 KiB
Python
from typing import Any, Callable
|
|
|
|
from arcadepy import NOT_GIVEN, Arcade
|
|
from arcadepy.types import ToolGetResponse as ToolDefinition
|
|
from langchain_core.runnables import RunnableConfig
|
|
from langchain_core.tools import StructuredTool
|
|
from pydantic import BaseModel, Field, create_model
|
|
|
|
# Check if LangGraph is enabled
|
|
LANGGRAPH_ENABLED = True
|
|
try:
|
|
from langgraph.errors import NodeInterrupt
|
|
except ImportError:
|
|
LANGGRAPH_ENABLED = False
|
|
|
|
# Mapping of Arcade value types to Python types
|
|
TYPE_MAPPING = {
|
|
"string": str,
|
|
"number": float,
|
|
"integer": int,
|
|
"boolean": bool,
|
|
"array": list,
|
|
"json": dict,
|
|
}
|
|
|
|
|
|
def get_python_type(val_type: str) -> Any:
|
|
"""Map Arcade value types to Python types.
|
|
|
|
Args:
|
|
val_type: The value type as a string.
|
|
|
|
Returns:
|
|
Corresponding Python type.
|
|
"""
|
|
_type = TYPE_MAPPING.get(val_type)
|
|
if _type is None:
|
|
raise ValueError(f"Invalid value type: {val_type}")
|
|
return _type
|
|
|
|
|
|
def tool_definition_to_pydantic_model(tool_def: ToolDefinition) -> type[BaseModel]:
|
|
"""Convert a ToolDefinition's inputs into a Pydantic BaseModel.
|
|
|
|
Args:
|
|
tool_def: The ToolDefinition object to convert.
|
|
|
|
Returns:
|
|
A Pydantic BaseModel class representing the tool's input schema.
|
|
"""
|
|
try:
|
|
fields: dict[str, Any] = {}
|
|
for param in tool_def.input.parameters or []:
|
|
param_type = get_python_type(param.value_schema.val_type)
|
|
if param_type == list and param.value_schema.inner_val_type: # noqa: E721
|
|
inner_type: type[Any] = get_python_type(param.value_schema.inner_val_type)
|
|
param_type = list[inner_type] # type: ignore[valid-type]
|
|
param_description = param.description or "No description provided."
|
|
default = ... if param.required else None
|
|
fields[param.name] = (
|
|
param_type,
|
|
Field(default=default, description=param_description),
|
|
)
|
|
return create_model(f"{tool_def.name}Args", **fields)
|
|
except ValueError as e:
|
|
raise ValueError(
|
|
f"Error converting {tool_def.name} parameters into pydantic model for langchain: {e}"
|
|
)
|
|
|
|
|
|
def create_tool_function(
|
|
client: Arcade,
|
|
tool_name: str,
|
|
tool_def: ToolDefinition,
|
|
args_schema: type[BaseModel],
|
|
langgraph: bool = False,
|
|
) -> Callable:
|
|
"""Create a callable function to execute an Arcade tool.
|
|
|
|
Args:
|
|
client: The Arcade client instance.
|
|
tool_name: The name of the tool to wrap.
|
|
tool_def: The ToolDefinition of the tool to wrap.
|
|
args_schema: The Pydantic model representing the tool's arguments.
|
|
langgraph: Whether to enable LangGraph-specific behavior.
|
|
|
|
Returns:
|
|
A callable function that executes the tool.
|
|
"""
|
|
if langgraph and not LANGGRAPH_ENABLED:
|
|
raise ImportError("LangGraph is not installed. Please install it to use this feature.")
|
|
|
|
requires_authorization = (
|
|
tool_def.requirements is not None and tool_def.requirements.authorization is not None
|
|
)
|
|
|
|
def tool_function(config: RunnableConfig, **kwargs: Any) -> Any:
|
|
"""Execute the Arcade tool with the given parameters.
|
|
|
|
Args:
|
|
config: RunnableConfig containing execution context.
|
|
**kwargs: Tool input arguments.
|
|
|
|
Returns:
|
|
The output from the tool execution.
|
|
"""
|
|
user_id = config.get("configurable", {}).get("user_id") if config else None
|
|
|
|
if requires_authorization:
|
|
if user_id is None:
|
|
error_message = f"user_id is required to run {tool_name}"
|
|
if langgraph:
|
|
raise NodeInterrupt(error_message)
|
|
return {"error": error_message}
|
|
|
|
# Authorize the user for the tool
|
|
auth_response = client.tools.authorize(tool_name=tool_name, user_id=user_id)
|
|
if auth_response.status != "completed":
|
|
auth_message = f"Please use the following link to authorize: {auth_response.url}"
|
|
if langgraph:
|
|
raise NodeInterrupt(auth_message)
|
|
return {"error": auth_message}
|
|
|
|
# Execute the tool with provided inputs
|
|
execute_response = client.tools.execute(
|
|
tool_name=tool_name,
|
|
input=kwargs,
|
|
user_id=user_id if user_id is not None else NOT_GIVEN,
|
|
)
|
|
|
|
if execute_response.success:
|
|
return execute_response.output.value # type: ignore[union-attr]
|
|
error_message = str(execute_response.output.error) # type: ignore[union-attr]
|
|
if langgraph:
|
|
raise NodeInterrupt(error_message)
|
|
return {"error": error_message}
|
|
|
|
return tool_function
|
|
|
|
|
|
def wrap_arcade_tool(
|
|
client: Arcade,
|
|
tool_name: str,
|
|
tool_def: ToolDefinition,
|
|
langgraph: bool = False,
|
|
) -> StructuredTool:
|
|
"""Wrap an Arcade `ToolDefinition` as a LangChain `StructuredTool`.
|
|
|
|
Args:
|
|
client: The Arcade client instance.
|
|
tool_name: The name of the tool to wrap.
|
|
tool_def: The ToolDefinition object to wrap.
|
|
langgraph: Whether to enable LangGraph-specific behavior.
|
|
|
|
Returns:
|
|
A StructuredTool instance representing the Arcade tool.
|
|
"""
|
|
description = tool_def.description or "No description provided."
|
|
|
|
# Create a Pydantic model for the tool's input arguments
|
|
args_schema = tool_definition_to_pydantic_model(tool_def)
|
|
|
|
# Create the action function
|
|
action_func = create_tool_function(
|
|
client=client,
|
|
tool_name=tool_name,
|
|
tool_def=tool_def,
|
|
args_schema=args_schema,
|
|
langgraph=langgraph,
|
|
)
|
|
|
|
# Create the StructuredTool instance
|
|
return StructuredTool.from_function(
|
|
func=action_func,
|
|
name=tool_name,
|
|
description=description,
|
|
args_schema=args_schema,
|
|
inject_kwargs={"user_id"},
|
|
)
|