Skip to main content
Sonamu allows you to create custom AI agents by extending BaseAgentClass. By defining tools using the @tools decorator, the LLM can select and execute appropriate tools based on the context.

BaseAgentClass

This is the base class for AI agents.

Basic Structure

import { BaseAgentClass, tools } from "sonamu/ai";
import { openai } from "@ai-sdk/openai";
import { z } from "zod";

class CustomerSupportAgentClass extends BaseAgentClass<{
  userId: number;
}> {
  constructor() {
    super('CustomerSupportAgent');
  }

  @tools({
    description: "Retrieves order information",
    schema: {
      input: z.object({
        orderId: z.number(),
      }),
      output: z.object({
        orderId: z.number(),
        status: z.string(),
        items: z.array(z.string()),
      }),
    }
  })
  async getOrder(input: { orderId: number }) {
    // Order retrieval logic
    return {
      orderId: input.orderId,
      status: 'shipped',
      items: ['Product A', 'Product B'],
    };
  }
}

export const CustomerSupportAgent = new CustomerSupportAgentClass();

@tools Decorator

A decorator for defining tools.

Basic Usage

@tools({
  description: "Tool description",
  schema: {
    input: z.object({
      // Input schema
    }),
    output: z.object({
      // Output schema (optional)
    }),
  }
})
async toolMethod(input: InputType) {
  // Tool logic
  return output;
}

Options

Tool Description (required)
@tools({
  description: "Retrieves user order history. Takes an order ID and returns order information.",
  schema: { /* ... */ }
})
Important: The LLM reads this description to decide whether to use the tool.

Practical Examples

1. Hotel Booking Agent

import { BaseAgentClass, tools } from "sonamu/ai";
import { z } from "zod";

class HotelBookingAgentClass extends BaseAgentClass<{
  userId: number;
  locale: string;
}> {
  constructor() {
    super('HotelBookingAgent');
  }

  @tools({
    description: "Searches for available hotels by city and date",
    schema: {
      input: z.object({
        city: z.string().describe("City name"),
        checkIn: z.string().describe("Check-in date (YYYY-MM-DD)"),
        checkOut: z.string().describe("Check-out date (YYYY-MM-DD)"),
        guests: z.number().describe("Number of guests"),
      }),
      output: z.array(z.object({
        hotelId: z.number(),
        name: z.string(),
        price: z.number(),
        rating: z.number(),
      })),
    }
  })
  async searchHotels(input: {
    city: string;
    checkIn: string;
    checkOut: string;
    guests: number;
  }) {
    // Search hotels from DB
    const hotels = await HotelModel.findMany({
      wq: [
        ['city', input.city],
        ['available_from', '<=', input.checkIn],
        ['available_to', '>=', input.checkOut],
        ['capacity', '>=', input.guests],
      ],
    });

    return hotels.data.map(hotel => ({
      hotelId: hotel.id,
      name: hotel.name,
      price: hotel.price_per_night,
      rating: hotel.rating,
    }));
  }

  @tools({
    description: "Books a hotel",
    schema: {
      input: z.object({
        hotelId: z.number().describe("Hotel ID"),
        checkIn: z.string(),
        checkOut: z.string(),
        guests: z.number(),
      }),
      output: z.object({
        bookingId: z.number(),
        confirmationNumber: z.string(),
        totalPrice: z.number(),
      }),
    },
    needsApproval: true,  // User approval before booking
  })
  async bookHotel(input: {
    hotelId: number;
    checkIn: string;
    checkOut: string;
    guests: number;
  }) {
    const userId = this.store?.userId;

    // Create booking
    const booking = await BookingModel.saveOne({
      user_id: userId,
      hotel_id: input.hotelId,
      check_in: input.checkIn,
      check_out: input.checkOut,
      guests: input.guests,
      status: 'confirmed',
    });

    return {
      bookingId: booking.id,
      confirmationNumber: booking.confirmation_number,
      totalPrice: booking.total_price,
    };
  }

  @tools({
    description: "Cancels a booking",
    schema: {
      input: z.object({
        bookingId: z.number().describe("Booking ID"),
      }),
      output: z.object({
        success: z.boolean(),
        refundAmount: z.number(),
      }),
    },
    needsApproval: true,  // User approval before cancellation
  })
  async cancelBooking(input: { bookingId: number }) {
    const booking = await BookingModel.findById(input.bookingId);

    // Process cancellation
    await BookingModel.updateOne(['id', booking.id], {
      status: 'cancelled',
    });

    // Process refund
    const refundAmount = booking.total_price * 0.9;  // 10% fee

    return {
      success: true,
      refundAmount,
    };
  }
}

