> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firecrawl.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Cómo crear un asistente de investigación con IA usando Firecrawl y el AI SDK

> Crea un asistente de investigación completo con IA con capacidades de web scraping y búsqueda

Crea un asistente de investigación completo con IA que pueda extraer datos de sitios web y buscar en la web para responder preguntas. El asistente decide automáticamente cuándo usar herramientas de web scraping o de búsqueda para recopilar información y luego ofrece respuestas completas basadas en los datos recopilados.

<video autoPlay muted loop playsInline src="https://mintcdn.com/firecrawl/2R2EKZFeF2zvZsk6/images/guides/cookbooks/ai-sdk-cookbook/firecrawl-ai-sdk-chatbot.mp4?fit=max&auto=format&n=2R2EKZFeF2zvZsk6&q=85&s=c4808ae7711fd41c18cfbbfba9ef1692" aria-label="Interfaz de chatbot de asistente de investigación con IA que muestra web scraping en tiempo real con Firecrawl y respuestas conversacionales generadas por OpenAI" data-path="images/guides/cookbooks/ai-sdk-cookbook/firecrawl-ai-sdk-chatbot.mp4" />

<div id="what-youll-build">
  ## Qué vas a construir
</div>

Una interfaz de chat con IA donde los usuarios pueden hacer preguntas sobre cualquier tema. El asistente decide automáticamente cuándo usar scraping web o herramientas de búsqueda para recopilar información y luego ofrece respuestas completas basadas en los datos obtenidos.

<div id="prerequisites">
  ## Requisitos previos
</div>

