From 48937d9adc8907aadc41542436dabe5d33b53a1c Mon Sep 17 00:00:00 2001 From: Callum Reid Date: Tue, 10 Mar 2026 15:09:34 -0700 Subject: [PATCH 1/7] fix: use sync _check_sim_id in LiveKit injection to avoid asyncio import --- src/prompts.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/prompts.ts b/src/prompts.ts index c73a77b..9b7c270 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -336,14 +336,15 @@ const FRAMEWORK_RULES: Record = { - Inject \`setup_coval_tracing()\` BEFORE \`AgentSession()\` or \`VoicePipelineAgent()\` construction - Extract the simulation ID from the SIP participant attributes: \`\`\`python - async def _check_sim_id(participant): + def _check_sim_id(participant): sim_id = participant.attributes.get("sip.h.X-Coval-Simulation-Id") if sim_id: set_simulation_id(sim_id) - ctx.room.on("participant_connected", lambda p: asyncio.ensure_future(_check_sim_id(p))) - ctx.room.on("participant_attributes_changed", lambda old, p: asyncio.ensure_future(_check_sim_id(p))) + ctx.room.on("participant_connected", _check_sim_id) + ctx.room.on("participant_attributes_changed", lambda old, p: _check_sim_id(p)) \`\`\` +- Do NOT use \`asyncio.ensure_future\` — \`set_simulation_id\` is synchronous, so \`_check_sim_id\` must be a plain \`def\` - After \`await session.start()\`, add \`instrument_session(session)\` - Import \`instrument_session\` from \`coval_tracing\` alongside \`setup_coval_tracing\` and \`set_simulation_id\``, From 7a114df656f2f3b8d13766fa60b9f2f91b34d102 Mon Sep 17 00:00:00 2001 From: Callum Reid Date: Mon, 30 Mar 2026 16:54:23 -0700 Subject: [PATCH 2/7] fix: prefer bot.py for pipecat detection and add OTel dep injection - findEntryPoint now accepts optional framework param; pipecat uses bot.py-first order (Pipecat Cloud convention) to avoid picking a co-located agent.py over the real cloud entry point - addOtelDeps() in files.ts writes missing opentelemetry-* packages to requirements.txt or pyproject.toml after wizard writes its files - index.ts calls addOtelDeps and logs which packages were added - detect.test.ts: 5 new tests covering pipecat/livekit priority and the full-pipeline pipecat case --- src/__tests__/detect.test.ts | 26 ++++++++++++++++++++++++++ src/constants.ts | 12 +++++++++++- src/detect.ts | 16 +++++++++++----- src/files.ts | 35 ++++++++++++++++++++++++++++++++++- src/index.ts | 7 ++++++- 5 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/__tests__/detect.test.ts b/src/__tests__/detect.test.ts index 4092525..fcfa408 100644 --- a/src/__tests__/detect.test.ts +++ b/src/__tests__/detect.test.ts @@ -128,6 +128,20 @@ describe('findEntryPoint', () => { touch(dir, 'bot.py') expect(findEntryPoint(dir)).toBe('bot.py') }) + + it('prefers bot.py over agent.py for pipecat framework', () => { + const dir = makeTempDir() + touch(dir, 'agent.py') + touch(dir, 'bot.py') + expect(findEntryPoint(dir, 'pipecat')).toBe('bot.py') + }) + + it('keeps agent.py priority for livekit framework', () => { + const dir = makeTempDir() + touch(dir, 'agent.py') + touch(dir, 'bot.py') + expect(findEntryPoint(dir, 'livekit')).toBe('agent.py') + }) }) describe('detectFramework edge cases', () => { @@ -166,6 +180,18 @@ describe('detect (full pipeline)', () => { expect(result!.additionalFiles['pyproject.toml']).toBeDefined() }) + it('detects a Pipecat project and prefers bot.py over agent.py', () => { + const dir = makeTempDir() + touch(dir, 'requirements.txt', 'pipecat-ai\n') + touch(dir, 'agent.py', 'from pipecat.pipeline import Pipeline\n') + touch(dir, 'bot.py', 'from pipecat.pipeline import Pipeline\n') + + const result = detect(dir) + expect(result).not.toBeNull() + expect(result!.framework).toBe('pipecat') + expect(result!.entryPointPath).toBe('bot.py') + }) + it('returns null without a project file', () => { const dir = makeTempDir() touch(dir, 'agent.py') diff --git a/src/constants.ts b/src/constants.ts index 0ba4b37..d8c7cde 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -72,5 +72,15 @@ export const MAX_FILE_SIZE_BYTES = 50 * 1024 /** Files that indicate a Python project, checked in priority order. */ export const PROJECT_FILES = ['pyproject.toml', 'requirements.txt', 'Pipfile', 'setup.py'] as const -/** Entry point filenames to look for, in priority order. */ +/** 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 + +/** 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 + +/** OTel packages required by coval_tracing.py. */ +export const OTEL_PACKAGES = [ + 'opentelemetry-api>=1.0.0', + 'opentelemetry-sdk>=1.0.0', + 'opentelemetry-exporter-otlp-proto-http>=1.0.0', +] as const diff --git a/src/detect.ts b/src/detect.ts index cdaace0..bb07004 100644 --- a/src/detect.ts +++ b/src/detect.ts @@ -1,6 +1,11 @@ import { readFileSync, readdirSync, existsSync } from 'node:fs' import { join } from 'node:path' -import { FRAMEWORKS, PROJECT_FILES, ENTRY_POINT_NAMES } from './constants.js' +import { + FRAMEWORKS, + PROJECT_FILES, + ENTRY_POINT_NAMES, + PIPECAT_ENTRY_POINT_NAMES, +} from './constants.js' import type { Framework, DetectionResult } from './types.js' /** Safely read a file, returning null on error. */ @@ -45,9 +50,10 @@ export const detectFramework = (dir: string): Framework => { return FRAMEWORKS.GENERIC } -/** Find the most likely entry point file. */ -export const findEntryPoint = (dir: string): string | null => { - for (const name of ENTRY_POINT_NAMES) { +/** Find the most likely entry point file. Uses framework-specific priority when provided. */ +export const findEntryPoint = (dir: string, framework?: Framework): string | null => { + const names = framework === FRAMEWORKS.PIPECAT ? PIPECAT_ENTRY_POINT_NAMES : ENTRY_POINT_NAMES + for (const name of names) { if (existsSync(join(dir, name))) return name } @@ -80,7 +86,7 @@ export const detect = (dir: string): DetectionResult | null => { if (!projectFile) return null const framework = detectFramework(dir) - const entryPointPath = findEntryPoint(dir) + const entryPointPath = findEntryPoint(dir, framework) if (!entryPointPath) return null return { diff --git a/src/files.ts b/src/files.ts index 47b9a5e..fd1db7d 100644 --- a/src/files.ts +++ b/src/files.ts @@ -1,8 +1,9 @@ import { readFileSync, writeFileSync, copyFileSync, existsSync } from 'node:fs' +import { join } from 'node:path' import { createTwoFilesPatch } from 'diff' import chalk from 'chalk' import * as p from '@clack/prompts' -import { MAX_FILE_SIZE_BYTES } from './constants.js' +import { MAX_FILE_SIZE_BYTES, OTEL_PACKAGES } from './constants.js' export const readFile = (path: string): string => { const content = readFileSync(path, 'utf-8') @@ -42,3 +43,35 @@ export const writeFile = (path: string, content: string): void => { } export const fileExists = (path: string): boolean => existsSync(path) + +/** + * Add missing OTel packages to the project's dependency file. + * Handles requirements.txt (plain append) and pyproject.toml (injects into dependencies array). + * Returns the list of packages that were added, or an empty array if none were missing. + */ +export const addOtelDeps = (dir: string, projectFile: string): readonly string[] => { + const filePath = join(dir, projectFile) + const content = readFileSync(filePath, 'utf-8') + + const missing = OTEL_PACKAGES.filter((pkg) => { + const name = pkg.split('>=')[0] + return !content.includes(name) + }) + + if (missing.length === 0) return [] + + if (projectFile === 'requirements.txt') { + writeFileSync(filePath, content.trimEnd() + '\n' + missing.join('\n') + '\n', 'utf-8') + } else if (projectFile === 'pyproject.toml') { + const updated = content.replace( + /(\[project\][\s\S]*?dependencies\s*=\s*\[)([\s\S]*?)(\])/, + (_, open: string, inner: string, close: string) => { + const additions = missing.map((p) => ` "${p}",`).join('\n') + return `${open}${inner}${additions}\n${close}` + }, + ) + if (updated !== content) writeFileSync(filePath, updated, 'utf-8') + } + + return missing +} diff --git a/src/index.ts b/src/index.ts index 8ff7305..8374968 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,7 +8,7 @@ import chalk from 'chalk' import { getApiKey, verifyApiKey } from './auth.js' import { detect } from './detect.js' import { callWizardLLM, getLLMConfig } from './llm.js' -import { readFile, backupFile, showDiff, writeFile, fileExists } from './files.js' +import { readFile, backupFile, showDiff, writeFile, fileExists, addOtelDeps } from './files.js' import { sendTestSpan } from './validate.js' import { FRAMEWORK_LABELS, VERIFY_RESULTS, COVAL_TRACING_FILE } from './constants.js' @@ -116,6 +116,11 @@ const main = async () => { p.log.success(`${isCreate ? 'Created' : 'Updated'} ${COVAL_TRACING_FILE}`) p.log.success(`Modified ${detection.entryPointPath}`) + const addedDeps = addOtelDeps(targetDir, detection.projectFile) + if (addedDeps.length > 0) { + p.log.success(`Added OTel packages to ${detection.projectFile}: ${addedDeps.join(', ')}`) + } + // ── Validate ────────────────────────────────────────────────────────── let shouldValidate = true if (!autoYes) { From d694b788bb3dbb3df2b4fe2ce20d8a0593d13ca0 Mon Sep 17 00:00:00 2001 From: Callum Reid Date: Mon, 30 Mar 2026 17:04:02 -0700 Subject: [PATCH 3/7] fix: match closing array bracket on its own line in pyproject.toml dep injection The previous regex used lazy [\s\S]*?(\]) which matched the first ] found, breaking when packages have extras like pipecat-ai[daily,openai]>=0.0.60. Now matches \n] so only the line-leading closing bracket is targeted. --- src/files.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/files.ts b/src/files.ts index fd1db7d..c51ee54 100644 --- a/src/files.ts +++ b/src/files.ts @@ -63,8 +63,10 @@ export const addOtelDeps = (dir: string, projectFile: string): readonly string[] if (projectFile === 'requirements.txt') { writeFileSync(filePath, content.trimEnd() + '\n' + missing.join('\n') + '\n', 'utf-8') } else if (projectFile === 'pyproject.toml') { + // Match the closing ] that sits on its own line to avoid matching ] inside + // package extras like pipecat-ai[daily,openai]>=0.0.60 const updated = content.replace( - /(\[project\][\s\S]*?dependencies\s*=\s*\[)([\s\S]*?)(\])/, + /(\[project\][\s\S]*?dependencies\s*=\s*\[)([\s\S]*?)(\n\])/, (_, open: string, inner: string, close: string) => { const additions = missing.map((p) => ` "${p}",`).join('\n') return `${open}${inner}${additions}\n${close}` From 0a374e54dcd990eb46e0c98c67d324ba4e4f2521 Mon Sep 17 00:00:00 2001 From: Callum Reid Date: Mon, 30 Mar 2026 17:08:01 -0700 Subject: [PATCH 4/7] fix: add newline before injected deps in pyproject.toml to avoid same-line cramming --- src/files.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/files.ts b/src/files.ts index c51ee54..6b97e66 100644 --- a/src/files.ts +++ b/src/files.ts @@ -69,7 +69,7 @@ export const addOtelDeps = (dir: string, projectFile: string): readonly string[] /(\[project\][\s\S]*?dependencies\s*=\s*\[)([\s\S]*?)(\n\])/, (_, open: string, inner: string, close: string) => { const additions = missing.map((p) => ` "${p}",`).join('\n') - return `${open}${inner}${additions}\n${close}` + return `${open}${inner}\n${additions}${close}` }, ) if (updated !== content) writeFileSync(filePath, updated, 'utf-8') From 453fdf5b34f685ad2b1943e7151f02804cd7604c Mon Sep 17 00:00:00 2001 From: Callum Reid Date: Mon, 30 Mar 2026 17:20:35 -0700 Subject: [PATCH 5/7] fix: prefer requirements.txt for OTel dep injection when present alongside pyproject.toml pip/Docker deployments use requirements.txt regardless of which project file was detected first. Without this, projects with both files get OTel packages added only to pyproject.toml, causing ModuleNotFoundError at deploy time. --- src/files.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/files.ts b/src/files.ts index 6b97e66..23c13ff 100644 --- a/src/files.ts +++ b/src/files.ts @@ -49,8 +49,17 @@ export const fileExists = (path: string): boolean => existsSync(path) * Handles requirements.txt (plain append) and pyproject.toml (injects into dependencies array). * Returns the list of packages that were added, or an empty array if none were missing. */ +/** + * Add missing OTel packages to the project's dependency file. + * Prefers requirements.txt when it exists (used by pip/Docker), falling back + * to pyproject.toml. Returns the list of packages added, or [] if none. + */ export const addOtelDeps = (dir: string, projectFile: string): readonly string[] => { - const filePath = join(dir, projectFile) + // Always prefer requirements.txt when present — pip/Docker deployments use it + // regardless of whether pyproject.toml was detected as the primary project file. + const reqTxt = join(dir, 'requirements.txt') + const targetFile = existsSync(reqTxt) ? 'requirements.txt' : projectFile + const filePath = join(dir, targetFile) const content = readFileSync(filePath, 'utf-8') const missing = OTEL_PACKAGES.filter((pkg) => { @@ -60,9 +69,9 @@ export const addOtelDeps = (dir: string, projectFile: string): readonly string[] if (missing.length === 0) return [] - if (projectFile === 'requirements.txt') { + if (targetFile === 'requirements.txt') { writeFileSync(filePath, content.trimEnd() + '\n' + missing.join('\n') + '\n', 'utf-8') - } else if (projectFile === 'pyproject.toml') { + } else if (targetFile === 'pyproject.toml') { // Match the closing ] that sits on its own line to avoid matching ] inside // package extras like pipecat-ai[daily,openai]>=0.0.60 const updated = content.replace( From 3de6bb772edb1ebbe668617b2877520f7cefe36d Mon Sep 17 00:00:00 2001 From: Callum Reid Date: Mon, 30 Mar 2026 17:30:15 -0700 Subject: [PATCH 6/7] fix: correct pipecat SIP sim ID extraction path The old prompt used body.dialin_settings.custom_context.coval_simulation_id which doesn't exist. The Coval SIP header X-Coval-Simulation-Id is forwarded by the PCC webhook under body.dialin_settings.sip_headers, so the correct path is body.dialin_settings.sip_headers['X-Coval-Simulation-Id']. Also moved extraction to bot() top-level (not on_dialin_connected) since args.body is available at session start. --- src/prompts.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/prompts.ts b/src/prompts.ts index 9b7c270..8dbc2a2 100644 --- a/src/prompts.ts +++ b/src/prompts.ts @@ -323,13 +323,15 @@ def create_tool_call_span(name: str = "", call_id: str = "", arguments: str = "" const FRAMEWORK_RULES: Record = { pipecat: `## Pipecat Framework Rules - Inject \`setup_coval_tracing()\` BEFORE \`PipelineTask()\`, \`PipelineRunner()\`, or \`Pipeline()\` construction -- Extract the simulation ID from \`args.body\` in the \`on_dialin_connected\` handler if present: +- Extract the simulation ID from the SIP headers in \`args.body\` at the top of \`bot()\`, right after \`setup_coval_tracing()\`: \`\`\`python - sim_id = (args.body or {}).get("dialin_settings", {}).get("custom_context", {}).get("coval_simulation_id") + sip_headers = (body or {}).get("dialin_settings", {}).get("sip_headers", {}) + sim_id = sip_headers.get("X-Coval-Simulation-Id") or sip_headers.get("x-coval-simulation-id") if sim_id: set_simulation_id(sim_id) \`\`\` -- If there is NO \`on_dialin_connected\` handler, add a TODO comment for where to call \`set_simulation_id()\` + Place this immediately after \`body = getattr(args, "body", None) or {}\` (or after \`setup_coval_tracing()\` if body is extracted later). Do NOT put it inside \`on_dialin_connected\`. +- If there is NO \`body\` variable extracted from \`args\`, add the body extraction then the sim ID lines above - Add \`enable_metrics=True\` and \`enable_tracing=True\` to PipelineTask if not already present`, livekit: `## LiveKit Agents Framework Rules From 3b63b1b762f85f435d6a68e085b2a9f9fda6c0d0 Mon Sep 17 00:00:00 2001 From: Callum Reid Date: Mon, 30 Mar 2026 17:56:04 -0700 Subject: [PATCH 7/7] fix: ensure trailing comma on last dep before TOML injection --- package-lock.json | 40 ---------------------------------------- src/files.ts | 4 +++- 2 files changed, 3 insertions(+), 41 deletions(-) diff --git a/package-lock.json b/package-lock.json index b9e54e4..5b6e5c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -73,7 +73,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2615,7 +2614,6 @@ "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", @@ -3096,7 +3094,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3399,7 +3396,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3814,7 +3810,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -3880,7 +3875,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4810,7 +4804,6 @@ "integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.3.0", "@jest/types": "30.3.0", @@ -6041,26 +6034,6 @@ "thenify-all": "^1.0.0" } }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "optional": true, - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/napi-postinstall": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", @@ -6809,17 +6782,6 @@ "node": ">= 12" } }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -7298,7 +7260,6 @@ "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" @@ -7355,7 +7316,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/src/files.ts b/src/files.ts index 23c13ff..56c9d1b 100644 --- a/src/files.ts +++ b/src/files.ts @@ -77,8 +77,10 @@ export const addOtelDeps = (dir: string, projectFile: string): readonly string[] const updated = content.replace( /(\[project\][\s\S]*?dependencies\s*=\s*\[)([\s\S]*?)(\n\])/, (_, open: string, inner: string, close: string) => { + // Ensure the last existing dep has a trailing comma before appending + const innerNormalized = inner.trimEnd().endsWith(',') ? inner : inner.trimEnd() + ',' const additions = missing.map((p) => ` "${p}",`).join('\n') - return `${open}${inner}\n${additions}${close}` + return `${open}${innerNormalized}\n${additions}${close}` }, ) if (updated !== content) writeFileSync(filePath, updated, 'utf-8')