在 Next.js 中使用 Agents Platform 收集和分析数据

使用 Agents Platform 和 Next.js 在通话后 webhook 中收集和分析数据。

教程 · 假设你已完成 ElevenAgents 快速入门,并已设置 Next.js 项目。

简介

本教程将介绍如何构建语音智能体,通过对话收集用户信息,然后以结构化方式分析和提取数据,并通过通话后 webhook 将数据发送到应用。

要求

  • 拥有 API 密钥的 ElevenLabs 账户。
  • 本机已安装 Node.js v18 或更高版本。

设置

创建新的 Next.js 项目

建议使用我们的 v0.dev Agents Platform 模板作为应用的起点。该模板是已集成 ElevenLabs 智能体、可用于生产环境的 Next.js 应用。

设置 Agents Platform

按照我们的 Next.js 指南完成安装和配置步骤。然后返回这里构建高级功能。

智能体配置

1

登录 ElevenLabs

前往 elevenlabs.io 并登录账户。

2

创建新智能体

前往 Agents Platform > Agents, 从空白模板创建新智能体。

3

设置首条消息

设置首条消息并指定平台的动态变量。

Hi {{user_name}}, I'm Jess from the ElevenLabs team. I'm here to help you design your very own ElevenLabs agent! To kick things off, let me know what kind of agent you're looking to create. For example, do you want a support agent, to help your users answer questions, or a sales agent to sell your products, or just a friend to chat with?
4

设置系统提示词

设置系统提示词。也可以在此处加入动态变量。

You are Jess, a helpful agent helping {{user_name}} to design their very own ElevenLabs agent. The design process involves the following steps:
"initial": In the first step, collect the information about the kind of agent the user is looking to create. Summarize the user's needs back to them and ask if they are ready to continue to the next step. Only once they confirm proceed to the next step.
"training": Tell the user to create the agent's knowledge base by uploading documents, or submitting URLs to public websites with information that should be available to the agent. Wait patiently without talking to the user. Only when the user confirms that they've provided everything then proceed to the next step.
"voice": Tell the user to describe the voice they want their agent to have. For example: "A professional, strong spoken female voice with a slight British accent." Repeat the description of their voice back to them and ask if they are ready to continue to the next step. Only once they confirm proceed to the next step.
"email": Tell the user that we've collected all necessary information to create their ElevenLabs agent and ask them to provide their email address to get notified when the agent is ready.
Always call the `set_ui_state` tool when moving between steps!
5

设置客户端工具

设置以下客户端工具,以便在各步骤之间导航:

  • 名称:set_ui_state
    • 描述:使用此客户端工具在不同 UI 状态之间导航。
    • 等待响应:true
    • 响应超时(秒):1
    • 参数:
      • 数据类型:string
      • 标识符:step
      • 必填:true
      • 值类型:LLM Prompt
      • 描述:要在 UI 中导航至的步骤。仅使用系统提示词中定义的步骤!
6

设置智能体音色

前往 Voice 标签页,为智能体设置音色。可在对话音色设计文档中查看 Agents Platform 推荐音色列表。

7

设置评估标准

前往 Analysis 标签页,添加新的评估标准。

  • 名称:all_data_provided
    • 提示词:评估用户是否提供了想要生成的智能体描述,以及该智能体应具备的音色描述。
8

配置数据收集

可使用通话后分析从对话中提取数据。在 Analysis 标签页的 Data Collection 下,添加以下项目:

  • 标识符:voice_description
    • data-type:String
    • 描述:根据用户希望智能体具备的音色描述,生成简洁的音色描述,包括年龄、口音、语调和角色特点(如有)。
  • 标识符:agent_description
    • data-type:String
    • 描述:根据用户希望设计的智能体描述,生成可用于训练模型扮演该智能体的提示词。
9

配置通话后 webhook

通话后 webhook 用于在通话结束且分析和数据提取完成后通知你。

在此示例中,通话后 webhook 会执行以下几个步骤:

  1. 根据 voice_description 创建自定义音色设计。
  2. 根据用户提供的 agent_description 为用户创建 ElevenLabs 智能体。
  3. 从 Redis 中存储的对话状态获取知识库文档,并将知识库附加到智能体。
  4. 向用户发送电子邮件,通知其自定义 ElevenLabs 智能体已可开始对话。

在本地运行时,需要使用 ngrok 等工具将本地服务器暴露到互联网。

