/** * 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) { 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 ) { 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) { 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; 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", }, }); }