Zbieranie i analiza danych z Agents Platform w Next.js

Zbieraj i analizuj dane w webhookach po rozmowie z użyciem Agents Platform i Next.js.

Samouczek · Zakłada, że masz już za sobą szybki start z ElevenAgents i skonfigurowany projekt Next.js.

Wprowadzenie

W tym samouczku dowiesz się, jak stworzyć agenta głosowego, który zbiera informacje od użytkownika podczas rozmowy, a następnie analizuje je, wyodrębnia dane w ustrukturyzowany sposób i wysyła je do aplikacji przez webhook po rozmowie.

Wymagania

  • Konto ElevenLabs z kluczem API.
  • Node.js v18 lub nowszy zainstalowany na twoim komputerze.

Konfiguracja

Utwórz nowy projekt Next.js

Zalecamy użycie naszego szablonu Agents Platform na v0.dev jako punktu wyjścia dla aplikacji. To gotowa do wdrożenia aplikacja Next.js z już zintegrowanym agentem ElevenLabs.

Skonfiguruj Agents Platform

Wykonaj kroki instalacji i konfiguracji z naszego przewodnika po Next.js. Następnie wróć tutaj, aby dodać zaawansowane funkcje.

Konfiguracja agenta

1

Zaloguj się do ElevenLabs

Przejdź na elevenlabs.io i zaloguj się na swoje konto.

2

Utwórz nowego agenta

Przejdź do Agents Platform > Agents i utwórz nowego agenta z pustego szablonu.

3

Ustaw pierwszą wiadomość

Ustaw pierwszą wiadomość i określ zmienną dynamiczną platformy.

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

Ustaw prompt systemowy

Ustaw prompt systemowy. Możesz też dodać tu zmienne dynamiczne.

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

Skonfiguruj narzędzia klienta

Skonfiguruj poniższe narzędzie klienta do przechodzenia między krokami:

  • Nazwa: set_ui_state
    • Opis: Użyj tego narzędzia po stronie klienta, aby przechodzić między różnymi stanami interfejsu.
    • Czekaj na odpowiedź: true
    • Limit czasu odpowiedzi (sekundy): 1
    • Parametry:
      • Typ danych: string
      • Identyfikator: step
      • Wymagane: true
      • Typ wartości: LLM Prompt
      • Opis: Krok, do którego ma przejść interfejs. Używaj tylko kroków zdefiniowanych w prompcie systemowym!
6

Ustaw głos agenta

Przejdź do karty Voice i ustaw głos dla agenta. Listę polecanych głosów dla Agents Platform znajdziesz w dokumentacji Conversational Voice Design.

7

Ustaw kryteria oceny

Przejdź do karty Analysis i dodaj nowe kryterium oceny.

  • Nazwa: all_data_provided
    • Prompt: Oceń, czy użytkownik podał opis agenta, którego chce stworzyć, oraz opis głosu, jaki powinien mieć agent.
8

Skonfiguruj zbieranie danych

Możesz użyć analizy po rozmowie, aby wyodrębnić dane z konwersacji. W karcie Analysis, w sekcji Data Collection, dodaj poniższe elementy:

  • Identyfikator: voice_description
    • data-type: String
    • Opis: Na podstawie opisu głosu, jaki użytkownik chce nadać agentowi, wygeneruj zwięzły opis głosu, uwzględniając wiek, akcent, ton i charakter, jeśli są dostępne.
  • Identyfikator: agent_description
    • data-type: String
    • Opis: Na podstawie opisu agenta, którego użytkownik chce zaprojektować, wygeneruj prompt, który można wykorzystać do wytrenowania modelu działającego jako ten agent.
9

Skonfiguruj webhook po rozmowie

Webhooki po rozmowie służą do powiadamiania cię, gdy rozmowa się kończy oraz gdy analiza i wyodrębnianie danych zostaną ukończone.

W tym przykładzie webhook po rozmowie wykonuje kilka kroków:

  1. Tworzy własny Voice Design na podstawie voice_description.
  2. Tworzy agenta ElevenLabs dla użytkowników na podstawie podanego przez nich agent_description.
  3. Pobiera dokumenty bazy wiedzy ze stanu rozmowy zapisanego w Redis i dołącza bazę wiedzy do agenta.
  4. Wysyła użytkownikowi e-mail z informacją, że jego własny agent ElevenLabs jest gotowy do rozmowy.

Podczas pracy lokalnie potrzebujesz narzędzia takiego jak ngrok, aby udostępnić lokalny serwer w internecie.

ngrok http 3000

Przejdź do ustawień Agents Platform i w sekcji Post-Call Webhook utwórz nowy webhook, a następnie wklej adres URL ngrok: https://<your-url>.ngrok-free.app/api/convai-webhook.

Po zapisaniu webhooka otrzymasz sekret webhooków. Przechowuj go bezpiecznie, ponieważ później musisz ustawić go w pliku .env.

Zintegruj zaawansowane funkcje

Skonfiguruj bazę danych Redis do przechowywania stanu rozmowy

W tym przykładzie używamy Redis do przechowywania stanu rozmowy. Dzięki temu po zakończeniu rozmowy możemy pobrać dokumenty bazy wiedzy ze stanu rozmowy.

