mcpField
V
vapi-bootstrap-framework

vapi-bootstrap-framework

github.com/vapiaiエージェントワークフロー67295 回インストール今日更新
リポジトリ

vapi-bootstrap-framework について

`ROUGH_DRAFT.md`を基に、シナリオ別プロンプトやアシスタントの初期化処理を含むVapi音声エージェントプロジェクトを構成する実験的なスキルです。明示的に依頼されたランディングページ向けのワークフロー用で、通常のVapiアシスタント開発には使いません。

主な機能

  1. 01BunとTypeScriptのプロジェクト構成
  2. 02草稿からシナリオとプロンプトを生成
  3. 03シナリオと言語別のアシスタント設定
  4. 04冪等なアシスタント初期化スクリプト
  5. 05英語とスペイン語の初期設定

活用シーン

  1. 01実験的なVapiランディングページ向けエージェントの開始
  2. 02複数の通話シナリオ向けアシスタントの生成
  3. 03プロジェクト概要から多言語版を初期化

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/vapiai/skills --skill vapi-bootstrap-framework

インストール後、Claude Code で/vapi-bootstrap-frameworkを入力して読み込まれたか確認します。

詳細

Vapi ブートストラップフレームワーク

実験的なリファレンスワークフロー: このスキルは、Vapi のランディングページ用エージェントで採用されているアーキテクチャを再現します。Vapi アシスタントを構築するための標準的なワークフローではなく、明示的に依頼された場合にのみ使用してください。

単一の ROUGH_DRAFT.md 仕様から、Vapi の音声エージェントプロジェクト全体をスキャフォールディングします。空のフォルダーでも、既存の Bun + TypeScript プロジェクトと併用しても動作します。このスキルの完了後に bun run bootstrap を一度実行すると、エージェント群全体が dashboard.vapi.ai で稼働します。

このスキルが生成するもの

プロジェクトのスキャフォールディング(存在しない場合のみ作成し、上書きはしません):

  • package.json — Bun + @vapi-ai/server-sdk + bootstrap スクリプト
  • tsconfig.json — 厳格な no-emit TypeScript 検証
  • .env.example — VAPI_API_KEY のプレースホルダー + (scenario × language) タプルごとに 1 つのスロット
  • .gitignore — node_modules、.env*.local、OS の不要ファイル

フレームワークの基盤(常に作成。アーキテクチャの中核であり、以降の変更はすべてここにあるスロットを拡張し、基盤自体には手を加えません):

  • src/assistants/languages.ts — 言語ごとの音声 + トランスクライバースタック
  • src/assistants/loadPrompt.ts — loadPrompt(scenarioId, languageId) コンポーザー
  • src/assistants/scenarios/index.ts — シナリオレジストリ
  • src/assistants/buildAssistant.ts — 任意の (scenarioId, languageId) タプルに対する Vapi アシスタントの完全なボディを構成
  • src/assistants/prompts/shared/preambles/es.md — スペイン語の言語プレアンブル(後からほかの言語を追加できます)

コンテンツ(常に作成。ラフドラフトで検出したシナリオごとに 1 セット):

  • src/assistants/scenarios/<scenarioId>.ts — id、name、言語別の最初のメッセージ
  • src/assistants/prompts/<scenarioId>/body.md — システムプロンプトのラフな初稿
  • src/assistants/prompts/<scenarioId>/off-topic-es.md — スペイン語の話題転換用フレーズ

エントリーポイント(常に作成):

  • src/bootstrap.ts — シナリオ × 言語の二重ループを冪等に実行

デフォルト:言語は en + es。モデルは openai gpt-4.1、temperature は 0.5。音声は ElevenLabs eleven_turbo_v2(EN)/ eleven_multilingual_v2(ES)。トランスクライバーは Deepgram nova-3(EN)/ Soniox stt-rt-v4(ES)。ユーザーから明示的に依頼された場合にのみ、これらを上書きしてください。

