diff --git a/docs/docs.json b/docs/docs.json index 609ff7b3e16..44328ba3a89 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -472,6 +472,7 @@ }, "pages": [ "guides/ai-agents/overview", + "guides/ai-agents/chat-agent", "guides/ai-agents/generate-translate-copy", "guides/ai-agents/route-question", "guides/ai-agents/respond-and-check-content", diff --git a/docs/guides/ai-agents/chat-agent.mdx b/docs/guides/ai-agents/chat-agent.mdx new file mode 100644 index 00000000000..f6465163a9b --- /dev/null +++ b/docs/guides/ai-agents/chat-agent.mdx @@ -0,0 +1,124 @@ +--- +title: "Build a chat agent" +sidebarTitle: "Chat agent" +description: "Create a durable, multi-turn chat agent with chat.agent(), then add tools to it like any AI SDK agent." +--- + +## Overview + +Build a **durable, multi-turn chat agent**. A durable session owns the conversation, streams tokens to your UI, and stays alive across many back-and-forth messages. The other guides in this section are one-shot workflows (trigger a task, run a fixed sequence of LLM calls, return a result); a chat agent instead owns the session for its whole lifetime. + +[`chat.agent()`](/ai-chat/overview) handles the queuing, retries, resumability and streaming for you. You write the model call, Trigger.dev owns the session. For the full feature set (sessions, fast starts, compaction, sub-agents, the frontend transport), see the [AI chat docs](/ai-chat/overview). + +## A minimal agent + +Define an agent with `chat.agent()`. The `run` function receives the conversation `messages` (already converted from the frontend's `UIMessage[]`) and an abort `signal`. Return a `StreamTextResult` and it's piped to the frontend automatically. + +```typescript trigger/chat.ts +import { chat } from "@trigger.dev/sdk/ai"; +import { anthropic } from "@ai-sdk/anthropic"; +import { streamText, stepCountIs } from "ai"; + +export const myChat = chat.agent({ + id: "my-chat", + run: async ({ messages, signal }) => { + return streamText({ + // Spread chat.toStreamTextOptions() FIRST: it wires up prepareStep + // (compaction, steering, background injection) and telemetry. + ...chat.toStreamTextOptions(), + model: anthropic("claude-sonnet-4-5"), + messages, + abortSignal: signal, + stopWhen: stepCountIs(15), + }); + }, +}); +``` + + + Always spread `chat.toStreamTextOptions()` into your `streamText` call, and spread it first. It + wires up the `prepareStep` callback that drives compaction, mid-turn steering and background + injection. Those features silently no-op if the spread is missing. + + +## Add tools + +A chat agent uses tools exactly like any other AI SDK agent. Declare them on the config so their results survive across turns, then pass the `tools` you receive in `run` straight to `streamText`: + +```typescript trigger/chat.ts +import { chat } from "@trigger.dev/sdk/ai"; +import { anthropic } from "@ai-sdk/anthropic"; +import { streamText, stepCountIs, tool } from "ai"; +import { z } from "zod"; + +const getCurrentTime = tool({ + description: "Get the current server time as an ISO string.", + inputSchema: z.object({}), + execute: async () => ({ now: new Date().toISOString() }), +}); + +export const myChat = chat.agent({ + id: "my-chat", + // Declared here so tool results survive history re-conversion across turns. + tools: { getCurrentTime }, + run: async ({ messages, tools, signal }) => { + return streamText({ + // Pass tools INTO toStreamTextOptions (not separately to streamText): + // this is what detects tool calls needing HITL approval and merges any + // auto-injected skill tools. It sets streamText's `tools` for you. + ...chat.toStreamTextOptions({ tools }), + model: anthropic("claude-sonnet-4-5"), + messages, + stopWhen: stepCountIs(15), + abortSignal: signal, + }); + }, +}); +``` + +Swap `getCurrentTime` for whatever your agent needs to do: query a database, call an API, or trigger another Trigger.dev task. See [Tools](/ai-chat/tools) for how tool results are persisted and replayed across turns. + +## Wire up the frontend + +The browser talks to Trigger.dev directly through the [chat transport](/ai-chat/frontend), so there's no API route to maintain. Expose two server actions (one to start the session, one to mint a session-scoped token) and pass them to `useTriggerChatTransport`, then hand the transport to the AI SDK's `useChat`: + +```typescript app/actions.ts +"use server"; + +import { auth } from "@trigger.dev/sdk"; +import { chat } from "@trigger.dev/sdk/ai"; + +export const startChatSession = chat.createStartSessionAction("my-chat"); + +export async function mintChatAccessToken(chatId: string) { + // Authorize the caller for this chatId before minting: confirm the logged-in + // user owns this session (e.g. look it up in your database). Otherwise anyone + // who learns a session ID could mint read/write access to it. + return auth.createPublicToken({ + scopes: { read: { sessions: chatId }, write: { sessions: chatId } }, + expirationTime: "1h", + }); +} +``` + +See the [Quick Start](/ai-chat/quick-start) for the complete frontend component. + +## A full example + +For a complete, real-world chat agent, see the ClickHouse chat agent example. It builds on everything above with generative UI, a versioned system prompt, and real tools against a live database. + + + + A full example project: a chat agent that answers questions about your data with charts, tables + and maps. + + + How chat agents, sessions and the turn loop work. + + + Declaring tools on your agent and how they persist across turns. + + + Cut first-turn latency with preload and head start. + + diff --git a/docs/guides/ai-agents/generate-translate-copy.mdx b/docs/guides/ai-agents/generate-translate-copy.mdx index cb5e034aa31..1c16a05a3cb 100644 --- a/docs/guides/ai-agents/generate-translate-copy.mdx +++ b/docs/guides/ai-agents/generate-translate-copy.mdx @@ -16,14 +16,14 @@ In this example, we'll create a workflow that generates and translates copy. Thi **This task:** -- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models -- Uses `experimental_telemetry` to provide LLM logs +- Uses `generateText` from the [AI SDK](https://ai-sdk.dev/) to call Anthropic's Claude models +- Uses `experimental_telemetry` to surface each LLM call on the Run page in the dashboard - Generates marketing copy based on subject and target word count - Validates the generated copy meets word count requirements (±10 words) - Translates the validated copy to the target language while preserving tone ```typescript -import { openai } from "@ai-sdk/openai"; +import { anthropic } from "@ai-sdk/anthropic"; import { task } from "@trigger.dev/sdk"; import { generateText } from "ai"; @@ -39,7 +39,7 @@ export const generateAndTranslateTask = task({ run: async (payload: TranslatePayload) => { // Step 1: Generate marketing copy const generatedCopy = await generateText({ - model: openai("o1-mini"), + model: anthropic("claude-sonnet-4-5"), messages: [ { role: "system", @@ -72,7 +72,7 @@ export const generateAndTranslateTask = task({ // Step 2: Translate to target language const translatedCopy = await generateText({ - model: openai("o1-mini"), + model: anthropic("claude-sonnet-4-5"), messages: [ { role: "system", @@ -103,9 +103,9 @@ On the Test page in the dashboard, select the `generate-and-translate-copy` task ```json { - marketingSubject: "The controversial new Jaguar electric concept car", - targetLanguage: "Spanish", - targetWordCount: 100, + "marketingSubject": "The controversial new Jaguar electric concept car", + "targetLanguage": "Spanish", + "targetWordCount": 100 } ``` diff --git a/docs/guides/ai-agents/overview.mdx b/docs/guides/ai-agents/overview.mdx index 0c27982a450..fd1d5c0a572 100644 --- a/docs/guides/ai-agents/overview.mdx +++ b/docs/guides/ai-agents/overview.mdx @@ -22,6 +22,14 @@ description: "Real world AI agent example tasks using Trigger.dev" Generate and maintain GitHub wiki documentation with Claude-powered analysis. + + Build a chat agent that answers questions about your ClickHouse data with charts, tables and maps + using `chat.agent()` and generative UI. + +## Chat agents + +Build a durable, multi-turn chat agent with [`chat.agent()`](/ai-chat/overview). A durable session per conversation, with streaming and resumability handled for you. + + + + Create a durable, multi-turn chat agent with `chat.agent()`, then add tools to it. + + + ## Agent fundamentals These guides will show you how to set up different types of AI agent workflows with Trigger.dev. The examples take inspiration from Anthropic's blog post on [building effective agents](https://www.anthropic.com/research/building-effective-agents). diff --git a/docs/guides/ai-agents/respond-and-check-content.mdx b/docs/guides/ai-agents/respond-and-check-content.mdx index 560b69b80c5..116216c673f 100644 --- a/docs/guides/ai-agents/respond-and-check-content.mdx +++ b/docs/guides/ai-agents/respond-and-check-content.mdx @@ -15,14 +15,14 @@ In this example, we'll create a workflow that simultaneously checks content for **This task:** -- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models -- Uses `experimental_telemetry` to provide LLM logs +- Uses `generateText` from the [AI SDK](https://ai-sdk.dev/) to call Anthropic's Claude models +- Uses `experimental_telemetry` to surface each LLM call on the Run page in the dashboard - Uses [`batch.triggerByTaskAndWait`](/triggering#batch-triggerbytaskandwait) to run customer response and content moderation tasks in parallel -- Generates customer service responses using an AI model +- Answers with `claude-sonnet-4-5` and moderates with the faster, cheaper `claude-haiku-4-5` - Simultaneously checks for inappropriate content while generating responses ```typescript -import { openai } from "@ai-sdk/openai"; +import { anthropic } from "@ai-sdk/anthropic"; import { batch, task } from "@trigger.dev/sdk"; import { generateText } from "ai"; @@ -31,7 +31,7 @@ export const generateCustomerResponse = task({ id: "generate-customer-response", run: async (payload: { question: string }) => { const response = await generateText({ - model: openai("o1-mini"), + model: anthropic("claude-sonnet-4-5"), messages: [ { role: "system", @@ -54,7 +54,7 @@ export const checkInappropriateContent = task({ id: "check-inappropriate-content", run: async (payload: { text: string }) => { const response = await generateText({ - model: openai("o1-mini"), + model: anthropic("claude-haiku-4-5"), messages: [ { role: "system", diff --git a/docs/guides/ai-agents/route-question.mdx b/docs/guides/ai-agents/route-question.mdx index de1cb3e92fd..717b82cbbca 100644 --- a/docs/guides/ai-agents/route-question.mdx +++ b/docs/guides/ai-agents/route-question.mdx @@ -16,78 +16,57 @@ In this example, we'll create a workflow that routes a question to a different A **This task:** -- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models -- Uses `experimental_telemetry` in the source verification and historical analysis tasks to provide LLM logs -- Routes questions using a lightweight model (`o1-mini`) to classify complexity -- Directs simple questions to `gpt-4o` and complex ones to `gpt-o3-mini` +- Uses `generateObject` from the [AI SDK](https://ai-sdk.dev/) to classify the question into a typed routing decision +- Uses `experimental_telemetry` to surface each LLM call on the Run page in the dashboard +- Classifies complexity with a fast, cheap model (`claude-haiku-4-5`) +- Directs simple questions to `claude-haiku-4-5` and complex ones to `claude-sonnet-4-5` - Returns both the answer and metadata about the routing decision ```typescript -import { openai } from "@ai-sdk/openai"; +import { anthropic } from "@ai-sdk/anthropic"; import { task } from "@trigger.dev/sdk"; -import { generateText } from "ai"; +import { generateObject, generateText } from "ai"; import { z } from "zod"; -// Schema for router response +// The router's structured decision. generateObject validates the model +// output against this schema, so there's no manual JSON parsing. const routingSchema = z.object({ - model: z.enum(["gpt-4o", "gpt-o3-mini"]), + model: z.enum(["claude-haiku-4-5", "claude-sonnet-4-5"]), reason: z.string(), }); -// Router prompt template -const ROUTER_PROMPT = `You are a routing assistant that determines the complexity of questions. -Analyze the following question and route it to the appropriate model: - -- Use "gpt-4o" for simple, common, or straightforward questions -- Use "gpt-o3-mini" for complex, unusual, or questions requiring deep reasoning - -Respond with a JSON object in this exact format: -{"model": "gpt-4o" or "gpt-o3-mini", "reason": "your reasoning here"} - -Question: `; - export const routeAndAnswerQuestion = task({ id: "route-and-answer-question", run: async (payload: { question: string }) => { - // Step 1: Route the question - const routingResponse = await generateText({ - model: openai("o1-mini"), - messages: [ - { - role: "system", - content: - "You must respond with a valid JSON object containing only 'model' and 'reason' fields. No markdown, no backticks, no explanation.", - }, - { - role: "user", - content: ROUTER_PROMPT + payload.question, - }, - ], - temperature: 0.1, + // Step 1: Classify the question and pick a model + const { object: routing } = await generateObject({ + model: anthropic("claude-haiku-4-5"), + schema: routingSchema, + system: + "You are a routing assistant. Pick the model best suited to answer the question:\n" + + "- claude-haiku-4-5 for simple, common, or straightforward questions\n" + + "- claude-sonnet-4-5 for complex, unusual, or questions needing deep reasoning", + prompt: payload.question, experimental_telemetry: { isEnabled: true, - functionId: "route-and-answer-question", + functionId: "route-question", }, }); - // Add error handling and cleanup - let jsonText = routingResponse.text.trim(); - if (jsonText.startsWith("```")) { - jsonText = jsonText.replace(/```json\n|\n```/g, ""); - } - - const routingResult = routingSchema.parse(JSON.parse(jsonText)); - - // Step 2: Get the answer using the selected model - const answerResult = await generateText({ - model: openai(routingResult.model), - messages: [{ role: "user", content: payload.question }], + // Step 2: Answer with the selected model + const answer = await generateText({ + model: anthropic(routing.model), + prompt: payload.question, + experimental_telemetry: { + isEnabled: true, + functionId: "answer-question", + }, }); return { - answer: answerResult.text, - selectedModel: routingResult.model, - routingReason: routingResult.reason, + answer: answer.text, + selectedModel: routing.model, + routingReason: routing.reason, }; }, }); @@ -97,7 +76,7 @@ export const routeAndAnswerQuestion = task({ ## Run a test -Triggering our task with a simple question shows it routing to the gpt-4o model and returning the answer with reasoning: +Triggering our task with a simple question shows it routing to the `claude-haiku-4-5` model and returning the answer with reasoning: ```json { diff --git a/docs/guides/ai-agents/translate-and-refine.mdx b/docs/guides/ai-agents/translate-and-refine.mdx index 80f557fd0c3..f5958524c55 100644 --- a/docs/guides/ai-agents/translate-and-refine.mdx +++ b/docs/guides/ai-agents/translate-and-refine.mdx @@ -25,7 +25,7 @@ This example task translates text into a target language and refines the transla ```typescript import { task } from "@trigger.dev/sdk"; import { generateText } from "ai"; -import { openai } from "@ai-sdk/openai"; +import { anthropic } from "@ai-sdk/anthropic"; interface TranslationPayload { text: string; @@ -35,9 +35,17 @@ interface TranslationPayload { rejectionCount?: number; } +interface TranslationResult { + finalTranslation: string | undefined; + iterations: number; + status: "MAX_ITERATIONS_REACHED" | "APPROVED"; +} + export const translateAndRefine = task({ id: "translate-and-refine", - run: async (payload: TranslationPayload) => { + // Explicit return type: the task returns its own recursive result, so + // annotate it to avoid TypeScript's circular-inference error. + run: async (payload: TranslationPayload): Promise => { const rejectionCount = payload.rejectionCount || 0; // Bail out if we've hit the maximum attempts @@ -55,7 +63,7 @@ export const translateAndRefine = task({ : `Translate this text into ${payload.targetLanguage}, preserving style and meaning: "${payload.text}"`; const translation = await generateText({ - model: openai("o1-mini"), + model: anthropic("claude-sonnet-4-5"), messages: [ { role: "system", @@ -75,7 +83,7 @@ export const translateAndRefine = task({ // Evaluate the translation const evaluation = await generateText({ - model: openai("o1-mini"), + model: anthropic("claude-sonnet-4-5"), messages: [ { role: "system", @@ -83,7 +91,7 @@ export const translateAndRefine = task({ Your goal is to ensure translations are accurate and natural, but not necessarily perfect. This is iteration ${ rejectionCount + 1 - } of a maximum 5 iterations. + } of a maximum 10 iterations. RESPONSE FORMAT: - If the translation meets 90%+ quality: Respond with exactly "APPROVED" (nothing else) @@ -134,8 +142,9 @@ export const translateAndRefine = task({ }; } - // If not approved, recursively call the task with feedback - await translateAndRefine + // If not approved, recursively refine with feedback and return the + // refined result so the final translation propagates back up. + return await translateAndRefine .triggerAndWait({ text: payload.text, targetLanguage: payload.targetLanguage, diff --git a/docs/guides/ai-agents/verify-news-article.mdx b/docs/guides/ai-agents/verify-news-article.mdx index e235ae9d35c..e6bfe076c36 100644 --- a/docs/guides/ai-agents/verify-news-article.mdx +++ b/docs/guides/ai-agents/verify-news-article.mdx @@ -16,18 +16,18 @@ Our example task uses multiple LLM calls to extract claims from a news article a **This task:** -- Uses `generateText` from [Vercel's AI SDK](https://sdk.vercel.ai/docs/introduction) to interact with OpenAI models -- Uses `experimental_telemetry` to provide LLM logs +- Uses `generateText` from the [AI SDK](https://ai-sdk.dev/) to call Anthropic's Claude models +- Uses `experimental_telemetry` to surface each LLM call on the Run page in the dashboard - Uses [`batch.triggerByTaskAndWait`](/triggering#batch-triggerbytaskandwait) to orchestrate parallel processing of claims -- Extracts factual claims from news articles using the `o1-mini` model +- Extracts factual claims from news articles using `claude-sonnet-4-5` - Evaluates claims against recent sources and analyzes historical context in parallel - Combines results into a structured analysis report ```typescript -import { openai } from "@ai-sdk/openai"; +import { anthropic } from "@ai-sdk/anthropic"; import { batch, logger, task } from "@trigger.dev/sdk"; -import { CoreMessage, generateText } from "ai"; +import { ModelMessage, generateText } from "ai"; // Define types for our workers' outputs interface Claim { @@ -53,7 +53,7 @@ export const extractClaims = task({ id: "extract-claims", run: async ({ article }: { article: string }) => { try { - const messages: CoreMessage[] = [ + const messages: ModelMessage[] = [ { role: "system", content: @@ -66,8 +66,12 @@ export const extractClaims = task({ ]; const response = await generateText({ - model: openai("o1-mini"), + model: anthropic("claude-sonnet-4-5"), messages, + experimental_telemetry: { + isEnabled: true, + functionId: "extract-claims", + }, }); const claims = response.text @@ -94,7 +98,7 @@ export const verifySource = task({ id: "verify-source", run: async (claim: Claim) => { const response = await generateText({ - model: openai("o1-mini"), + model: anthropic("claude-sonnet-4-5"), messages: [ { role: "system", @@ -126,7 +130,7 @@ export const analyzeHistory = task({ id: "analyze-history", run: async (claim: Claim) => { const response = await generateText({ - model: openai("o1-mini"), + model: anthropic("claude-sonnet-4-5"), messages: [ { role: "system", diff --git a/docs/guides/introduction.mdx b/docs/guides/introduction.mdx index 116c8539b0d..aebad856f64 100644 --- a/docs/guides/introduction.mdx +++ b/docs/guides/introduction.mdx @@ -23,6 +23,7 @@ Get set up fast using our detailed walk-through guides. | Guide | Description | | :----------------------------------------------------------------------------------------- | :------------------------------------------------------------------- | +| [AI Agent: Chat agent](/guides/ai-agents/chat-agent) | Build a durable, multi-turn chat agent with `chat.agent()` | | [AI Agent: Content moderation](/guides/ai-agents/respond-and-check-content) | Parallel check content while responding to customers | | [AI Agent: Generate and translate copy](/guides/ai-agents/generate-translate-copy) | Chain prompts to generate and translate content | | [AI Agent: News verification](/guides/ai-agents/verify-news-article) | Orchestrate fact checking of news articles | @@ -56,6 +57,7 @@ Example projects are full projects with example repos you can fork and use. Thes | [Claude changelog generator](/guides/example-projects/claude-changelog-generator) | Automatically generate professional changelogs from git commits using Claude. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/changelog-generator) | | [Claude GitHub wiki agent](/guides/example-projects/claude-github-wiki) | Generate and maintain GitHub wiki documentation with Claude-powered analysis. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/claude-agent-github-wiki) | | [Claude thinking chatbot](/guides/example-projects/claude-thinking-chatbot) | Use Vercel's AI SDK and Anthropic's Claude 3.7 model to create a thinking chatbot. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/claude-thinking-chatbot) | +| [ClickHouse chat agent](/guides/example-projects/clickhouse-chat-agent) | Build a chat agent that answers questions about your ClickHouse data with charts, tables and maps using `chat.agent()` and generative UI. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/clickhouse-chat-agent) | | [Cursor background agent](/guides/example-projects/cursor-background-agent) | Run Cursor's headless CLI agent as a background task, streaming live output to the browser. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/cursor-cli-demo) | | [Human-in-the-loop workflow](/guides/example-projects/human-in-the-loop-workflow) | Create audio summaries of newspaper articles using a human-in-the-loop workflow built with ReactFlow and Trigger.dev waitpoint tokens. | Next.js | [View the repo](https://github.com/triggerdotdev/examples/tree/main/article-summary-workflow) | | [Mastra agents with memory](/guides/example-projects/mastra-agents-with-memory) | Use Mastra to create a weather agent that can collect live weather data and generate clothing recommendations. | — | [View the repo](https://github.com/triggerdotdev/examples/tree/main/mastra-agents) |