export const HotelBookingAgent = new HotelBookingAgentClass();

2. Data Analysis Agent

import { BaseAgentClass, tools } from "sonamu/ai";
import { z } from "zod";

class DataAnalystAgentClass extends BaseAgentClass<{
  sessionId: string;
}> {
  constructor() {
    super('DataAnalystAgent');
  }

  @tools({
    description: "Executes a SQL query to retrieve data",
    schema: {
      input: z.object({
        query: z.string().describe("SQL query to execute"),
      }),
      output: z.object({
        rows: z.array(z.record(z.any())),
        rowCount: z.number(),
      }),
    }
  })
  async executeQuery(input: { query: string }) {
    // Safety validation (only allow SELECT)
    if (!/^SELECT/i.test(input.query.trim())) {
      throw new Error('Only SELECT queries are allowed');
    }

    const result = await DB.query(input.query);

    return {
      rows: result.rows,
      rowCount: result.rowCount,
    };
  }

  @tools({
    description: "Creates chart configuration for data visualization",
    schema: {
      input: z.object({
        data: z.array(z.record(z.any())),
        xAxis: z.string().describe("X-axis column name"),
        yAxis: z.string().describe("Y-axis column name"),
        chartType: z.enum(['bar', 'line', 'pie']),
      }),
      output: z.object({
        chartId: z.string(),
        config: z.any(),
      }),
    }
  })
  async createChart(input: {
    data: Array<Record<string, any>>;
    xAxis: string;
    yAxis: string;
    chartType: 'bar' | 'line' | 'pie';
  }) {
    const chartId = `chart_${Date.now()}`;

    // Create chart configuration
    const config = {
      type: input.chartType,
      data: {
        labels: input.data.map(row => row[input.xAxis]),
        datasets: [{
          label: input.yAxis,
          data: input.data.map(row => row[input.yAxis]),
        }],
      },
    };

    return { chartId, config };
  }

  @tools({
    description: "Calculates statistical summary (mean, median, standard deviation, etc.)",
    schema: {
      input: z.object({
        data: z.array(z.number()),
      }),
      output: z.object({
        mean: z.number(),
        median: z.number(),
        stdDev: z.number(),
        min: z.number(),
        max: z.number(),
      }),
    }
  })
  async calculateStats(input: { data: number[] }) {
    const sorted = [...input.data].sort((a, b) => a - b);
    const sum = input.data.reduce((a, b) => a + b, 0);
    const mean = sum / input.data.length;

    const median = sorted.length % 2 === 0
      ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2
      : sorted[Math.floor(sorted.length / 2)];

    const variance = input.data.reduce((acc, val) =>
      acc + Math.pow(val - mean, 2), 0) / input.data.length;
    const stdDev = Math.sqrt(variance);

    return {
      mean,
      median,
      stdDev,
      min: sorted[0],
      max: sorted[sorted.length - 1],
    };
  }
}

export const DataAnalystAgent = new DataAnalystAgentClass();

3. Email Automation Agent

import { BaseAgentClass, tools } from "sonamu/ai";
import { z } from "zod";

class EmailAutomationAgentClass extends BaseAgentClass<{
  userId: number;
}> {
  constructor() {
    super('EmailAutomationAgent');
  }

  @tools({
    description: "Searches for emails in the inbox",
    schema: {
      input: z.object({
        query: z.string().describe("Search query"),
        limit: z.number().default(10),
      }),
      output: z.array(z.object({
        id: z.number(),
        from: z.string(),
        subject: z.string(),
        preview: z.string(),
        receivedAt: z.string(),
      })),
    }
  })
  async searchEmails(input: { query: string; limit: number }) {
    const emails = await EmailModel.findMany({
      wq: [
        ['user_id', this.store?.userId],
        ['subject', 'LIKE', `%${input.query}%`],
      ],
      num: input.limit,
      order: [['received_at', 'DESC']],
    });

    return emails.data.map(email => ({
      id: email.id,
      from: email.from_address,
      subject: email.subject,
      preview: email.body.substring(0, 100),
      receivedAt: email.received_at.toISOString(),
    }));
  }

  @tools({
    description: "Moves an email to a specific folder",
    schema: {
      input: z.object({
        emailId: z.number(),
        folder: z.enum(['inbox', 'archive', 'trash', 'spam']),
      }),
      output: z.object({
        success: z.boolean(),
      }),
    }
  })
  async moveEmail(input: { emailId: number; folder: string }) {
    await EmailModel.updateOne(['id', input.emailId], {
      folder: input.folder,
    });

    return { success: true };
  }

  @tools({
    description: "Adds labels to an email",
    schema: {
      input: z.object({
        emailId: z.number(),
        labels: z.array(z.string()),
      }),
      output: z.object({
        success: z.boolean(),
      }),
    }
  })
  async addLabels(input: { emailId: number; labels: string[] }) {
    const email = await EmailModel.findById(input.emailId);
    const currentLabels = email.labels || [];

    await EmailModel.updateOne(['id', input.emailId], {
      labels: [...currentLabels, ...input.labels],
    });

    return { success: true };
  }

  @tools({
    description: "Sends an email",
    schema: {
      input: z.object({
        to: z.string().email(),
        subject: z.string(),
        body: z.string(),
      }),
      output: z.object({
        messageId: z.string(),
        sentAt: z.string(),
      }),
    },
    needsApproval: true,  // Approval before sending
  })
  async sendEmail(input: {
    to: string;
    subject: string;
    body: string;
  }) {
    // Email sending logic
    const result = await sendEmailService(input);

    return {
      messageId: result.messageId,
      sentAt: new Date().toISOString(),
    };
  }
}

