通話中の道具
通話の
道具
Google カレンダーや
道具の定義#
"tools": [
{
"name": "find_free_slots",
"description": "予約の空きを調べる。相手が希望の日を言ったときに使う",
"url": "https://saas.example.com/voicast/tools",
"parameters": {
"properties": {
"date": { "type": "string", "description": "希望の日(YYYY-MM-DD)" },
"menu": { "type": "string", "enum": ["検診", "治療", "クリーニング"] }
},
"required": ["date"]
}
},
{
"name": "create_booking",
"description": "予約を入れる。空きを伝えて、相手が日時を選んだら使う",
"url": "https://saas.example.com/voicast/tools",
"confirm": true,
"parameters": {
"properties": {
"start": { "type": "string", "description": "開始の日時(ISO 8601)" },
"name": { "type": "string" },
"phone": { "type": "string" }
},
"required": ["start", "name"]
}
}
]| 項目 | 必須 | 内容 |
|---|---|---|
name |
○ | 英小文字で_ のfinish は |
description |
○ | いつ |
url |
○ | 呼ぶ先。localhost・社内向けの |
parameters |
引数。properties はstring・number・integer・boolean、enum は |
|
confirm |
true なら、 |
道具は
届く要求#
AI がurl に
POST https://saas.example.com/voicast/tools
content-type: application/json
user-agent: voicast-tools/1
webhook-id: msg_6f1c…
webhook-timestamp: 1790000042
webhook-signature: v1,Rk9x…
idempotency-key: toolu_01AbC…
{
"type": "tool.call",
"tool": "find_free_slots",
"tool_call_id": "toolu_01AbC…",
"arguments": { "date": "2026-10-07" },
"call": {
"id": "call_…",
"channel": "phone",
"provider_call_id": "CA…",
"from": "+819012345678",
"tenant": { "id": "ten_…", "external_id": "clinic-123" }
}
}| 項目 | 内容 |
|---|---|
tool |
道具の |
tool_call_id |
呼び出しごとにidempotency-key も |
arguments |
AI が |
call.tenant.external_id |
自社の |
call.from |
かけてきたfrom を |
返すもの#
2xx と
{ "slots": ["2026-10-07T10:00:00+09:00", "2026-10-07T14:30:00+09:00"] }- 待つのは
8 秒までです。 超えると 失敗と して 扱います。 - 2xx 以外の
応答・JSON でない 応答・時間切れ・接続の 失敗は、 AI に 失敗と して 伝わり、 AI は おわびして 担当者からの 折り返しに します。 - 応答の
JSON が 長すぎると、 4,000 文字で 切って 渡します。 AI に 要る 項目だけを 返します。 - 別の
URL へ 移す応答 (3xx) には 付いて 行きません。 「空きが 無い」 「見つからない」 など、 想定した 結果は 2xx で { "slots": [] }や{ "found": false, "message": "その番号の方は見つかりませんでした" }のように返すと、 AI が 自然に 伝えられます。
署名の検証#
要求には
署名のwhsec_…)GET /v1/tools/secret、get_tools_secret でPOST /v1/tools/secret/rotate・rotate_tools_secret で
import express from 'express';
import { Webhook } from 'standardwebhooks';
const wh = new Webhook(process.env.VOICAST_TOOLS_SECRET); // whsec_…
const app = express();
app.post('/voicast/tools', express.raw({ type: 'application/json' }), async (req, res) => {
let body;
try {
body = wh.verify(req.body, req.headers);
} catch {
return res.sendStatus(401);
}
const clinic = body.call.tenant.external_id;
switch (body.tool) {
case 'find_free_slots':
return res.json({ slots: await findSlots(clinic, body.arguments.date) });
case 'create_booking':
// 同じ tool_call_id は 1 回だけ実行する
return res.json(await createBookingOnce(body.tool_call_id, clinic, body.arguments));
default:
return res.status(404).json({ error: 'unknown tool' });
}
});二重実行の防止#
予約をtool_call_ididempotency-key)
async function createBookingOnce(toolCallId, clinic, args) {
const done = await db.toolRuns.get(toolCallId);
if (done) return done.result;
const result = await createBooking(clinic, args);
await db.toolRuns.put(toolCallId, { result });
return result;
}確認してから使う道具#
confirm: true の
気をつけること#
- 道具の
結果に 無い ことを AI は 答えません。 答えてほしい 情報 (料金・ 場所など) は 応答に 入れるか、 会話の 設定の faqに書きます。 - 個人情報は、
AI に 要る 分だけを 返します (生年月日などを そのまま 返さない) 。 - 呼ばれる
URL は、 署名を 確かめるまで 何もしないようにします。 署名の 無い 要求・古い webhook-timestampの要求は 断ります。