BaseAgentClass
This is the base class for AI agents.Basic Structure
Copy
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
Copy
@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
- description
- schema
- name
- needsApproval
Tool Description (required)Important: The LLM reads this description to decide whether to use the tool.
Copy
@tools({
description: "Retrieves user order history. Takes an order ID and returns order information.",
schema: { /* ... */ }
})
Input/Output Schema (required)input: Required
output: Optional (helps with type inference)
Copy
@tools({
description: "Order lookup",
schema: {
input: z.object({
orderId: z.number().describe("Order ID"),
}),
output: z.object({
status: z.string(),
total: z.number(),
}),
}
})
Custom Name (optional)
Copy
@tools({
name: 'order_lookup', // Default: method name
description: "Order lookup",
schema: { /* ... */ }
})
Requires Approval (optional)Use Case: Dangerous operations like data deletion or payments
Copy
@tools({
description: "Cancel order (dangerous operation)",
schema: { /* ... */ },
needsApproval: true, // Requires user approval
})
Practical Examples
1. Hotel Booking Agent
Copy
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
Copy
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
Copy
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.Copy
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
Copy
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
Copy
class MyAgentClass extends BaseAgentClass<{
userId: number;
sessionId: string;
preferences: {
language: string;
timezone: string;
};
}> {
// ...
}
Usage
Copy
@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:
-
Tool Description Clarity: Write detailed descriptions that the LLM can understand
Copy
// ❌ Unclear description: "Order lookup" // ✅ Clear description: "Takes an order ID and returns detailed order information (status, items, price)" -
Detailed Input Schema: Use describe()
Copy
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"), }) -
Require Approval for Dangerous Operations: needsApproval
Copy
@tools({ description: "Cancel order", schema: { /* ... */ }, needsApproval: true, // Required! }) -
Error Handling: Clear error messages
Copy
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; } -
Check for Undefined When Accessing Store
Copy
const userId = this.store?.userId; if (!userId) { throw new Error('User information not available'); } -
Avoid Tool Name Conflicts: Use clear names
Copy
@tools({ name: 'hotel_search', // Clear name // ... })