SDK
Node.js / TypeScript の
SDK は
公開の 準備中です (0.1.0) 。 npm に 出るまでは、API を 直接呼んでください。
- 依存する
パッケージは ありません ( fetchとWeb Crypto を 使います) 。 - Node.js 18 以上
・Bun・Deno・Cloudflare Workers で 動きます。 voicast/webはブラウザで 動きます。 - ESM と
CommonJS、 TypeScript の 型付きです。
インストール#
npm install voicastはじめかた#
import { Voicast } from 'voicast';
const voicast = new Voicast(process.env.VOICAST_API_KEY); // 省くと環境変数 VOICAST_API_KEY を使う
// 会話の設定
const agent = await voicast.agents.create({
name: '受付',
config: {
greeting: 'お電話ありがとうございます。さくら歯科です。',
persona: '丁寧で落ち着いた受付',
fields: [
{ key: 'name', label: 'お名前', required: true },
{ key: 'phone', label: '電話番号', type: 'phone' },
],
handoff_when: ['急患', '担当者と話したいと言われた'],
closing: '承りました。担当者から折り返しご連絡します。',
voice: 'Kore',
},
});
// テナント(電話を受ける会社)
const tenant = await voicast.tenants.create({ external_id: 'clinic-123', name: 'さくら歯科', agent_id: agent.id });
// tenant.secret(vts_…)はこのときだけ返る。Twilio の <Stream> の secret に渡すAPI キーは
ブラウザ・アプリから話す#
サーバーでclient_token・webrtc_url・ice_servers だけを
// サーバー
const session = await voicast.sessions.create({ tenant_id: tenant.id }); // output: 'audio'(既定)か 'text'
return { client_token: session.client_token, webrtc_url: session.webrtc_url, ice_servers: session.ice_servers };// ブラウザ
import { VoiceSession } from 'voicast/web';
const session = new VoiceSession({ clientToken, webrtcUrl, iceServers }); // iceServers はセッションの ice_servers
session.on('user-transcript', ({ text, final }) => showUser(text, final));
session.on('bot-output', ({ text }) => showBot(text)); // AI が話した文(output: 'audio')
session.on('closed', ({ reason }) => console.log('closed', reason));
await session.start(); // マイクの許可を求めてからつなぐ
hangupButton.onclick = () => session.stop();output: 'text' ではreply で
session.on('reply', ({ text }) => avatar.speak(text));
session.on('interrupted', () => avatar.interrupt()); // 相手が話し始めた
session.on('end', ({ outcome }) => console.log('ended', outcome));| イベント | 中身 | いつ |
|---|---|---|
ready |
{} |
AI の |
reply |
{ text } |
返事のoutput: 'text') |
interrupted |
{} |
相手が |
end |
{ outcome } |
AI が |
user-transcript |
{ text, final } |
相手の |
bot-output |
{ text } |
AI がoutput: 'audio') |
bot-started-speaking・bot-stopped-speaking |
{} |
AI が |
track |
{ stream } |
AI のplayAudio: false でなければ |
state |
{ state } |
RTCPeerConnection.connectionState が |
message |
RTVI の |
音声サーバーからの |
error |
{ error } |
|
closed |
{ reason } |
ended・hangup・failed・disconnected・error。 |
オプション: mediaStreamaudioElement・playAudio・iceServers・iceGatheringTimeout・disconnectTimeout・fetch。setMuted(true) で
通話のvoicast.calls.getByProvider(session.id) で
Webhook の検証#
import { verifyWebhook } from 'voicast';
const event = await verifyWebhook(rawBody, headers, process.env.VOICAST_WEBHOOK_SECRET!); // whsec_…
switch (event.type) {
case 'call.ended':
console.log(event.data.outcome, event.data.fields, event.data.tenant.external_id);
break;
case 'call.summary_ready':
console.log(event.data.summary, event.data.category);
break;
case 'billing.balance_low':
console.log(event.data.balance_yen);
break;
}- 本文は
JSON に 直す前の 文字列か バイト列を 渡します。 - 署名が
合わない・時刻が 5 分以上ずれている ときは WebhookVerificationErrorを投げます (4 つ目の 引数 { tolerance: 300 }で変えられます) 。 - 鍵を
切り 替える あいだは、 鍵の 配列を 渡せます。
Express:
app.post('/webhooks/voicast', express.raw({ type: 'application/json' }), async (req, res) => {
try {
const event = await verifyWebhook(req.body, req.headers, secret);
// event を処理する
res.sendStatus(204);
} catch {
res.sendStatus(400);
}
});Hono・Next.js
const event = await verifyWebhook(await req.text(), req.headers, secret);通話中の道具の検証#
import { verifyToolCall } from 'voicast';
app.post('/voicast/tools', express.raw({ type: 'application/json' }), async (req, res) => {
const call = await verifyToolCall<{ date: string }>(req.body, req.headers, process.env.VOICAST_TOOLS_SECRET!);
if (call.tool === 'find_free_slots') {
res.json({ slots: await findSlots(call.arguments.date, call.call.tenant.external_id) });
}
});call.tool_call_ididempotency-key と
メソッド#
| メソッド | API |
|---|---|
agents.create({ name, config }) |
POST /v1/agents |
agents.list() |
GET /v1/agents |
agents.get(id, { version? }) |
GET /v1/agents/{id} |
agents.update(id, { name?, config }) |
PUT /v1/agents/{id} |
agents.versions(id) |
GET /v1/agents/{id}/versions |
agents.delete(id) |
DELETE /v1/agents/{id} |
tenants.create({ external_id, name?, agent_id? }) |
POST /v1/tenants |
tenants.list() |
GET /v1/tenants |
tenants.get(id) |
GET /v1/tenants/{id} |
tenants.update(id, { name?, agent_id?, status? }) |
PATCH /v1/tenants/{id} |
tenants.rotateSecret(id) |
POST /v1/tenants/{id}/secret |
tenants.delete(id) |
DELETE /v1/tenants/{id} |
tenants.listConnections(id) |
GET /v1/tenants/{id}/connections |
tenants.createConnectLink(id, { connectors?, frame_origin? }) |
POST /v1/tenants/{id}/connect-links |
calls.list({ tenant_id?, cursor?, limit? }) |
GET /v1/callsbefore も |
calls.get(id) |
GET /v1/calls/{id} |
calls.getByProvider(providerCallId) |
GET /v1/calls/by-provider/{providerCallId} |
calls.transcript(id) |
GET /v1/calls/{id}/transcript |
sessions.create({ tenant_id, agent_id?, output? }) |
POST /v1/sessions |
usage.get({ from?, to?, group_by? }) |
GET /v1/usage |
connectors.list() |
GET /v1/connectors |
connections.list({ tenant_id? }) |
GET /v1/connections |
connections.create({ connector, secret, input?, tenant_id? }) |
POST /v1/connections |
connections.get(id) |
GET /v1/connections/{id} |
connections.update(id, { config }) |
PATCH /v1/connections/{id} |
connections.options(id, query?) |
GET /v1/connections/{id}/options |
connections.delete(id) |
DELETE /v1/connections/{id} |
webhooks.createPortalLink({ frame_origin?, locale? }) |
POST /v1/webhooks/portal |
billing.get() |
GET /v1/billing |
billing.createTopup({ amount_yen }) |
POST /v1/billing/topups |
tools.getSecret() |
GET /v1/tools/secret |
tools.rotateSecret() |
POST /v1/tools/secret/rotate |
どの{ timeout?, maxRetries?, signal? } をbilling.get() はbilling.createTopup() は
通話のページ送り#
await すると{ data, has_more, next_cursor })next_cursor をcursor にfor await ですべての
const { data, has_more } = await voicast.calls.list({ tenant_id: 'ten_…', limit: 100 });
for await (const call of voicast.calls.list({ tenant_id: 'ten_…' })) {
console.log(call.id, call.outcome);
}エラー#
どのVoicastError をstatus・codeinvalid_request・in_use・balance_depleted など)message・details を
| クラス | いつ |
|---|---|
AuthenticationError |
401: API キーが |
PaymentRequiredError |
402: 新しいbalance_depleted・payment_overdue・spending_cap_reached) |
PermissionError |
403 |
NotFoundError |
404 |
ConflictError |
409: external_id のin_use・tenant_disabled・needs_reauth |
ValidationError |
400・422。details に |
RateLimitError |
429retryAfter は |
ApiError |
ほかの |
ConnectionError・TimeoutError |
応答がtimeout に |
WebhookVerificationError |
verifyWebhook・verifyToolCall の |
import { PaymentRequiredError } from 'voicast';
try {
await voicast.sessions.create({ tenant_id });
} catch (e) {
if (e instanceof PaymentRequiredError && e.code === 'balance_depleted') showTopUpNotice();
else throw e;
}送り直しと時間切れ#
既定はnew Voicast(key, { timeout: 30_000, maxRetries: 2 }) です。retry-after だけ
ほかの言語#
Python などの