Tool Creation Guide¶
This guide explains the two approaches for creating tools in the Azad system: using the builder pattern or directly defining a ToolSchema object.
Introduction to Tools¶
Tools in the Azad system are defined with a consistent structure that includes:
- Name: A unique identifier for the tool
- Schema: A Zod schema that defines the input parameters
- Description: A concise explanation of what the tool does
- Examples: Sample usages of the tool for reference
- Context: Tool-specific state that persists across invocations
- Lifecycle methods: Functions that handle initialization, disposal, etc.
- Execution logic: The core function that implements the tool's behavior
Approach 1: Builder Pattern¶
The builder pattern provides a fluent API for creating tools. This approach uses method chaining to build up a tool definition step by step.
Example: Creating a Tool with the Builder Pattern¶
import { tool } from '../utils/tool-builder';
import { z } from 'zod';
// Define the schema
const greetingSchema = z
.object({
name: z
.string()
.min(1)
.describe('The name to greet')
.isStreamable(true),
language: z
.enum(['en', 'es', 'fr'])
.optional()
.describe('Language for the greeting (default: en)')
.isStreamable(false),
})
.strict();
// Define result interface
interface GreetingResult {
message: string;
timestamp: string;
}
// Define context interface
interface GreetingContext {
count: number;
}
// Create the tool using the builder pattern
const greetingTool = tool
.name('greeting_tool')
.input(greetingSchema)
.description('Generate a personalized greeting message')
.examples([
{
explanation: 'Simple English greeting',
parameters: {
name: 'John',
},
},
{
explanation: 'Spanish greeting',
parameters: {
name: 'Maria',
language: 'es',
},
},
])
.output<GreetingResult>()
.context<GreetingContext>({
count: 0,
})
.onInit((ctx) => {
console.log('Initializing greeting tool');
})
.onDispose((ctx) => {
console.log(`Tool was used ${ctx.count} times before disposal`);
})
.executeTool(async (params, { executionContext, toolContext }) => {
// Increment usage count
toolContext.count++;
const { name, language = 'en' } = params;
// Generate greeting based on language
let greeting;
switch (language) {
case 'es':
greeting = `¡Hola, ${name}!`;
break;
case 'fr':
greeting = `Bonjour, ${name}!`;
break;
default:
greeting = `Hello, ${name}!`;
}
// Return success response
return executionContext.createSuccessResponse({
message: greeting,
timestamp: new Date().toISOString(),
});
})
.build();
export { greetingTool, greetingSchema };
Builder Pattern Flow¶
The builder pattern enforces a specific sequence of method calls:
.name()- Set the tool name.input()- Define the input schema.description()- Provide a description.examples()- Add usage examples.output<T>()- Specify the result type.context<C>()- Define context (optional).onInit(),.onDispose()- Add lifecycle methods (optional).executeTool()- Implement the core functionality.build()- Finalize and create the tool instance
Approach 2: Direct ToolSchema Object¶
Alternatively, tools can be created by directly defining a ToolSchema object using the createToolSchema function. This approach is more concise and allows for defining the entire tool in a single object.
Example: Creating a Tool with ToolSchema Object¶
import { z } from 'zod';
import { createToolSchema } from '../utils/tool-schema';
// Define the schema
const greetingSchema = z
.object({
name: z
.string()
.min(1)
.describe('The name to greet')
.isStreamable(true),
language: z
.enum(['en', 'es', 'fr'])
.optional()
.describe('Language for the greeting (default: en)')
.isStreamable(false),
})
.strict();
// Define result interface
interface GreetingResult {
message: string;
timestamp: string;
}
// Define context interface
interface GreetingContext {
count: number;
}
// Create the tool using the direct object approach
const greetingTool = createToolSchema<
'greeting_tool',
typeof greetingSchema,
GreetingResult,
GreetingContext
>('greeting_tool', {
schema: greetingSchema,
description: 'Generate a personalized greeting message',
examples: [
{
explanation: 'Simple English greeting',
parameters: {
name: 'John',
},
},
{
explanation: 'Spanish greeting',
parameters: {
name: 'Maria',
language: 'es',
},
},
],
initialContext: {
count: 0,
},
onInit: (ctx) => {
console.log('Initializing greeting tool');
},
onDispose: (ctx) => {
console.log(`Tool was used ${ctx.count} times before disposal`);
},
execute: async (params, { executionContext, toolContext }) => {
// Increment usage count
toolContext.count++;
const { name, language = 'en' } = params;
// Generate greeting based on language
let greeting;
switch (language) {
case 'es':
greeting = `¡Hola, ${name}!`;
break;
case 'fr':
greeting = `Bonjour, ${name}!`;
break;
default:
greeting = `Hello, ${name}!`;
}
// Return success response
return executionContext.createSuccessResponse({
message: greeting,
timestamp: new Date().toISOString(),
});
},
executeDirect: async (params) => {
const { name, language = 'en' } = params;
// Generate greeting based on language
let greeting;
switch (language) {
case 'es':
greeting = `¡Hola, ${name}!`;
break;
case 'fr':
greeting = `Bonjour, ${name}!`;
break;
default:
greeting = `Hello, ${name}!`;
}
return {
message: greeting,
timestamp: new Date().toISOString(),
};
}
});
export { greetingTool, greetingSchema };
Comparing the Approaches¶
Builder Pattern Advantages¶
- Guided Creation: The builder enforces a specific sequence of steps
- Progressive Disclosure: Each method provides a clear focus on one aspect
- Type Safety: Type inference flows naturally through the chain
- Readability: The chain of method calls clearly shows the tool's structure
ToolSchema Object Advantages¶
- Conciseness: Defines the entire tool in a single object
- At-a-glance Overview: All tool properties are visible in one place
- Less Boilerplate: Avoids repeated method chaining
- Direct Structure: Represents the final structure more closely
When to Use Each Approach¶
Use the Builder Pattern when:¶
- You're new to creating tools and want guidance on the required parts
- You prefer a step-by-step construction process
- You're creating a complex tool with many options
- You value explicit type inference at each step
Use the ToolSchema Object when:¶
- You're experienced with creating tools
- You prefer a more concise definition
- You want to see the entire tool structure at once
- You're creating a simple tool with standard behavior
Best Practices¶
Regardless of the approach you choose, follow these best practices:
- Clear Descriptions: Provide clear, concise descriptions for the tool and all parameters
- Thorough Examples: Include examples that cover common use cases
- Proper Error Handling: Handle errors gracefully in the execute function
- Contextual State: Use the context object for persistent state
- Cleanup: Implement onDispose to clean up resources
- Type Safety: Always define proper types for parameters, results, and context
- Parameter Validation: Use Zod to enforce parameter validation
Tool Registration¶
After creating your tool, register it in the appropriate index file:
// In src/environment/tools/definitions/index.ts
import { greetingTool } from './greeting-tool';
export const agentTools = [
// ... other tools
greetingTool,
];
Conclusion¶
Both approaches to creating tools have their merits. The builder pattern provides a guided, step-by-step process, while the direct ToolSchema object approach offers a more concise definition. Choose the approach that best fits your preferences and the complexity of the tool you're creating.