Files
gh-jezweb-claude-skills-ski…/templates/nextjs/tool-calling-route.ts
2025-11-30 08:25:37 +08:00

326 lines
8.9 KiB
TypeScript

/**
* Next.js API Route with Tool Calling
*
* File: app/api/chat-with-tools/route.ts
*
* Demonstrates tool calling integration with TheSys C1.
* Includes:
* - Zod schema definitions
* - Web search tool (Tavily)
* - Product inventory tool
* - Order creation tool
* - Streaming with tool execution
*/
import { NextRequest, NextResponse } from "next/server";
import OpenAI from "openai";
import { z } from "zod";
import zodToJsonSchema from "zod-to-json-schema";
import { transformStream } from "@crayonai/stream";
import { TavilySearchAPIClient } from "@tavily/core";
const client = new OpenAI({
baseURL: "https://api.thesys.dev/v1/embed",
apiKey: process.env.THESYS_API_KEY,
});
const tavily = new TavilySearchAPIClient({
apiKey: process.env.TAVILY_API_KEY || "",
});
// ============================================================================
// Tool Schemas
// ============================================================================
const webSearchSchema = z.object({
query: z.string().describe("The search query"),
max_results: z
.number()
.int()
.min(1)
.max(10)
.default(5)
.describe("Maximum number of results"),
});
const productLookupSchema = z.object({
product_type: z
.enum(["gloves", "hat", "scarf", "all"])
.optional()
.describe("Type of product to lookup, or 'all' for everything"),
});
const orderItemSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("gloves"),
size: z.enum(["S", "M", "L", "XL"]),
color: z.string(),
quantity: z.number().int().min(1),
}),
z.object({
type: z.literal("hat"),
style: z.enum(["beanie", "baseball", "fedora"]),
color: z.string(),
quantity: z.number().int().min(1),
}),
z.object({
type: z.literal("scarf"),
length: z.enum(["short", "medium", "long"]),
material: z.enum(["wool", "cotton", "silk"]),
quantity: z.number().int().min(1),
}),
]);
const createOrderSchema = z.object({
customer_email: z.string().email().describe("Customer email address"),
items: z.array(orderItemSchema).min(1).describe("Items to order"),
});
// ============================================================================
// Tool Definitions
// ============================================================================
const webSearchTool = {
type: "function" as const,
function: {
name: "web_search",
description: "Search the web for current information using Tavily API",
parameters: zodToJsonSchema(webSearchSchema),
},
};
const productLookupTool = {
type: "function" as const,
function: {
name: "lookup_product",
description: "Look up products in inventory",
parameters: zodToJsonSchema(productLookupSchema),
},
};
const createOrderTool = {
type: "function" as const,
function: {
name: "create_order",
description: "Create a new product order",
parameters: zodToJsonSchema(createOrderSchema),
},
};
// ============================================================================
// Tool Execution Functions
// ============================================================================
async function executeWebSearch(args: z.infer<typeof webSearchSchema>) {
const validated = webSearchSchema.parse(args);
const results = await tavily.search(validated.query, {
maxResults: validated.max_results,
includeAnswer: true,
});
return {
query: validated.query,
answer: results.answer,
results: results.results.map((r) => ({
title: r.title,
url: r.url,
snippet: r.content,
})),
};
}
async function executeProductLookup(
args: z.infer<typeof productLookupSchema>
) {
const validated = productLookupSchema.parse(args);
// Mock inventory - replace with actual database query
const inventory = {
gloves: [
{ id: 1, size: "M", color: "blue", price: 29.99, stock: 15 },
{ id: 2, size: "L", color: "red", price: 29.99, stock: 8 },
],
hat: [
{ id: 3, style: "beanie", color: "black", price: 19.99, stock: 20 },
{ id: 4, style: "baseball", color: "navy", price: 24.99, stock: 12 },
],
scarf: [
{ id: 5, length: "medium", material: "wool", price: 34.99, stock: 10 },
],
};
if (validated.product_type && validated.product_type !== "all") {
return {
type: validated.product_type,
products: inventory[validated.product_type],
};
}
return { type: "all", inventory };
}
async function executeCreateOrder(args: z.infer<typeof createOrderSchema>) {
const validated = createOrderSchema.parse(args);
// Mock order creation - replace with actual database insert
const orderId = `ORD-${Date.now()}`;
// Simulate saving to database
console.log("Creating order:", {
orderId,
customer: validated.customer_email,
items: validated.items,
});
return {
success: true,
orderId,
customer_email: validated.customer_email,
items: validated.items,
total: validated.items.reduce(
(sum, item) => sum + item.quantity * 29.99,
0
), // Mock price
message: `Order ${orderId} created successfully`,
};
}
// ============================================================================
// API Route Handler
// ============================================================================
export async function POST(req: NextRequest) {
try {
const { prompt, previousC1Response } = await req.json();
if (!prompt || typeof prompt !== "string") {
return NextResponse.json(
{ error: "Invalid prompt" },
{ status: 400 }
);
}
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{
role: "system",
content: `You are a helpful shopping assistant with access to tools.
You can:
1. Search the web for product information
2. Look up products in our inventory
3. Create orders for customers
Always use tools when appropriate. Be friendly and helpful.`,
},
{ role: "user", content: prompt },
];
if (previousC1Response) {
messages.splice(1, 0, {
role: "assistant",
content: previousC1Response,
});
}
// Create streaming completion with tools
const llmStream = await client.beta.chat.completions.runTools({
model: "c1/anthropic/claude-sonnet-4/v-20250617",
messages,
stream: true,
tools: [webSearchTool, productLookupTool, createOrderTool],
toolChoice: "auto", // Let AI decide when to use tools
temperature: 0.7,
});
// Handle tool execution
llmStream.on("message", async (event) => {
if (event.tool_calls) {
for (const toolCall of event.tool_calls) {
try {
let result;
switch (toolCall.function.name) {
case "web_search":
const searchArgs = JSON.parse(toolCall.function.arguments);
result = await executeWebSearch(searchArgs);
break;
case "lookup_product":
const lookupArgs = JSON.parse(toolCall.function.arguments);
result = await executeProductLookup(lookupArgs);
break;
case "create_order":
const orderArgs = JSON.parse(toolCall.function.arguments);
result = await executeCreateOrder(orderArgs);
break;
default:
throw new Error(`Unknown tool: ${toolCall.function.name}`);
}
console.log(`Tool ${toolCall.function.name} executed:`, result);
// Tool results are automatically sent back to the LLM
// by the runTools method
} catch (error) {
console.error(`Tool execution error:`, error);
// Error will be sent back to LLM
}
}
}
});
// Transform stream to C1 format
const responseStream = transformStream(llmStream, (chunk) => {
return chunk.choices[0]?.delta?.content || "";
}) as ReadableStream<string>;
return new NextResponse(responseStream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache, no-transform",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
},
});
} catch (error) {
console.error("Chat API Error:", error);
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: "Validation error",
details: error.errors,
},
{ status: 400 }
);
}
if (error instanceof OpenAI.APIError) {
return NextResponse.json(
{
error: error.message,
type: error.type,
},
{ status: error.status || 500 }
);
}
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
export async function OPTIONS() {
return new NextResponse(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}