ワークフロー

  1. プロジェクトのルートに ROUGH_DRAFT.md があるか確認します。
    • 存在する場合 → ステップ 2 に進みます。
    • 存在しない場合 → このスキルでは下書きが必要であることをユーザーに伝えます。以下の「ROUGH_DRAFT.md のテンプレート」を提示し、記入してもらうために貼り付けるか、独自の下書きを提供してもらうまで待ちます。下書きがない状態で、このステップより先に進んではいけません。
  2. シナリオを検出します — ## N. <scenario name> の見出しごとに 1 つのシナリオとして扱います。名前から snake_case の scenarioId を導き出します(例:「Lead Qualification & Screening」→ qualification、「Appointment Scheduling」→ appointment)。迷った場合は、曖昧さのない最短の名詞を選びます。ID は短く保ちます — 環境変数名として使われるためです。
  3. 冒頭の行を抽出します — 各シナリオの下にある **On the page**、**Opening**、**Greeting**、またはセクション内で最初に引用されている文字列を探します。これを firstMessage.en にそのまま設定します。
  4. フローを要約します — **What happens**(または同等の説明文)を、初稿の body.md にします。契約ではなく、ペルソナに沿った内容にします — 失敗時のルール、正確な文言、台本化された離脱手順はまだ含めません。短くまとめます。
  5. プロジェクトのルートにひな形を作成します — package.json、tsconfig.json、.env.example、.gitignore の各ファイルについて、存在しない場合は以下のテンプレートから作成します。.env.example がすでに存在する場合は、不足している VAPI_ASSISTANT_<SCENARIO>_<LANG> の枠を追記します。既存の行の順序変更や削除はしません。package.json が存在する場合は変更せず、依存関係に @vapi-ai/server-sdk があり、bootstrap と typecheck のスクリプトがあることを確認します。不足しているものがあれば、編集せずにユーザーへ伝えます。
  6. フレームワークの骨格を生成します — 5 つの骨格ファイルを以下のテンプレートからそのままコピーします。これらはプロジェクト間で変更されません。変わるのはシナリオレジストリの import のみです。
  7. シナリオごとのファイルを生成します — 検出したシナリオごとに、scenarios/<id>.ts、prompts/<id>/body.md、prompts/<id>/off-topic-es.md を 1 つずつ作成します。
  8. レジストリを接続します — scenarios/index.ts で各シナリオを import し、SCENARIOS 定数、ScenarioId、SCENARIO_IDS、scenarioFor をエクスポートします。
  9. 以下のダブルループのテンプレートを使って src/bootstrap.ts を作成します。
  10. 最初のメッセージをスペイン語に翻訳します — 自然なラテンアメリカのスペイン語にし、ブランド名は翻訳しません。
  11. 実行方法をユーザーに伝えます(以下の「検証」を参照)。

このスキルでは、各シナリオの clientTools 配列は空のままにします — 機能するキャプチャツールは後続のステップで追加します。ここで body.md を契約として書き直してはいけません — それは別のステップです。

ファイルテンプレート

テンプレートには、プロジェクトごとに置き換える以下のプレースホルダーがあります:

  • <PROJECT_NAME> — 下書きのタイトル(# Rough draft — <X>)から作るスラッグ(X を snake_case または kebab-case にします)。該当するタイトルがない場合は vapi-voice-agents を使います。
  • <SCENARIO_ID> — snake_case のシナリオ ID(例:qualification)
  • <SCENARIO_NAME> — 人が読める名前(例:Lead Qualification)
  • <FIRST_MESSAGE_EN> — 下書きにある冒頭の行をそのまま使用します
  • <FIRST_MESSAGE_ES> — 自然なスペイン語訳
  • <BODY_DRAFT> — 要約した初稿のプロンプト
  • <SCENARIO_IMPORTS> — シナリオごとに import { <id> } from "./<id>.ts"; を 1 行ずつ記述し、アルファベット順に並べます
  • <SCENARIO_KEYS> — SCENARIOS = { ... } 内の、カンマ区切りのシナリオ ID
  • <ENV_ASSISTANT_SLOTS> — (シナリオ × 言語) ごとに 1 行コメントを記述します:# VAPI_ASSISTANT_<SCENARIO>_<LANG>=
package.json(存在しない場合のみ)
{
  "name": "<PROJECT_NAME>",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "bootstrap": "bun run src/bootstrap.ts",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@vapi-ai/server-sdk": "^1.2.0"
  },
  "devDependencies": {
    "@types/bun": "^1.3.13",
    "typescript": "^5.9.3"
  },
  "packageManager": "[email protected]"
}
tsconfig.json(存在しない場合のみ)
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "allowImportingTsExtensions": true,
    "noEmit": true,
    "strict": true,
    "types": ["bun"]
  },
  "include": ["src/**/*.ts"]
}
.env.example(存在しない場合は作成、存在する場合は拡張)
# Get this from https://dashboard.vapi.ai/keys
VAPI_API_KEY=

