Actions as Tools
View SourceOverview
Jido Actions can be converted into tools compatible with LLM frameworks like Langchain. This guide demonstrates how to transform your Actions into Langchain tools, enabling seamless integration with AI-powered workflows.
Key Concepts
- An Action defines a discrete unit of functionality with validated inputs and outputs
- Langchain tools represent capabilities that can be invoked by LLMs
- The
Jido.Actions.Tool
module handles the conversion between these formats
Basic Tool Conversion
Let's start with a simple example of converting a basic Action to a Langchain tool:
defmodule WeatherAction do
use Jido.Action,
name: "get_weather",
description: "Gets the current weather for a location",
schema: [
location: [
type: :string,
required: true,
doc: "The city or location to get weather for"
],
units: [
type: {:in, ["celsius", "fahrenheit"]},
default: "celsius",
doc: "Temperature units to use"
]
]
@impl true
def run(%{location: location, units: units}, _context) do
# Simulated weather API call
{:ok, %{
temperature: 22,
units: units,
conditions: "sunny",
location: location
}}
end
end
Convert this Action to a Langchain tool:
# Convert to tool format
weather_tool = WeatherAction.to_tool()
# The tool can now be used with Langchain
{:ok, chain} = Langchain.Chain.new([
weather_tool,
Langchain.Chains.ChatPrompt
])
Tool Schema Generation
The to_tool/0
function automatically generates a JSON Schema for the tool's parameters based on your Action's schema:
%{
"name" => "get_weather",
"description" => "Gets the current weather for a location",
"parameters" => %{
"type" => "object",
"properties" => %{
"location" => %{
"type" => "string",
"description" => "The city or location to get weather for"
},
"units" => %{
"type" => "string",
"enum" => ["celsius", "fahrenheit"],
"description" => "Temperature units to use"
}
},
"required" => ["location"]
}
}
Advanced Tool Patterns
1. Complex Parameter Validation
For tools with complex parameter requirements:
defmodule DataProcessingAction do
use Jido.Action,
name: "process_data",
description: "Processes data with specified options",
schema: [
input_format: [
type: {:in, ["json", "csv", "xml"]},
required: true,
doc: "Format of the input data"
],
transformations: [
type: {:list, :atom},
default: [],
doc: "List of transformations to apply"
],
options: [
type: :map,
default: %{},
doc: "Additional processing options"
]
]
@impl true
def run(params, context) do
# Processing logic here
{:ok, %{result: "processed"}}
end
end
2. Error Handling in Tools
Tools should provide clear error messages that can be understood by the LLM:
defmodule RobustAction do
use Jido.Action,
name: "robust_operation",
description: "Performs an operation with comprehensive error handling",
schema: [
input: [type: :string, required: true]
]
@impl true
def run(%{input: input}, _context) do
case process_input(input) do
{:ok, result} ->
{:ok, %{result: result}}
{:error, :invalid_format} ->
{:error, "Input must be in valid format X,Y,Z"}
{:error, :not_found} ->
{:error, "The requested resource could not be found"}
end
end
end
3. Contextual Tool Execution
Pass relevant context to your tools:
defmodule ContextAwareAction do
use Jido.Action,
name: "context_aware",
description: "Uses execution context for enhanced functionality",
schema: [
operation: [type: :string, required: true]
]
@impl true
def run(params, %{user_id: user_id, tenant: tenant}) do
# Use context in processing
{:ok, %{
operation: params.operation,
user: user_id,
tenant: tenant
}}
end
end
# Use with Langchain
{:ok, chain} = Langchain.Chain.new([
{ContextAwareAction.to_tool(), context: %{
user_id: "user_123",
tenant: "tenant_456"
}},
Langchain.Chains.ChatPrompt
])
Integration with LangChain
Here's a complete example of using a Jido Action as a tool in a Langchain workflow:
defmodule CalculatorAction do
use Jido.Action,
name: "calculate",
description: "Performs basic arithmetic calculations",
schema: [
operation: [
type: {:in, ["add", "subtract", "multiply", "divide"]},
required: true,
doc: "The arithmetic operation to perform"
],
x: [type: :float, required: true, doc: "First number"],
y: [type: :float, required: true, doc: "Second number"]
]
@impl true
def run(%{operation: op, x: x, y: y}, _context) do
result = case op do
"add" -> x + y
"subtract" -> x - y
"multiply" -> x * y
"divide" when y != 0 -> x / y
"divide" -> {:error, "Division by zero"}
end
case result do
{:error, reason} -> {:error, reason}
value -> {:ok, %{result: value}}
end
end
end
# Create Langchain workflow
defmodule MathWorkflow do
def execute(prompt) do
# Convert calculator to tool
calculator_tool = CalculatorAction.to_tool()
# Create chain with tool
{:ok, chain} = Langchain.Chain.new([
calculator_tool,
{Langchain.Chains.ChatPrompt,
prompt: """
You are a helpful math assistant. Use the calculator tool to solve problems.
User query: #{prompt}
"""}
])
# Execute chain
Langchain.run(chain)
end
end
# Example usage
MathWorkflow.execute("What is 123.45 multiplied by 67.89?")
Best Practices
Clear Descriptions
- Provide detailed descriptions for both the tool and its parameters
- Include examples in parameter documentation
- Use consistent terminology
Robust Validation
- Implement comprehensive parameter validation
- Return clear error messages
- Handle edge cases explicitly
Context Handling
- Pass relevant context through the tool chain
- Document expected context requirements
- Provide defaults for missing context
Error Handling
- Return structured error responses
- Include actionable error messages
- Handle both expected and unexpected errors
Testing
- Test tool conversion explicitly
- Verify schema generation
- Test integration with Langchain
Common Issues and Solutions
1. Schema Conversion
If your Action schema uses custom types, implement custom type conversion:
defmodule CustomTypeAction do
use Jido.Action,
name: "custom_type_action",
description: "Handles custom types",
schema: [
date: [
type: :date,
required: true,
doc: "Date in ISO format"
]
]
# Custom type validation
def validate_params(%{date: date} = params) when is_binary(date) do
case Date.from_iso8601(date) do
{:ok, _valid_date} -> {:ok, params}
{:error, _} -> {:error, "Invalid date format"}
end
end
end
2. Context Persistence
Ensure context is properly passed through the tool chain:
# Create chain with persistent context
{:ok, chain} = Langchain.Chain.new([
{MyAction.to_tool(),
context: %{tenant_id: "t1"},
persist_context: true},
Langchain.Chains.ChatPrompt
])
Testing Tools
Example test cases for your tools:
defmodule WeatherActionTest do
use ExUnit.Case
test "converts to valid tool" do
tool = WeatherAction.to_tool()
assert tool.name == "get_weather"
assert tool.description =~ "Gets the current weather"
assert Map.has_key?(tool.parameters_schema["properties"], "location")
end
test "executes as tool" do
tool = WeatherAction.to_tool()
assert {:ok, result} = tool.function.(%{
"location" => "Portland",
"units" => "celsius"
}, %{})
assert is_binary(result)
assert Jason.decode!(result)["temperature"]
end
end