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 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"},
|
|
)
|