# One slot per (scenario × language). First `bun run bootstrap` prints the
# ids; paste them here, then re-run for idempotent updates.
<ENV_ASSISTANT_SLOTS>
.gitignore(存在しない場合のみ)
# dependencies
node_modules

# env (local-only secrets)
.env*.local

# os junk
.DS_Store

# bun
*.tsbuildinfo
src/assistants/languages.ts
/**
 * Per-language voice + transcriber stack. Adding a 3rd language is one
 * entry in each record below.
 */
import type { LanguageId } from "./loadPrompt.ts";

export type { LanguageId };

interface VoiceConfig {
  provider: "11labs";
  model: string;
  voiceId: string;
}

interface TranscriberConfig {
  provider: "deepgram" | "soniox";
  model: string;
  language: string;
}

const VOICE_BY_LANGUAGE: Record<LanguageId, VoiceConfig> = {
  en: {
    provider: "11labs",
    model: "eleven_turbo_v2",
    voiceId: "ZoiZ8fuDWInAcwPXaVeq",
  },
  es: {
    provider: "11labs",
    model: "eleven_multilingual_v2",
    voiceId: "JYyJjNPfmNJdaby8LdZs",
  },
};

const TRANSCRIBER_BY_LANGUAGE: Record<LanguageId, TranscriberConfig> = {
  en: { provider: "deepgram", model: "nova-3", language: "en" },
  es: { provider: "soniox", model: "stt-rt-v4", language: "es" },
};

export const voiceFor = (languageId: LanguageId): VoiceConfig =>
  VOICE_BY_LANGUAGE[languageId];

export const transcriberFor = (languageId: LanguageId): TranscriberConfig =>
  TRANSCRIBER_BY_LANGUAGE[languageId];
src/assistants/loadPrompt.ts
/**
 * EN returns body.md unchanged. ES prepends a Spanish preamble with the
 * scenario's off-topic redirects spliced into {{OFF_TOPIC_LINES}}.
 * One body.md per scenario drives every language variant.
 */
import { readFileSync } from "node:fs";
import { resolve } from "node:path";

export type LanguageId = "en" | "es";

const PROMPT_DIR = resolve(import.meta.dir, "prompts");

const read = (relativePath: string): string =>
  readFileSync(resolve(PROMPT_DIR, relativePath), "utf8");

export const loadPrompt = (
  scenarioId: string,
  languageId: LanguageId,
): string => {
  const body = read(`${scenarioId}/body.md`);
  if (languageId === "en") return body;

  const offTopic = read(`${scenarioId}/off-topic-${languageId}.md`).trim();
  const preamble = read(`shared/preambles/${languageId}.md`).replace(
    "{{OFF_TOPIC_LINES}}",
    offTopic,
  );
  return `${preamble}\n\n${body}`;
};
src/assistants/scenarios/index.ts
/**
 * Scenario registry. Adding a new scenario is one entry here plus one new
 * file under `./<scenario-id>.ts` and matching prompts under `../prompts/<id>/`.
 */
// <SCENARIO_IMPORTS>

export const SCENARIOS = {
  // <SCENARIO_KEYS>
} as const;

export type ScenarioId = keyof typeof SCENARIOS;

export const SCENARIO_IDS = Object.keys(SCENARIOS) as ScenarioId[];

export const scenarioFor = (id: ScenarioId) => SCENARIOS[id];
src/assistants/buildAssistant.ts
/**
 * Compose a full Vapi assistant body for a (scenario, language) tuple.
 * Voice + transcriber come from languages.ts; prompt from loadPrompt;
 * name + firstMessage + clientTools from the scenario.
 */
import { transcriberFor, voiceFor } from "./languages.ts";
import { loadPrompt, type LanguageId } from "./loadPrompt.ts";
import { scenarioFor, type ScenarioId } from "./scenarios/index.ts";

export type { LanguageId, ScenarioId };

