GenAINextVercel AI SDKLangchainGoogle Gemini

Function Calling y Herramientas (Tools). Parte 2

Antonio Pérez
2025-09-07
8
Function Calling y Herramientas (Tools). Parte 2

Preparando el terreno: Una base de datos y API simuladas

Podrás encontrar el código de este artículo en: https://github.com/aperezl/ai-fullstack-serie/tree/tools-2

Para demostrar el function calling sin depender de servicios externos, crearemos una pequeña base de datos en memoria y funciones que actúen como si fueran los controladores de una API REST que interactúa con ella.

Paso 1: Crear el módulo de la "base de datos"

Crea un nuevo archivo src/lib/db/mock-db.ts. Este archivo simulará nuestra capa de datos.

typescriptsrc/lib/db/mock-db.ts

// Tipos de datos para nuestra base de datos simulada
export interface User {
  id: string;
  name: string;
  email: string;
  tickets: string[];
}

export interface Ticket {
  id: string;
  subject: string;
  status: 'open' | 'closed' | 'in-progress';
  userEmail: string;
}

// Nuestra "base de datos" en memoria
const users: User[] = [
  { id: 'usr_1', name: 'Antonio Perez', email: 'antonio@unuko.com', tickets: ['tkt_1', 'tkt_3'] },
  { id: 'usr_2', name: 'Ada Lovelace', email: 'ada@unuko.com', tickets: ['tkt_2'] },
];

const tickets: Ticket[] = [
  { id: 'tkt_1', subject: 'Empezar un blog', status: 'closed', userEmail: 'antonio@unuko.com' },
  { id: 'tkt_2', subject: 'Consulta sobre el Motor Analítico', status: 'open', userEmail: 'ada@unuko.com' },
  { id: 'tkt_3', subject: 'Terminar el capítulo 5 de la serie AI fullstack', status: 'in-progress', userEmail: 'antonio@unuko.com' },
];

// Funciones que simulan el acceso a la base de datos
export const db = {
  findUserByEmail: async (email: string): Promise<User | undefined> => {
    console.log(`[DB] Buscando usuario por email: ${email}`);
    return users.find(user => user.email === email);
  },
  findTicketsByUserEmail: async (email: string): Promise<Ticket[]> => {
    console.log(`[DB] Buscando tickets para el email: ${email}`);
    return tickets.filter(ticket => ticket.userEmail === email);
  },
  createTicket: async (subject: string, userEmail: string): Promise<Ticket> => {
    console.log(`[DB] Creando ticket para ${userEmail} con asunto: ${subject}`);
    const newTicket: Ticket = {
      id: `tkt_${tickets.length + 1}`,
      subject,
      status: 'open',
      userEmail,
    };
    tickets.push(newTicket);
    // Añadimos el ticket al usuario correspondiente
    const user = await db.findUserByEmail(userEmail);
    user?.tickets.push(newTicket.id);
    return newTicket;
  },
};

Esta simulación es suficiente para nuestros propósitos y nos permite centrarnos en la lógica de las herramientas sin preocuparnos por la configuración de una base de datos real.

Ejercicio: Definiendo las herramientas del agente

Ahora, construiremos el puente entre nuestro código (mock-db.ts) y el LLM. Crearemos un archivo que defina las herramientas que el agente podrá utilizar.

Paso 1: Crear el Archivo de Herramientas

Crea un nuevo archivo en src/lib/ai/tools.ts.

typescriptsrc/lib/ai/tools.ts
import { tool } from 'ai';
import { z } from 'zod';
import { db } from '../db/mock-db';

export const tools = {
  // Herramienta de solo lectura
  findUserTickets: tool({
    description: 'Busca los tickets de soporte para un usuario específico usando su dirección de correo electrónico.',
    inputSchema: z.object({
      email: z.string().email({ message: "Se requiere un correo electrónico válido." }),
    }),
    execute: async ({ email }) => {
      try {
        const user = await db.findUserByEmail(email);
        if (!user) {
          return { error: 'usuario_no_encontrado', message: `El usuario con el email ${email} no existe.` };
        }
        const tickets = await db.findTicketsByUserEmail(email);
        if (tickets.length === 0) {
          return { message: `El usuario ${email} no tiene tickets.` };
        }
        return { tickets };
      } catch (error) {
        return { error: 'error_inesperado', message: 'Ha ocurrido un error al buscar los tickets.' };
      }
    },
  }),

  // Herramienta de escritura
  createSupportTicket: tool({
    description: 'Crea un nuevo ticket de soporte para un usuario. Devuelve el ticket recién creado.',
    inputSchema: z.object({
      userEmail: z.string().email({ message: "Se requiere un correo electrónico válido para asociar el ticket." }),
      subject: z.string().min(10, { message: "El asunto debe tener al menos 10 caracteres." }),
    }),
    execute: async ({ userEmail, subject }) => {
      try {
        const user = await db.findUserByEmail(userEmail);
        if (!user) {
          return { error: 'usuario_no_encontrado', message: `No se puede crear un ticket para el email ${userEmail} porque el usuario no existe.` };
        }
        const newTicket = await db.createTicket(subject, userEmail);
        return { newTicket };
      } catch (error) {
        return { error: 'creacion_fallida', message: 'No se pudo crear el ticket de soporte.' };
      }
    },
  }),
};