ngrok http 3000

前往 Agents Platform 设置,在 Post-Call Webhook 下创建新的 webhook,并粘贴 ngrok URL:https://<your-url>.ngrok-free.app/api/convai-webhook。

保存 webhook 后,会收到 webhook 密钥。请妥善保管该密钥,稍后需要在 .env 文件中设置。

集成高级功能

设置用于存储对话状态的 Redis 数据库

本示例使用 Redis 存储对话状态。这样可以在通话结束后从对话状态中获取知识库文档。

如果要部署到 Vercel,可以配置 Upstash for Redis 集成,或者注册免费的 Upstash 账户并创建新数据库。

设置 Resend 以发送通话后电子邮件

本示例使用 Resend 向用户发送通话后电子邮件。为此,需要创建免费的 Resend 账户并设置新的 API 密钥。

设置环境变量

在项目根目录创建 .env 文件,并添加以下变量:

ELEVENLABS_CONVAI_WEBHOOK_SECRET=
ELEVENLABS_API_KEY=
ELEVENLABS_AGENT_ID=
# Resend
RESEND_API_KEY=
RESEND_FROM_EMAIL=
# Upstash Redis
KV_URL=
KV_REST_API_READ_ONLY_TOKEN=
REDIS_URL=
KV_REST_API_TOKEN=
KV_REST_API_URL=

配置安全性和身份验证

要保护 ElevenLabs 智能体,需要在智能体配置的 Security 标签页中启用身份验证。

启用身份验证后,需要在安全的服务器端环境中创建签名 URL,以便与智能体发起对话。在 Next.js 中,可以通过设置新的 API 路由实现。

./app/api/signed-url/route.ts
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import { NextResponse } from "next/server";
export async function GET() {
const agentId = process.env.ELEVENLABS_AGENT_ID;
if (!agentId) {
throw Error("ELEVENLABS_AGENT_ID is not set");
}
try {
const elevenlabs = new ElevenLabsClient();
const response = await elevenlabs.conversationalAi.conversations.getSignedUrl({
agentId,
});
return NextResponse.json({ signedUrl: response.signedUrl });
} catch (error) {
console.error("Error:", error);
return NextResponse.json({ error: "Failed to get signed URL" }, { status: 500 });
}
}

开始对话会话

要开始对话,请先调用 API 路由获取签名 URL,然后使用 useConversation Hook 设置对话会话。

./page.tsx
import { useConversation } from "@elevenlabs/react";
async function getSignedUrl(): Promise<string> {
const response = await fetch("/api/signed-url");
if (!response.ok) {
throw Error("Failed to get signed url");
}
const data = await response.json();
return data.signedUrl;
}
export default function Home() {
// ...
const [currentStep, setCurrentStep] = useState<
"initial" | "training" | "voice" | "email" | "ready"
>("initial");
const [conversationId, setConversationId] = useState("");
const [userName, setUserName] = useState("");
const conversation = useConversation({
onConnect: () => console.log("Connected"),
onDisconnect: () => console.log("Disconnected"),
onMessage: (message: string) => console.log("Message:", message),
onError: (error: Error) => console.error("Error:", error),
});
const startConversation = useCallback(async () => {
try {
// Request microphone permission
await navigator.mediaDevices.getUserMedia({ audio: true });
// Start the conversation with your agent
const signedUrl = await getSignedUrl();
const convId = await conversation.startSession({
signedUrl,
dynamicVariables: {
user_name: userName,
},
clientTools: {
set_ui_state: ({ step }: { step: string }): string => {
// Allow agent to navigate the UI.
setCurrentStep(step as "initial" | "training" | "voice" | "email" | "ready");
return `Navigated to ${step}`;
},
},
});
setConversationId(convId);
console.log("Conversation ID:", convId);
} catch (error) {
console.error("Failed to start conversation:", error);
}
}, [conversation, userName]);
const stopConversation = useCallback(async () => {
await conversation.endSession();
}, [conversation]);
// ...
}

客户端工具和动态变量

在前面的智能体配置中,已注册 set_ui_state 客户端工具,让智能体能够在不同 UI 状态之间导航。要将其整合起来,需要将客户端工具实现传递给 conversation.startSession 选项。

此处也可将动态变量传入对话。

