vapi-bootstrap-framework について
`ROUGH_DRAFT.md`を基に、シナリオ別プロンプトやアシスタントの初期化処理を含むVapi音声エージェントプロジェクトを構成する実験的なスキルです。明示的に依頼されたランディングページ向けのワークフロー用で、通常のVapiアシスタント開発には使いません。
主な機能
- 01BunとTypeScriptのプロジェクト構成
- 02草稿からシナリオとプロンプトを生成
- 03シナリオと言語別のアシスタント設定
- 04冪等なアシスタント初期化スクリプト
- 05英語とスペイン語の初期設定
活用シーン
- 01実験的なVapiランディングページ向けエージェントの開始
- 02複数の通話シナリオ向けアシスタントの生成
- 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)。ユーザーから明示的に依頼された場合にのみ、これらを上書きしてください。
ワークフロー
- プロジェクトのルートに
ROUGH_DRAFT.mdがあるか確認します。- 存在する場合 → ステップ 2 に進みます。
- 存在しない場合 → このスキルでは下書きが必要であることをユーザーに伝えます。以下の「ROUGH_DRAFT.md のテンプレート」を提示し、記入してもらうために貼り付けるか、独自の下書きを提供してもらうまで待ちます。下書きがない状態で、このステップより先に進んではいけません。
- シナリオを検出します —
## N. <scenario name>の見出しごとに 1 つのシナリオとして扱います。名前から snake_case のscenarioIdを導き出します(例:「Lead Qualification & Screening」→qualification、「Appointment Scheduling」→appointment)。迷った場合は、曖昧さのない最短の名詞を選びます。ID は短く保ちます — 環境変数名として使われるためです。 - 冒頭の行を抽出します — 各シナリオの下にある
**On the page**、**Opening**、**Greeting**、またはセクション内で最初に引用されている文字列を探します。これをfirstMessage.enにそのまま設定します。 - フローを要約します —
**What happens**(または同等の説明文)を、初稿のbody.mdにします。契約ではなく、ペルソナに沿った内容にします — 失敗時のルール、正確な文言、台本化された離脱手順はまだ含めません。短くまとめます。 - プロジェクトのルートにひな形を作成します —
package.json、tsconfig.json、.env.example、.gitignoreの各ファイルについて、存在しない場合は以下のテンプレートから作成します。.env.exampleがすでに存在する場合は、不足しているVAPI_ASSISTANT_<SCENARIO>_<LANG>の枠を追記します。既存の行の順序変更や削除はしません。package.jsonが存在する場合は変更せず、依存関係に@vapi-ai/server-sdkがあり、bootstrapとtypecheckのスクリプトがあることを確認します。不足しているものがあれば、編集せずにユーザーへ伝えます。 - フレームワークの骨格を生成します — 5 つの骨格ファイルを以下のテンプレートからそのままコピーします。これらはプロジェクト間で変更されません。変わるのはシナリオレジストリの import のみです。
- シナリオごとのファイルを生成します — 検出したシナリオごとに、
scenarios/<id>.ts、prompts/<id>/body.md、prompts/<id>/off-topic-es.mdを 1 つずつ作成します。 - レジストリを接続します —
scenarios/index.tsで各シナリオを import し、SCENARIOS定数、ScenarioId、SCENARIO_IDS、scenarioForをエクスポートします。 - 以下のダブルループのテンプレートを使って
src/bootstrap.tsを作成します。 - 最初のメッセージをスペイン語に翻訳します — 自然なラテンアメリカのスペイン語にし、ブランド名は翻訳しません。
- 実行方法をユーザーに伝えます(以下の「検証」を参照)。
このスキルでは、各シナリオの 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個のアシスタントが表示されるはずです。それぞれに緑色の「アシスタントと話す」ボタンがあります。エージェントはフローに沿って進みますが、まだ大まかな状態です(失敗時のルールや情報取得ツールはまだありません)。これは想定どおりです。後続のプロンプトで動作を強化します。