> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Creating Agents

> Building custom AI agents with BaseAgentClass

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

```typescript theme={null}
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

```typescript theme={null}
@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

<Tabs>
  <Tab title="description" icon="book">
    **Tool Description** (required)

    ```typescript theme={null}
    @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.
  </Tab>

  <Tab title="schema" icon="code">
    **Input/Output Schema** (required)

    ```typescript theme={null}
    @tools({
      description: "Order lookup",
      schema: {
        input: z.object({
          orderId: z.number().describe("Order ID"),
        }),
        output: z.object({
          status: z.string(),
          total: z.number(),
        }),
      }
    })
    ```

    **input**: Required
    **output**: Optional (helps with type inference)
  </Tab>

  <Tab title="name" icon="tag">
    **Custom Name** (optional)

    ```typescript theme={null}
    @tools({
      name: 'order_lookup',  // Default: method name
      description: "Order lookup",
      schema: { /* ... */ }
    })
    ```
  </Tab>

  <Tab title="needsApproval" icon="check">
    **Requires Approval** (optional)

    ```typescript theme={null}
    @tools({
      description: "Cancel order (dangerous operation)",
      schema: { /* ... */ },
      needsApproval: true,  // Requires user approval
    })
    ```

    **Use Case**: Dangerous operations like data deletion or payments
  </Tab>
</Tabs>

## Practical Examples

### 1. Hotel Booking Agent

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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.

```typescript theme={null}
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

```typescript theme={null}
import { BaseModelClass, api } from "sonamu";
import { HotelBookingAgent } from "./agents/hotel-booking";
import { openai } from "@ai-sdk/openai";

class ChatModelClass extends BaseModelClass {
  @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

```typescript theme={null}
class MyAgentClass extends BaseAgentClass<{
  userId: number;
  sessionId: string;
  preferences: {
    language: string;
    timezone: string;
  };
}> {
  // ...
}
```

### Usage

```typescript theme={null}
@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

<Warning>
  **Important considerations when writing agents**:

  1. **Tool Description Clarity**: Write detailed descriptions that the LLM can understand
     ```typescript theme={null}
     // ❌ Unclear
     description: "Order lookup"

     // ✅ Clear
     description: "Takes an order ID and returns detailed order information (status, items, price)"
     ```

  2. **Detailed Input Schema**: Use describe()
     ```typescript theme={null}
     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
     ```typescript theme={null}
     @tools({
       description: "Cancel order",
       schema: { /* ... */ },
       needsApproval: true,  // Required!
     })
     ```

  4. **Error Handling**: Clear error messages
     ```typescript theme={null}
     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**
     ```typescript theme={null}
     const userId = this.store?.userId;
     if (!userId) {
       throw new Error('User information not available');
     }
     ```

  6. **Avoid Tool Name Conflicts**: Use clear names
     ```typescript theme={null}
     @tools({
       name: 'hotel_search',  // Clear name
       // ...
     })
     ```
</Warning>

## Next Steps

<CardGroup cols={2}>
  <Card title="Agent Configuration" icon="gear" href="/en/advanced-features/ai-agents/agent-configuration">
    Configure agent basics
  </Card>

  <Card title="Using the AI SDK" icon="code" href="/en/advanced-features/ai-agents/using-ai-sdk">
    Leverage the Vercel AI SDK
  </Card>
</CardGroup>