./page.tsx
const convId = await conversation.startSession({
signedUrl,
dynamicVariables: {
user_name: userName,
},
clientTools: {
set_ui_state: ({ step }: { step: string }): string => {
// Allow agent to navigate the UI.
setCurrentStep(step as "initial" | "training" | "voice" | "email" | "ready");
return `Navigated to ${step}`;
},
},
});

将文档上传到知识库

在 Training 步骤中,智能体会要求用户上传文档,或提交包含智能体所需信息的公开网站 URL。这里可以利用 Next.js 15 新增的 after 函数,在后台上传文档。

创建新的 upload 服务器操作,以便在提交表单时处理知识库创建。创建所有知识库文档后,将对话 ID 和知识库 ID 存储到 Redis 数据库中。

./app/actions/upload.ts
"use server";
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import { Redis } from "@upstash/redis";
import { redirect } from "next/navigation";
import { after } from "next/server";
// Initialize Redis
const redis = Redis.fromEnv();
const elevenlabs = new ElevenLabsClient({
apiKey: process.env.ELEVENLABS_API_KEY,
});
export async function uploadFormData(formData: FormData) {
const knowledgeBase: Array<{
id: string;
type: "file" | "url";
name: string;
}> = [];
const files = formData.getAll("file-upload") as File[];
const email = formData.get("email-input");
const urls = formData.getAll("url-input");
const conversationId = formData.get("conversation-id");
after(async () => {
// Upload files as background job
// Create knowledge base entries
// Loop through files and create knowledge base entries
for (const file of files) {
if (file.size > 0) {
const response = await elevenlabs.conversationalAi.knowledgeBase.documents.createFromFile({
file,
});
if (response.id) {
knowledgeBase.push({
id: response.id,
type: "file",
name: file.name,
});
}
}
}
// Append all urls
for (const url of urls) {
const response = await elevenlabs.conversationalAi.knowledgeBase.documents.createFromUrl({
url: url as string,
});
if (response.id) {
knowledgeBase.push({
id: response.id,
type: "url",
name: `url for ${conversationId}`,
});
}
}
// Store knowledge base IDs and conversation ID in database.
const redisRes = await redis.set(
conversationId as string,
JSON.stringify({ email, knowledgeBase })
);
console.log({ redisRes });
});
redirect("/success");
}

处理通话后 webhook

通话结束且分析和数据提取完成后,会触发通话后 webhook。

这里会执行以下几个步骤:

  1. 验证 webhook 密钥并构建 webhook 载荷。
  2. 根据 voice_description 创建自定义音色设计。
  3. 根据用户提供的 agent_description 为用户创建 ElevenLabs 智能体。
  4. 从 Redis 中存储的对话状态获取知识库文档,并将知识库附加到智能体。
  5. 向用户发送电子邮件,通知其自定义 ElevenLabs 智能体已可开始对话。