export const EmailAutomationAgent = new EmailAutomationAgentClass();

Using Agents

use() Method

This method executes the agent.
import { HotelBookingAgent } from "./agents/hotel-booking";
import { openai } from "@ai-sdk/openai";

const result = await HotelBookingAgent.use(
  {
    model: openai('gpt-4o'),
    instructions: "You are a hotel booking assistant.",
    toolChoice: 'auto',
  },
  { userId: 123, locale: 'en' },  // Initial store values
  async (agent) => {
    // Execute agent
    const response = await agent.generateText({
      prompt: "Find me a hotel in Seoul for 3 nights starting tomorrow",
    });

    return response.text;
  }
);

Usage in API

import { BaseModel, api } from "sonamu";
import { HotelBookingAgent } from "./agents/hotel-booking";
import { openai } from "@ai-sdk/openai";

class ChatModelClass extends BaseModel {
  @api({ httpMethod: 'POST' })
  async chat(message: string, ctx: Context) {
    const result = await HotelBookingAgent.use(
      {
        model: openai('gpt-4o'),
        instructions: "You are a hotel booking assistant.",
        toolChoice: 'auto',
      },
      { userId: ctx.user.id, locale: ctx.locale || 'en' },
      async (agent) => {
        const response = await agent.generateText({
          prompt: message,
        });

        return response;
      }
    );

    return {
      text: result.text,
      toolCalls: result.toolCalls,
    };
  }
}

Store (State Management)

Manages the agent’s state.

Definition

class MyAgentClass extends BaseAgentClass<{
  userId: number;
  sessionId: string;
  preferences: {
    language: string;
    timezone: string;
  };
}> {
  // ...
}

Usage

@tools({
  description: "Retrieves user profile",
  schema: {
    input: z.object({}),
    output: z.object({
      userId: z.number(),
      name: z.string(),
    }),
  }
})
async getUserProfile() {
  // Access store
  const userId = this.store?.userId;
  const language = this.store?.preferences.language;

  const user = await UserModel.findById(userId);

  return {
    userId: user.id,
    name: user.name,
  };
}

Precautions

Important considerations when writing agents:
  1. Tool Description Clarity: Write detailed descriptions that the LLM can understand
    // ❌ Unclear
    description: "Order lookup"
    
    // ✅ Clear
    description: "Takes an order ID and returns detailed order information (status, items, price)"
    
  2. Detailed Input Schema: Use describe()
    input: z.object({
      orderId: z.number().describe("Unique ID of the order to retrieve"),
      includeItems: z.boolean().describe("Whether to include the list of order items"),
    })
    
  3. Require Approval for Dangerous Operations: needsApproval
    @tools({
      description: "Cancel order",
      schema: { /* ... */ },
      needsApproval: true,  // Required!
    })
    
  4. Error Handling: Clear error messages
    async getOrder(input: { orderId: number }) {
      const order = await OrderModel.findById(input.orderId);
      if (!order) {
        throw new Error(`Order ID ${input.orderId} not found`);
      }
      return order;
    }
    
  5. Check for Undefined When Accessing Store
    const userId = this.store?.userId;
    if (!userId) {
      throw new Error('User information not available');
    }
    
  6. Avoid Tool Name Conflicts: Use clear names
    @tools({
      name: 'hotel_search',  // Clear name
      // ...
    })
    

Next Steps