๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu๋Š” BaseAgentClass๋ฅผ ์ƒ์†ํ•˜์—ฌ ์ปค์Šคํ…€ AI ์—์ด์ „ํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. @tools ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋กœ ๋„๊ตฌ(tools)๋ฅผ ์ •์˜ํ•˜๋ฉด LLM์ด ์ƒํ™ฉ์— ๋งž๊ฒŒ ๋„๊ตฌ๋ฅผ ์„ ํƒํ•˜์—ฌ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค.

BaseAgentClass

AI ์—์ด์ „ํŠธ์˜ ๋ฒ ์ด์Šค ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ๊ตฌ์กฐ

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: "์ฃผ๋ฌธ ์ •๋ณด๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค",
    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 }) {
    // ์ฃผ๋ฌธ ์กฐํšŒ ๋กœ์ง
    return {
      orderId: input.orderId,
      status: 'shipped',
      items: ['Product A', 'Product B'],
    };
  }
}

export const CustomerSupportAgent = new CustomerSupportAgentClass();

@tools ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ

๋„๊ตฌ๋ฅผ ์ •์˜ํ•˜๋Š” ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ์ž…๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•

@tools({
  description: "๋„๊ตฌ ์„ค๋ช…",
  schema: {
    input: z.object({
      // ์ž…๋ ฅ ์Šคํ‚ค๋งˆ
    }),
    output: z.object({
      // ์ถœ๋ ฅ ์Šคํ‚ค๋งˆ (์„ ํƒ)
    }),
  }
})
async toolMethod(input: InputType) {
  // ๋„๊ตฌ ๋กœ์ง
  return output;
}

์˜ต์…˜

๋„๊ตฌ ์„ค๋ช… (ํ•„์ˆ˜)
@tools({
  description: "์‚ฌ์šฉ์ž์˜ ์ฃผ๋ฌธ ๋‚ด์—ญ์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. ์ฃผ๋ฌธ ID๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ์ฃผ๋ฌธ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.",
  schema: { /* ... */ }
})
์ค‘์š”: LLM์ด ์ด ์„ค๋ช…์„ ์ฝ๊ณ  ๋„๊ตฌ ์‚ฌ์šฉ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

์‹ค์ „ ์˜ˆ์ œ

1. ํ˜ธํ…” ์˜ˆ์•ฝ ์—์ด์ „ํŠธ

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

class HotelBookingAgentClass extends BaseAgentClass<{
  userId: number;
  locale: string;
}> {
  constructor() {
    super('HotelBookingAgent');
  }
  
  @tools({
    description: "๋„์‹œ์™€ ๋‚ ์งœ๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ˜ธํ…”์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค",
    schema: {
      input: z.object({
        city: z.string().describe("๋„์‹œ ์ด๋ฆ„"),
        checkIn: z.string().describe("์ฒดํฌ์ธ ๋‚ ์งœ (YYYY-MM-DD)"),
        checkOut: z.string().describe("์ฒดํฌ์•„์›ƒ ๋‚ ์งœ (YYYY-MM-DD)"),
        guests: z.number().describe("ํˆฌ์ˆ™๊ฐ ์ˆ˜"),
      }),
      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;
  }) {
    // 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: "ํ˜ธํ…”์„ ์˜ˆ์•ฝํ•ฉ๋‹ˆ๋‹ค",
    schema: {
      input: z.object({
        hotelId: z.number().describe("ํ˜ธํ…” ID"),
        checkIn: z.string(),
        checkOut: z.string(),
        guests: z.number(),
      }),
      output: z.object({
        bookingId: z.number(),
        confirmationNumber: z.string(),
        totalPrice: z.number(),
      }),
    },
    needsApproval: true,  // ์˜ˆ์•ฝ ์ „ ์‚ฌ์šฉ์ž ์Šน์ธ
  })
  async bookHotel(input: {
    hotelId: number;
    checkIn: string;
    checkOut: string;
    guests: number;
  }) {
    const userId = this.store?.userId;
    
    // ์˜ˆ์•ฝ ์ƒ์„ฑ
    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: "์˜ˆ์•ฝ์„ ์ทจ์†Œํ•ฉ๋‹ˆ๋‹ค",
    schema: {
      input: z.object({
        bookingId: z.number().describe("์˜ˆ์•ฝ ID"),
      }),
      output: z.object({
        success: z.boolean(),
        refundAmount: z.number(),
      }),
    },
    needsApproval: true,  // ์ทจ์†Œ ์ „ ์‚ฌ์šฉ์ž ์Šน์ธ
  })
  async cancelBooking(input: { bookingId: number }) {
    const booking = await BookingModel.findById(input.bookingId);
    
    // ์ทจ์†Œ ์ฒ˜๋ฆฌ
    await BookingModel.updateOne(['id', booking.id], {
      status: 'cancelled',
    });
    
    // ํ™˜๋ถˆ ์ฒ˜๋ฆฌ
    const refundAmount = booking.total_price * 0.9;  // 10% ์ˆ˜์ˆ˜๋ฃŒ
    
    return {
      success: true,
      refundAmount,
    };
  }
}