./app/api/convai-webhook/route.ts
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import { Redis } from "@upstash/redis";
import crypto from "crypto";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import { Resend } from "resend";
import { EmailTemplate } from "@/components/email/post-call-webhook-email";
// Initialize Redis
const redis = Redis.fromEnv();
// Initialize Resend
const resend = new Resend(process.env.RESEND_API_KEY);
const elevenlabs = new ElevenLabsClient({
apiKey: process.env.ELEVENLABS_API_KEY,
});
export async function GET() {
return NextResponse.json({ status: "webhook listening" }, { status: 200 });
}
export async function POST(req: NextRequest) {
const secret = process.env.ELEVENLABS_CONVAI_WEBHOOK_SECRET; // Add this to your env variables
const { event, error } = await constructWebhookEvent(req, secret);
if (error) {
return NextResponse.json({ error: error }, { status: 401 });
}
if (event.type === "post_call_transcription") {
const { conversation_id, analysis, agent_id } = event.data;
if (
agent_id === process.env.ELEVENLABS_AGENT_ID &&
analysis.evaluation_criteria_results.all_data_provided?.result === "success" &&
analysis.data_collection_results.voice_description?.value
) {
try {
// Design the voice
const voicePreview = await elevenlabs.textToVoice.createPreviews({
voiceDescription: analysis.data_collection_results.voice_description.value,
text: "The night air carried whispers of betrayal, thick as London fog. I adjusted my cufflinks - after all, even spies must maintain appearances, especially when the game is afoot.",
});
const voice = await elevenlabs.textToVoice.createVoiceFromPreview({
voiceName: `voice-${conversation_id}`,
voiceDescription: `Voice for ${conversation_id}`,
generatedVoiceId: voicePreview.previews[0].generatedVoiceId,
});
// Get the knowledge base from redis
const redisRes = await getRedisDataWithRetry(conversation_id);
if (!redisRes) throw new Error("Conversation data not found!");
// Handle agent creation
const agent = await elevenlabs.conversationalAi.agents.create({
name: `Agent for ${conversation_id}`,
conversationConfig: {
tts: { voiceId: voice.voiceId },
agent: {
prompt: {
prompt:
analysis.data_collection_results.agent_description?.value ??
"You are a helpful assistant.",
knowledgeBase: redisRes.knowledgeBase,
},
firstMessage: "Hello, how can I help you today?",
},
},
});
console.log("Agent created", { agent: agent.agentId });
// Send email to user
console.log("Sending email to", redisRes.email);
await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL!,
to: redisRes.email,
subject: "Your ElevenLabs agent is ready to chat!",
react: EmailTemplate({ agentId: agent.agentId }),
});
} catch (error) {
console.error(error);
return NextResponse.json({ error }, { status: 500 });
}
}
}
return NextResponse.json({ received: true }, { status: 200 });
}
const constructWebhookEvent = async (req: NextRequest, secret?: string) => {
const body = await req.text();
const signatureHeader = req.headers.get("ElevenLabs-Signature");
return await elevenlabs.webhooks.constructEvent(body, signatureHeader, secret);
};
async function getRedisDataWithRetry(
conversationId: string,
maxRetries = 5
): Promise<{
email: string;
knowledgeBase: Array<{
id: string;
type: "file" | "url";
name: string;
}>;
} | null> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const data = await redis.get(conversationId);
return data as any;
} catch (error) {
if (attempt === maxRetries) throw error;
console.log(`Redis get attempt ${attempt} failed, retrying...`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
return null;
}

下面详细介绍每个步骤。

验证 webhook 密钥并构建 webhook 载荷

收到 webhook 请求后,首先验证 webhook 密钥并构建 webhook 载荷。

./app/api/convai-webhook/route.ts
// ...
export async function POST(req: NextRequest) {
const secret = process.env.ELEVENLABS_CONVAI_WEBHOOK_SECRET;
const { event, error } = await constructWebhookEvent(req, secret);
// ...
}
// ...
const constructWebhookEvent = async (req: NextRequest, secret?: string) => {
const body = await req.text();
const signatureHeader = req.headers.get("ElevenLabs-Signature");
return await elevenlabs.webhooks.constructEvent(body, signatureHeader, secret);
};
async function getRedisDataWithRetry(
conversationId: string,
maxRetries = 5
): Promise<{
email: string;
knowledgeBase: Array<{
id: string;
type: "file" | "url";
name: string;
}>;
} | null> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const data = await redis.get(conversationId);
return data as any;
} catch (error) {
if (attempt === maxRetries) throw error;
console.log(`Redis get attempt ${attempt} failed, retrying...`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
return null;
}

根据 voice_description 创建自定义音色设计

使用 webhook 载荷中的 voice_description 创建自定义音色设计。

./app/api/convai-webhook/route.ts
// ...
// Design the voice
const voicePreview = await elevenlabs.textToVoice.createPreviews({
voiceDescription: analysis.data_collection_results.voice_description.value,
text: "The night air carried whispers of betrayal, thick as London fog. I adjusted my cufflinks - after all, even spies must maintain appearances, especially when the game is afoot.",
});
const voice = await elevenlabs.textToVoice.createVoiceFromPreview({
voiceName: `voice-${conversation_id}`,
voiceDescription: `Voice for ${conversation_id}`,
generatedVoiceId: voicePreview.previews[0].generatedVoiceId,
});
// ...

从 Redis 中存储的对话状态获取知识库文档

文档上传可能比 webhook 数据分析耗时更长,因此需要轮询 Redis 中的对话状态,直到文档上传完成。

./app/api/convai-webhook/route.ts
// ...
// Get the knowledge base from redis
const redisRes = await getRedisDataWithRetry(conversation_id);
if (!redisRes) throw new Error("Conversation data not found!");
// ...
async function getRedisDataWithRetry(
conversationId: string,
maxRetries = 5
): Promise<{
email: string;
knowledgeBase: Array<{
id: string;
type: "file" | "url";
name: string;
}>;
} | null> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const data = await redis.get(conversationId);
return data as any;
} catch (error) {
if (attempt === maxRetries) throw error;
console.log(`Redis get attempt ${attempt} failed, retrying...`);
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
return null;
}

