from typing import Any, Callable, Union from arcadepy import NOT_GIVEN, Arcade, AsyncArcade from arcadepy.types import ExecuteToolResponse, 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 process_tool_execution_response( execute_response: ExecuteToolResponse, tool_name: str, langgraph: bool ) -> Any: """Process the response from tool execution and handle errors appropriately. Args: execute_response: The response from tool execution tool_name: The name of the tool that was executed langgraph: Whether LangGraph-specific behavior is enabled Returns: The output value on success, or error details on failure """ if execute_response.success and execute_response.output is not None: return execute_response.output.value # Extract detailed error information error_details = { "error": "Unknown error occurred", "tool": tool_name, } if execute_response.output is not None and execute_response.output.error is not None: error = execute_response.output.error error_message = str(error.message) if hasattr(error, "message") else "Unknown error" error_details["error"] = error_message # Add all non-None optional error fields to the details if ( hasattr(error, "additional_prompt_content") and error.additional_prompt_content is not None ): error_details["additional_prompt_content"] = error.additional_prompt_content if hasattr(error, "can_retry") and error.can_retry is not None: error_details["can_retry"] = str(error.can_retry) if hasattr(error, "developer_message") and error.developer_message is not None: error_details["developer_message"] = str(error.developer_message) if hasattr(error, "retry_after_ms") and error.retry_after_ms is not None: error_details["retry_after_ms"] = str(error.retry_after_ms) if langgraph: raise NodeInterrupt(error_details) return error_details 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, ) return process_tool_execution_response(execute_response, tool_name, langgraph) return tool_function def wrap_arcade_tool( client: Union[Arcade, AsyncArcade], 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 if isinstance(client, Arcade): action_func = create_tool_function( client=client, tool_name=tool_name, tool_def=tool_def, args_schema=args_schema, langgraph=langgraph, ) else: # Use async tool function for AsyncArcade client action_func = create_async_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"}, ) def create_async_tool_function( client: AsyncArcade, tool_name: str, tool_def: ToolDefinition, args_schema: type[BaseModel], langgraph: bool = False, ) -> Callable: """Create an async callable function to execute an Arcade tool. Args: client: The AsyncArcade 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: An async 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 ) async def tool_function(config: RunnableConfig, **kwargs: Any) -> Any: """Run 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 = await 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 = await client.tools.execute( tool_name=tool_name, input=kwargs, user_id=user_id if user_id is not None else NOT_GIVEN, ) return process_tool_execution_response(execute_response, tool_name, langgraph) return tool_function