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フックを使用して会話セッションを設定します。

./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]);
// ...
}

クライアントツールと動的変数

前述のエージェント設定では、エージェントが異なるUI状態間を移動できるように、set_ui_stateクライアントツールを登録しました。すべてを連携させるには、クライアントツールの実装を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ステップでは、エージェントがユーザーに対し、ドキュメントをアップロードするか、エージェントが利用できるようにすべき情報を含む公開Webサイトの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をpropsとして追加します。

./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エージェントをデザインしましょう!

次のステップ