根据用户提供的 agent_description 为用户创建 ElevenLabs 智能体

根据用户提供的 agent_description 为用户创建 ElevenLabs 智能体,并将新创建的音色设计和知识库附加到智能体。

./app/api/convai-webhook/route.ts
// ...
// Handle agent creation
const agent = await elevenlabs.conversationalAi.agents.create({
name: `Agent for ${conversationId}`,
conversationConfig: {
tts: { voiceId: voice.voiceId },
agent: {
prompt: {
prompt:
analysis.data_collection_results.agent_description?.value ??
"You are a helpful assistant.",
knowledgeBase: redisRes.knowledgeBase,
},
firstMessage: "Hello, how can I help you today?",
},
},
});
console.log("Agent created", { agent: agent.agentId });
// ...

向用户发送电子邮件,通知其自定义 ElevenLabs 智能体已可开始对话

智能体创建后,可以向用户发送电子邮件,通知其自定义 ElevenLabs 智能体已可开始对话。

./app/api/convai-webhook/route.ts
import { Resend } from "resend";
import { EmailTemplate } from "@/components/email/post-call-webhook-email";
// ...
// Send email to user
console.log("Sending email to", redisRes.email);
await resend.emails.send({
from: process.env.RESEND_FROM_EMAIL!,
to: redisRes.email,
subject: "Your ElevenLabs agent is ready to chat!",
react: EmailTemplate({ agentId: agent.agentId }),
});
// ...

可以使用 Resend 团队推出的便捷工具 new.email 轻松设计电子邮件模板。模板满意后,创建新组件并将智能体 ID 作为 prop 添加。

./components/email/post-call-webhook-email.tsx
import {
Body,
Button,
Container,
Head,
Html,
Section,
Text,
Tailwind,
} from "@react-email/components";
import * as React from "react";
const EmailTemplate = (props: any) => {
const { agentId } = props;
return (
<Html>
<Head />
<Tailwind>
<Body className="bg-[#151516] font-sans">
<Container className="mx-auto my-[40px] max-w-[600px] rounded-[8px] bg-[#0a1929] p-[20px]">
{/* Top Section */}
<Section className="mb-[32px] mt-[32px] text-center">
<Text className="m-0 text-[28px] font-bold text-[#9c27b0]">
Your ElevenLabs agent is ready to chat!
</Text>
</Section>
{/* Content Area with Icon */}
<Section className="mb-[32px] text-center">
{/* Circle Icon with Checkmark */}
<div className="mx-auto mb-[24px] flex h-[80px] w-[80px] items-center justify-center rounded-full bg-gradient-to-r from-[#9c27b0] to-[#3f51b5]">
<div className="text-[40px] text-white">✓</div>
</div>
{/* Descriptive Text */}
<Text className="mb-[24px] text-[18px] text-white">
Your ElevenLabs agent is ready to chat!
</Text>
</Section>
{/* Call to Action Button */}
<Section className="mb-[32px] text-center">
<Button
href={`https://elevenlabs.io/app/talk-to?agent_id=${agentId}`}
className="box-border rounded-[8px] bg-[#9c27b0] px-[40px] py-[20px] text-[24px] font-bold text-white no-underline"
>
Chat now!
</Button>
</Section>
{/* Footer */}
<Section className="mt-[40px] border-t border-[#2d3748] pt-[20px] text-center">
<Text className="m-0 text-[14px] text-white">
Powered by{" "}
<a
href="https://elevenlabs.io/conversational-ai"
target="_blank"
rel="noopener noreferrer"
className="underline transition-colors hover:text-gray-400"
>
ElevenLabs Agents
</a>
</Text>
</Section>
</Container>
</Body>
</Tailwind>
</Html>
);
};
export { EmailTemplate };

运行应用

要在本地端到端运行应用,首先需要启动 Next.js 开发服务器,然后在单独的终端中运行 ngrok 隧道,将 webhook 处理程序暴露到互联网。

  • 终端 1:
    • 运行 pnpm dev 启动 Next.js 开发服务器。
pnpm dev
  • 终端 2:
    • 运行 ngrok http 3000,将 webhook 处理程序暴露到互联网。
ngrok http 3000

现在打开 http://localhost:3000,开始使用自己的音色设计自定义 ElevenLabs 智能体!

后续步骤