Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/__tests__/detect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ describe('detectFramework', () => {
expect(detectFramework(dir)).toBe('livekit')
})

it('detects vapi from requirements.txt', () => {
const dir = makeTempDir()
touch(dir, 'requirements.txt', 'vapi-python\nfastapi\n')
expect(detectFramework(dir)).toBe('vapi')
})

it('detects vapi from webhook patterns in Python code', () => {
const dir = makeTempDir()
touch(dir, 'server.py', '# vapi webhook handler for assistant-request\n')
expect(detectFramework(dir)).toBe('vapi')
})
Comment thread
callumreid marked this conversation as resolved.

it('returns generic for unknown project', () => {
const dir = makeTempDir()
touch(dir, 'app.py', 'import flask\n')
Expand Down
8 changes: 8 additions & 0 deletions src/__tests__/prompts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ describe('buildSystemPrompt', () => {
expect(prompt).toContain('AgentSession')
expect(prompt).toContain('instrument_session')
expect(prompt).toContain('sip.h.X-Coval-Simulation-Id')
expect(prompt).toContain('Do NOT use')
})

it('includes vapi-specific rules', () => {
const prompt = buildSystemPrompt('vapi')
expect(prompt).toContain('assistantOverrides')
expect(prompt).toContain('coval-simulation-id')
expect(prompt).toContain('end-of-call-report')
})
Comment thread
callumreid marked this conversation as resolved.

it('includes generic rules', () => {
Expand Down
4 changes: 3 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const CONTENT_TYPE_JSON = 'application/json'
export const FRAMEWORKS = {
PIPECAT: 'pipecat',
LIVEKIT: 'livekit',
VAPI: 'vapi',
GENERIC: 'generic',
} as const

Expand All @@ -35,6 +36,7 @@ export type Framework = (typeof FRAMEWORKS)[keyof typeof FRAMEWORKS]
export const FRAMEWORK_LABELS: Record<Framework, string> = {
[FRAMEWORKS.PIPECAT]: 'Pipecat',
[FRAMEWORKS.LIVEKIT]: 'LiveKit Agents',
[FRAMEWORKS.VAPI]: 'Vapi',
[FRAMEWORKS.GENERIC]: 'Generic Python',
}

Expand Down Expand Up @@ -73,7 +75,7 @@ export const MAX_FILE_SIZE_BYTES = 50 * 1024
export const PROJECT_FILES = ['pyproject.toml', 'requirements.txt', 'Pipfile', 'setup.py'] as const

/** Entry point filenames to look for, in priority order (default / LiveKit). */
export const ENTRY_POINT_NAMES = ['agent.py', 'main.py', 'bot.py', 'app.py'] as const
export const ENTRY_POINT_NAMES = ['agent.py', 'main.py', 'bot.py', 'app.py', 'server.py'] as const
Comment thread
callumreid marked this conversation as resolved.

/** Entry point filenames for Pipecat projects — bot.py takes priority (Pipecat Cloud convention). */
export const PIPECAT_ENTRY_POINT_NAMES = ['bot.py', 'main.py', 'agent.py', 'app.py'] as const
Expand Down
5 changes: 4 additions & 1 deletion src/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,18 @@ export const detectFramework = (dir: string): Framework => {
if (!content) continue
if (/pipecat[-_]ai|"pipecat"/.test(content)) return FRAMEWORKS.PIPECAT
if (/livekit[-_]agents|"livekit"/.test(content)) return FRAMEWORKS.LIVEKIT
if (/vapi[-_]python|"vapi"/.test(content)) return FRAMEWORKS.VAPI
}

// Scan .py files for framework imports
// Scan .py files for framework imports and Vapi webhook patterns
try {
for (const f of readdirSync(dir).filter((f) => f.endsWith('.py'))) {
const content = tryRead(join(dir, f))
if (!content) continue
if (/from pipecat|import pipecat/.test(content)) return FRAMEWORKS.PIPECAT
if (/from livekit|import livekit/.test(content)) return FRAMEWORKS.LIVEKIT
if (/vapi.*(webhook|assistant-request|end-of-call-report)/.test(content))
return FRAMEWORKS.VAPI
Comment thread
callumreid marked this conversation as resolved.
}
} catch {
// directory not readable
Expand Down
18 changes: 18 additions & 0 deletions src/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,24 @@ const FRAMEWORK_RULES: Record<Framework, string> = {
- After \`await session.start()\`, add \`instrument_session(session)\`
- Import \`instrument_session\` from \`coval_tracing\` alongside \`setup_coval_tracing\` and \`set_simulation_id\``,

vapi: `## Vapi Webhook Framework Rules
- Add \`setup_coval_tracing()\` at module level, after imports and app creation
- Vapi delivers a SIP header \`X-Coval-Simulation-Id\` which appears in \`message.call.assistantOverrides.variableValues["coval-simulation-id"]\` (Vapi auto-lowercases X- headers)
- Extract the simulation ID from every webhook event as early as possible and cache it by call ID:
\`\`\`python
_call_sim_map: dict[str, str] = {}

# Inside the webhook handler, before any event-specific logic:
if call_id and call_id not in _call_sim_map:
var_values = call.get("assistantOverrides", {}).get("variableValues", {})
sim_id = var_values.get("coval-simulation-id")
if sim_id:
set_simulation_id(str(sim_id))
_call_sim_map[call_id] = str(sim_id)
\`\`\`
Comment thread
callumreid marked this conversation as resolved.
- In the \`end-of-call-report\` handler, retrieve the cached simulation ID and use it
- Do NOT import \`instrument_session\` — Vapi is webhook-based, not SDK-based`,

generic: `## Generic Python Agent Rules
- Add \`setup_coval_tracing()\` at module level, after imports
- Add a TODO comment for where to call \`set_simulation_id()\` with the actual simulation ID
Expand Down
Loading