Análisis de la definición de herramientas:

  • Descripciones claras: Las descripciones son para el LLM. Deben ser concisas y explicar exactamente qué hace la herramienta. "Busca los tickets de soporte..." es una instrucción mucho mejor que "busca_tickets".
  • Esquemas de entrada rigurosos: Usamos z.string().email() y z.string().min(10) para definir contratos estrictos. Si el LLM intenta llamar a la herramienta con un email mal formado o un asunto demasiado corto, la validación de Zod fallará antes de que se ejecute nuestro código, añadiendo una capa de seguridad.
  • Manejo de errores dentro de execute: La lógica de execute no asume que todo saldrá bien. Comprueba si el usuario existe y devuelve objetos de error estructurados. Esto es crucial: el resultado de la herramienta (sea éxito o error) se devolverá al LLM, que podrá usar esa información para formular su siguiente respuesta. Por ejemplo, si el usuario no existe, el LLM podrá informar al usuario final de manera inteligente.

Actualizando el backend del chat para usar herramientas

Ahora, integramos estas herramientas en nuestro endpoint de chat. Copiaremos la página /rag y crearemos la API src/app/api/tools/route.ts que creamos para el asistente de Aaron Swartz, transformándolo en un agente de soporte.

Paso 1: Crear el Endpoint `src/app/api/tools/route.ts`

Creamos un nuevo fichero donde reemplazaremos la lógica de nuestro anterior RAG con la lógica de herramientas. AI SDK hace que este cambio sea sorprendentemente simple.

typescriptapp/api/tools/route.ts
import { google } from '@ai-sdk/google';
import { streamText, convertToModelMessages, UIMessage, stepCountIs } from 'ai';
import { tools } from '@/lib/ai/tools'; // Importamos nuestras nuevas herramientas

export const maxDuration = 30;

export async function POST(req: Request) {
  try {
    const { messages }: { messages: UIMessage[] } = await req.json();

    const systemPrompt = `
      Eres un asistente de soporte técnico para una plataforma digital.
      Tu nombre es "ChipiBot". Eres amable, extremadamente servicial y eficiente.
      Utiliza las herramientas disponibles para responder a las preguntas del usuario y realizar tareas.
      Cuando busques información de un usuario, siempre dirígete a él por su nombre si lo encuentras.
      El correo del usuario actual es "antonio@demandprogress.org". Solo puedes operar sobre este usuario a menos que se te proporcione otro correo explícitamente.
    `;

    const result = await streamText({
      model: google('gemini-2.5-flash'),
      system: systemPrompt,
      messages: convertToModelMessages(messages),
      tools,
      stopWhen: stepCountIs(5),

    });

    return result.toUIMessageStreamResponse();

  } catch (error) {
    console.error("Error en la API de chat con herramientas:", error);
    return new Response("Un error inesperado ha ocurrido.", { status: 500 });
  }
}

Análisis del backend actualizado:

Simplicidad: ¡Observa qué poco ha cambiado! Hemos quitado la lógica de RAG (findRelevantChunks) y hemos añadido el objeto tools a la llamada de streamText.

El AI SDK hace el trabajo pesado: El SDK se encarga del ciclo completo que discutimos en la teoría:

  1. Envía el prompt y la descripción de las herramientas al LLM.
  2. Recibe la intención de llamada a la herramienta del LLM.
  3. Valida los argumentos.
  4. Llama a nuestra función execute correspondiente.
  5. Toma el resultado y lo vuelve a enviar al LLM.
  6. Finalmente, streamea la respuesta textual del LLM de vuelta al cliente.

Contexto en el prompt: Hemos añadido una frase clave al systemPrompt: El correo del usuario actual es "antonio@unuko.com". Esto le da al LLM una pieza de información crucial para que pueda usar las herramientas de forma autónoma sin tener que preguntar primero por el email del usuario. Este es un ejemplo de cómo inyectar contexto de sesión en el agente.

Paso 2: Crear la página `src/app/tools/page.tsx`

Nuestra página de tools sigue la misma estructura que el resto de páginas, donde definimos los Avatares del asistente y usuario y cargamos el componente con su correspondiente endpoint.

tsxsrc/app/tools/page.tsx
import { Chatbot } from "@/components/ui/Chatbot/Chatbot";
import { BotMessageSquare, UserIcon } from 'lucide-react';

// Avatares (podrían estar en un archivo compartido)
const AssistantAvatar = () => (
  <div className="w-8 h-8 bg-green-500 rounded-full flex items-center justify-center flex-shrink-0">
    <BotMessageSquare className="text-white w-5 h-5" />
  </div>
);

const UserAvatar = () => (
  <div className="w-8 h-8 bg-slate-500 rounded-full flex items-center justify-center flex-shrink-0">
    <UserIcon className="text-white w-5 h-5" />
  </div>
);

export default function PdfChatPage() {
  return (
    <Chatbot
      apiEndpoint="/api/tools"
      assistantName="Gestión de Tickets"
      assistantDescription="Gestiona tus tickets desde este asistente."
      assistantAvatar={<AssistantAvatar />}
      userAvatar={<UserAvatar />}
      initialMessageTitle="Hola, ¿en qué puedo ayudarte?"
      initialMessageDescription="Tengo información sobre el estado de todos tus tickets."
      inputPlaceholder="Escribe tu consulta sobre los tickets..."
      fileSupport={false}
    />
  );
}

Conclusión de la Práctica

Hemos construido con éxito un agente funcional. Nuestro backend ahora está equipado con herramientas que le permiten interactuar con una capa de datos (aunque sea simulada). La separación entre la definición de la herramienta (src/lib/ai/tools.ts), la lógica de negocio (src/lib/db/mock-db.ts) y la orquestación de la IA (src/app/api/tools/route.ts) es un patrón arquitectónico limpio y escalable.

En el ejemplo web completo, veremos a este agente en acción, respondiendo preguntas que requieren búsqueda de datos y realizando acciones que modifican el estado de nuestro sistema.

Podrás encontrar el código de este artículo en: https://github.com/aperezl/ai-fullstack-serie/tree/tools-2