Voice agents
エージェントを電話やブラウザの音声会話に参加させ、バックエンドからそれらの通話を活用します。
Voice Agents を使う理由
エージェントはすでにチャットで質問に答え、アクションを実行しています。次に人々はそれへ電話をかけたいと考えます。たとえば予約を取りたい患者、配送状況を確認したい顧客、入力より会話を好むサイト訪問者です。音声機能をゼロから構築するには、telephony webhook、speech recognition、speech synthesis、割り込み処理、モデルにツールを提供する仕組みが必要であり、しかもすべてリアルタイムで行う必要があります。
Squid では、すでにある同じ agent definition を使ってエージェントに音声を与えられます。ブラウザ会話は 1 回の呼び出しです。
const call = await squid.ai().voice().startWebCall({ agentId: 'front-desk' });
call.onTurn((turn) => console.log(`${turn.role}: ${turn.text}`));
電話番号は connector と Agent Studio 内のいくつかの設定だけで利用でき、コードは不要です。また、通話に関するすべての情報がバックエンドに届きます。進行中の transcript、call record、受信テキスト、AI function 内の通話 context です。
概要
音声会話は、音声のフロントエンドを持つ通常の agent conversation です。相手が話し、エージェントはテキストを受け取り、その回答が音声で返されます。エージェントは instructions、knowledge bases、connectors、AI functions、memory、security rules を維持します。変わるのは、聞くことと話すことを担う voice engine です。
| Engine | 実行場所 |
|---|---|
| Twilio speech (Twilio connector) | Twilio が発信者を transcription し、エージェントの streamed replies を音読します。エージェントは発話ごとに応答します。 |
| OpenAI Voice (OpenAI Voice connector) | OpenAI realtime model が話し聞き取り、knowledge や action が必要になるたびにエージェントを tool として呼び出します。ブラウザ通話も提供します。 |
| ElevenLabs (ElevenLabs connector) | ElevenLabs voices を持つ ElevenLabs agent が通話をホストし、Squid agent を LLM として使用します。 |
Voice connectors overview では engine を比較しています。このページではコードで行うことを説明します。
Voice agents を使う場面
| Use Case | 推奨事項 |
|---|---|
| 電話の応答と発信、テキストの送受信 | エージェントに設定した Twilio phone line |
| 電話番号なしで web page からエージェントと会話する | OpenAI Voice connector を使用した startWebCall() |
| 通話の記録・分析、staff への通知、CRM の更新 | voice session event handler |
| エージェントによる発信者認識、本人確認、予約 | call context を使用する AI functions |
| audio file をテキストへ、またはテキストを audio file へ変換する | live call ではなく AI audio |
| chat widget での音声入力 | enable-transcription を使用する AI chat widget |
仕組み
- 通話が開始されます。Twilio がエージェントの番号に着信する、バックエンドが発信する、またはブラウザが
startWebCall()を呼び出します。 - Squid は通話用に voice session を開きます。1 つの conversation memory、エージェントの instructions に加えた電話応対と通話情報、phone line または request で指定された chat options が含まれます。
- 発信者の各完了発話によってエージェントの turn が実行されます(OpenAI engine では、realtime model がエージェントを必要とするたびに turn を実行します)。回答は音声で再生されます。
- 各 milestone は application の event bus に送信されます。
started、双方の各 spoken turn に対する 1 つのturn、endedです。Twilio connector はこれらを記録し、handler でも記録できます。 - どちらかが電話を切る、エージェントが通話を終了する、または転送されると、通話は終了します。
voice API は TypeScript client および backend SDK の一部です。Python では phone line にコードは不要で、connector operation は execute_function から利用できます。
Quick Start
電話番号を使用する前に、5 分でブラウザの音声会話を開始します。
Prerequisites
- How to build an AI agent のような、agent を持つ Squid application
- application に設定された OpenAI Voice connector(OpenAI project id と API key。ブラウザ通話には webhook は不要です)
squid initで初期化された backend project と、Squidclient を持つ frontend
Step 1: ユーザーがエージェントと会話できるようにする
ブラウザ通話には、エージェントとのチャットと同じ security rules が適用されます。signed-in user を許可します。
import { secureAiAgent, SquidService } from '@squidcloud/backend';
export class FrontDeskSecurityService extends SquidService {
@secureAiAgent('front-desk')
allowSignedInUsers(): boolean {
return this.isAuthenticated();
}
}
Step 2: backend を起動または deploy する
squid start
cloud に deploy するには、deploying your backend を参照してください。
Step 3: ブラウザから通話を開始する
const call = await squid.ai().voice().startWebCall({
agentId: 'front-desk',
greeting: 'Hi, this is the front desk. How can I help?',
});
// transcription された発信者の言葉と、音声で話されたエージェントの言葉。
call.onTurn((turn) => appendToTranscript(turn.role, turn.text));
call.onEnd(() => showCallEnded());
// ページ上のボタン。
muteButton.onclick = () => call.setMuted(true);
hangUpButton.onclick = () => call.stop();
ブラウザは microphone permission を要求し、WebRTC 経由で OpenAI realtime model に接続し、通話が作成する <audio> element を通じてエージェントの音声を再生します(独自の要素を sink として渡すこともできます)。Squid は signed-in user として通話の背後でエージェントを実行するため、エージェントの tools と data rules はその user を認識します。
Step 4: エージェントに電話番号を設定する
Twilio connector を追加し、Agent Studio で account の番号を使用する Twilio ability をエージェントに追加します。phone line は同じ OpenAI Voice connector を再利用し、ブラウザ通話では line の voice と language がデフォルトになります。
Authentication と Configuration
| Operation | 呼び出し可能な対象 |
|---|---|
startWebCall() および startOpenAiWebCall() | エージェントの @secureAiAgent rules に従う application user(または public agent の任意の user)、および API key を持つ backend code。エージェントは呼び出し元として実行されます。 |
createSession() および answerOpenAiCall() | application API key を持つ backend code のみ。phone call をエージェントに紐付けます。 |
Connector operations (placeCall, sendSms, getCallTranscript, …) | API key を持つ backend code。using the Twilio connector from code を参照してください。 |
Phone line settings (connectedIntegrations[].options) | Agent Studio、または setAgentOptionInPath() による API key を持つ backend code。setting up the phone line from code を参照してください。 |
frontend code は API key が必要なすべての操作について backend の executable を呼び出すため、key がブラウザに到達することはありません。
ブラウザ通話の設定の取得元
startWebCall() のすべての設定は optional です。各設定は順番に、OpenAI engine を使用する agent の phone line、OpenAI Voice connector の defaults、Squid の defaults(gpt-realtime-2.1、marin voice、en-US、通話全体で 1 つの language、記載どおりに発話される greeting)へ fallback します。integrationId は、agent にそのような phone line がなく、application に複数の OpenAI Voice connector がある場合にのみ必要です。
Core Concepts
voice session
すべての通話は sessionId を持つ voice session です。session の全 turn は、その id を key とする 1 つの conversation memory を共有するため、エージェントは最初の言葉から最後まで通話の context を維持し、session id は通話が発行するすべての event に含まれます。phone call では session に externalCallId(Twilio call SID)も含まれ、これは connector の call record の key です。
Turns と transcripts
turn は 1 つの完了発話です。{ role: 'user' | 'agent', text, at }。発信者の turn は speech の transcript であり、エージェントの turn は発話された内容です。割り込まれた回答は、発信者が割り込んだ箇所で切り取られます。OpenAI engine では、transcript は realtime model が独自に処理する small talk を含む会話全体を対象とします。
通話でエージェントに伝えられる内容
Squid は通話の各 turn で、電話応対をエージェントの instructions に追加します。短い spoken reply、markdown や link を使わないこと、名前・日付・番号を繰り返して確認すること、人が話すように日付を言うこと、会話が完了したら通話を終えることです。また、発信者の番号、着信先番号、現在の日付と時刻、応答する language という通話情報も追加します。独自の instructions はその上に適用されます。電話応対を自ら記述する必要はなく、エージェントに何をさせるかだけを記述します。
通話を開始する
デフォルトでは、line は通話に応答するとすぐ greeting を話します。greetingMode: 'agent' では、通話接続後にエージェント自身が opening line を作成します。該当する tool がある場合はまず発信者を検索するよう指示されるため、再度電話をかけた発信者には一般的な greeting ではなく「お帰りなさい、Dana」と応答できます。greeting text は personalization できる情報がない場合に発話する内容です。代償として、この turn の実行中には応答後に少し無音の時間が発生します。
Languages
language(es-MX などの BCP-47 tag)は、通話の開始 language です。OpenAI および ElevenLabs engine では、languageMode により発信者が切り替えられるかを決定します。single は通話全体をその language に維持し、selected は additionalLanguages 内の任意の language へ発信者に合わせて切り替え、any は対応する任意の language へ発信者に合わせて切り替えます。切り替えは意図的に行われます。名前や 1 つの外国語の単語では language は変更されず、文全体の場合に変更されます。
AI functions における call context
Twilio および OpenAI engine では、phone call 中にエージェントが呼び出すすべての AI function が、agent context 内で通話を受け取ります。
ctx.agentContext field | 説明 |
|---|---|
twilioCallSid | Twilio call SID。connector の call record および transcript の id です。 |
twilioIntegrationId | 通話が経由した Twilio connector です。 |
webCallerKey | Twilio 経由の browser call で、backend が発信者に渡した key です。 |
startWebCall() で開始した browser call には、chatOptions.agentContext で渡した内容に加え、通常の request context による発信者の identity(function 内の this.getUserAuth())が含まれます。
通話の活用
進行中の通話への反応
event handler で AI_VOICE_SESSION_EVENT_TYPE を subscribe し、すべての通話を live で追跡します。この handler は独自の transcript を保持し、通話終了時に staff 向けの summary を書き込みます。
import { eventHandler, SquidService, TriggerEvent } from '@squidcloud/backend';
import { AI_VOICE_SESSION_EVENT_TYPE, AiVoiceSessionEvent } from '@squidcloud/client';
interface CallTurn {
sessionId: string;
role: 'user' | 'agent';
text: string;
at: string;
}
interface CallSummary {
sessionId: string;
agentId: string;
callSid?: string;
summary: string;
endedAt: string;
}
export class CallLogService extends SquidService {
private readonly turns = this.squid.collection<CallTurn>('call_turns');
private readonly summaries = this.squid.collection<CallSummary>('call_summaries');
@eventHandler<AiVoiceSessionEvent>(AI_VOICE_SESSION_EVENT_TYPE)
async onVoiceSession(event: TriggerEvent<AiVoiceSessionEvent>): Promise<void> {
const { kind, sessionId, agentId, externalCallId, turn, endReason } = event.payload;
switch (kind) {
case 'turn':
if (!turn) return;
// 各 turn に時刻を key とする 1 つの document。再配信された event は重複せず上書きされます。
await this.turns.doc(`${sessionId}_${turn.at}_${turn.role}`).upsert({ sessionId, ...turn });
return;
case 'ended': {
console.log(`Call ${sessionId} of ${agentId} ended: ${endReason}`);
const turns = await this.turns.query().eq('sessionId', sessionId).sortBy('at').snapshot();
if (turns.length === 0) return;
const transcript = turns.map((doc) => `${doc.data.role}: ${doc.data.text}`).join('\n');
// built-in agent は memory なしで要約するため、通話内容が別の chat に漏れることはありません。
const summary = await this.squid
.ai()
.agent()
.ask(`Summarize this phone call in three sentences for the staff, then list any follow-ups.\n\n${transcript}`, {
memoryOptions: { memoryMode: 'none' },
});
await this.summaries.doc(sessionId).upsert({
sessionId,
agentId,
callSid: externalCallId,
summary,
endedAt: new Date().toISOString(),
});
return;
}
case 'started':
return;
}
}
}
payload は AiVoiceSessionEvent です。
| Field | Type | 説明 |
|---|---|---|
kind | 'started' | 'turn' | 'ended' | この event が報告する milestone。 |
sessionId | string | voice session。 |
provider | 'twilio' | 'openai' | 通話の実行元。Twilio ConversationRelay、または OpenAI Realtime API(phone と browser)です。 |
agentId | string | 通話中の agent。 |
externalCallId | string, optional | phone call における Twilio call SID。 |
metadata | object, optional | 通話開始時に渡された free-form data。connectors は channel(phone または web)と callSid を設定します。 |
turn | { role, text, at }, on turn events | 完了した turn。 |
endReason | string, on ended events | session が終了した理由。 |
event は少なくとも 1 回、順序保証なしで配信されるため、上記の handler は timestamp を key にして turn を保存します。turn-based Twilio calls(voiceMode: 'gather')および ElevenLabs engine の通話は voice session 外で実行され、event を発行しません。前者は引き続き connector の collections に記録されます。
AI functions で call context を使用する
エージェントが通話でできる最も便利なことは、発信者を認識することです。この function は Twilio connector が保持する call record を読み、番号から発信者を検索します。line が greetingMode: 'agent' を使用している場合、エージェントは opening turn でこれを呼び出します。
import { aiFunction, AiFunctionCallContext, SquidService } from '@squidcloud/backend';
import { TwilioCallRecord } from '@squidcloud/twilio-client';
interface CallAgentContext {
twilioCallSid?: string;
twilioIntegrationId?: string;
}
interface Patient {
phone: string;
firstName: string;
lastName: string;
}
export class FrontDeskService extends SquidService {
private readonly calls = this.squid.collection<TwilioCallRecord>('twilio_calls');
private readonly patients = this.squid.collection<Patient>('patients');
@aiFunction('Looks up whether the caller is a known patient, by the number they are calling from', [])
async lookupCaller(_params: unknown, ctx: AiFunctionCallContext<unknown, CallAgentContext>): Promise<string> {
const callSid = ctx.agentContext?.twilioCallSid;
if (!callSid) return 'This conversation has no phone number. Ask the caller for the number on file.';
const call = await this.calls.doc(callSid).snapshot();
if (!call) return 'The call record is not available yet. Ask the caller for the number on file.';
// 発信者の番号は inbound call では origin、outbound call では destination です。
const phone = call.direction === 'inbound' ? call.from : call.to;
const [patient] = await this.patients.query().eq('phone', phone).limit(1).snapshot();
if (!patient) return 'No patient has this number on file. Offer to register the caller.';
// first name のみを使用します。ほかの情報を共有する前に発信者を確認してください。
return `A patient named ${patient.data.firstName} has this number. Greet them by name and verify before sharing details.`;
}
}
上記のように raw record ではなく、エージェントが次に行うべきことを伝える短い文を返してください。個人情報を共有する function では、まず本人確認を必須にする必要があります。たとえば登録済み番号に送信した code を使用します。phone assistant tutorial では全体の flow を示しています。
テキストへの応答と送信
エージェントの番号への incoming text は SMS_EVENT_TYPE event であり、引き継がない限りエージェントが応答します。エージェントと backend はエージェントの番号からテキストを送信します。Twilio connector ページの SMS を参照してください。
発信
backend は placeCall({ agentId, to, instructions }) で発信します。エージェントは応答者に挨拶し、指定した目的を遂行します。outbound calls を参照してください。
Transfers と human handoff
エージェントは built-in の transferTwilioCall function で live call を転送します。いつ、どの番号へ転送するかを instructions に記述してください。代わりに application が通話を引き継ぐ場合、たとえば発信者を staff conference につなぐ場合は、エージェントが退出しても発信者が切断されないように、まず human handoff operation で claim してください。
Browser calls
squid.ai().voice().startWebCall(options) はブラウザで実行されます。microphone を開き、WebRTC offer を生成して Squid に渡します。Squid は agent の setup で OpenAI Realtime API 上の通話を作成し、OpenAI の SDP で応答して audio を接続します。Squid を通過するのは offer と answer だけです。audio は browser と OpenAI の間で流れ、Squid は realtime model から要求されたときに agent を実行し、transcript event を発行することで側方から通話を制御します。
const call = await squid
.ai()
.voice()
.startWebCall({
agentId: 'front-desk',
// 以下はすべて optional で、agent の phone line、次に connector がデフォルトになります。
voice: 'cedar',
language: 'es-MX',
languageMode: 'selected',
additionalLanguages: ['en'],
greeting: 'Hola, habla la recepción. ¿En qué puedo ayudarle?',
instructions: 'The visitor is on the pricing page; help them pick a plan.',
chatOptions: { agentContext: { pageUrl: window.location.href } },
metadata: { source: 'pricing-page' },
sink: document.querySelector('audio#agent-voice') as HTMLAudioElement,
});
console.log(call.sessionId, call.callId);
call.onTurn((turn) => console.log(turn.role, turn.text));
call.onEvent((event) => {
// talking indicator または debugging 用の raw Realtime API events。
if (event.type === 'response.done') console.log('The agent finished speaking');
});
call.onEnd(() => console.log('Over'));
ブラウザが microphone を公開するのは https:// page と localhost のみです。custom WebRTC setup では、独自の offer とともに startOpenAiWebCall({ agentId, sdp }) を呼び出し、remote description として設定する { sessionId, callId, sdp } を受け取れます。
API key を持つ backend code も、startOpenAiWebCall() を使用して browser call を broker できます。page が offer を生成し、独自のチェックを行う executable が通話を作成して answer を返し、page がそれを remote description として設定します。この場合、エージェントは user としてではなく API key で実行されます。
API Reference
squid.ai().voice()
| Method | Returns | 説明 |
|---|---|---|
startWebCall(options) | Promise<AiWebVoiceCall> | ブラウザ専用。microphone を開き、OpenAI realtime model に接続して、通話の背後で agent を実行します。 |
startOpenAiWebCall(request) | Promise<StartOpenAiWebVoiceCallResponse> | 生成した WebRTC offer から通話を作成し、OpenAI の answer、call id、session id を返します。 |
createSession(request) | Promise<CreateAiVoiceSessionResponse> | API key。Twilio ConversationRelay call 用の voice session を開き、Twilio を接続する wss:// relay URL を返します。Twilio speech engine のすべての通話で Twilio connector が行う処理です。 |
answerOpenAiCall(request) | Promise<AnswerOpenAiVoiceCallResponse> | API key。OpenAI project の SIP endpoint に届いた phone call に、agent で応答します。OpenAI Voice connector がすべての phone call で行う処理です。 |
createSession と answerOpenAiCall は、connectors の代わりに独自の Twilio webhook または OpenAI webhook を実行する application 向けです。createSession は agentId、provider: 'twilio'、optional の externalCallId、すべての turn に適用される chatOptions、すべての event で echo される metadata、greetingMode と greeting、connectTimeoutSeconds(relay URL の有効期間。デフォルトは 5 分)、tunneled backend 用の relayBaseUrl を受け取ります。answerOpenAiCall は agentId、OpenAI の realtime.call.incoming webhook の callId、OpenAI Voice connector の integrationId、realtime model の instructions、および optional の model、voice、allowInterruptions、interruptionSensitivity、greeting、greetingMode、externalCallId、chatOptions、metadata を受け取ります。
startWebCall() options
| Option | Type | 説明 |
|---|---|---|
agentId | string | realtime model が knowledge と actions を求める agent。必須です。 |
integrationId | string | OpenAI Voice connector。agent にその connector 上の phone line がなく、application に複数ある場合のみ必要です。 |
model, voice | string | realtime model とその voice。 |
language | string | 通話を開始する language。ISO code または BCP-47 tag で指定します。 |
languageMode | 'single' | 'selected' | 'any' | 発信者が通話途中で language を切り替えられるか。 |
additionalLanguages | string[] | selected mode で発信者が切り替えられる languages。 |
greeting | string | 通話開始時にすぐ発話されます。greeting がない場合、model は発信者を待ちます。 |
greetingMode | 'phrase' | 'agent' | agent では、agent が opening line を作成し、greeting を fallback として使用します。 |
instructions | string | この通話の目的。realtime model の instructions に追加されます。 |
allowInterruptions | boolean | 発信者の speech が model を中断するか。 |
interruptionSensitivity | 'low' | 'normal' | 'high' | 発信者の speech を検出する感度。 |
chatOptions | AiChatOptions | agent が実行するすべての turn に適用されます。instructions、agentContext、functions などです。 |
metadata | object | session のすべての event で echo されます。 |
sink | { srcObject, autoplay, play() } | voice の再生先。<audio> element です。省略時は page 上に作成されます。 |
AiWebVoiceCall
| Member | 説明 |
|---|---|
sessionId | Squid 内の voice session。その events に含まれます。 |
callId | OpenAI 上の call。 |
onTurn(listener) | すべての spoken turn、{ role, text, at } を受け取ります。listening を停止する function を返します。 |
onEvent(listener) | data channel 経由で送信される、call のすべての Realtime API event、{ type, ...rest } を受け取ります。 |
onEnd(listener) | 通話終了時に一度実行されます。agent が電話を切った、connection が切断された、または stop() が呼ばれた場合です。 |
setMuted(muted) | 通話を終了せずに microphone を mute または復元します。 |
stop() | ブラウザ側から通話を終了し、microphone を解放します。 |
Error Handling
| Error | Cause | Solution |
|---|---|---|
A web voice call needs a browser with WebRTC support | startWebCall() がブラウザ外、または RTCPeerConnection と getUserMedia のないブラウザで実行されました。 | https:// page または localhost の browser code から呼び出してください。 |
ブラウザからの NotAllowedError | user が microphone access を拒否した、または page が secure に配信されていません。 | user gesture から microphone を要求し、page を HTTPS で配信してください。 |
| security error で通話が拒否される | agent が public ではなく、チャット拒否時と同様に @secureAiAgent rule も user を許可していません。 | rule を追加してください。securing AI agents を参照してください。 |
AGENT_NOT_FOUND | application にその id の agent が存在しません。 | agent id を確認してください。 |
OPENAI_VOICE_INTEGRATION_REQUIRED | agent に OpenAI engine の phone line がなく、application に OpenAI Voice connector が 0 個または複数あります。 | connector を追加するか、integrationId を渡してください。 |
OPENAI_VOICE_API_KEY_SECRET_NOT_FOUND | connector の API key secret が削除されました。 | key を使用して connector を再度保存してください。 |
API_KEY_REQUIRED | createSession() または answerOpenAiCall() が application API key なしで呼び出されました。 | backend code から呼び出してください。 |
OPENAI_CALLS_ARE_ANSWERED_NOT_RELAYED | createSession() が provider: 'openai' で呼び出されました。 | OpenAI call は answerOpenAiCall() で応答します。relay は Twilio 用です。 |
| エージェントが謝罪して繰り返しを求める | たとえば AI function が throw した、または model provider が error を返したため、agent の turn が失敗しました。 | application log を確認してください。発信者を無音のままにはせず、失敗内容が音声で伝えられます。 |
Best Practices
- 電話ではなく業務のための instructions を書く。 Squid は各 turn で電話応対と通話情報を追加します。必要な flow を記述してください。挨拶、識別、確認、支援、確定、終了です。
- 共有する前に確認する。 発信者の番号はヒントであり、identity の証明ではありません。登録済み番号に code を送信し、個人情報を読み上げたり予約したりする前に agent に確認させてください。phone assistant tutorial のとおりです。
- 通話用 tools を高速に保つ。 function の実行中、発信者は無音で待ちます。OpenAI engine では、少し待つと model が待機を案内します。indexed lookup を優先し、遅い処理は通話後の event handler に延期してください。
- agent 向けの文で functions に応答する。 「この番号の患者はいません。発信者への登録を案内してください」は会話を導きますが、JSON dump にはその効果がありません。
- まずブラウザでテストする。
startWebCall()は、電話番号なしで、通話時間に課金されることなく、phone line と同じ agent、tools、voice settings をテストします。 - event handlers を idempotent にする。 event は少なくとも 1 回、順不同で到着します。書き込むデータを session id と turn timestamp で key にしてください。
- browser calls を chats と同様に secure にする。 public agent では page 上の誰でも通話できます。
@secureAiAgentrule は通話を必要な users に限定し、agent はその user として実行されます。 - transcript の内容に注意する。 call transcript には発信者が言った内容がすべて含まれます。data retention rules を
twilio_call_transcriptsと handler が保存するすべてのデータに適用し、個人データを model に到達させてはならない場合は prompt privacy rules を使用してください。
See Also
- Voice connectors: Twilio、OpenAI Voice、ElevenLabs、および engine の選び方
- Twilio connector: phone lines、SMS、outbound calls、records、browser calls、human handoff
- Build a phone assistant that books appointments: 完全な example
- AI functions: agent に tools を与える
- Events: event handlers の仕組み
- Securing AI agents: agent と会話できる対象