vapi-bootstrap-framework
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVapi bootstrap framework
Vapi Bootstrap框架
Experimental reference workflow: This skill recreates the architecture used for Vapi's landing-page agents. It is not the standard workflow for building Vapi assistants and should only be used when explicitly requested.
Scaffold an entire Vapi voice-agent project from a single spec. Works in an empty folder or alongside an existing Bun + TypeScript project. One after this skill finishes puts the whole fleet live in .
ROUGH_DRAFT.mdbun run bootstrapdashboard.vapi.ai实验性参考工作流: 本技能用于重建Vapi着陆页代理所使用的架构。它并非构建Vapi助手的标准工作流,仅在明确请求时使用。
从单个规范快速搭建完整的Vapi语音代理项目。可在空文件夹或现有Bun + TypeScript项目中运行。技能执行完成后,只需运行一次,即可在上部署所有代理。
ROUGH_DRAFT.mdbun run bootstrapdashboard.vapi.aiWhat this skill produces
本技能生成的内容
Project scaffolding (only created if absent — never overwritten):
- — Bun +
package.json+ a@vapi-ai/server-sdkscriptbootstrap - — strict no-emit TypeScript validation
tsconfig.json - —
.env.exampleplaceholder + one slot perVAPI_API_KEYtuple(scenario × language) - —
.gitignore,node_modules, OS junk.env*.local
Framework spine (always created — architectural; every later change extends a slot here, never the spine itself):
- — per-language voice + transcriber stack
src/assistants/languages.ts - —
src/assistants/loadPrompt.tscomposerloadPrompt(scenarioId, languageId) - — the scenario registry
src/assistants/scenarios/index.ts - — composes a full Vapi assistant body for any
src/assistants/buildAssistant.tstuple(scenarioId, languageId) - — Spanish language preamble (extend with more languages later)
src/assistants/prompts/shared/preambles/es.md
Content (always created — one set per scenario detected in the rough draft):
- — id, name, language-keyed first message
src/assistants/scenarios/<scenarioId>.ts - — rough first-draft system prompt
src/assistants/prompts/<scenarioId>/body.md - — Spanish redirect lines
src/assistants/prompts/<scenarioId>/off-topic-es.md
Entry point (always created):
- — idempotent double loop over scenarios × languages
src/bootstrap.ts
Defaults: languages + . Model temperature . Voices ElevenLabs (EN) / (ES). Transcriber Deepgram (EN) / Soniox (ES). Override these only if the user explicitly asks.
enesopenai gpt-4.10.5eleven_turbo_v2eleven_multilingual_v2nova-3stt-rt-v4项目脚手架(仅在文件不存在时创建,绝不会覆盖现有文件):
- — 包含Bun、
package.json和@vapi-ai/server-sdk脚本bootstrap - — 严格的无输出TypeScript验证配置
tsconfig.json - —
.env.example占位符,以及每个VAPI_API_KEY元组对应的配置项(场景 × 语言) - — 忽略
.gitignore、node_modules和系统临时文件.env*.local
框架核心(始终创建,属于架构层;后续所有更改仅扩展此处的配置项,绝不修改核心本身):
- — 各语言对应的语音+转录器栈
src/assistants/languages.ts - —
src/assistants/loadPrompt.ts提示编辑器loadPrompt(scenarioId, languageId) - — 场景注册表
src/assistants/scenarios/index.ts - — 为任意
src/assistants/buildAssistant.ts元组生成完整的Vapi助手配置(scenarioId, languageId) - — 西班牙语前置提示(后续可扩展更多语言)
src/assistants/prompts/shared/preambles/es.md
场景内容(始终创建,每个从草稿中识别的场景对应一套内容):
- — 场景ID、名称、多语言初始消息
src/assistants/scenarios/<scenarioId>.ts - — 初稿系统提示
src/assistants/prompts/<scenarioId>/body.md - — 西班牙语话题重定向语句
src/assistants/prompts/<scenarioId>/off-topic-es.md
入口文件(始终创建):
- — 遍历场景×语言的幂等双循环脚本
src/bootstrap.ts
默认配置:语言为(英文)+ (西班牙语);模型为OpenAI GPT-4.1,温度值0.5;语音使用ElevenLabs的(英文)/ (西班牙语);转录器使用Deepgram的(英文)/ Soniox的(西班牙语)。仅当用户明确要求时才修改这些默认配置。
eneseleven_turbo_v2eleven_multilingual_v2nova-3stt-rt-v4Workflow
工作流程
- Check for at the project root.
ROUGH_DRAFT.md- If present → continue with step 2.
- If missing → tell the user the skill needs a rough draft to work from. Offer the template under "ROUGH_DRAFT.md template" below; either paste it in for them to fill out, or wait for them to provide their own. Don't proceed past this step without one.
- Detect scenarios — every heading is one scenario. Derive a snake_case
## N. <scenario name>from the name (e.g. "Lead Qualification & Screening" →scenarioId; "Appointment Scheduling" →qualification). When in doubt, pick the shortest unambiguous noun. Keep ids short — they become env var names.appointment - Extract the opening line — under each scenario, look for ,
**On the page**,**Opening**, or the first quoted string in the section. That's**Greeting**verbatim.firstMessage.en - Distill the flow — (or equivalent prose) becomes a rough first-draft
**What happens**. Persona-driven, not a contract — no failure rules, no exact wordings, no scripted off-ramps yet. Keep it short.body.md - Scaffold the project root — for each of ,
package.json,tsconfig.json,.env.example: if the file is absent, create it from the template below. If.gitignorealready exists, append any missing.env.exampleslots; never reorder or remove existing lines. IfVAPI_ASSISTANT_<SCENARIO>_<LANG>exists, leave it alone but verify it haspackage.jsonin deps plus@vapi-ai/server-sdkandbootstrapscripts — tell the user if any are missing instead of editing.typecheck - Generate the framework spine — copy the five spine files verbatim from the templates below. They don't change between projects; only the scenario registry's imports do.
- Generate per-scenario files — one , one
scenarios/<id>.ts, oneprompts/<id>/body.mdper detected scenario.prompts/<id>/off-topic-es.md - Wire up the registry — imports every scenario and exports the
scenarios/index.tsconst,SCENARIOS,ScenarioId,SCENARIO_IDS.scenarioFor - Write with the double-loop template below.
src/bootstrap.ts - Translate first messages to Spanish — natural Latin American Spanish, brand names untranslated.
- Tell the user how to run it (see "Verification" below).
Keep each scenario's array empty in this skill — functional capture tools land in a follow-up step. Do not rewrite as a contract here either — that's a separate step.
clientToolsbody.md- 检查项目根目录下的
ROUGH_DRAFT.md- 若存在 → 继续步骤2
- 若缺失 → 告知用户本技能需要草稿文件才能运行。提供下方的「ROUGH_DRAFT.md模板」,可直接粘贴供用户填写,或等待用户提供自定义草稿。没有草稿文件则不继续执行后续步骤。
- 识别场景 — 每个标题对应一个场景。从场景名称中派生蛇形命名法的
## N. <场景名称>(例如:"Lead Qualification & Screening" →scenarioId;"Appointment Scheduling" →qualification)。如有疑问,选择最短的明确名词。ID需简短,因为它们会成为环境变量名称。appointment - 提取初始语句 — 在每个场景下,查找、
**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个核心文件,这些文件在不同项目中不会改变;仅场景注册表的导入内容会变化。
- 生成场景专属文件 — 每个识别出的场景对应一个、一个
scenarios/<id>.ts和一个prompts/<id>/body.md。prompts/<id>/off-topic-es.md - 关联场景注册表 — 导入所有场景并导出
scenarios/index.ts常量、SCENARIOS类型、ScenarioId数组和SCENARIO_IDS函数。scenarioFor - 编写— 使用下方的双循环模板。
src/bootstrap.ts - 翻译初始语句为西班牙语 — 使用自然的拉美西班牙语,品牌名称不翻译。
- 告知用户运行方式(见下方「验证步骤」)。
在本技能中,每个场景的数组需保持为空 — 功能捕获工具将在后续步骤中添加。同时,请勿将重写为严格契约,这是单独的后续步骤。
clientToolsbody.mdFile templates
文件模板
Templates use these placeholders that you substitute per project:
- — slug from the rough draft title (
<PROJECT_NAME>→ snake/kebab-case of X). Fallback:# Rough draft — <X>.vapi-voice-agents - — snake_case scenario id (e.g.
<SCENARIO_ID>)qualification - — human-readable name (e.g.
<SCENARIO_NAME>)Lead Qualification - — verbatim opening line from the rough draft
<FIRST_MESSAGE_EN> - — natural Spanish translation
<FIRST_MESSAGE_ES> - — distilled rough first-draft prompt
<BODY_DRAFT> - — one
<SCENARIO_IMPORTS>per scenario, alphabetizedimport { <id> } from "./<id>.ts"; - — comma-separated scenario ids inside
<SCENARIO_KEYS>SCENARIOS = { ... } - — one commented line per
<ENV_ASSISTANT_SLOTS>:(scenario × language)# VAPI_ASSISTANT_<SCENARIO>_<LANG>=
模板使用以下占位符,需根据项目替换:
- — 从草稿标题中提取的短名称(
<PROJECT_NAME>→ X的蛇形/短横线命名法)。默认值:# Rough draft — <X>vapi-voice-agents - — 蛇形命名法的场景ID(例如:
<SCENARIO_ID>)qualification - — 易读的场景名称(例如:
<SCENARIO_NAME>)Lead Qualification - — 从草稿中提取的原始英文初始语句
<FIRST_MESSAGE_EN> - — 自然的西班牙语翻译
<FIRST_MESSAGE_ES> - — 提炼后的初稿提示内容
<BODY_DRAFT> - — 每个场景对应一行
<SCENARIO_IMPORTS>,按字母顺序排列import { <id> } from "./<id>.ts"; - — 逗号分隔的场景ID,放在
<SCENARIO_KEYS>内部SCENARIOS = { ... } - — 每个
<ENV_ASSISTANT_SLOTS>对应一行注释:(场景 × 语言)# VAPI_ASSISTANT_<SCENARIO>_<LANG>=
package.json
(only if absent)
package.jsonpackage.json
(仅当不存在时创建)
package.jsonjson
{
"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": "bun@1.3.1"
}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": "bun@1.3.1"
}tsconfig.json
(only if absent)
tsconfig.jsontsconfig.json
(仅当不存在时创建)
tsconfig.jsonjson
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"allowImportingTsExtensions": true,
"noEmit": true,
"strict": true,
"types": ["bun"]
},
"include": ["src/**/*.ts"]
}json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"allowImportingTsExtensions": true,
"noEmit": true,
"strict": true,
"types": ["bun"]
},
"include": ["src/**/*.ts"]
}.env.example
(create if absent; extend if present)
.env.example.env.example
(不存在则创建,已存在则扩展)
.env.exampledotenv
undefineddotenv
undefinedVAPI_API_KEY=
VAPI_API_KEY=
One slot per (scenario × language). First bun run bootstrap
prints the
bun run bootstrap每个(场景 × 语言)对应一个配置项。首次运行bun run bootstrap
会打印ID;
bun run bootstrapids; paste them here, then re-run for idempotent updates.
将ID粘贴到此处,然后重新运行以实现幂等更新。
<ENV_ASSISTANT_SLOTS>
undefined<ENV_ASSISTANT_SLOTS>
undefined.gitignore
(only if absent)
.gitignore.gitignore
(仅当不存在时创建)
.gitignoregitignore
undefinedgitignore
undefineddependencies
依赖
node_modules
node_modules
env (local-only secrets)
环境变量(本地私密文件)
.env*.local
.env*.local
os junk
系统临时文件
.DS_Store
.DS_Store
bun
Bun相关
*.tsbuildinfo
undefined*.tsbuildinfo
undefinedsrc/assistants/languages.ts
src/assistants/languages.tssrc/assistants/languages.ts
src/assistants/languages.tsts
/**
* 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];ts
/**
* 各语言对应的语音+转录器栈。添加第三种语言只需在下方每个对象中新增一项。
*/
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
src/assistants/loadPrompt.tssrc/assistants/loadPrompt.ts
src/assistants/loadPrompt.tsts
/**
* 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}`;
};ts
/**
* 英文直接返回body.md内容。西班牙语会在body.md前添加西班牙语前置提示,并将场景的话题重定向语句插入{{OFF_TOPIC_LINES}}。
* 每个场景的body.md驱动所有语言变体。
*/
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
src/assistants/scenarios/index.tssrc/assistants/scenarios/index.ts
src/assistants/scenarios/index.tsts
/**
* 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];ts
/**
* 场景注册表。添加新场景只需在此处新增一项,同时在`./<scenario-id>.ts`下创建新文件,并在`../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
src/assistants/buildAssistant.tssrc/assistants/buildAssistant.ts
src/assistants/buildAssistant.tsts
/**
* 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,
},
};
};ts
/**
* 为(场景, 语言)元组生成完整的Vapi助手配置。
* 语音+转录器来自languages.ts;提示来自loadPrompt;
* 名称+初始消息+clientTools来自场景配置。
*/
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
src/assistants/scenarios/<SCENARIO_ID>.tssrc/assistants/scenarios/<SCENARIO_ID>.ts
src/assistants/scenarios/<SCENARIO_ID>.tsts
/**
* <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;If is long, break it across concatenated string segments for readability (one logical clause per line, joined with ).
<FIRST_MESSAGE_EN>" + "ts
/**
* <SCENARIO_NAME>场景。纯数据:ID、名称、多语言初始消息。
* 后续步骤会添加`clientTools`(通话中触发的捕获工具)。
*/
// 将`scenarioId`重命名为生成的<SCENARIO_ID>标识符。
export const scenarioId = {
id: "<SCENARIO_ID>" as const,
name: "<SCENARIO_NAME>",
firstMessage: {
en: "<FIRST_MESSAGE_EN>",
es: "<FIRST_MESSAGE_ES>",
},
clientTools: [] as const,
};
// 将此类型重命名为生成的<帕斯卡命名法场景ID>Scenario。
export type ScenarioIdScenario = typeof scenarioId;若较长,可拆分为多个字符串片段以提高可读性(每个逻辑分句一行,用连接)。
<FIRST_MESSAGE_EN>" + "src/assistants/prompts/<SCENARIO_ID>/body.md
src/assistants/prompts/<SCENARIO_ID>/body.mdsrc/assistants/prompts/<SCENARIO_ID>/body.md
src/assistants/prompts/<SCENARIO_ID>/body.mdRough first draft — persona-driven, not a contract. Suggested shape:
md
undefined初稿内容 — 以角色为导向,而非严格契约。建议格式:
md
undefined<SCENARIO_NAME> voice agent
<SCENARIO_NAME>语音代理
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.
No `## Absolute rules`, no failure rules, no scripted off-ramps. Those are a later step.你是Vapi的<SCENARIO_NAME>语音代理。<一句话说明上下文:对话对象和目的。>
<一段提炼自草稿中What happens的内容:需要询问的问题、收集的数据、路由逻辑、结束方式。>
语气热情且好奇。一次只问一个问题。若用户偏离话题,简要引导后回到下一个待收集的信息点。回复需简短 — 你是在说话,而非打字。
暂不包含`## 绝对规则`、失败规则或脚本化退出流程,这些将在后续步骤中添加。src/assistants/prompts/<SCENARIO_ID>/off-topic-es.md
src/assistants/prompts/<SCENARIO_ID>/off-topic-es.mdsrc/assistants/prompts/<SCENARIO_ID>/off-topic-es.md
src/assistants/prompts/<SCENARIO_ID>/off-topic-es.mdTwo short Spanish redirect lines specific to this scenario:
md
- "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>."两条针对该场景的简短西班牙语重定向语句:
md
- "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
src/assistants/prompts/shared/preambles/es.mdsrc/assistants/prompts/shared/preambles/es.md
src/assistants/prompts/shared/preambles/es.mdmd
undefinedmd
undefinedIDIOMA / LANGUAGE OVERRIDE
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}}
undefined若访问者偏离话题,选择以下语句之一,然后回到下一个待收集的信息点:
{{OFF_TOPIC_LINES}}
undefinedsrc/bootstrap.ts
src/bootstrap.tssrc/bootstrap.ts
src/bootstrap.tsts
/**
* 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);
});ts
/**
* 针对(场景 × 语言)的幂等更新/插入操作。每个元组对应一个条目,键为VAPI_ASSISTANT_<SCENARIO>_<LANG>。
* 首次运行会创建并打印ID;后续运行会原地更新。
*
* 使用`bun run bootstrap`运行。Bun会自动加载.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(`✗ 缺失环境变量:${name}。请查看.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(`✓ 更新${label} → ${existingId}`);
updated = true;
} catch (err) {
const statusCode = (err as { statusCode?: number })?.statusCode;
if (statusCode === 404) {
console.log(
` ${envVar}=${existingId}在当前组织中不存在;将创建新助手。`,
);
} else {
throw err;
}
}
}
if (!updated) {
const assistant = await vapi.assistants.create(body);
console.log(`✓ 创建${label} → ${assistant.id}`);
created.push({ envVar, id: assistant.id });
}
}
}
if (created.length > 0) {
console.log("\n请将以下内容添加到.env.local:");
for (const { envVar, id } of created) {
console.log(` ${envVar}=${id}`);
}
console.log(
"\n然后重新运行`bun run bootstrap`以确认幂等更新。",
);
} else {
console.log(
`\n所有${SCENARIO_IDS.length * LANGUAGES.length}个助手已原地更新。`,
);
}
};
main().catch((err) => {
console.error("✗ 启动失败:", err);
process.exit(1);
});ROUGH_DRAFT.md template
ROUGH_DRAFT.md模板
If the user has no rough draft yet, offer this skeleton (they fill in the angle-brackets and the skill scaffolds the project from it):
md
undefined若用户尚无草稿,提供以下框架(用户填写尖括号中的内容后,技能即可搭建项目):
md
undefinedRough draft — <project name>
Rough draft — <project name>
1. <First scenario 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.>
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>
2. <Second scenario name>
Opening: "<one verbatim opening line>"
What happens: <One paragraph.>
One scenario is fine. Three is fine too. The skill scales the bootstrap loop to whatever's in the rough draft.Opening: "<one verbatim opening line>"
What happens: <One paragraph.>
单个场景或三个场景均可。技能会根据草稿中的场景数量扩展启动循环。Adapting the defaults
修改默认配置
If the user asks for different defaults, change only the matching slot — never the surrounding shape:
- Different languages: add an entry to and
VOICE_BY_LANGUAGE, extend theTRANSCRIBER_BY_LANGUAGEunion inLanguageId, add aloadPrompt.ts, add aprompts/shared/preambles/<lang>.mdper scenario, add the lang tofirstMessage.<lang>inLANGUAGES, and add the matchingbootstrap.tsslots toVAPI_ASSISTANT_<SCENARIO>_<LANG>..env.example - Different model / temperature: change only.
buildAssistant.ts - Different voice provider: update +
VoiceConfiginVOICE_BY_LANGUAGEonly.languages.ts
若用户要求修改默认配置,仅修改对应的配置项 — 绝不改变整体结构:
- 添加新语言:在和
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
验证步骤
After generating files, tell the user to run:
bash
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)生成文件后,告知用户运行以下命令:
bash
bun install
bun run typecheck # 本地验证;不会在Vapi中创建任何内容
cp .env.example .env.local # 从dashboard.vapi.ai/org/api-keys添加VAPI_API_KEY
bun run bootstrap # 为每个(场景 × 语言)创建一个助手paste the printed VAPI_ASSISTANT_<SCENARIO>_<LANG>=<id> lines into .env.local
将打印的VAPI_ASSISTANT_<SCENARIO>_<LANG>=<id>行粘贴到.env.local
bun run bootstrap # second run prints "Updated ..." for every tuple
For N scenarios × 2 languages they should see N × 2 assistants in [dashboard.vapi.ai](https://dashboard.vapi.ai). Each one has a green "Talk to assistant" button — the agents will walk the flow but are still rough (no failure rules, no capture tools yet). That's expected; later prompts harden them.bun run bootstrap # 第二次运行会为每个元组打印"Updated ..."
对于N个场景 × 2种语言,用户应在[dashboard.vapi.ai](https://dashboard.vapi.ai)中看到N×2个助手。每个助手都有绿色的"Talk to assistant"按钮 — 代理会执行流程,但仍为初稿(尚无失败规则和捕获工具)。这是预期情况;后续步骤会优化提示内容。