export const HotelBookingAgent = new HotelBookingAgentClass();

2. ๋ฐ์ดํ„ฐ ๋ถ„์„ ์—์ด์ „ํŠธ

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

class DataAnalystAgentClass extends BaseAgentClass<{
  sessionId: string;
}> {
  constructor() {
    super('DataAnalystAgent');
  }
  
  @tools({
    description: "SQL ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค",
    schema: {
      input: z.object({
        query: z.string().describe("์‹คํ–‰ํ•  SQL ์ฟผ๋ฆฌ"),
      }),
      output: z.object({
        rows: z.array(z.record(z.any())),
        rowCount: z.number(),
      }),
    }
  })
  async executeQuery(input: { query: string }) {
    // ์•ˆ์ „์„ฑ ๊ฒ€์ฆ (SELECT๋งŒ ํ—ˆ์šฉ)
    if (!/^SELECT/i.test(input.query.trim())) {
      throw new Error('SELECT ์ฟผ๋ฆฌ๋งŒ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค');
    }
    
    const result = await DB.query(input.query);
    
    return {
      rows: result.rows,
      rowCount: result.rowCount,
    };
  }
  
  @tools({
    description: "๋ฐ์ดํ„ฐ๋ฅผ ์‹œ๊ฐํ™”ํ•˜๊ธฐ ์œ„ํ•œ ์ฐจํŠธ ์„ค์ •์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค",
    schema: {
      input: z.object({
        data: z.array(z.record(z.any())),
        xAxis: z.string().describe("X์ถ• ์ปฌ๋Ÿผ๋ช…"),
        yAxis: z.string().describe("Y์ถ• ์ปฌ๋Ÿผ๋ช…"),
        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()}`;
    
    // ์ฐจํŠธ ์„ค์ • ์ƒ์„ฑ
    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: "ํ†ต๊ณ„ ์š”์•ฝ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค (ํ‰๊ท , ์ค‘์•™๊ฐ’, ํ‘œ์ค€ํŽธ์ฐจ ๋“ฑ)",
    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. ์ด๋ฉ”์ผ ์ž๋™ํ™” ์—์ด์ „ํŠธ

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

class EmailAutomationAgentClass extends BaseAgentClass<{
  userId: number;
}> {
  constructor() {
    super('EmailAutomationAgent');
  }
  
  @tools({
    description: "๋ฐ›์€ ํŽธ์ง€ํ•จ์—์„œ ์ด๋ฉ”์ผ์„ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค",
    schema: {
      input: z.object({
        query: z.string().describe("๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ"),
        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: "์ด๋ฉ”์ผ์„ ํŠน์ • ํด๋”๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค",
    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: "์ด๋ฉ”์ผ์— ๋ผ๋ฒจ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค",
    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: "์ด๋ฉ”์ผ์„ ๋ณด๋ƒ…๋‹ˆ๋‹ค",
    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,  // ์ „์†ก ์ „ ์Šน์ธ
  })
  async sendEmail(input: {
    to: string;
    subject: string;
    body: string;
  }) {
    // ์ด๋ฉ”์ผ ์ „์†ก ๋กœ์ง
    const result = await sendEmailService(input);
    
    return {
      messageId: result.messageId,
      sentAt: new Date().toISOString(),
    };
  }
}

export const EmailAutomationAgent = new EmailAutomationAgentClass();

์—์ด์ „ํŠธ ์‚ฌ์šฉํ•˜๊ธฐ

use() ๋ฉ”์„œ๋“œ

์—์ด์ „ํŠธ๋ฅผ ์‹คํ–‰ํ•˜๋Š” ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค.
import { HotelBookingAgent } from "./agents/hotel-booking";
import { openai } from "@ai-sdk/openai";

const result = await HotelBookingAgent.use(
  {
    model: openai('gpt-4o'),
    instructions: "๋‹น์‹ ์€ ํ˜ธํ…” ์˜ˆ์•ฝ ๋„์šฐ๋ฏธ์ž…๋‹ˆ๋‹ค.",
    toolChoice: 'auto',
  },
  { userId: 123, locale: 'ko' },  // store ์ดˆ๊ธฐ๊ฐ’
  async (agent) => {
    // ์—์ด์ „ํŠธ ์‹คํ–‰
    const response = await agent.generateText({
      prompt: "์„œ์šธ์—์„œ ๋‚ด์ผ๋ถ€ํ„ฐ 3์ผ๊ฐ„ ๋ฌต์„ ํ˜ธํ…”์„ ์ฐพ์•„์ค˜",
    });
    
    return response.text;
  }
);

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: "๋‹น์‹ ์€ ํ˜ธํ…” ์˜ˆ์•ฝ ๋„์šฐ๋ฏธ์ž…๋‹ˆ๋‹ค.",
        toolChoice: 'auto',
      },
      { userId: ctx.user.id, locale: ctx.locale || 'ko' },
      async (agent) => {
        const response = await agent.generateText({
          prompt: message,
        });
        
        return response;
      }
    );
    
    return {
      text: result.text,
      toolCalls: result.toolCalls,
    };
  }
}

Store (์ƒํƒœ ๊ด€๋ฆฌ)

์—์ด์ „ํŠธ์˜ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

์ •์˜

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

์‚ฌ์šฉ

@tools({
  description: "์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์กฐํšŒ",
  schema: {
    input: z.object({}),
    output: z.object({
      userId: z.number(),
      name: z.string(),
    }),
  }
})
async getUserProfile() {
  // 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,
  };
}

์ฃผ์˜์‚ฌํ•ญ

Agent ์ž‘์„ฑ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. ๋„๊ตฌ ์„ค๋ช… ๋ช…ํ™•์„ฑ: LLM์ด ์ดํ•ดํ•  ์ˆ˜ ์žˆ๋„๋ก ์ž์„ธํžˆ ์ž‘์„ฑ
    // โŒ ๋ถˆ๋ช…ํ™•
    description: "์ฃผ๋ฌธ ์กฐํšŒ"
    
    // โœ… ๋ช…ํ™•
    description: "์ฃผ๋ฌธ ID๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ํ•ด๋‹น ์ฃผ๋ฌธ์˜ ์ƒ์„ธ ์ •๋ณด(์ƒํƒœ, ์•„์ดํ…œ, ๊ฐ€๊ฒฉ)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค"
    
  2. ์ž…๋ ฅ ์Šคํ‚ค๋งˆ ์ƒ์„ธ ์„ค๋ช…: describe() ์‚ฌ์šฉ
    input: z.object({
      orderId: z.number().describe("์กฐํšŒํ•  ์ฃผ๋ฌธ์˜ ๊ณ ์œ  ID"),
      includeItems: z.boolean().describe("์ฃผ๋ฌธ ์•„์ดํ…œ ๋ชฉ๋ก ํฌํ•จ ์—ฌ๋ถ€"),
    })
    
  3. ์œ„ํ—˜ํ•œ ์ž‘์—…์€ ์Šน์ธ ํ•„์š”: needsApproval
    @tools({
      description: "์ฃผ๋ฌธ ์ทจ์†Œ",
      schema: { /* ... */ },
      needsApproval: true,  // ํ•„์ˆ˜!
    })
    
  4. ์—๋Ÿฌ ์ฒ˜๋ฆฌ: ๋ช…ํ™•ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
    async getOrder(input: { orderId: number }) {
      const order = await OrderModel.findById(input.orderId);
      if (!order) {
        throw new Error(`์ฃผ๋ฌธ ID ${input.orderId}๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค`);
      }
      return order;
    }
    
  5. store ์ ‘๊ทผ ์‹œ undefined ์ฒดํฌ
    const userId = this.store?.userId;
    if (!userId) {
      throw new Error('์‚ฌ์šฉ์ž ์ •๋ณด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค');
    }
    
  6. ๋„๊ตฌ ์ด๋ฆ„ ์ถฉ๋Œ ๋ฐฉ์ง€: ๋ช…ํ™•ํ•œ ์ด๋ฆ„ ์‚ฌ์šฉ
    @tools({
      name: 'hotel_search',  // ๋ช…ํ™•ํ•œ ์ด๋ฆ„
      // ...
    })
    

๋‹ค์Œ ๋‹จ๊ณ„