Skip to main content

Outline

In this module, we'll focus on the practical aspects of defining your custom tools using the @optimizely-opal/opal-tools-sdk. We'll cover the syntax for applying the tool() decorator, how to structure your tool functions to accept parameters, and how to define parameter schemas with various properties.

By the end of this module, you will be able to:

  • Define and register a custom Opal tool using the @tool() decorator.

  • Structure tool functions to accept and validate various types of parameters.

  • Use TypeScript interfaces and parameter schemas for type safety and clarity.

Using the tool() Decorator to Mark a Function as an Opal Tool

As introduced in the previous lesson, the @tool() decorator is how you tell the SDK that a particular class contains an Opal tool. The execute method within that class is what Opal will call when the tool is invoked.

Basic Structure:

import { tool } from '@optimizely-opal/opal-tools-sdk';

@tool({
    name: 'my_tool_name', // Unique programmatic name for the tool
    description: 'A brief description of what this tool does.',
    parameters: [] // We'll define these next
})
export class MyToolClass {
    // The 'execute' method is the entry point for Opal to run your tool
    async execute(params: any): Promise {
        // Your tool's logic goes here
        console.log('Tool executed with parameters:', params);
        return { status: 'success', data: 'Tool completed its task.' };
    }
}
  • name: This is a crucial identifier. It should be unique within your tool service and descriptive, often using snake_case (e.g., get_user_profile, send_email). This is what Opal's AI will use to refer to your tool.
  • description: This provides a human-readable explanation of the tool's purpose. It's used in the Opal UI to help users understand what the tool does. Make it clear and concise.
  • parameters: This is an array where you define the inputs your tool expects. We'll explore this in detail below.
  • execute(params: any): Promise<any>: This is the method that gets called when Opal invokes your tool.
    • params: An object containing the input parameters provided by Opal. The structure of this object will match the parameters you define in the @tool decorator.
    • Promise<any>: Your execute method should be async and return a Promise that resolves to a JSON-serializable object. This object will be the response Opal receives.

Structuring Your Tool Functions to Accept Parameters

When Opal invokes your tool, it sends the input data as a JSON object in the request body. The SDK automatically parses this and passes it to your execute method as the params argument. It's important to note that depending on how Opal's AI assistant constructs the invocation, parameters might be nested under keys like parameters, arguments, or directly in the root of the request body. Your tool should be designed to handle this flexibility.

Type Safety with TypeScript: One of the biggest advantages of using TypeScript is the ability to define the shape of your params object, ensuring type safety and providing excellent autocompletion in your IDE.

// Define an interface for your tool's parameters
interface GreetPersonParams {
    name: string;
    language?: string; // Optional parameter
}

@tool({
    name: 'greet_person',
    description: 'Greets a person by name in a specified language.',
    parameters: [
        // ... parameter definitions (see next section)
    ]
})
export class GreeterTool {
    async execute(params: GreetPersonParams) { // Use the interface here
        const { name, language } = params; // Destructure for easier access
        let greeting = `Hello, ${name}!`;
        if (language === 'es') {
            greeting = `¡Hola, ${name}!`;
        } else if (language === 'fr') {
            greeting = `Bonjour, ${name}!`;
        }
        return { greeting: greeting, language: language || 'en' };
    }
}

Defining Parameter Schemas with name, type, description, and required Properties

The parameters array in the @tool() decorator is where you specify each input your tool expects. Each object in this array describes a single parameter.

  • name (string): The programmatic name of the parameter. This must match the key in the params object that Opal sends.
  • type (ParameterType enum): The data type of the parameter. Use the ParameterType enum from the SDK. This is crucial for validation.
  • description (string): A clear, user-friendly description of what the parameter represents. This is displayed in the Opal UI and helps users understand what input is expected.
  • required (boolean): Set to true if the parameter must always be provided by the caller; false otherwise. The SDK will enforce this validation.
  • enum (array of strings, optional): For parameters that should have a fixed set of values (like a dropdown selection), you can provide an enum array. Opal's UI can then render this as a selection list.

Example: greet_person Tool with Detailed Parameter Definitions

Let's refine our GreeterTool example with full parameter definitions:

import { tool, ParameterType } from '@optimizely-opal/opal-tools-sdk';

// Define an interface for type safety
interface GreetPersonParams {
    personName: string; // Renamed to avoid conflict with 'name' property of Parameter object
    language?: 'en' | 'es' | 'fr'; // Using literal types for enum-like behavior
    includeEmoji?: boolean;
}

@tool({
    name: 'greet_person',
    description: 'Generates a personalized greeting for a person in a specified language, with an optional emoji.',
    parameters: [
        {
            name: 'personName', // This name matches the property in GreetPersonParams
            type: ParameterType.String,
            description: 'The name of the person to greet. This is a mandatory field.',
            required: true
        },
        {
            name: 'language',
            type: ParameterType.String,
            description: 'Optional language for the greeting. Choose from English (en), Spanish (es), or French (fr). Defaults to English.',
            required: false,
            enum: ['en', 'es', 'fr'] // Provides a list of valid options
        },
        {
            name: 'includeEmoji',
            type: ParameterType.Boolean,
            description: 'Set to true to include a greeting emoji. Defaults to false.',
            required: false
        }
    ]
})
export class GreeterTool {
    async execute(params: GreetPersonParams) {
        const { personName, language = 'en', includeEmoji = false } = params; // Destructure with defaults

        let greetingText: string;
        switch (language) {
            case 'es':
                greetingText = `¡Hola, ${personName}!`;
                break;
            case 'fr':
                greetingText = `Bonjour, ${personName}!`;
                break;
            case 'en':
            default:
                greetingText = `Hello, ${personName}!`;
                break;
        }

        const emoji = includeEmoji ? ' 👋' : ''; // Add emoji if requested

        return {
            greeting: greetingText + emoji,
            languageUsed: language,
            personGreeted: personName
        };
    }
}

Key Takeaways:

  • The @tool() decorator is your primary interface for defining tools.
  • The execute method is where your tool's logic resides, accepting parameters and returning a result.
  • TypeScript interfaces provide strong type checking for your parameters, improving code quality.
  • Parameter definitions in the @tool() decorator are crucial for Opal to understand and present your tool correctly. Use name, type, description, required, and enum effectively.

In the next module, we'll build on these concepts to create more simple, yet practical, Opal tools.