# 通話中の道具

> 通話の途中で AI に自社の API を呼ばせる方法。道具の定義、署名付きの呼び出し、確認してから使う道具、時間切れ、二重実行の防止。

Source: https://voicast.jp/docs/tools

道具（tools）を会話の設定に書くと、AI は通話の途中で自社の API を呼び、その結果を使って答えます。予約の空きを調べる、電話番号で顧客を探す、予約を入れる、といった使い方ができます。連携先を問わない汎用の形で、サービスの側は 1 本の URL で受けて、道具の名前で振り分けても構いません。

Google カレンダーや kintone などとつなぐだけなら、道具を自作せずに[連携](https://voicast.jp/docs/connectors.md)を使えます。

## 道具の定義

```json
"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` | ○ | 英小文字で始まり、英小文字・数字・`_` の 40 文字まで。`finish` は使えません。連携の道具の名前と重ならないこと |
| `description` | ○ | いつ使うか（500 文字まで）。AI はこれを読んで使うかを決めます |
| `url` | ○ | 呼ぶ先。https で、インターネットから引ける名前だけ（IP アドレスの直書き・`localhost`・社内向けの名前は不可）。500 文字まで |
| `parameters` | | 引数。`properties` は 20 個まで、型は `string`・`number`・`integer`・`boolean`、`enum` は 50 個まで |
| `confirm` | | `true` なら、AI は内容（日時・名前など）を読み上げ、相手が同意してから使います。予約・取り消しなど書き換える道具に付けます |

道具は 1 つの会話の設定に 10 個までです。AI は道具を使う前に「お調べしますので、少々お待ちください」と一言伝えます。

## 届く要求

AI が道具を使うと決めると、voicast が `url` に POST します。

```
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` | 呼び出しごとに 1 つの ID。見出しの `idempotency-key` も同じ |
| `arguments` | AI が決めた引数 |
| `call.tenant.external_id` | 自社の顧客の ID。どの会社の予約表を見るかを決めるのに使います |
| `call.from` | かけてきた番号（電話で、TwiML の `from` を渡したとき） |

## 返すもの

2xx と JSON を返します。JSON はそのまま AI に渡ります。

```json
{ "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 が自然に伝えられます。

## 署名の検証

要求には [Standard Webhooks](https://www.standardwebhooks.com/) の署名が付いています。確かめ方とコード（Node.js・Python・PHP・Go）は [結果の受け取り](https://voicast.jp/docs/results.md#署名の検証) と同じで、鍵だけが違います。

署名の鍵（`whsec_…`）は環境ごとに 1 つです。管理画面の環境の設定（管理者以上）、API の `GET /v1/tools/secret`、MCP の `get_tools_secret` で確かめ、「作り直す」・`POST /v1/tools/secret/rotate`・`rotate_tools_secret` で作り直せます。作り直してから 24 時間は、古い鍵の署名も並べて送るので、そのあいだに自社の側の鍵を差し替えます（ライブラリには新旧の両方の鍵を渡せます）。

```js
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' });
  }
});
```

## 二重実行の防止

予約を入れる道具などは、同じ要求が 2 度届いても 1 回だけ実行します。`tool_call_id`（見出しの `idempotency-key`）を保存し、同じ ID には前の結果を返します。

```js
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 が内容を読み上げ、相手が「はい」と答えてから使います。予約・変更・取り消し・登録など、相手の情報を書き換える道具には必ず付けます。読み取るだけの道具（空きを調べる・顧客を探す）には要りません。

## 気をつけること

- 道具の結果に無いことを AI は答えません。答えてほしい情報（料金・場所など）は応答に入れるか、会話の設定の `faq` に書きます。
- 個人情報は、AI に要る分だけを返します（生年月日などをそのまま返さない）。
- 呼ばれる URL は、署名を確かめるまで何もしないようにします。署名の無い要求・古い `webhook-timestamp` の要求は断ります。