export const buildAssistant = (
  scenarioId: ScenarioId,
  languageId: LanguageId,
) => {
  const scenario = scenarioFor(scenarioId);
  const systemPrompt = loadPrompt(scenarioId, languageId);

  return {
    name: `${languageId.toUpperCase()} - ${scenario.name}`,
    firstMessage: scenario.firstMessage[languageId],
    voice: voiceFor(languageId),
    transcriber: transcriberFor(languageId),
    model: {
      provider: "openai" as const,
      model: "gpt-4.1",
      temperature: 0.5,
      messages: [{ role: "system" as const, content: systemPrompt }],
      tools: scenario.clientTools,
    },
  };
};
src/assistants/scenarios/<SCENARIO_ID>.ts
/**
 * <SCENARIO_NAME> scenario. Plain data: id, name, language-keyed first
 * message. Later steps add `clientTools` (capture tools fire mid-call).
 */
// Rename `scenarioId` to the generated <SCENARIO_ID> identifier.
export const scenarioId = {
  id: "<SCENARIO_ID>" as const,
  name: "<SCENARIO_NAME>",
  firstMessage: {
    en: "<FIRST_MESSAGE_EN>",
    es: "<FIRST_MESSAGE_ES>",
  },
  clientTools: [] as const,
};

// Rename this type to the generated <PascalCase scenario id>Scenario name.
export type ScenarioIdScenario = typeof scenarioId;

<FIRST_MESSAGE_EN> が長い場合は、読みやすいように連結する文字列セグメントに分割します(論理的な節ごとに 1 行とし、" + " でつなぎます)。

src/assistants/prompts/<SCENARIO_ID>/body.md

大まかな初稿です。ペルソナ主導であり、契約ではありません。推奨する構成:

# <SCENARIO_NAME> voice agent

You are the <SCENARIO_NAME> voice agent for Vapi. <One sentence on context — who you're talking to and why.>

<One paragraph distilling **What happens** from the rough draft: the questions to ask, the data to collect, the routing logic, the wrap-up.>

Be warm and curious. Ask one question at a time. If they go off-topic, redirect briefly and return to the next missing field. Keep replies short — you are speaking, not typing.

## Absolute rules も、失敗時のルールも、定型の終了文も含めません。これらは後のステップで追加します。

src/assistants/prompts/<SCENARIO_ID>/off-topic-es.md

このシナリオに固有の、短いスペイン語の話題転換文を2行記述します。

- "Buena pregunta — el equipo te puede ayudar con eso. ¿Podemos seguir con <next field>?"
- "Tomo nota, lo vemos después. Mientras tanto, cuéntame <one short ask tied to the scenario>."
src/assistants/prompts/shared/preambles/es.md
# IDIOMA / LANGUAGE OVERRIDE

The contract that follows this preamble is written in English. **This call is in Spanish.**

Override the language rule of the contract:

- ALL agent speech MUST be in natural, conversational Latin American Spanish. Translate the exact wordings, examples, and acks from the contract — don't switch back to English mid-sentence.
- Read brand and product names in their original form. Don't translate them.
- When you call any capture tool, **always pass free-text fields as a short English summary**, regardless of the call language. Cross-language analytics depend on it.

## Off-topic redirects (use verbatim)

If the visitor goes off-topic, pick one of these and then return to the next missing field:

{{OFF_TOPIC_LINES}}

---
src/bootstrap.ts
/**
 * Idempotent upsert across (scenario × language). One entry per tuple, keyed
 * by VAPI_ASSISTANT_<SCENARIO>_<LANG>. First run creates + prints ids;
 * subsequent runs update in place.
 *
 * Run with `bun run bootstrap`. Bun auto-loads .env.local.
 */
import { VapiClient } from "@vapi-ai/server-sdk";

import {
  buildAssistant,
  type LanguageId,
  type ScenarioId,
} from "./assistants/buildAssistant.ts";
import { SCENARIO_IDS } from "./assistants/scenarios/index.ts";

const LANGUAGES: LanguageId[] = ["en", "es"];

const envVarFor = (scenarioId: ScenarioId, languageId: LanguageId): string =>
  `VAPI_ASSISTANT_${scenarioId.toUpperCase()}_${languageId.toUpperCase()}`;

const requireEnv = (name: string): string => {
  const value = process.env[name];
  if (!value) {
    console.error(`✗ Missing env var: ${name}. See .env.example.`);
    process.exit(1);
  }
  return value;
};