* Node.js 18 o posterior instalado
* Una clave de la API de OpenAI de [platform.openai.com](https://platform.openai.com)
* Una clave de la API de Firecrawl de [firecrawl.dev](https://firecrawl.dev)
* Conocimientos básicos de React y Next.js

<Steps>
  <Step title="Crear un nuevo proyecto de Next.js">
    Empieza creando una nueva aplicación de Next.js y navega al directorio del proyecto:

    ```bash theme={null}
    npx create-next-app@latest ai-sdk-firecrawl && cd ai-sdk-firecrawl
    ```

    Cuando se te solicite, selecciona las siguientes opciones:

    * TypeScript: Sí
    * ESLint: Sí
    * Tailwind CSS: Sí
    * App Router: Sí
    * Usar directorio `src/`: No
    * Alias de importación: Sí (@/\*)
  </Step>

  <Step title="Instalar las dependencias">
    ### Instalar paquetes del AI SDK

    El AI SDK es un conjunto de herramientas de TypeScript que proporciona una API unificada para trabajar con distintos proveedores de LLM:

    ```bash theme={null}
    npm i ai @ai-sdk/react zod
    ```

    Estos paquetes ofrecen:

    * `ai`: SDK principal con streaming, invocación de herramientas y manejo de respuestas
    * `@ai-sdk/react`: Hooks de React como `useChat` para crear interfaces de chat
    * `zod`: Validación de esquemas para entradas de herramientas

    Más información en [ai-sdk.dev/docs](https://ai-sdk.dev/docs).

    ### Instalar AI Elements

    AI Elements proporciona componentes de UI prediseñados para aplicaciones de IA. Ejecuta el siguiente comando para generar todos los componentes necesarios:

    ```bash theme={null}
    npx ai-elements@latest
    ```

    Esto configura AI Elements en tu proyecto, incluidos componentes de conversación, visualización de mensajes, campos de entrada de prompts y representaciones de llamadas a herramientas.

    Documentación: [ai-sdk.dev/elements/overview](https://ai-sdk.dev/elements/overview).

    ### Instalar el proveedor de OpenAI

    Instala el proveedor de OpenAI para conectarte con los modelos de OpenAI:

    ```bash theme={null}
    npm install @ai-sdk/openai
    ```
  </Step>

  <Step title="Construye la interfaz de chat del frontend">
    Crea la página principal en `app/page.tsx` y copia el código de la pestaña Code que aparece a continuación. Esta será la interfaz de chat donde los usuarios interactúan con el asistente de IA.

    <Tabs>
      <Tab title="Previsualización">
        <video autoPlay muted loop playsInline src="https://mintcdn.com/firecrawl/2R2EKZFeF2zvZsk6/images/guides/cookbooks/ai-sdk-cookbook/firecrawl-ai-sdk-chatbot.mp4?fit=max&auto=format&n=2R2EKZFeF2zvZsk6&q=85&s=c4808ae7711fd41c18cfbbfba9ef1692" aria-label="Interfaz de chatbot asistente de investigación en IA que muestra scraping web en tiempo real con Firecrawl y respuestas conversacionales impulsadas por OpenAI" data-path="images/guides/cookbooks/ai-sdk-cookbook/firecrawl-ai-sdk-chatbot.mp4" />
      </Tab>

      <Tab title="Código">
        ```typescript app/page.tsx theme={null}
        "use client";

        import {
          Conversation,
          ConversationContent,
          ConversationScrollButton,
        } from "@/components/ai-elements/conversation";
        import {
          PromptInput,
          PromptInputActionAddAttachments,
          PromptInputActionMenu,
          PromptInputActionMenuContent,
          PromptInputActionMenuTrigger,
          PromptInputAttachment,
          PromptInputAttachments,
          PromptInputBody,
          PromptInputButton,
          PromptInputHeader,
          type PromptInputMessage,
          PromptInputSelect,
          PromptInputSelectContent,
          PromptInputSelectItem,
          PromptInputSelectTrigger,
          PromptInputSelectValue,
          PromptInputSubmit,
          PromptInputTextarea,
          PromptInputFooter,
          PromptInputTools,
        } from "@/components/ai-elements/prompt-input";
        import {
          MessageResponse,
          Message,
          MessageContent,
          MessageActions,
          MessageAction,
        } from "@/components/ai-elements/message";

        import { Fragment, useState } from "react";
        import { useChat } from "@ai-sdk/react";
        import type { ToolUIPart } from "ai";
        import {
          Tool,
          ToolContent,
          ToolHeader,
          ToolInput,
          ToolOutput,
        } from "@/components/ai-elements/tool";

        import { CopyIcon, GlobeIcon, RefreshCcwIcon } from "lucide-react";
        import {
          Source,
          Sources,
          SourcesContent,
          SourcesTrigger,
        } from "@/components/ai-elements/sources";
        import {
          Reasoning,
          ReasoningContent,
          ReasoningTrigger,
        } from "@/components/ai-elements/reasoning";
        import { Loader } from "@/components/ai-elements/loader";

        const models = [
          {
            name: "GPT 5 Mini (Thinking)",
            value: "gpt-5-mini",
          },
          {
            name: "GPT 4o Mini",
            value: "gpt-4o-mini",
          },
        ];

        const ChatBotDemo = () => {
          const [input, setInput] = useState("");
          const [model, setModel] = useState<string>(models[0].value);
          const [webSearch, setWebSearch] = useState(false);
          const { messages, sendMessage, status, regenerate } = useChat();

          const handleSubmit = (message: PromptInputMessage) => {
            const hasText = Boolean(message.text);
            const hasAttachments = Boolean(message.files?.length);

            if (!(hasText || hasAttachments)) {
              return;
            }

            sendMessage(
              {
                text: message.text || "Sent with attachments",
                files: message.files,
              },
              {
                body: {
                  model: model,
                  webSearch: webSearch,
                },
              }
            );
            setInput("");
          };

          return (
            <div className="max-w-4xl mx-auto p-6 relative size-full h-screen">
              <div className="flex flex-col h-full">
                <Conversation className="h-full">
                  <ConversationContent>
                    {messages.map((message) => (
                      <div key={message.id}>
                        {message.role === "assistant" &&
                          message.parts.filter((part) => part.type === "source-url")
                            .length > 0 && (
                            <Sources>
                              <SourcesTrigger
                                count={
                                  message.parts.filter(
                                    (part) => part.type === "source-url"
                                  ).length
                                }
                              />
                              {message.parts
                                .filter((part) => part.type === "source-url")
                                .map((part, i) => (
                                  <SourcesContent key={`${message.id}-${i}`}>
                                    <Source
                                      key={`${message.id}-${i}`}
                                      href={part.url}
                                      title={part.url}
                                    />
                                  </SourcesContent>
                                ))}
                            </Sources>
                          )}
                        {message.parts.map((part, i) => {
                          switch (part.type) {
                            case "text":
                              return (
                                <Fragment key={`${message.id}-${i}`}>
                                  <Message from={message.role}>
                                    <MessageContent>
                                      <MessageResponse>{part.text}</MessageResponse>
                                    </MessageContent>
                                  </Message>
                                  {message.role === "assistant" &&
                                    i === messages.length - 1 && (
                                      <MessageActions className="mt-2">
                                        <MessageAction
                                          onClick={() => regenerate()}
                                          label="Retry"
                                        >
                                          <RefreshCcwIcon className="size-3" />
                                        </MessageAction>
                                        <MessageAction
                                          onClick={() =>
                                            navigator.clipboard.writeText(part.text)
                                          }
                                          label="Copy"
                                        >
                                          <CopyIcon className="size-3" />
                                        </MessageAction>
                                      </MessageActions>
                                    )}
                                </Fragment>
                              );
                            case "reasoning":
                              return (
                                <Reasoning
                                  key={`${message.id}-${i}`}
                                  className="w-full"
                                  isStreaming={
                                    status === "streaming" &&
                                    i === message.parts.length - 1 &&
                                    message.id === messages.at(-1)?.id
                                  }
                                >
                                  <ReasoningTrigger />
                                  <ReasoningContent>{part.text}</ReasoningContent>
                                </Reasoning>
                              );
                            default: {
                              if (part.type.startsWith("tool-")) {
                                const toolPart = part as ToolUIPart;
                                return (
                                  <Tool
                                    key={`${message.id}-${i}`}
                                    defaultOpen={toolPart.state === "output-available"}
                                  >
                                    <ToolHeader
                                      type={toolPart.type}
                                      state={toolPart.state}
                                    />
                                    <ToolContent>
                                      <ToolInput input={toolPart.input} />
                                      <ToolOutput
                                        output={toolPart.output}
                                        errorText={toolPart.errorText}
                                      />
                                    </ToolContent>
                                  </Tool>
                                );
                              }
                              return null;
                            }
                          }
                        })}
                      </div>
                    ))}
                    {status === "submitted" && <Loader />}
                  </ConversationContent>
                  <ConversationScrollButton />
                </Conversation>

                <PromptInput
                  onSubmit={handleSubmit}
                  className="mt-4"
                  globalDrop
                  multiple
                >
                  <PromptInputHeader>
                    <PromptInputAttachments>
                      {(attachment) => <PromptInputAttachment data={attachment} />}
                    </PromptInputAttachments>
                  </PromptInputHeader>
                  <PromptInputBody>
                    <PromptInputTextarea
                      onChange={(e) => setInput(e.target.value)}
                      value={input}
                    />
                  </PromptInputBody>
                  <PromptInputFooter>
                    <PromptInputTools>
                      <PromptInputActionMenu>
                        <PromptInputActionMenuTrigger />
                        <PromptInputActionMenuContent>
                          <PromptInputActionAddAttachments />
                        </PromptInputActionMenuContent>
                      </PromptInputActionMenu>
                      <PromptInputButton
                        variant={webSearch ? "default" : "ghost"}
                        onClick={() => setWebSearch(!webSearch)}
                      >
                        <GlobeIcon size={16} />
                        <span>Search</span>
                      </PromptInputButton>
                      <PromptInputSelect
                        onValueChange={(value) => {
                          setModel(value);
                        }}
                        value={model}
                      >
                        <PromptInputSelectTrigger>
                          <PromptInputSelectValue />
                        </PromptInputSelectTrigger>
                        <PromptInputSelectContent>
                          {models.map((model) => (
                            <PromptInputSelectItem
                              key={model.value}
                              value={model.value}
                            >
                              {model.name}
                            </PromptInputSelectItem>
                          ))}
                        </PromptInputSelectContent>
                      </PromptInputSelect>
                    </PromptInputTools>
                    <PromptInputSubmit disabled={!input && !status} status={status} />
                  </PromptInputFooter>
                </PromptInput>
              </div>
            </div>
          );
        };

        export default ChatBotDemo;
        ```
      </Tab>
    </Tabs>

    ### Entendiendo el Frontend

    El frontend usa componentes de AI Elements para proporcionar una interfaz de chat completa:

    **Características principales:**

    * **Visualización de la conversación**: El componente `Conversation` gestiona automáticamente el desplazamiento y la presentación de los mensajes
    * **Representación de mensajes**: Cada parte del mensaje se representa según su tipo (texto, razonamiento, llamadas a herramientas)
    * **Visualización de herramientas**: Las llamadas a herramientas se muestran con secciones plegables que muestran entradas y salidas
    * **Controles interactivos**: Los usuarios pueden activar o desactivar la búsqueda web, elegir modelos y adjuntar archivos
    * **Acciones de mensaje**: Acciones de copiar y reintentar para los mensajes del asistente
  </Step>

  <Step title="Agregar compatibilidad con el renderizado de Markdown">
    Para asegurarte de que el markdown del LLM se renderice correctamente, añade la siguiente importación a tu archivo `app/globals.css`:

    ```css theme={null}
    @source "../node_modules/streamdown/dist/index.js";
    ```

    Esto importa los estilos necesarios para mostrar contenido Markdown en las respuestas de los mensajes.
  </Step>

  <Step title="Crear la ruta básica de API">
    Crea el endpoint de la API de chat en `app/api/chat/route.ts`. Esta ruta gestionará los mensajes entrantes y transmitirá respuestas de la IA.

    ```typescript theme={null}
    import { streamText, UIMessage, convertToModelMessages } from "ai";
    import { createOpenAI } from "@ai-sdk/openai";

    const openai = createOpenAI({
      apiKey: process.env.OPENAI_API_KEY!,
    });

    // Permitir respuestas de streaming hasta por 5 minutos
    export const maxDuration = 300;

    export async function POST(req: Request) {
      const {
        messages,
        model,
        webSearch,
      }: {
        messages: UIMessage[];
        model: string;
        webSearch: boolean;
      } = await req.json();

      const result = streamText({
        model: openai(model),
        messages: convertToModelMessages(messages),
        system:
          "You are a helpful assistant that can answer questions and help with tasks.",
      });

      // enviar fuentes y razonamiento de vuelta al cliente
      return result.toUIMessageStreamResponse({
        sendSources: true,
        sendReasoning: true,
      });
    }
    ```

    Esta ruta básica:

    * Recibe mensajes del frontend
    * Usa el modelo de OpenAI seleccionado por el usuario
    * Transmite respuestas al cliente
    * Aún no incluye herramientas; las añadiremos a continuación
  </Step>

  <Step title="Configurar las variables de entorno">
    Crea un archivo `.env.local` en la raíz de tu proyecto:

    ```bash theme={null}
    touch .env.local
    ```

    Añade tu clave de la API de OpenAI:

    ```env theme={null}
    OPENAI_API_KEY=sk-your-openai-api-key
    ```

    Se requiere la variable `OPENAI_API_KEY` para que el modelo de IA funcione.
  </Step>

  <Step title="Prueba el chat básico">
    Ahora puedes probar el chatbot del SDK de IA sin la integración con Firecrawl. Inicia el servidor de desarrollo:

    ```bash theme={null}
    npm run dev
    ```

    Abre [localhost:3000](http://localhost:3000) en tu navegador y prueba la funcionalidad básica del chat. El asistente debería responder a los mensajes, pero aún no tendrá capacidades de scraping web ni de búsqueda.

    <video autoPlay muted loop playsInline src="https://mintcdn.com/firecrawl/2R2EKZFeF2zvZsk6/images/guides/cookbooks/ai-sdk-cookbook/simple-ai-sdk-chatbot.mp4?fit=max&auto=format&n=2R2EKZFeF2zvZsk6&q=85&s=9adcf51535b3c4ada93d6b850c532010" aria-label="Chatbot de IA básico sin capacidades de scraping web" data-path="images/guides/cookbooks/ai-sdk-cookbook/simple-ai-sdk-chatbot.mp4" />
  </Step>

  <Step title="Agregar herramientas de Firecrawl">
    Ahora vamos a mejorar el asistente con capacidades de scraping web y búsqueda usando Firecrawl.

    ### Instalar el SDK de Firecrawl

    Firecrawl convierte sitios web en formatos listos para LLM con funciones de scraping y búsqueda:

    ```bash theme={null}
    npm i firecrawl
    ```

    ### Crea el archivo de herramientas

    Crea una carpeta `lib` y añade un archivo `tools.ts` dentro:

    ```bash theme={null}
    mkdir lib && touch lib/tools.ts
    ```

    Agrega el siguiente código para definir las herramientas de web scraping y búsqueda:

    ```typescript lib/tools.ts theme={null}
    import { Firecrawl } from "firecrawl";
    import { tool } from "ai";
    import { z } from "zod";

    const firecrawl = new Firecrawl({ apiKey: process.env.FIRECRAWL_API_KEY });

    export const scrapeWebsiteTool = tool({
      description: 'Scrape content from any website URL',
      inputSchema: z.object({
        url: z.string().url().describe('The URL to scrape')
      }),
      execute: async ({ url }) => {
        console.log('Scraping:', url);
        const result = await firecrawl.scrape(url, {
          formats: ['markdown'],
          onlyMainContent: true,
          timeout: 30000
        });
        console.log('Scraped content preview:', result.markdown?.slice(0, 200) + '...');
        return { content: result.markdown };
      }
    });

    export const searchWebTool = tool({
      description: 'Search the web using Firecrawl',
      inputSchema: z.object({
        query: z.string().describe('The search query'),
        limit: z.number().optional().describe('Number of results'),
        location: z.string().optional().describe('Location for localized results'),
        tbs: z.string().optional().describe('Time filter (qdr:h, qdr:d, qdr:w, qdr:m, qdr:y)'),
        sources: z.array(z.enum(['web', 'news', 'images'])).optional().describe('Result types'),
        categories: z.array(z.enum(['github', 'research', 'pdf'])).optional().describe('Filter categories'),
      }),
      execute: async ({ query, limit, location, tbs, sources, categories }) => {
        console.log('Searching:', query);
        const response = await firecrawl.search(query, {
          ...(limit && { limit }),
          ...(location && { location }),
          ...(tbs && { tbs }),
          ...(sources && { sources }),
          ...(categories && { categories }),
        }) as { web?: Array<{ title?: string; url?: string; description?: string }> };

        const results = (response.web || []).map((item) => ({
          title: item.title || item.url || 'Untitled',
          url: item.url || '',
          description: item.description || '',
        }));

        console.log('Search results:', results.length);
        return { results };
      },
    });
    ```

    ### Entender las herramientas

    **Herramienta de extracción de sitios web:**

    * Acepta una URL como entrada (validada por un esquema de Zod)
    * Usa el método `scrape` de Firecrawl para obtener la página en Markdown
    * Extrae solo el contenido principal para reducir el uso de tokens
    * Devuelve el contenido extraído para que la IA lo analice

    **Herramienta de búsqueda web:**

    * Acepta una consulta de búsqueda con filtros opcionales
    * Usa el método `search` de Firecrawl para encontrar páginas web relevantes
    * Admite filtros avanzados como ubicación, rango de tiempo y categorías de contenido
    * Devuelve resultados estructurados con títulos, URL y descripciones

    Más información sobre las herramientas: [ai-sdk.dev/docs/foundations/tools](https://ai-sdk.dev/docs/foundations/tools).
  </Step>

  <Step title="Actualiza la ruta de la API con las herramientas de Firecrawl">
    Ahora actualiza tu `app/api/chat/route.ts` para incluir las herramientas de Firecrawl que acabamos de crear.

    <Accordion title="Ver el código completo de app/api/chat/route.ts">
      ```typescript theme={null}
      import { streamText, UIMessage, stepCountIs, convertToModelMessages } from "ai";
      import { createOpenAI } from "@ai-sdk/openai";
      import { scrapeWebsiteTool, searchWebTool } from "@/lib/tools";

      const openai = createOpenAI({
        apiKey: process.env.OPENAI_API_KEY!,
      });

      export const maxDuration = 300;

      export async function POST(req: Request) {
        const {
          messages,
          model,
          webSearch,
        }: {
          messages: UIMessage[];
          model: string;
          webSearch: boolean;
        } = await req.json();

        const result = streamText({
          model: openai(model),
          messages: convertToModelMessages(messages),
          system:
            "You are a helpful assistant that can answer questions and help with tasks.",
          // Agrega aquí las herramientas de Firecrawl
          tools: {
            scrapeWebsite: scrapeWebsiteTool,
            searchWeb: searchWebTool,
          },
          stopWhen: stepCountIs(5),
          toolChoice: webSearch ? "auto" : "none",
        });

        return result.toUIMessageStreamResponse({
          sendSources: true,
          sendReasoning: true,
        });
      }
      ```
    </Accordion>

    Cambios clave respecto a la ruta básica:

    * Importa `stepCountIs` del AI SDK
    * Importa las herramientas de Firecrawl desde `@/lib/tools`
    * Agrega el objeto `tools` con las herramientas `scrapeWebsite` y `searchWeb`
    * Agrega `stopWhen: stepCountIs(5)` para limitar los pasos de ejecución
    * Establece `toolChoice` en "auto" cuando la búsqueda web esté habilitada; en "none" en caso contrario

    Más información sobre `streamText`: [ai-sdk.dev/docs/reference/ai-sdk-core/stream-text](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text).
  </Step>

  <Step title="Añade tu clave de API de Firecrawl">
    Actualiza tu archivo `.env.local` para incluir tu clave de la API de Firecrawl:

    ```env theme={null}
    OPENAI_API_KEY=sk-your-openai-api-key
    FIRECRAWL_API_KEY=fc-your-firecrawl-api-key
    ```

    Obtén tu clave de la API de Firecrawl en [firecrawl.dev](https://firecrawl.dev).
  </Step>

  <Step title="Prueba la aplicación completa">
    Reinicia el servidor de desarrollo:

    ```bash theme={null}
    npm run dev
    ```

    <video autoPlay muted loop playsInline src="https://mintcdn.com/firecrawl/2R2EKZFeF2zvZsk6/images/guides/cookbooks/ai-sdk-cookbook/active-firecrawl-tools-ai-sdk.mp4?fit=max&auto=format&n=2R2EKZFeF2zvZsk6&q=85&s=86246b0e745ffcd4ca84aa9f6261b041" aria-label="Chatbot de IA con herramientas activas de Firecrawl" data-path="images/guides/cookbooks/ai-sdk-cookbook/active-firecrawl-tools-ai-sdk.mp4" />

    Abre [localhost:3000](http://localhost:3000) y prueba el asistente mejorado:

    1. Activa el botón "Search" para habilitar la búsqueda web
    2. Pregunta: "¿Cuáles son las funciones más recientes de firecrawl.dev?"
    3. Observa cómo la IA invoca la herramienta `searchWeb` o `scrapeWebsite`
    4. Mira la ejecución de la herramienta en la interfaz con sus entradas y salidas
    5. Lee el análisis de la IA basado en los datos extraídos
  </Step>
</Steps>

<div id="how-it-works">
  ## Cómo funciona
</div>

<div id="message-flow">
  ### Flujo de mensajes
</div>

1. **El usuario envía un mensaje**: El usuario escribe una pregunta y hace clic en Enviar
2. **El frontend envía la solicitud**: `useChat` envía el mensaje a `/api/chat` con el modelo seleccionado y el ajuste de búsqueda web
3. **El backend procesa el mensaje**: La ruta de la API recibe el mensaje y llama a `streamText`
4. **La IA decide qué herramientas usar**: El modelo analiza la pregunta y decide si usar `scrapeWebsite` o `searchWeb` (solo si la búsqueda web está habilitada)
5. **Ejecución de herramientas**: Si se invocan herramientas, Firecrawl hace scraping o busca en la web
6. **La IA genera la respuesta**: El modelo analiza los resultados de las herramientas y genera una respuesta en lenguaje natural
7. **El frontend muestra los resultados**: La interfaz muestra las llamadas a herramientas y la respuesta final en tiempo real

<div id="tool-calling-process">
  ### Proceso de llamada a herramientas
</div>

El sistema de llamadas a herramientas del AI SDK ([ai-sdk.dev/docs/foundations/tools](https://ai-sdk.dev/docs/foundations/tools)) funciona de la siguiente manera:

1. El modelo recibe el mensaje del usuario y las descripciones de las herramientas disponibles
2. Si el modelo determina que se necesita una herramienta, genera una llamada a la herramienta con parámetros
3. El SDK ejecuta la función de la herramienta con esos parámetros
4. El resultado de la herramienta se envía de vuelta al modelo
5. El modelo usa el resultado para generar su respuesta final

Todo esto ocurre automáticamente dentro de una única llamada a `streamText`, con los resultados transmitiéndose al frontend en tiempo real.

<div id="key-features">
  ## Características clave
</div>

<div id="model-selection">
  ### Selección de modelo
</div>

La aplicación admite varios modelos de OpenAI:

* **GPT-5 Mini (Thinking)**: Modelo reciente de OpenAI con capacidades avanzadas de razonamiento
* **GPT-4o Mini**: Modelo rápido y rentable

Los usuarios pueden alternar entre modelos usando el selector desplegable.

<div id="web-search-toggle">
  ### Interruptor de búsqueda web
</div>

El botón Search controla si la IA puede usar las herramientas de Firecrawl:

* **Enabled**: la IA puede invocar las herramientas `scrapeWebsite` y `searchWeb` según sea necesario
* **Disabled**: la IA responde solo con el conocimiento de su entrenamiento

Esto les da a los usuarios control sobre cuándo usar datos de la web frente al conocimiento incorporado del modelo.

<div id="customization-ideas">
  ## Ideas de personalización
</div>

<div id="add-more-tools">
  ### Agregar más herramientas
</div>

Amplía el asistente con herramientas adicionales:

* Consultas a la base de datos para datos internos de la empresa
* Integración con el CRM para obtener información de clientes
* Envío de correos electrónicos
* Generación de documentos

Cada herramienta sigue el mismo patrón: define un esquema con Zod, implementa la función `execute` y regístrala en el objeto `tools`.

<div id="change-the-ai-model">
  ### Cambiar el modelo de IA
</div>

Sustituye OpenAI por otro proveedor:

```typescript theme={null}
import { anthropic } from "@ai-sdk/anthropic";

const result = streamText({
  model: anthropic("claude-4.5-sonnet"),
  // ... resto de config
});
```

El SDK de IA es compatible con más de 20 proveedores con la misma API. Más información: [ai-sdk.dev/docs/foundations/providers-and-models](https://ai-sdk.dev/docs/foundations/providers-and-models).

<div id="customize-the-ui">
  ### Personaliza la interfaz de usuario
</div>

Los componentes de AI Elements están basados en shadcn/ui, por lo que puedes:

* Modificar los estilos de los componentes en sus archivos
* Añadir nuevas variantes a los componentes existentes
* Crear componentes personalizados que se ajusten al sistema de diseño

<div id="best-practices">
  ## Mejores prácticas
</div>

1. **Usa las herramientas adecuadas**: Elige `searchWeb` para encontrar primero páginas relevantes, `scrapeWebsite` para páginas individuales o deja que la IA decida

2. **Supervisa el uso de la API**: Controla tu uso de las API de Firecrawl y OpenAI para evitar costos inesperados

3. **Gestiona los errores con elegancia**: Las herramientas incluyen manejo de errores, pero considera añadir mensajes de error visibles para el usuario

4. **Optimiza el rendimiento**: Usa streaming para ofrecer retroalimentación inmediata y considera cachear el contenido de acceso frecuente

5. **Establece límites razonables**: `stopWhen: stepCountIs(5)` evita llamadas excesivas a herramientas y costos descontrolados

***

<div id="related-resources">
  ## Recursos relacionados
</div>

<CardGroup cols={2}>
  <Card title="Documentación del AI SDK" href="https://ai-sdk.dev/docs">
    Explora el AI SDK para crear aplicaciones con IA que admitan streaming, llamadas a herramientas y múltiples proveedores.
  </Card>

  <Card title="Componentes de AI Elements" href="https://ai-sdk.dev/elements/overview">
    Componentes de UI listos para usar para aplicaciones de IA basadas en shadcn/ui.
  </Card>
</CardGroup>