Jeśli wdrażasz aplikację na Vercel, możesz skonfigurować integrację Upstash for Redis albo założyć darmowe konto Upstash i utworzyć nową bazę danych.

Skonfiguruj Resend do wysyłania e-maili po rozmowie

W tym przykładzie używamy Resend do wysyłania użytkownikowi e-maila po rozmowie. Aby to zrobić, musisz utworzyć darmowe konto Resend i skonfigurować nowy klucz API.

Ustaw zmienne środowiskowe

W katalogu głównym projektu utwórz plik .env i dodaj poniższe zmienne:

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=

Skonfiguruj zabezpieczenia i uwierzytelnianie

Aby zabezpieczyć agenta ElevenLabs, włącz uwierzytelnianie na karcie Security w konfiguracji agenta.

Po włączeniu uwierzytelniania musisz utworzyć podpisany URL w bezpiecznym środowisku po stronie serwera, aby rozpocząć rozmowę z agentem. W Next.js możesz to zrobić, konfigurując nową trasę 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 });
}
}

Rozpocznij sesję rozmowy

Aby rozpocząć rozmowę, najpierw wywołaj trasę API, aby uzyskać podpisany URL, a następnie użyj hooka useConversation, aby skonfigurować sesję rozmowy.

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

Narzędzie klienta i zmienne dynamiczne

W konfiguracji agenta wcześniej zarejestrowałeś narzędzie klienta set_ui_state, aby umożliwić agentowi przechodzenie między różnymi stanami interfejsu. Aby wszystko połączyć, musisz przekazać implementację narzędzia klienta w opcjach conversation.startSession.

W tym miejscu możesz też przekazać do rozmowy zmienne dynamiczne.

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

Przesyłanie dokumentów do bazy wiedzy

Na etapie Training agent poprosi użytkownika o przesłanie dokumentów lub podanie adresów URL publicznych stron z informacjami, które powinny być dostępne dla jego agenta. Możesz tu użyć nowej funkcji after z Next.js 15, aby umożliwić przesyłanie dokumentów w tle.

Utwórz nową akcję serwera upload, która obsłuży tworzenie bazy wiedzy po wysłaniu formularza. Gdy wszystkie dokumenty bazy wiedzy zostaną utworzone, zapisz identyfikator rozmowy i identyfikatory bazy wiedzy w bazie 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");
}

Obsługa webhooka po rozmowie

Webhook po rozmowie uruchamia się po zakończeniu rozmowy oraz zakończeniu analizy i ekstrakcji danych.

Dzieje się tu kilka rzeczy:

  1. Weryfikacja sekretu webhooka i utworzenie ładunku webhooka.
  2. Utworzenie własnego projektu głosu na podstawie voice_description.
  3. Utworzenie agenta ElevenLabs dla użytkownika na podstawie podanego przez niego agent_description.
  4. Pobranie dokumentów bazy wiedzy ze stanu rozmowy zapisanego w Redis i dołączenie bazy wiedzy do agenta.
  5. Wysłanie użytkownikowi e-maila z informacją, że jego własny agent ElevenLabs jest gotowy do rozmowy.
./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;
}

Omówmy każdy krok szczegółowo.

Zweryfikuj sekret webhooka i utwórz ładunek webhooka

Po otrzymaniu żądania webhooka najpierw weryfikujemy jego sekret i tworzymy ładunek webhooka.

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

Utwórz własny projekt głosu na podstawie voice_description

Na podstawie voice_description z ładunku webhooka tworzymy własny projekt głosu.

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

Pobierz dokumenty bazy wiedzy ze stanu rozmowy zapisanego w Redis

Przesyłanie dokumentów może trwać dłużej niż analiza danych webhooka, więc musimy odpytywać stan rozmowy w Redis, aż dokumenty zostaną przesłane.

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

Utwórz agenta ElevenLabs dla użytkownika na podstawie podanego agent_description

Utwórz agenta ElevenLabs dla użytkownika na podstawie podanego przez niego agent_description i dołącz do niego nowy projekt głosu oraz bazę wiedzy.

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

Wyślij użytkownikowi e-mail z informacją, że jego własny agent ElevenLabs jest gotowy do rozmowy

Gdy agent zostanie utworzony, możesz wysłać użytkownikowi e-mail z informacją, że jego własny agent ElevenLabs jest gotowy do rozmowy.

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

Możesz użyć new.email, przydatnego narzędzia zespołu Resend, do swobodnego projektowania szablonów e-maili. Gdy szablon będzie gotowy, utwórz nowy komponent i dodaj identyfikator agenta jako 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 };

Uruchom aplikację

Aby uruchomić aplikację lokalnie end-to-end, najpierw uruchom serwer deweloperski Next.js, a potem w osobnym terminalu tunel ngrok, aby udostępnić handler webhooka w internecie.

  • Terminal 1:
    • Uruchom pnpm dev, aby włączyć serwer deweloperski Next.js.
pnpm dev
  • Terminal 2:
    • Uruchom ngrok http 3000, aby udostępnić handler webhooka w internecie.
ngrok http 3000

Teraz otwórz http://localhost:3000 i zacznij projektować własnego agenta ElevenLabs swoim głosem!

Kolejne kroki