const main = async () => {
  const vapi = new VapiClient({ token: requireEnv("VAPI_API_KEY") });
  const created: Array<{ envVar: string; id: string }> = [];

  for (const scenarioId of SCENARIO_IDS) {
    for (const languageId of LANGUAGES) {
      const envVar = envVarFor(scenarioId, languageId);
      const existingId = process.env[envVar];
      const body = buildAssistant(
        scenarioId,
        languageId,
      ) as unknown as Parameters<typeof vapi.assistants.create>[0];

      const label = `${scenarioId}/${languageId}`;
      let updated = false;

      if (existingId) {
        try {
          await vapi.assistants.update({
            id: existingId,
            ...body,
          } as unknown as Parameters<typeof vapi.assistants.update>[0]);
          console.log(`✓ Updated ${label} → ${existingId}`);
          updated = true;
        } catch (err) {
          const statusCode = (err as { statusCode?: number })?.statusCode;
          if (statusCode === 404) {
            console.log(
              `  ${envVar}=${existingId} not found in this org; creating a new assistant.`,
            );
          } else {
            throw err;
          }
        }
      }

      if (!updated) {
        const assistant = await vapi.assistants.create(body);
        console.log(`✓ Created ${label} → ${assistant.id}`);
        created.push({ envVar, id: assistant.id });
      }
    }
  }

  if (created.length > 0) {
    console.log("\nAdd these to .env.local:");
    for (const { envVar, id } of created) {
      console.log(`  ${envVar}=${id}`);
    }
    console.log(
      "\nThen re-run `bun run bootstrap` to confirm idempotent updates.",
    );
  } else {
    console.log(
      `\nAll ${SCENARIO_IDS.length * LANGUAGES.length} assistants updated in place.`,
    );
  }
};

main().catch((err) => {
  console.error("✗ Bootstrap failed:", err);
  process.exit(1);
});

ROUGH_DRAFT.md template

ユーザーがまだ大まかな下書きを用意していない場合は、このひな形を提示します(山括弧の部分をユーザーが記入し、それをもとにスキルがプロジェクトの骨組みを作成します)。

# Rough draft — <project name>

## 1. <First scenario name>

**On the page**: "<one verbatim opening line the agent will say>"

**What happens**: <One paragraph: the questions the agent asks, the data it collects, what it does with edge cases, how it wraps up.>

## 2. <Second scenario name>

**Opening**: "<one verbatim opening line>"

**What happens**: <One paragraph.>

シナリオは1つでも問題ありません。3つでも問題ありません。スキルは大まかな下書きの内容に合わせて、ブートストラップのループを調整します。

Adapting the defaults

ユーザーからデフォルト設定の変更を求められた場合は、周囲の構成はそのままに、該当する箇所だけを変更します。

  • 使用言語を変更する場合:VOICE_BY_LANGUAGE と TRANSCRIBER_BY_LANGUAGE にエントリーを追加し、loadPrompt.ts の LanguageId ユニオンを拡張し、prompts/shared/preambles/<lang>.md を追加します。また、各シナリオに firstMessage.<lang> を追加し、bootstrap.ts の LANGUAGES に言語を追加して、.env.example に対応する VAPI_ASSISTANT_<SCENARIO>_<LANG> スロットを追加します。
  • モデル/温度を変更する場合:buildAssistant.ts だけを変更します。
  • 音声プロバイダーを変更する場合:languages.ts の VoiceConfig と VOICE_BY_LANGUAGE だけを更新します。

Verification

ファイルの生成後、ユーザーに次のコマンドを実行するよう伝えます。

bun install
bun run typecheck              # validates locally; creates nothing in Vapi
cp .env.example .env.local     # add VAPI_API_KEY from dashboard.vapi.ai/org/api-keys
bun run bootstrap              # creates one assistant per (scenario × language)
# paste the printed VAPI_ASSISTANT_<SCENARIO>_<LANG>=<id> lines into .env.local
bun run bootstrap              # second run prints "Updated ..." for every tuple

シナリオがN個、言語が2つの場合、dashboard.vapi.ai にN × 2個のアシスタントが表示されるはずです。それぞれに緑色の「アシスタントと話す」ボタンがあります。エージェントはフローに沿って進みますが、まだ大まかな状態です(失敗時のルールや情報取得ツールはまだありません)。これは想定どおりです。後続のプロンプトで動作を強化します。

コメント