@databricks/appkit 0.73.0 → 0.74.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +7 -0
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +6 -6
- package/dist/beta.js +6 -6
- package/dist/cli/commands/agent/eval.js +85 -21
- package/dist/cli/commands/agent/eval.js.map +1 -1
- package/dist/cli/commands/registry/add.js +77 -6
- package/dist/cli/commands/registry/add.js.map +1 -1
- package/dist/evals/dataset.d.ts +14 -1
- package/dist/evals/dataset.d.ts.map +1 -1
- package/dist/evals/dataset.js +16 -1
- package/dist/evals/dataset.js.map +1 -1
- package/dist/evals/define-eval.d.ts +4 -2
- package/dist/evals/define-eval.d.ts.map +1 -1
- package/dist/evals/define-eval.js +5 -1
- package/dist/evals/define-eval.js.map +1 -1
- package/dist/evals/discover.d.ts +15 -1
- package/dist/evals/discover.d.ts.map +1 -1
- package/dist/evals/discover.js +40 -10
- package/dist/evals/discover.js.map +1 -1
- package/dist/evals/http-driver.d.ts.map +1 -1
- package/dist/evals/http-driver.js +31 -9
- package/dist/evals/http-driver.js.map +1 -1
- package/dist/evals/index.d.ts +5 -5
- package/dist/evals/index.js +5 -5
- package/dist/evals/report.d.ts +16 -1
- package/dist/evals/report.d.ts.map +1 -1
- package/dist/evals/report.js +64 -2
- package/dist/evals/report.js.map +1 -1
- package/dist/evals/run-eval.d.ts +5 -0
- package/dist/evals/run-eval.d.ts.map +1 -1
- package/dist/evals/run-eval.js +44 -3
- package/dist/evals/run-eval.js.map +1 -1
- package/dist/evals/run-evals.d.ts +32 -3
- package/dist/evals/run-evals.d.ts.map +1 -1
- package/dist/evals/run-evals.js +128 -26
- package/dist/evals/run-evals.js.map +1 -1
- package/dist/evals/types.d.ts +40 -2
- package/dist/evals/types.d.ts.map +1 -1
- package/dist/registry/manifest-loader.d.ts +1 -1
- package/docs/api/appkit/Function.defineEvalConfig.md +18 -0
- package/docs/api/appkit/Function.discoverEvalConfigs.md +18 -0
- package/docs/api/appkit/Function.formatResultsJUnit.md +18 -0
- package/docs/api/appkit/Function.formatResultsJson.md +18 -0
- package/docs/api/appkit/Function.runWithRetries.md +28 -0
- package/docs/api/appkit/Function.userTurns.md +20 -0
- package/docs/api/appkit/Interface.DiscoveredEvalConfig.md +25 -0
- package/docs/api/appkit/Interface.DriveResult.md +28 -0
- package/docs/api/appkit/Interface.EvalDefinition.md +22 -0
- package/docs/api/appkit/Interface.EvalDriver.md +10 -4
- package/docs/api/appkit/Interface.EvalResult.md +11 -0
- package/docs/api/appkit/Interface.EvalSummary.md +11 -0
- package/docs/api/appkit/Interface.RunEvalOptions.md +11 -0
- package/docs/api/appkit/Interface.RunEvalsOptions.md +23 -1
- package/docs/api/appkit/Interface.TestContext.md +30 -8
- package/docs/api/appkit.md +7 -0
- package/docs/plugins/agents.md +255 -4
- package/llms.txt +7 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"add.js","names":[],"sources":["../../../../src/cli/commands/registry/add.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\n\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\n\nimport {\n fetchRegistryItem,\n fetchVerifiedNames,\n isValidItemName,\n type RegistryItem,\n type RegistryItemFile,\n stripNamespace,\n} from \"./client\";\nimport {\n buildConfigPlan,\n collectBindingValueNeeds,\n planHasContent,\n} from \"./config-plan\";\nimport {\n reportConfigWrite,\n validateBundle,\n writeConfig,\n} from \"./config-writer\";\nimport {\n JS_IDENTIFIER,\n REGISTRY_REPO,\n type RegistryToken,\n resolveToken,\n} from \"./constants\";\nimport { parseEnv } from \"./env-reconcile\";\nimport {\n extractRequirements,\n type ResourceRequirementRow,\n renderRequirements,\n} from \"./requirements\";\n\n/** Subdirectories that commonly hold the frontend / server in an AppKit app. */\nconst FRONTEND_SUBDIRS = [\"client\", \"frontend\", \"web\", \"app\"];\nconst SERVER_SUBDIRS = [\"server\", \"api\", \"backend\"];\n\nfunction isDir(p: string): boolean {\n return fs.existsSync(p) && fs.statSync(p).isDirectory();\n}\n\n/** A registry item is a server plugin if it ships a manifest.json. */\nfunction isPluginItem(item: RegistryItem): boolean {\n return (item.files ?? []).some(\n (f) => path.basename(f.target ?? f.path) === \"manifest.json\",\n );\n}\n\n/**\n * Locates the frontend root for UI components. AppKit apps put the client in a\n * client/ subdir (with its own components.json + src/); the CLI is typically\n * run from the repo root. Prefer the dir with components.json, then a src/.\n */\nfunction findFrontendRoot(cwd: string): string {\n if (fs.existsSync(path.join(cwd, \"components.json\"))) return cwd;\n for (const sub of FRONTEND_SUBDIRS) {\n if (fs.existsSync(path.join(cwd, sub, \"components.json\"))) {\n return path.join(cwd, sub);\n }\n }\n if (isDir(path.join(cwd, \"src\"))) return cwd;\n for (const sub of FRONTEND_SUBDIRS) {\n if (isDir(path.join(cwd, sub, \"src\"))) return path.join(cwd, sub);\n }\n return cwd;\n}\n\n/** Locates the server root for plugins (the server/ subdir, else cwd). */\nfunction findServerRoot(cwd: string): string {\n for (const sub of SERVER_SUBDIRS) {\n if (isDir(path.join(cwd, sub))) return path.join(cwd, sub);\n }\n return cwd;\n}\n\n/** Nearest dir with a package.json, walking up from start (for dep install). */\nfunction findNearestPackageJson(start: string): string {\n let dir = start;\n for (;;) {\n if (fs.existsSync(path.join(dir, \"package.json\"))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) return start;\n dir = parent;\n }\n}\n\n/**\n * The Databricks profile the app is configured with, read from\n * `DATABRICKS_CONFIG_PROFILE` in `cwd/.env` via the same dotenv parser the app\n * loads its env with. The CLI's top-level `dotenv/config` only loads the launch\n * dir's `.env` into `process.env`; when `--cwd` points at a different app dir,\n * that file isn't loaded, so the workspace picker and bundle validate would\n * miss the app's profile — this reads it directly. Undefined if the file or key\n * is absent, so the SDK's own default resolution still applies.\n */\nexport function profileFromEnv(cwd: string): string | undefined {\n try {\n const content = fs.readFileSync(path.join(cwd, \".env\"), \"utf-8\");\n return parseEnv(content).DATABRICKS_CONFIG_PROFILE || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Resolves a registry item's `target` under `base`, enforcing that the result\n * stays inside `base`. Registry items are untrusted remote data; a `target`\n * like `../../../.zshrc` or an absolute path could otherwise write files\n * anywhere on disk (arbitrary-write → RCE). Throws on any escape.\n */\nexport function resolveWithinBase(base: string, target: string): string {\n if (path.isAbsolute(target)) {\n throw new Error(`Refusing absolute file target from registry: ${target}`);\n }\n const baseResolved = path.resolve(base);\n const resolved = path.resolve(baseResolved, target);\n if (\n resolved !== baseResolved &&\n !resolved.startsWith(baseResolved + path.sep)\n ) {\n throw new Error(\n `Refusing file target that escapes the destination directory: ${target}`,\n );\n }\n return resolved;\n}\n\n/** UI file destination (relative to the frontend root): placed in src/ if present. */\nfunction uiTargetPath(base: string, file: RegistryItemFile): string {\n let target = file.target ?? path.join(\"components\", path.basename(file.path));\n if (!target.startsWith(\"src/\") && isDir(path.join(base, \"src\"))) {\n target = path.join(\"src\", target);\n }\n return target;\n}\n\n/** Best-effort: the `toPlugin` export name from the item's index.ts. Returns\n * null (caller falls back to printed instructions) unless the name is a plain\n * JS identifier — the item is untrusted remote content and the value is\n * interpolated into the user's server.ts. */\nexport function pluginExportName(item: RegistryItem): string | null {\n const index = (item.files ?? []).find(\n (f) => path.basename(f.target ?? f.path) === \"index.ts\",\n );\n const match = index?.content.match(/export\\s*\\{([^}]*)\\}/);\n if (!match) return null;\n const names = match[1].split(\",\").map((s) => s.trim());\n // Prefer the camelCase toPlugin instance over the PascalCase class.\n const chosen = names.find((n) => /^[a-z]/.test(n)) ?? names[0];\n return chosen && JS_IDENTIFIER.test(chosen) ? chosen : null;\n}\n\nfunction detectPackageManager(cwd: string): \"pnpm\" | \"yarn\" | \"bun\" | \"npm\" {\n if (fs.existsSync(path.join(cwd, \"pnpm-lock.yaml\"))) return \"pnpm\";\n if (fs.existsSync(path.join(cwd, \"yarn.lock\"))) return \"yarn\";\n if (fs.existsSync(path.join(cwd, \"bun.lockb\"))) return \"bun\";\n return \"npm\";\n}\n\n/**\n * A safe npm dependency spec: `[@scope/]name` with an optional `@version`\n * range. Registry `dependencies` are untrusted remote data passed to the\n * package manager, so we reject anything that isn't a plain name+range —\n * blocks tarball/git URL specs (install-script RCE) and `-`-prefixed entries\n * that the PM would parse as flags (argument injection).\n */\nconst SAFE_DEP_SPEC =\n /^(@[a-z0-9][\\w.-]*\\/)?[a-z0-9][\\w.-]*(@[\\w.\\-+~^><=|* ]+)?$/i;\n\n/** Splits deps into safe (installable) and rejected (surfaced to the user). */\nexport function partitionDeps(deps: string[]): {\n safe: string[];\n rejected: string[];\n} {\n const safe: string[] = [];\n const rejected: string[] = [];\n for (const dep of deps) {\n if (dep.startsWith(\"-\") || !SAFE_DEP_SPEC.test(dep)) rejected.push(dep);\n else safe.push(dep);\n }\n return { safe, rejected };\n}\n\nfunction installDependencies(deps: string[], cwd: string): void {\n if (deps.length === 0) return;\n\n const { safe, rejected } = partitionDeps(deps);\n if (rejected.length > 0) {\n console.warn(\n pc.yellow(\n `Skipping suspicious dependenc${rejected.length === 1 ? \"y\" : \"ies\"} from the registry (not a plain name@version): ${rejected.join(\", \")}. Install manually if you trust them.`,\n ),\n );\n }\n if (safe.length === 0) return;\n\n if (!fs.existsSync(path.join(cwd, \"package.json\"))) {\n console.warn(\n pc.yellow(\n `No package.json found — install these manually: ${safe.join(\" \")}`,\n ),\n );\n return;\n }\n const pm = detectPackageManager(cwd);\n const subcommand = pm === \"npm\" ? \"install\" : \"add\";\n console.log(`\\nInstalling dependencies with ${pm}: ${safe.join(\" \")}`);\n // `--` stops the PM from parsing any dep as a flag (defense in depth).\n const result = spawnSync(pm, [subcommand, \"--\", ...safe], {\n stdio: \"inherit\",\n cwd,\n });\n if (result.status !== 0) {\n console.warn(\n pc.yellow(\n `Dependency install exited with code ${result.status ?? \"unknown\"} — install manually if needed: ${safe.join(\" \")}`,\n ),\n );\n }\n}\n\n/** Runs `appkit plugin sync --write` via this same CLI binary. */\nfunction runPluginSync(cwd: string): void {\n const result = spawnSync(\n process.execPath,\n [process.argv[1], \"plugin\", \"sync\", \"--write\"],\n { stdio: \"inherit\", cwd },\n );\n if (result.status !== 0) {\n console.warn(\n pc.yellow(\n \" Plugin sync did not complete cleanly — run `appkit plugin sync --write` manually.\",\n ),\n );\n }\n}\n\nfunction writeItemFile(\n base: string,\n target: string,\n content: string,\n force: boolean,\n cwd: string,\n): void {\n const dest = resolveWithinBase(base, target);\n const existed = fs.existsSync(dest);\n if (existed && !force) {\n console.error(\n pc.red(\n `Refusing to overwrite ${path.relative(cwd, dest)} — pass --force to replace it.`,\n ),\n );\n process.exit(1);\n }\n fs.mkdirSync(path.dirname(dest), { recursive: true });\n fs.writeFileSync(dest, content);\n const label = existed ? pc.yellow(\"Updated\") : pc.green(\"Created\");\n console.log(`${label} ${path.relative(cwd, dest)}`);\n}\n\ninterface PluginSummary {\n importPath: string;\n exportName: string | null;\n}\n\n/**\n * Fetches the requested items plus their transitive registryDependencies.\n * Dependencies are resolved breadth-first and de-duplicated by name, so a\n * plugin that depends on another registry item pulls the whole graph in one\n * `add`. Explicitly-requested items keep their request order and come first.\n */\nexport async function resolveItems(\n names: string[],\n token: RegistryToken | null,\n fetchItem: (\n name: string,\n token: RegistryToken | null,\n ) => Promise<RegistryItem> = fetchRegistryItem,\n): Promise<RegistryItem[]> {\n const seen = new Set<string>();\n const ordered: RegistryItem[] = [];\n // A name is both the fetch path and the on-disk `plugins/<name>` dir, so\n // reject non-slug refs (`/`, `..`, control chars) before they reach either.\n const enqueue = (ref: string): string => {\n const name = stripNamespace(ref);\n if (!isValidItemName(name)) {\n throw new Error(`Invalid registry item name: ${JSON.stringify(ref)}`);\n }\n return name;\n };\n // Breadth-first over the dependency graph, one level per iteration. Items in\n // a level are fetched concurrently (fetch latency is additive otherwise), but\n // levels stay ordered and dedup/cycle handling is unchanged: a name is marked\n // seen before its level is fetched, so it's never fetched or queued twice.\n let level = names.map(enqueue).filter((name) => {\n if (seen.has(name)) return false;\n seen.add(name);\n return true;\n });\n\n while (level.length > 0) {\n const items = await Promise.all(\n level.map(async (key) => {\n const item = await fetchItem(key, token);\n // Pin to the fetch key: the body's self-reported `name` is untrusted\n // and could claim a verified name to slip past the gate. The key is the\n // trustworthy identity the index keys `verified` on.\n item.name = key;\n return item;\n }),\n );\n ordered.push(...items);\n const next: string[] = [];\n for (const item of items) {\n for (const dep of item.registryDependencies ?? []) {\n const depName = enqueue(dep);\n if (seen.has(depName)) continue;\n seen.add(depName);\n next.push(depName);\n }\n }\n level = next;\n }\n\n return ordered;\n}\n\ninterface AddOptions {\n force?: boolean;\n cwd?: string;\n register?: boolean;\n /** false = don't reconcile resource env vars into .env. */\n resources?: boolean;\n /** true = never prompt; use --env flags or leave unset (agent/CI). */\n yes?: boolean;\n /** Pre-supplied env values from repeated --env KEY=VALUE flags. */\n env?: Record<string, string>;\n /** Databricks profile passed to `bundle validate` after writing config. */\n profile?: string;\n /** true = install items the registry index doesn't mark verified. */\n allowUnverified?: boolean;\n}\n\n/**\n * Splits requested names into verified and unverified against the index's\n * verified set. When `verified` is null the index couldn't be read — we can't\n * prove anything is verified, so every name is treated as unverified (the gate\n * then decides whether to warn-and-continue or block). Names are compared with\n * the namespace stripped, matching how items are fetched.\n */\nexport function partitionVerified(\n refs: string[],\n verified: Set<string> | null,\n): { verified: string[]; unverified: string[] } {\n const ok: string[] = [];\n const bad: string[] = [];\n for (const ref of refs) {\n const name = stripNamespace(ref);\n if (verified?.has(name)) ok.push(name);\n else bad.push(name);\n }\n return { verified: ok, unverified: bad };\n}\n\nasync function runAdd(refs: string[], opts: AddOptions): Promise<void> {\n const cwd = opts.cwd ? path.resolve(opts.cwd) : process.cwd();\n const token = resolveToken();\n if (token) {\n console.log(\n `Using ${token.envName} to fetch from ${REGISTRY_REPO} (private).`,\n );\n }\n\n // Resolve the full graph and fetch the verified index concurrently (two\n // independent round-trips). Item resolution is read-only — nothing is written\n // or installed until after the gate below.\n const verifiedP = opts.allowUnverified\n ? Promise.resolve(null)\n : fetchVerifiedNames(token);\n const items = await resolveItems(refs, token);\n\n // Integrity gate over the *entire resolved set* (not just requested names, so\n // an unverified transitive dep can't ride in on a verified item). Fails closed:\n // a null verified set (unreadable index) makes every item count as unverified.\n if (!opts.allowUnverified) {\n const verified = await verifiedP;\n const { unverified } = partitionVerified(\n items.map((i) => i.name),\n verified,\n );\n if (unverified.length > 0) {\n const reason =\n verified === null\n ? \"could not read the registry index to verify these items\"\n : `not marked verified in ${REGISTRY_REPO}`;\n console.error(\n pc.red(\n `Refusing to add unverified item(s) (${reason}): ${unverified.join(\", \")}.`,\n ),\n );\n console.error(\n pc.dim(\n \" Re-run with --allow-unverified if you trust the source; unverified items run code in your app.\",\n ),\n );\n process.exit(1);\n }\n }\n\n const hasUi = items.some((i) => !isPluginItem(i));\n const hasPlugin = items.some(isPluginItem);\n const frontendRoot = hasUi ? findFrontendRoot(cwd) : cwd;\n const serverRoot = hasPlugin ? findServerRoot(cwd) : cwd;\n if (hasUi && frontendRoot !== cwd) {\n console.log(pc.dim(`UI components → ${path.relative(cwd, frontendRoot)}/`));\n }\n if (hasPlugin && serverRoot !== cwd) {\n console.log(pc.dim(`Plugins → ${path.relative(cwd, serverRoot)}/`));\n }\n\n const deps = new Set<string>();\n let wroteUi = false;\n const pluginSummaries: PluginSummary[] = [];\n const allRequirements: ResourceRequirementRow[] = [];\n\n for (const item of items) {\n for (const dep of item.dependencies ?? []) deps.add(dep);\n\n if (isPluginItem(item)) {\n let pluginRel = path.join(\"plugins\", item.name);\n for (const file of item.files ?? []) {\n const target =\n file.target ??\n path.join(\"plugins\", item.name, path.basename(file.path));\n writeItemFile(\n serverRoot,\n target,\n file.content,\n Boolean(opts.force),\n cwd,\n );\n if (path.basename(target) === \"manifest.json\") {\n pluginRel = path.dirname(target);\n }\n }\n const requirements = extractRequirements(item);\n if (requirements.length > 0) {\n console.log(`\\n${renderRequirements(item, requirements)}`);\n allRequirements.push(...requirements);\n }\n pluginSummaries.push({\n importPath: `./${pluginRel}`,\n exportName: pluginExportName(item),\n });\n } else {\n for (const file of item.files ?? []) {\n writeItemFile(\n frontendRoot,\n uiTargetPath(frontendRoot, file),\n file.content,\n Boolean(opts.force),\n cwd,\n );\n wroteUi = true;\n }\n }\n }\n\n installDependencies([...deps], findNearestPackageJson(cwd));\n\n if (hasPlugin) {\n console.log(pc.dim(\"\\nRegistering plugins (appkit plugin sync)...\"));\n runPluginSync(cwd);\n }\n\n if (wroteUi) {\n console.log(\n pc.dim(\n '\\nReminder: import \"@databricks/appkit-ui/styles.css\" once at your app root so components are themed.',\n ),\n );\n }\n // Lazy import: server-register pulls in @ast-grep/napi (a native addon), and\n // this CLI is imported eagerly by index.ts, so a static import would make\n // every unrelated command pay that cost.\n const registerPluginInServer =\n opts.register !== false && pluginSummaries.some((s) => s.exportName)\n ? (await import(\"./server-register.js\")).registerPluginInServer\n : null;\n for (const s of pluginSummaries) {\n // Try to wire the plugin into the server's createApp call automatically;\n // fall back to printing the snippet when the shape isn't the standard one.\n let wired = false;\n // Why auto-registration was skipped, so the fallback can say so (an empty\n // reason means the user opted out via --no-register — no nag then).\n let skipReason: string | undefined;\n if (opts.register === false) {\n // opted out — print the snippet without a \"couldn't\" message\n } else if (!s.exportName) {\n skipReason =\n \"couldn't read a plugin export name from the item's index.ts\";\n } else if (registerPluginInServer) {\n const result = registerPluginInServer(\n serverRoot,\n s.importPath,\n s.exportName,\n );\n // result.file is relative to serverRoot; show it from cwd for the user.\n const shown =\n result.file && path.relative(cwd, path.join(serverRoot, result.file));\n if (result.status === \"wired\") {\n console.log(`\\n${pc.green(\"Registered\")} ${s.exportName} in ${shown}`);\n wired = true;\n } else if (result.status === \"already\") {\n console.log(\n pc.dim(`\\n${s.exportName} is already registered in ${shown}`),\n );\n wired = true;\n } else {\n skipReason = result.reason;\n }\n }\n if (!wired) {\n const imp = s.exportName ?? \"<plugin>\";\n if (skipReason) {\n console.log(\n pc.yellow(`\\nCouldn't auto-register ${imp} — ${skipReason}.`),\n );\n }\n console.log(\n `${pc.bold(\"Add this to your server's createApp call:\")}\\n` +\n pc.dim(\n ` import { ${imp} } from \"${s.importPath}\";\\n` +\n ` const app = await createApp({ plugins: [${imp}(), /* ... */] });`,\n ),\n );\n }\n }\n\n if (opts.resources !== false && allRequirements.length > 0) {\n // Lazy import (same reason as server-register above): env-writer pulls in\n // the workspace picker and, through it, the Databricks SDK.\n const { collectBindingValues, reportEnvResolutions, syncEnv } =\n await import(\"./env-writer.js\");\n // Without --profile, fall back to the app's configured profile so the\n // picker and bundle validate reach the workspace the app runs against.\n const profile = opts.profile ?? profileFromEnv(cwd);\n console.log(pc.dim(\"\\nReconciling resource env vars into .env...\"));\n const resolutions = await syncEnv(allRequirements, {\n cwd,\n nonInteractive: Boolean(opts.yes),\n values: opts.env,\n profile,\n });\n reportEnvResolutions(resolutions);\n\n // Deploy config (app.yaml + databricks.yml). Values come from what the\n // user supplied for env fields (flags or prompts); other fields fall back\n // to their manifest defaults inside buildConfigPlan.\n const values: Record<string, string> = { ...(opts.env ?? {}) };\n for (const r of resolutions) {\n if (r.value !== undefined) values[r.env] = r.value;\n }\n // Binding fields with no env name (e.g. postgres project/branch/database)\n // never flow through .env, so collect them separately — else their\n // databricks.yml bundle variables stay unassigned and bundle validate fails.\n const bindingNeeds = collectBindingValueNeeds(allRequirements);\n if (bindingNeeds.length > 0) {\n const bindingValues = await collectBindingValues(bindingNeeds, {\n cwd,\n nonInteractive: Boolean(opts.yes),\n values: opts.env,\n profile,\n });\n Object.assign(values, bindingValues);\n }\n const plan = buildConfigPlan(allRequirements, values);\n if (planHasContent(plan)) {\n const result = writeConfig(cwd, plan);\n reportConfigWrite(result);\n if (result.databricksYmlChanged) validateBundle(cwd, profile);\n }\n warnScopeNeeding(allRequirements);\n }\n}\n\n/**\n * v1 does not write `user_api_scopes` (deferred to the manifest scope\n * extension). Warn when an added plugin's resource type is known to need one,\n * so the user adds it before deploy.\n */\n/** Resource types known to require a user_api_scope, and the scope each needs. */\nexport const SCOPE_BY_RESOURCE_TYPE: Record<string, string> = {\n genie_space: \"dashboards.genie\",\n serving_endpoint: \"serving.serving-endpoints\",\n // volumes/files-backed access uses files.files\n volume: \"files.files\",\n};\n\n/** Returns the user_api_scopes implied by a set of resource rows (deduped). */\nexport function scopesForResources(\n rows: ResourceRequirementRow[],\n): Map<string, string> {\n const needed = new Map<string, string>();\n for (const row of rows) {\n const scope = SCOPE_BY_RESOURCE_TYPE[row.type];\n if (scope) needed.set(row.type, scope);\n }\n return needed;\n}\n\nfunction warnScopeNeeding(rows: ResourceRequirementRow[]): void {\n const needed = scopesForResources(rows);\n if (needed.size === 0) return;\n const list = [...needed.entries()]\n .map(([type, scope]) => `${type} → ${scope}`)\n .join(\", \");\n console.warn(\n pc.yellow(\n `\\n Note: these resources may need a user_api_scope before deploy: ${list}.\\n` +\n \" Add it under resources.apps.app.user_api_scopes in databricks.yml.\",\n ),\n );\n}\n\n/** Commander reducer for repeatable `--env KEY=VALUE` flags. */\nfunction collectEnvFlag(\n raw: string,\n acc: Record<string, string>,\n): Record<string, string> {\n const eq = raw.indexOf(\"=\");\n if (eq === -1) {\n console.error(`Ignoring --env \"${raw}\" (expected KEY=VALUE).`);\n return acc;\n }\n const key = raw.slice(0, eq).trim();\n const value = raw.slice(eq + 1);\n if (key) acc[key] = value;\n return acc;\n}\n\nexport const addCommand = new Command(\"add\")\n .description(\"Add a UI component or server plugin from the AppKit registry\")\n .argument(\"<item...>\", \"Registry item name(s), e.g. metric-card or hello\")\n .option(\"-f, --force\", \"Overwrite existing files\")\n .option(\"-C, --cwd <dir>\", \"Run as if started in <dir>\")\n .option(\"--no-register\", \"Don't edit the server entry to register plugins\")\n .option(\"--no-resources\", \"Don't reconcile resource env vars into .env\")\n .option(\"-y, --yes\", \"Don't prompt; use --env values or leave vars unset\")\n .option(\n \"--env <KEY=VALUE>\",\n \"Pre-set a resource env var (repeatable)\",\n collectEnvFlag,\n {},\n )\n .option(\n \"-p, --profile <name>\",\n \"Databricks profile for the resource picker and bundle validate (defaults to the app's DATABRICKS_CONFIG_PROFILE)\",\n )\n .option(\n \"--allow-unverified\",\n \"Add items the registry doesn't mark verified (runs untrusted code)\",\n )\n .addHelpText(\n \"after\",\n `\nNo components.json is required. Item type is detected automatically:\n • UI components → <frontend>/src/components/appkit/ (client/ detected)\n • Server plugins → <server>/plugins/<name>/, runs plugin sync, and registers\n them in your createApp call (use --no-register to skip the server edit)\n\nServer plugins declare Databricks resources. On add, their env vars are\nreconciled into .env (and names into .env.example), and the deploy config\n(app.yaml + databricks.yml resource bindings) is patched to match — existing\nentries are never clobbered. Interactive by default; pass --yes for agents/CI\n(uses --env values, leaves the rest unset) and --env KEY=VALUE to supply\nvalues non-interactively. Pass --profile to validate the bundle after writing.\n\nThe frontend/server roots are detected from common layouts, so you can run\nthis from the repo root. While the registry repo is private, a read token is\nresolved from \\`gh auth token\\` or APPKIT_REGISTRY_TOKEN / GITHUB_TOKEN / GH_TOKEN.\n\nExamples:\n $ appkit add metric-card # UI component\n $ appkit add hello # server plugin\n $ appkit add metric-card hello # mix in one call\n $ appkit add analytics --yes --env DATABRICKS_WAREHOUSE_ID=abc123`,\n )\n .action((items: string[], opts: AddOptions) =>\n runAdd(items, opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"add.js","names":[],"sources":["../../../../src/cli/commands/registry/add.ts"],"sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport process from \"node:process\";\n\nimport { Command } from \"commander\";\nimport pc from \"picocolors\";\n\nimport {\n fetchRegistryItem,\n fetchVerifiedNames,\n isValidItemName,\n type RegistryItem,\n type RegistryItemFile,\n stripNamespace,\n} from \"./client\";\nimport {\n buildConfigPlan,\n collectBindingValueNeeds,\n planHasContent,\n} from \"./config-plan\";\nimport {\n reportConfigWrite,\n validateBundle,\n writeConfig,\n} from \"./config-writer\";\nimport {\n JS_IDENTIFIER,\n REGISTRY_REPO,\n type RegistryToken,\n resolveToken,\n} from \"./constants\";\nimport { parseEnv } from \"./env-reconcile\";\nimport {\n extractRequirements,\n type ResourceRequirementRow,\n renderRequirements,\n} from \"./requirements\";\n\n/** Subdirectories that commonly hold the frontend / server in an AppKit app. */\nconst FRONTEND_SUBDIRS = [\"client\", \"frontend\", \"web\", \"app\"];\nconst SERVER_SUBDIRS = [\"server\", \"api\", \"backend\"];\n\nfunction isDir(p: string): boolean {\n return fs.existsSync(p) && fs.statSync(p).isDirectory();\n}\n\n/** A registry item is a server plugin if it ships a manifest.json. */\nfunction isPluginItem(item: RegistryItem): boolean {\n return (item.files ?? []).some(\n (f) => path.basename(f.target ?? f.path) === \"manifest.json\",\n );\n}\n\n/**\n * Locates the frontend root for UI components. AppKit apps put the client in a\n * client/ subdir (with its own components.json + src/); the CLI is typically\n * run from the repo root. Prefer the dir with components.json, then a src/.\n */\nfunction findFrontendRoot(cwd: string): string {\n if (fs.existsSync(path.join(cwd, \"components.json\"))) return cwd;\n for (const sub of FRONTEND_SUBDIRS) {\n if (fs.existsSync(path.join(cwd, sub, \"components.json\"))) {\n return path.join(cwd, sub);\n }\n }\n if (isDir(path.join(cwd, \"src\"))) return cwd;\n for (const sub of FRONTEND_SUBDIRS) {\n if (isDir(path.join(cwd, sub, \"src\"))) return path.join(cwd, sub);\n }\n return cwd;\n}\n\n/** Locates the server root for plugins (the server/ subdir, else cwd). */\nfunction findServerRoot(cwd: string): string {\n for (const sub of SERVER_SUBDIRS) {\n if (isDir(path.join(cwd, sub))) return path.join(cwd, sub);\n }\n return cwd;\n}\n\n/** Nearest dir with a package.json, walking up from start (for dep install). */\nfunction findNearestPackageJson(start: string): string {\n let dir = start;\n for (;;) {\n if (fs.existsSync(path.join(dir, \"package.json\"))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) return start;\n dir = parent;\n }\n}\n\n/**\n * The Databricks profile the app is configured with, read from\n * `DATABRICKS_CONFIG_PROFILE` in `cwd/.env` via the same dotenv parser the app\n * loads its env with. The CLI's top-level `dotenv/config` only loads the launch\n * dir's `.env` into `process.env`; when `--cwd` points at a different app dir,\n * that file isn't loaded, so the workspace picker and bundle validate would\n * miss the app's profile — this reads it directly. Undefined if the file or key\n * is absent, so the SDK's own default resolution still applies.\n */\nexport function profileFromEnv(cwd: string): string | undefined {\n try {\n const content = fs.readFileSync(path.join(cwd, \".env\"), \"utf-8\");\n return parseEnv(content).DATABRICKS_CONFIG_PROFILE || undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Resolves a registry item's `target` under `base`, enforcing that the result\n * stays inside `base`. Registry items are untrusted remote data; a `target`\n * like `../../../.zshrc` or an absolute path could otherwise write files\n * anywhere on disk (arbitrary-write → RCE). Throws on any escape.\n */\nexport function resolveWithinBase(base: string, target: string): string {\n if (path.isAbsolute(target)) {\n throw new Error(`Refusing absolute file target from registry: ${target}`);\n }\n const baseResolved = path.resolve(base);\n const resolved = path.resolve(baseResolved, target);\n if (\n resolved !== baseResolved &&\n !resolved.startsWith(baseResolved + path.sep)\n ) {\n throw new Error(\n `Refusing file target that escapes the destination directory: ${target}`,\n );\n }\n return resolved;\n}\n\n/** UI file destination (relative to the frontend root): placed in src/ if present. */\nfunction uiTargetPath(base: string, file: RegistryItemFile): string {\n let target = file.target ?? path.join(\"components\", path.basename(file.path));\n if (!target.startsWith(\"src/\") && isDir(path.join(base, \"src\"))) {\n target = path.join(\"src\", target);\n }\n return target;\n}\n\n/** Best-effort: the `toPlugin` export name from the item's index.ts. Returns\n * null (caller falls back to printed instructions) unless the name is a plain\n * JS identifier — the item is untrusted remote content and the value is\n * interpolated into the user's server.ts. */\nexport function pluginExportName(item: RegistryItem): string | null {\n const index = (item.files ?? []).find(\n (f) => path.basename(f.target ?? f.path) === \"index.ts\",\n );\n const match = index?.content.match(/export\\s*\\{([^}]*)\\}/);\n if (!match) return null;\n const names = match[1].split(\",\").map((s) => s.trim());\n // Prefer the camelCase toPlugin instance over the PascalCase class.\n const chosen = names.find((n) => /^[a-z]/.test(n)) ?? names[0];\n return chosen && JS_IDENTIFIER.test(chosen) ? chosen : null;\n}\n\nfunction detectPackageManager(cwd: string): \"pnpm\" | \"yarn\" | \"bun\" | \"npm\" {\n if (fs.existsSync(path.join(cwd, \"pnpm-lock.yaml\"))) return \"pnpm\";\n if (fs.existsSync(path.join(cwd, \"yarn.lock\"))) return \"yarn\";\n if (fs.existsSync(path.join(cwd, \"bun.lockb\"))) return \"bun\";\n return \"npm\";\n}\n\n/**\n * A safe npm dependency spec: `[@scope/]name` with an optional `@version`\n * range. Registry `dependencies` are untrusted remote data passed to the\n * package manager, so we reject anything that isn't a plain name+range —\n * blocks tarball/git URL specs (install-script RCE) and `-`-prefixed entries\n * that the PM would parse as flags (argument injection).\n */\nconst SAFE_DEP_SPEC =\n /^(@[a-z0-9][\\w.-]*\\/)?[a-z0-9][\\w.-]*(@[\\w.\\-+~^><=|* ]+)?$/i;\n\n/** Splits deps into safe (installable) and rejected (surfaced to the user). */\nexport function partitionDeps(deps: string[]): {\n safe: string[];\n rejected: string[];\n} {\n const safe: string[] = [];\n const rejected: string[] = [];\n for (const dep of deps) {\n if (dep.startsWith(\"-\") || !SAFE_DEP_SPEC.test(dep)) rejected.push(dep);\n else safe.push(dep);\n }\n return { safe, rejected };\n}\n\n/** Splits an npm spec into `name` and optional version `range`, handling the\n * leading `@` of scoped names (`@databricks/appkit-ui@^0.41.0` → name\n * `@databricks/appkit-ui`, range `^0.41.0`; `lodash` → no range). */\nexport function parseDepSpec(spec: string): { name: string; range?: string } {\n const at = spec.lastIndexOf(\"@\");\n // at <= 0 means either no `@` (bare name) or the scope's `@` at index 0.\n if (at <= 0) return { name: spec };\n return { name: spec.slice(0, at), range: spec.slice(at + 1) };\n}\n\n/**\n * Registry `dependencies` are floors (\"needs at least this version\"), not pins.\n * Against the versions already in the target `package.json`, returns which specs\n * to install: missing packages (as declared) and genuine upgrades (installed\n * floor below the requested floor). A package already at a same-or-newer version\n * is skipped so the package manager never downgrades it; ranges that can't be\n * compared (workspace:*, catalog:, git) are skipped for the same reason.\n *\n * `semver` is injected so the caller can lazy-load it (see `installDependencies`).\n */\nexport function reconcileDeps(\n specs: string[],\n installed: Record<string, string>,\n { gte, minVersion }: Pick<typeof import(\"semver\"), \"gte\" | \"minVersion\">,\n): { install: string[]; skipped: string[] } {\n // minVersion() throws on ranges it can't parse (workspace:*, catalog:, git);\n // treat those as uncomparable and skip rather than risk a downgrade.\n const floor = (range: string): string | null => {\n try {\n return minVersion(range)?.version ?? null;\n } catch {\n return null;\n }\n };\n const install: string[] = [];\n const skipped: string[] = [];\n for (const spec of specs) {\n const { name, range } = parseDepSpec(spec);\n const have = installed[name];\n if (!have) {\n install.push(spec); // missing → install as declared\n continue;\n }\n if (!range) {\n skipped.push(spec); // present, no version asked → keep what's there\n continue;\n }\n const reqFloor = floor(range);\n const haveFloor = floor(have);\n if (!reqFloor || !haveFloor) {\n skipped.push(spec); // can't compare safely → don't touch\n } else if (gte(haveFloor, reqFloor)) {\n skipped.push(spec); // installed is same-or-newer → keep (no downgrade)\n } else {\n install.push(spec); // installed is older than required → upgrade\n }\n }\n return { install, skipped };\n}\n\n/** Merged dependencies + devDependencies (name → declared range) of a\n * package.json, tolerating a missing/unreadable file (returns {}). */\nfunction readInstalledRanges(pkgPath: string): Record<string, string> {\n try {\n const pkg = JSON.parse(fs.readFileSync(pkgPath, \"utf-8\")) as {\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n };\n return { ...pkg.dependencies, ...pkg.devDependencies };\n } catch {\n return {};\n }\n}\n\nasync function installDependencies(deps: string[], cwd: string): Promise<void> {\n if (deps.length === 0) return;\n\n const { safe, rejected } = partitionDeps(deps);\n if (rejected.length > 0) {\n console.warn(\n pc.yellow(\n `Skipping suspicious dependenc${rejected.length === 1 ? \"y\" : \"ies\"} from the registry (not a plain name@version): ${rejected.join(\", \")}. Install manually if you trust them.`,\n ),\n );\n }\n if (safe.length === 0) return;\n\n const pkgPath = path.join(cwd, \"package.json\");\n if (!fs.existsSync(pkgPath)) {\n console.warn(\n pc.yellow(\n `No package.json found — install these manually: ${safe.join(\" \")}`,\n ),\n );\n return;\n }\n\n // Lazy import: this file is loaded eagerly by the CLI entry, so keep semver\n // (only needed here) off the startup path — as with server-register/env-writer.\n const semver = await import(\"semver\");\n const { install, skipped } = reconcileDeps(\n safe,\n readInstalledRanges(pkgPath),\n semver,\n );\n if (skipped.length > 0) {\n console.log(\n pc.dim(\n `Keeping already-installed (registry asked for an older/equal range): ${skipped.join(\", \")}`,\n ),\n );\n }\n if (install.length === 0) return;\n\n const pm = detectPackageManager(cwd);\n const subcommand = pm === \"npm\" ? \"install\" : \"add\";\n console.log(`\\nInstalling dependencies with ${pm}: ${install.join(\" \")}`);\n // `--` stops the PM from parsing any dep as a flag (defense in depth).\n const result = spawnSync(pm, [subcommand, \"--\", ...install], {\n stdio: \"inherit\",\n cwd,\n });\n if (result.status !== 0) {\n console.warn(\n pc.yellow(\n `Dependency install exited with code ${result.status ?? \"unknown\"} — install manually if needed: ${install.join(\" \")}`,\n ),\n );\n }\n}\n\n/** Runs `appkit plugin sync --write` via this same CLI binary. */\nfunction runPluginSync(cwd: string): void {\n const result = spawnSync(\n process.execPath,\n [process.argv[1], \"plugin\", \"sync\", \"--write\"],\n { stdio: \"inherit\", cwd },\n );\n if (result.status !== 0) {\n console.warn(\n pc.yellow(\n \" Plugin sync did not complete cleanly — run `appkit plugin sync --write` manually.\",\n ),\n );\n }\n}\n\nfunction writeItemFile(\n base: string,\n target: string,\n content: string,\n force: boolean,\n cwd: string,\n): void {\n const dest = resolveWithinBase(base, target);\n const existed = fs.existsSync(dest);\n if (existed && !force) {\n console.error(\n pc.red(\n `Refusing to overwrite ${path.relative(cwd, dest)} — pass --force to replace it.`,\n ),\n );\n process.exit(1);\n }\n fs.mkdirSync(path.dirname(dest), { recursive: true });\n fs.writeFileSync(dest, content);\n const label = existed ? pc.yellow(\"Updated\") : pc.green(\"Created\");\n console.log(`${label} ${path.relative(cwd, dest)}`);\n}\n\ninterface PluginSummary {\n importPath: string;\n exportName: string | null;\n}\n\n/**\n * Fetches the requested items plus their transitive registryDependencies.\n * Dependencies are resolved breadth-first and de-duplicated by name, so a\n * plugin that depends on another registry item pulls the whole graph in one\n * `add`. Explicitly-requested items keep their request order and come first.\n */\nexport async function resolveItems(\n names: string[],\n token: RegistryToken | null,\n fetchItem: (\n name: string,\n token: RegistryToken | null,\n ) => Promise<RegistryItem> = fetchRegistryItem,\n): Promise<RegistryItem[]> {\n const seen = new Set<string>();\n const ordered: RegistryItem[] = [];\n // A name is both the fetch path and the on-disk `plugins/<name>` dir, so\n // reject non-slug refs (`/`, `..`, control chars) before they reach either.\n const enqueue = (ref: string): string => {\n const name = stripNamespace(ref);\n if (!isValidItemName(name)) {\n throw new Error(`Invalid registry item name: ${JSON.stringify(ref)}`);\n }\n return name;\n };\n // Breadth-first over the dependency graph, one level per iteration. Items in\n // a level are fetched concurrently (fetch latency is additive otherwise), but\n // levels stay ordered and dedup/cycle handling is unchanged: a name is marked\n // seen before its level is fetched, so it's never fetched or queued twice.\n let level = names.map(enqueue).filter((name) => {\n if (seen.has(name)) return false;\n seen.add(name);\n return true;\n });\n\n while (level.length > 0) {\n const items = await Promise.all(\n level.map(async (key) => {\n const item = await fetchItem(key, token);\n // Pin to the fetch key: the body's self-reported `name` is untrusted\n // and could claim a verified name to slip past the gate. The key is the\n // trustworthy identity the index keys `verified` on.\n item.name = key;\n return item;\n }),\n );\n ordered.push(...items);\n const next: string[] = [];\n for (const item of items) {\n for (const dep of item.registryDependencies ?? []) {\n const depName = enqueue(dep);\n if (seen.has(depName)) continue;\n seen.add(depName);\n next.push(depName);\n }\n }\n level = next;\n }\n\n return ordered;\n}\n\ninterface AddOptions {\n force?: boolean;\n cwd?: string;\n register?: boolean;\n /** false = don't reconcile resource env vars into .env. */\n resources?: boolean;\n /** true = never prompt; use --env flags or leave unset (agent/CI). */\n yes?: boolean;\n /** Pre-supplied env values from repeated --env KEY=VALUE flags. */\n env?: Record<string, string>;\n /** Databricks profile passed to `bundle validate` after writing config. */\n profile?: string;\n /** true = install items the registry index doesn't mark verified. */\n allowUnverified?: boolean;\n}\n\n/**\n * Splits requested names into verified and unverified against the index's\n * verified set. When `verified` is null the index couldn't be read — we can't\n * prove anything is verified, so every name is treated as unverified (the gate\n * then decides whether to warn-and-continue or block). Names are compared with\n * the namespace stripped, matching how items are fetched.\n */\nexport function partitionVerified(\n refs: string[],\n verified: Set<string> | null,\n): { verified: string[]; unverified: string[] } {\n const ok: string[] = [];\n const bad: string[] = [];\n for (const ref of refs) {\n const name = stripNamespace(ref);\n if (verified?.has(name)) ok.push(name);\n else bad.push(name);\n }\n return { verified: ok, unverified: bad };\n}\n\nasync function runAdd(refs: string[], opts: AddOptions): Promise<void> {\n const cwd = opts.cwd ? path.resolve(opts.cwd) : process.cwd();\n const token = resolveToken();\n if (token) {\n console.log(\n `Using ${token.envName} to fetch from ${REGISTRY_REPO} (private).`,\n );\n }\n\n // Resolve the full graph and fetch the verified index concurrently (two\n // independent round-trips). Item resolution is read-only — nothing is written\n // or installed until after the gate below.\n const verifiedP = opts.allowUnverified\n ? Promise.resolve(null)\n : fetchVerifiedNames(token);\n const items = await resolveItems(refs, token);\n\n // Integrity gate over the *entire resolved set* (not just requested names, so\n // an unverified transitive dep can't ride in on a verified item). Fails closed:\n // a null verified set (unreadable index) makes every item count as unverified.\n if (!opts.allowUnverified) {\n const verified = await verifiedP;\n const { unverified } = partitionVerified(\n items.map((i) => i.name),\n verified,\n );\n if (unverified.length > 0) {\n const reason =\n verified === null\n ? \"could not read the registry index to verify these items\"\n : `not marked verified in ${REGISTRY_REPO}`;\n console.error(\n pc.red(\n `Refusing to add unverified item(s) (${reason}): ${unverified.join(\", \")}.`,\n ),\n );\n console.error(\n pc.dim(\n \" Re-run with --allow-unverified if you trust the source; unverified items run code in your app.\",\n ),\n );\n process.exit(1);\n }\n }\n\n const hasUi = items.some((i) => !isPluginItem(i));\n const hasPlugin = items.some(isPluginItem);\n const frontendRoot = hasUi ? findFrontendRoot(cwd) : cwd;\n const serverRoot = hasPlugin ? findServerRoot(cwd) : cwd;\n if (hasUi && frontendRoot !== cwd) {\n console.log(pc.dim(`UI components → ${path.relative(cwd, frontendRoot)}/`));\n }\n if (hasPlugin && serverRoot !== cwd) {\n console.log(pc.dim(`Plugins → ${path.relative(cwd, serverRoot)}/`));\n }\n\n const deps = new Set<string>();\n let wroteUi = false;\n const pluginSummaries: PluginSummary[] = [];\n const allRequirements: ResourceRequirementRow[] = [];\n\n for (const item of items) {\n for (const dep of item.dependencies ?? []) deps.add(dep);\n\n if (isPluginItem(item)) {\n let pluginRel = path.join(\"plugins\", item.name);\n for (const file of item.files ?? []) {\n const target =\n file.target ??\n path.join(\"plugins\", item.name, path.basename(file.path));\n writeItemFile(\n serverRoot,\n target,\n file.content,\n Boolean(opts.force),\n cwd,\n );\n if (path.basename(target) === \"manifest.json\") {\n pluginRel = path.dirname(target);\n }\n }\n const requirements = extractRequirements(item);\n if (requirements.length > 0) {\n console.log(`\\n${renderRequirements(item, requirements)}`);\n allRequirements.push(...requirements);\n }\n pluginSummaries.push({\n importPath: `./${pluginRel}`,\n exportName: pluginExportName(item),\n });\n } else {\n for (const file of item.files ?? []) {\n writeItemFile(\n frontendRoot,\n uiTargetPath(frontendRoot, file),\n file.content,\n Boolean(opts.force),\n cwd,\n );\n wroteUi = true;\n }\n }\n }\n\n await installDependencies([...deps], findNearestPackageJson(cwd));\n\n if (hasPlugin) {\n console.log(pc.dim(\"\\nRegistering plugins (appkit plugin sync)...\"));\n runPluginSync(cwd);\n }\n\n if (wroteUi) {\n console.log(\n pc.dim(\n '\\nReminder: import \"@databricks/appkit-ui/styles.css\" once at your app root so components are themed.',\n ),\n );\n }\n // Lazy import: server-register pulls in @ast-grep/napi (a native addon), and\n // this CLI is imported eagerly by index.ts, so a static import would make\n // every unrelated command pay that cost.\n const registerPluginInServer =\n opts.register !== false && pluginSummaries.some((s) => s.exportName)\n ? (await import(\"./server-register.js\")).registerPluginInServer\n : null;\n for (const s of pluginSummaries) {\n // Try to wire the plugin into the server's createApp call automatically;\n // fall back to printing the snippet when the shape isn't the standard one.\n let wired = false;\n // Why auto-registration was skipped, so the fallback can say so (an empty\n // reason means the user opted out via --no-register — no nag then).\n let skipReason: string | undefined;\n if (opts.register === false) {\n // opted out — print the snippet without a \"couldn't\" message\n } else if (!s.exportName) {\n skipReason =\n \"couldn't read a plugin export name from the item's index.ts\";\n } else if (registerPluginInServer) {\n const result = registerPluginInServer(\n serverRoot,\n s.importPath,\n s.exportName,\n );\n // result.file is relative to serverRoot; show it from cwd for the user.\n const shown =\n result.file && path.relative(cwd, path.join(serverRoot, result.file));\n if (result.status === \"wired\") {\n console.log(`\\n${pc.green(\"Registered\")} ${s.exportName} in ${shown}`);\n wired = true;\n } else if (result.status === \"already\") {\n console.log(\n pc.dim(`\\n${s.exportName} is already registered in ${shown}`),\n );\n wired = true;\n } else {\n skipReason = result.reason;\n }\n }\n if (!wired) {\n const imp = s.exportName ?? \"<plugin>\";\n if (skipReason) {\n console.log(\n pc.yellow(`\\nCouldn't auto-register ${imp} — ${skipReason}.`),\n );\n }\n console.log(\n `${pc.bold(\"Add this to your server's createApp call:\")}\\n` +\n pc.dim(\n ` import { ${imp} } from \"${s.importPath}\";\\n` +\n ` const app = await createApp({ plugins: [${imp}(), /* ... */] });`,\n ),\n );\n }\n }\n\n if (opts.resources !== false && allRequirements.length > 0) {\n // Lazy import (same reason as server-register above): env-writer pulls in\n // the workspace picker and, through it, the Databricks SDK.\n const { collectBindingValues, reportEnvResolutions, syncEnv } =\n await import(\"./env-writer.js\");\n // Without --profile, fall back to the app's configured profile so the\n // picker and bundle validate reach the workspace the app runs against.\n const profile = opts.profile ?? profileFromEnv(cwd);\n console.log(pc.dim(\"\\nReconciling resource env vars into .env...\"));\n const resolutions = await syncEnv(allRequirements, {\n cwd,\n nonInteractive: Boolean(opts.yes),\n values: opts.env,\n profile,\n });\n reportEnvResolutions(resolutions);\n\n // Deploy config (app.yaml + databricks.yml). Values come from what the\n // user supplied for env fields (flags or prompts); other fields fall back\n // to their manifest defaults inside buildConfigPlan.\n const values: Record<string, string> = { ...(opts.env ?? {}) };\n for (const r of resolutions) {\n if (r.value !== undefined) values[r.env] = r.value;\n }\n // Binding fields with no env name (e.g. postgres project/branch/database)\n // never flow through .env, so collect them separately — else their\n // databricks.yml bundle variables stay unassigned and bundle validate fails.\n const bindingNeeds = collectBindingValueNeeds(allRequirements);\n if (bindingNeeds.length > 0) {\n const bindingValues = await collectBindingValues(bindingNeeds, {\n cwd,\n nonInteractive: Boolean(opts.yes),\n values: opts.env,\n profile,\n });\n Object.assign(values, bindingValues);\n }\n const plan = buildConfigPlan(allRequirements, values);\n if (planHasContent(plan)) {\n const result = writeConfig(cwd, plan);\n reportConfigWrite(result);\n if (result.databricksYmlChanged) validateBundle(cwd, profile);\n }\n warnScopeNeeding(allRequirements);\n }\n}\n\n/**\n * v1 does not write `user_api_scopes` (deferred to the manifest scope\n * extension). Warn when an added plugin's resource type is known to need one,\n * so the user adds it before deploy.\n */\n/** Resource types known to require a user_api_scope, and the scope each needs. */\nexport const SCOPE_BY_RESOURCE_TYPE: Record<string, string> = {\n genie_space: \"dashboards.genie\",\n serving_endpoint: \"serving.serving-endpoints\",\n // volumes/files-backed access uses files.files\n volume: \"files.files\",\n};\n\n/** Returns the user_api_scopes implied by a set of resource rows (deduped). */\nexport function scopesForResources(\n rows: ResourceRequirementRow[],\n): Map<string, string> {\n const needed = new Map<string, string>();\n for (const row of rows) {\n const scope = SCOPE_BY_RESOURCE_TYPE[row.type];\n if (scope) needed.set(row.type, scope);\n }\n return needed;\n}\n\nfunction warnScopeNeeding(rows: ResourceRequirementRow[]): void {\n const needed = scopesForResources(rows);\n if (needed.size === 0) return;\n const list = [...needed.entries()]\n .map(([type, scope]) => `${type} → ${scope}`)\n .join(\", \");\n console.warn(\n pc.yellow(\n `\\n Note: these resources may need a user_api_scope before deploy: ${list}.\\n` +\n \" Add it under resources.apps.app.user_api_scopes in databricks.yml.\",\n ),\n );\n}\n\n/** Commander reducer for repeatable `--env KEY=VALUE` flags. */\nfunction collectEnvFlag(\n raw: string,\n acc: Record<string, string>,\n): Record<string, string> {\n const eq = raw.indexOf(\"=\");\n if (eq === -1) {\n console.error(`Ignoring --env \"${raw}\" (expected KEY=VALUE).`);\n return acc;\n }\n const key = raw.slice(0, eq).trim();\n const value = raw.slice(eq + 1);\n if (key) acc[key] = value;\n return acc;\n}\n\nexport const addCommand = new Command(\"add\")\n .description(\"Add a UI component or server plugin from the AppKit registry\")\n .argument(\"<item...>\", \"Registry item name(s), e.g. metric-card or hello\")\n .option(\"-f, --force\", \"Overwrite existing files\")\n .option(\"-C, --cwd <dir>\", \"Run as if started in <dir>\")\n .option(\"--no-register\", \"Don't edit the server entry to register plugins\")\n .option(\"--no-resources\", \"Don't reconcile resource env vars into .env\")\n .option(\"-y, --yes\", \"Don't prompt; use --env values or leave vars unset\")\n .option(\n \"--env <KEY=VALUE>\",\n \"Pre-set a resource env var (repeatable)\",\n collectEnvFlag,\n {},\n )\n .option(\n \"-p, --profile <name>\",\n \"Databricks profile for the resource picker and bundle validate (defaults to the app's DATABRICKS_CONFIG_PROFILE)\",\n )\n .option(\n \"--allow-unverified\",\n \"Add items the registry doesn't mark verified (runs untrusted code)\",\n )\n .addHelpText(\n \"after\",\n `\nNo components.json is required. Item type is detected automatically:\n • UI components → <frontend>/src/components/appkit/ (client/ detected)\n • Server plugins → <server>/plugins/<name>/, runs plugin sync, and registers\n them in your createApp call (use --no-register to skip the server edit)\n\nServer plugins declare Databricks resources. On add, their env vars are\nreconciled into .env (and names into .env.example), and the deploy config\n(app.yaml + databricks.yml resource bindings) is patched to match — existing\nentries are never clobbered. Interactive by default; pass --yes for agents/CI\n(uses --env values, leaves the rest unset) and --env KEY=VALUE to supply\nvalues non-interactively. Pass --profile to validate the bundle after writing.\n\nThe frontend/server roots are detected from common layouts, so you can run\nthis from the repo root. While the registry repo is private, a read token is\nresolved from \\`gh auth token\\` or APPKIT_REGISTRY_TOKEN / GITHUB_TOKEN / GH_TOKEN.\n\nExamples:\n $ appkit add metric-card # UI component\n $ appkit add hello # server plugin\n $ appkit add metric-card hello # mix in one call\n $ appkit add analytics --yes --env DATABRICKS_WAREHOUSE_ID=abc123`,\n )\n .action((items: string[], opts: AddOptions) =>\n runAdd(items, opts).catch((err) => {\n console.error(err);\n process.exit(1);\n }),\n );\n"],"mappings":"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"}
|
package/dist/evals/dataset.d.ts
CHANGED
|
@@ -19,6 +19,19 @@ interface ReadEvalDatasetOptions {
|
|
|
19
19
|
/** Optional row cap. */
|
|
20
20
|
limit?: number;
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Extract every user-message content, in order, from an MLflow
|
|
24
|
+
* `{"messages":[{"role":"user","content":"..."}]}` input. A dataset row can
|
|
25
|
+
* carry a full multi-turn conversation; replaying these against one thread (one
|
|
26
|
+
* `t.send` per returned string) lets the agent see the accumulating history.
|
|
27
|
+
*
|
|
28
|
+
* Only `role === "user"` turns are returned — any interleaved `assistant`/
|
|
29
|
+
* `system` messages in the row are ignored, since the agent generates its own
|
|
30
|
+
* responses; you never inject the dataset's assistant turns. A single-user-turn
|
|
31
|
+
* row yields a one-element array (backward compatible); a row with no `messages`
|
|
32
|
+
* yields `[]`.
|
|
33
|
+
*/
|
|
34
|
+
declare function userTurns(input: Record<string, unknown>): string[];
|
|
22
35
|
/**
|
|
23
36
|
* Read a Databricks managed evaluation dataset (a Unity Catalog table with
|
|
24
37
|
* `inputs`/`expectations` columns) into rows, over the public SQL Statement
|
|
@@ -32,5 +45,5 @@ interface ReadEvalDatasetOptions {
|
|
|
32
45
|
*/
|
|
33
46
|
declare function readEvalDataset(client: WorkspaceClient, options: ReadEvalDatasetOptions): Promise<DatasetRow[]>;
|
|
34
47
|
//#endregion
|
|
35
|
-
export { DatasetRow, ReadEvalDatasetOptions, readEvalDataset };
|
|
48
|
+
export { DatasetRow, ReadEvalDatasetOptions, readEvalDataset, userTurns };
|
|
36
49
|
//# sourceMappingURL=dataset.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset.d.ts","names":[],"sources":["../../src/evals/dataset.ts"],"mappings":";;;;;AASA;;;;UAAiB,UAAA;EACf,MAAA,EAAQ,MAAA;EACR,YAAA,GAAe,MAAA;AAAA;AAAA,UAGA,sBAAA;EAHM;EAKrB,KAAA;EAFqC;EAIrC,WAAA;EAJqC;EAMrC,KAAA;AAAA;;;;
|
|
1
|
+
{"version":3,"file":"dataset.d.ts","names":[],"sources":["../../src/evals/dataset.ts"],"mappings":";;;;;AASA;;;;UAAiB,UAAA;EACf,MAAA,EAAQ,MAAA;EACR,YAAA,GAAe,MAAA;AAAA;AAAA,UAGA,sBAAA;EAHM;EAKrB,KAAA;EAFqC;EAIrC,WAAA;EAJqC;EAMrC,KAAA;AAAA;;;;AAeF;;;;;AAkCA;;;;iBAlCgB,SAAA,CAAU,KAAA,EAAO,MAAA;;;;;;;;;;;;iBAkCX,eAAA,CACpB,MAAA,EAAQ,eAAA,EACR,OAAA,EAAS,sBAAA,GACR,OAAA,CAAQ,UAAA"}
|
package/dist/evals/dataset.js
CHANGED
|
@@ -2,6 +2,21 @@ import { SQLWarehouseConnector } from "../connectors/sql-warehouse/client.js";
|
|
|
2
2
|
import "../connectors/index.js";
|
|
3
3
|
|
|
4
4
|
//#region src/evals/dataset.ts
|
|
5
|
+
/**
|
|
6
|
+
* Extract every user-message content, in order, from an MLflow
|
|
7
|
+
* `{"messages":[{"role":"user","content":"..."}]}` input. A dataset row can
|
|
8
|
+
* carry a full multi-turn conversation; replaying these against one thread (one
|
|
9
|
+
* `t.send` per returned string) lets the agent see the accumulating history.
|
|
10
|
+
*
|
|
11
|
+
* Only `role === "user"` turns are returned — any interleaved `assistant`/
|
|
12
|
+
* `system` messages in the row are ignored, since the agent generates its own
|
|
13
|
+
* responses; you never inject the dataset's assistant turns. A single-user-turn
|
|
14
|
+
* row yields a one-element array (backward compatible); a row with no `messages`
|
|
15
|
+
* yields `[]`.
|
|
16
|
+
*/
|
|
17
|
+
function userTurns(input) {
|
|
18
|
+
return (Array.isArray(input.messages) ? input.messages : []).filter((m) => typeof m === "object" && m !== null).filter((m) => m.role === "user").map((m) => typeof m.content === "string" ? m.content : "");
|
|
19
|
+
}
|
|
5
20
|
/** A managed eval dataset is a UC table; only 3-level names are valid. */
|
|
6
21
|
const UC_TABLE = /^[A-Za-z0-9_]+\.[A-Za-z0-9_]+\.[A-Za-z0-9_]+$/;
|
|
7
22
|
/** Coerce a cell (already JSON-parsed by the connector for JSON columns) to a record. */
|
|
@@ -32,5 +47,5 @@ async function readEvalDataset(client, options) {
|
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
//#endregion
|
|
35
|
-
export { readEvalDataset };
|
|
50
|
+
export { readEvalDataset, userTurns };
|
|
36
51
|
//# sourceMappingURL=dataset.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dataset.js","names":[],"sources":["../../src/evals/dataset.ts"],"sourcesContent":["import { SQLWarehouseConnector } from \"../connectors\";\nimport type { WorkspaceClient } from \"../workspace-client\";\n\n/**\n * One row of a managed evaluation dataset. `inputs` are the kwargs passed to the\n * agent for the turn; `expectations` (when present) is the row's ground truth /\n * guidelines. Mirrors the `{inputs, expectations}` shape of `mlflow.genai`\n * datasets and of the Unity Catalog table backing a managed eval dataset.\n */\nexport interface DatasetRow {\n inputs: Record<string, unknown>;\n expectations?: Record<string, unknown>;\n}\n\nexport interface ReadEvalDatasetOptions {\n /** Fully-qualified UC table: `catalog.schema.table`. */\n table: string;\n /** SQL warehouse id to run the read against. */\n warehouseId: string;\n /** Optional row cap. */\n limit?: number;\n}\n\n/** A managed eval dataset is a UC table; only 3-level names are valid. */\nconst UC_TABLE = /^[A-Za-z0-9_]+\\.[A-Za-z0-9_]+\\.[A-Za-z0-9_]+$/;\n\n/** Coerce a cell (already JSON-parsed by the connector for JSON columns) to a record. */\nfunction toRecord(value: unknown): Record<string, unknown> | undefined {\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return value as Record<string, unknown>;\n }\n return undefined;\n}\n\n/**\n * Read a Databricks managed evaluation dataset (a Unity Catalog table with\n * `inputs`/`expectations` columns) into rows, over the public SQL Statement\n * Execution API. Reuses {@link SQLWarehouseConnector} for submit/poll/transform\n * — its result transform already JSON-parses string columns into objects, so\n * `inputs`/`expectations` come back as records whether the table stores them as\n * JSON strings or structs.\n *\n * The Python `mlflow.genai.datasets` API needs a Spark session (no TS\n * equivalent), so we read the backing table directly.\n */\nexport async function readEvalDataset(\n client: WorkspaceClient,\n options: ReadEvalDatasetOptions,\n): Promise<DatasetRow[]> {\n if (!UC_TABLE.test(options.table)) {\n throw new Error(\n `Invalid dataset table \"${options.table}\" — expected catalog.schema.table`,\n );\n }\n\n const limit =\n typeof options.limit === \"number\" && options.limit > 0\n ? ` LIMIT ${Math.floor(options.limit)}`\n : \"\";\n const connector = new SQLWarehouseConnector({});\n const response = await connector.executeStatement(client, {\n warehouse_id: options.warehouseId,\n statement: `SELECT inputs, expectations FROM ${options.table}${limit}`,\n });\n\n const rows =\n (response.result as { data?: Array<Record<string, unknown>> } | undefined)\n ?.data ?? [];\n\n return rows.map((row) => ({\n inputs: toRecord(row.inputs) ?? {},\n expectations: toRecord(row.expectations),\n }));\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"dataset.js","names":[],"sources":["../../src/evals/dataset.ts"],"sourcesContent":["import { SQLWarehouseConnector } from \"../connectors\";\nimport type { WorkspaceClient } from \"../workspace-client\";\n\n/**\n * One row of a managed evaluation dataset. `inputs` are the kwargs passed to the\n * agent for the turn; `expectations` (when present) is the row's ground truth /\n * guidelines. Mirrors the `{inputs, expectations}` shape of `mlflow.genai`\n * datasets and of the Unity Catalog table backing a managed eval dataset.\n */\nexport interface DatasetRow {\n inputs: Record<string, unknown>;\n expectations?: Record<string, unknown>;\n}\n\nexport interface ReadEvalDatasetOptions {\n /** Fully-qualified UC table: `catalog.schema.table`. */\n table: string;\n /** SQL warehouse id to run the read against. */\n warehouseId: string;\n /** Optional row cap. */\n limit?: number;\n}\n\n/**\n * Extract every user-message content, in order, from an MLflow\n * `{\"messages\":[{\"role\":\"user\",\"content\":\"...\"}]}` input. A dataset row can\n * carry a full multi-turn conversation; replaying these against one thread (one\n * `t.send` per returned string) lets the agent see the accumulating history.\n *\n * Only `role === \"user\"` turns are returned — any interleaved `assistant`/\n * `system` messages in the row are ignored, since the agent generates its own\n * responses; you never inject the dataset's assistant turns. A single-user-turn\n * row yields a one-element array (backward compatible); a row with no `messages`\n * yields `[]`.\n */\nexport function userTurns(input: Record<string, unknown>): string[] {\n const messages = Array.isArray(input.messages) ? input.messages : [];\n // Dataset rows are external data: skip null/non-object entries; coerce non-string content to \"\".\n return messages\n .filter(\n (m): m is { role?: unknown; content?: unknown } =>\n typeof m === \"object\" && m !== null,\n )\n .filter((m) => m.role === \"user\")\n .map((m) => (typeof m.content === \"string\" ? m.content : \"\"));\n}\n\n/** A managed eval dataset is a UC table; only 3-level names are valid. */\nconst UC_TABLE = /^[A-Za-z0-9_]+\\.[A-Za-z0-9_]+\\.[A-Za-z0-9_]+$/;\n\n/** Coerce a cell (already JSON-parsed by the connector for JSON columns) to a record. */\nfunction toRecord(value: unknown): Record<string, unknown> | undefined {\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return value as Record<string, unknown>;\n }\n return undefined;\n}\n\n/**\n * Read a Databricks managed evaluation dataset (a Unity Catalog table with\n * `inputs`/`expectations` columns) into rows, over the public SQL Statement\n * Execution API. Reuses {@link SQLWarehouseConnector} for submit/poll/transform\n * — its result transform already JSON-parses string columns into objects, so\n * `inputs`/`expectations` come back as records whether the table stores them as\n * JSON strings or structs.\n *\n * The Python `mlflow.genai.datasets` API needs a Spark session (no TS\n * equivalent), so we read the backing table directly.\n */\nexport async function readEvalDataset(\n client: WorkspaceClient,\n options: ReadEvalDatasetOptions,\n): Promise<DatasetRow[]> {\n if (!UC_TABLE.test(options.table)) {\n throw new Error(\n `Invalid dataset table \"${options.table}\" — expected catalog.schema.table`,\n );\n }\n\n const limit =\n typeof options.limit === \"number\" && options.limit > 0\n ? ` LIMIT ${Math.floor(options.limit)}`\n : \"\";\n const connector = new SQLWarehouseConnector({});\n const response = await connector.executeStatement(client, {\n warehouse_id: options.warehouseId,\n statement: `SELECT inputs, expectations FROM ${options.table}${limit}`,\n });\n\n const rows =\n (response.result as { data?: Array<Record<string, unknown>> } | undefined)\n ?.data ?? [];\n\n return rows.map((row) => ({\n inputs: toRecord(row.inputs) ?? {},\n expectations: toRecord(row.expectations),\n }));\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAmCA,SAAgB,UAAU,OAA0C;AAGlE,SAFiB,MAAM,QAAQ,MAAM,SAAS,GAAG,MAAM,WAAW,EAAE,EAGjE,QACE,MACC,OAAO,MAAM,YAAY,MAAM,KAClC,CACA,QAAQ,MAAM,EAAE,SAAS,OAAO,CAChC,KAAK,MAAO,OAAO,EAAE,YAAY,WAAW,EAAE,UAAU,GAAI;;;AAIjE,MAAM,WAAW;;AAGjB,SAAS,SAAS,OAAqD;AACrE,KAAI,SAAS,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,MAAM,CAC7D,QAAO;;;;;;;;;;;;;AAgBX,eAAsB,gBACpB,QACA,SACuB;AACvB,KAAI,CAAC,SAAS,KAAK,QAAQ,MAAM,CAC/B,OAAM,IAAI,MACR,0BAA0B,QAAQ,MAAM,mCACzC;CAGH,MAAM,QACJ,OAAO,QAAQ,UAAU,YAAY,QAAQ,QAAQ,IACjD,UAAU,KAAK,MAAM,QAAQ,MAAM,KACnC;AAWN,UATiB,MADC,IAAI,sBAAsB,EAAE,CAAC,CACd,iBAAiB,QAAQ;EACxD,cAAc,QAAQ;EACtB,WAAW,oCAAoC,QAAQ,QAAQ;EAChE,CAAC,EAGU,QACN,QAAQ,EAAE,EAEJ,KAAK,SAAS;EACxB,QAAQ,SAAS,IAAI,OAAO,IAAI,EAAE;EAClC,cAAc,SAAS,IAAI,aAAa;EACzC,EAAE"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EvalDefinition } from "./types.js";
|
|
1
|
+
import { EvalConfig, EvalDefinition } from "./types.js";
|
|
2
2
|
|
|
3
3
|
//#region src/evals/define-eval.d.ts
|
|
4
4
|
/**
|
|
@@ -21,6 +21,8 @@ import { EvalDefinition } from "./types.js";
|
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
declare function defineEval(def: EvalDefinition): EvalDefinition;
|
|
24
|
+
/** Define per-directory eval config. Default-export from `evals.config.ts`. */
|
|
25
|
+
declare function defineEvalConfig(config: EvalConfig): EvalConfig;
|
|
24
26
|
//#endregion
|
|
25
|
-
export { defineEval };
|
|
27
|
+
export { defineEval, defineEvalConfig };
|
|
26
28
|
//# sourceMappingURL=define-eval.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-eval.d.ts","names":[],"sources":["../../src/evals/define-eval.ts"],"mappings":";;;;;AAqBA
|
|
1
|
+
{"version":3,"file":"define-eval.d.ts","names":[],"sources":["../../src/evals/define-eval.ts"],"mappings":";;;;;AAqBA;;;;;;;;;AAQA;;;;;;;;iBARgB,UAAA,CAAW,GAAA,EAAK,cAAA,GAAiB,cAAA;;iBAQjC,gBAAA,CAAiB,MAAA,EAAQ,UAAA,GAAa,UAAA"}
|
|
@@ -22,7 +22,11 @@ function defineEval(def) {
|
|
|
22
22
|
if (typeof def.test !== "function") throw new Error("defineEval: `test` must be a function");
|
|
23
23
|
return def;
|
|
24
24
|
}
|
|
25
|
+
/** Define per-directory eval config. Default-export from `evals.config.ts`. */
|
|
26
|
+
function defineEvalConfig(config) {
|
|
27
|
+
return config;
|
|
28
|
+
}
|
|
25
29
|
|
|
26
30
|
//#endregion
|
|
27
|
-
export { defineEval };
|
|
31
|
+
export { defineEval, defineEvalConfig };
|
|
28
32
|
//# sourceMappingURL=define-eval.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"define-eval.js","names":[],"sources":["../../src/evals/define-eval.ts"],"sourcesContent":["import type { EvalDefinition } from \"./types\";\n\n/**\n * Define an agent eval. Default-export the result from a\n * `server/agents/<id>/evals/*.eval.ts` file.\n *\n * @example\n * ```ts\n * import { defineEval, includes } from \"@databricks/appkit/beta\";\n *\n * export default defineEval({\n * description: \"Weather agent basic coverage\",\n * async test(t) {\n * await t.send(\"What's the weather in Brooklyn?\");\n * t.succeeded();\n * t.calledTool(\"get_weather\");\n * t.check(t.reply, includes(\"Sunny\"));\n * },\n * });\n * ```\n */\nexport function defineEval(def: EvalDefinition): EvalDefinition {\n if (typeof def.test !== \"function\") {\n throw new Error(\"defineEval: `test` must be a function\");\n }\n return def;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,WAAW,KAAqC;AAC9D,KAAI,OAAO,IAAI,SAAS,WACtB,OAAM,IAAI,MAAM,wCAAwC;AAE1D,QAAO"}
|
|
1
|
+
{"version":3,"file":"define-eval.js","names":[],"sources":["../../src/evals/define-eval.ts"],"sourcesContent":["import type { EvalConfig, EvalDefinition } from \"./types\";\n\n/**\n * Define an agent eval. Default-export the result from a\n * `server/agents/<id>/evals/*.eval.ts` file.\n *\n * @example\n * ```ts\n * import { defineEval, includes } from \"@databricks/appkit/beta\";\n *\n * export default defineEval({\n * description: \"Weather agent basic coverage\",\n * async test(t) {\n * await t.send(\"What's the weather in Brooklyn?\");\n * t.succeeded();\n * t.calledTool(\"get_weather\");\n * t.check(t.reply, includes(\"Sunny\"));\n * },\n * });\n * ```\n */\nexport function defineEval(def: EvalDefinition): EvalDefinition {\n if (typeof def.test !== \"function\") {\n throw new Error(\"defineEval: `test` must be a function\");\n }\n return def;\n}\n\n/** Define per-directory eval config. Default-export from `evals.config.ts`. */\nexport function defineEvalConfig(config: EvalConfig): EvalConfig {\n return config;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,WAAW,KAAqC;AAC9D,KAAI,OAAO,IAAI,SAAS,WACtB,OAAM,IAAI,MAAM,wCAAwC;AAE1D,QAAO;;;AAIT,SAAgB,iBAAiB,QAAgC;AAC/D,QAAO"}
|
package/dist/evals/discover.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ interface DiscoveredEval {
|
|
|
8
8
|
/** The agent id (the `server/agents/<agent>` directory name). */
|
|
9
9
|
agent: string;
|
|
10
10
|
}
|
|
11
|
+
/** A per-agent `evals.config.ts` found under `server/agents/<agent>/evals/`. */
|
|
12
|
+
interface DiscoveredEvalConfig {
|
|
13
|
+
/** Absolute path to the `evals.config.ts` file. */
|
|
14
|
+
file: string;
|
|
15
|
+
/** The agent id whose evals this config applies to. */
|
|
16
|
+
agent: string;
|
|
17
|
+
}
|
|
11
18
|
/**
|
|
12
19
|
* Discover evals under `<rootDir>/server/agents/<agent>/evals/` — co-located
|
|
13
20
|
* with each agent's `agent.{md,ts}` (same folder-per-agent layout the agents
|
|
@@ -15,6 +22,13 @@ interface DiscoveredEval {
|
|
|
15
22
|
* path relative to that evals dir with `.eval.ts` stripped. Sorted + stable.
|
|
16
23
|
*/
|
|
17
24
|
declare function discoverEvalFiles(rootDir: string): DiscoveredEval[];
|
|
25
|
+
/**
|
|
26
|
+
* Discover the per-agent `evals.config.ts` (from {@link defineEvalConfig}) at
|
|
27
|
+
* `<rootDir>/server/agents/<agent>/evals/evals.config.ts`. Config is per-agent:
|
|
28
|
+
* each agent's config applies only to that agent's evals. Agents without a
|
|
29
|
+
* config file are omitted. Returns a stable, sorted list.
|
|
30
|
+
*/
|
|
31
|
+
declare function discoverEvalConfigs(rootDir: string): DiscoveredEvalConfig[];
|
|
18
32
|
//#endregion
|
|
19
|
-
export { DiscoveredEval, discoverEvalFiles };
|
|
33
|
+
export { DiscoveredEval, DiscoveredEvalConfig, discoverEvalConfigs, discoverEvalFiles };
|
|
20
34
|
//# sourceMappingURL=discover.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discover.d.ts","names":[],"sources":["../../src/evals/discover.ts"],"mappings":";;UAOiB,cAAA;EAAc;EAE7B,IAAA;EAF6B;EAI7B,EAAA;EAAA;EAEA,KAAA;AAAA;;
|
|
1
|
+
{"version":3,"file":"discover.d.ts","names":[],"sources":["../../src/evals/discover.ts"],"mappings":";;UAOiB,cAAA;EAAc;EAE7B,IAAA;EAF6B;EAI7B,EAAA;EAAA;EAEA,KAAA;AAAA;;UAIe,oBAAA;EAAoB;EAEnC,IAAA;EAAA;EAEA,KAAA;AAAA;;;;;AA+DF;;iBA3BgB,iBAAA,CAAkB,OAAA,WAAkB,cAAA;;;;;;;iBA2BpC,mBAAA,CAAoB,OAAA,WAAkB,oBAAA"}
|
package/dist/evals/discover.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { agentDirNames } from "../core/agent/agent-dirs.js";
|
|
2
2
|
import { CODE_AGENTS_SOURCE_DIR } from "../core/agent/load-code-agents.js";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { readdirSync } from "node:fs";
|
|
4
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
5
5
|
|
|
6
6
|
//#region src/evals/discover.ts
|
|
7
7
|
/** Recursively collect `*.eval.ts` files under `dir`. Empty when `dir` is absent. */
|
|
@@ -16,21 +16,33 @@ function evalFilesIn(dir) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
|
+
* List the agent directory names under `<rootDir>/server/agents/` (empty if the
|
|
20
|
+
* dir is absent). Shared by the eval-file and eval-config discovery below.
|
|
21
|
+
*/
|
|
22
|
+
function listAgents(rootDir) {
|
|
23
|
+
const agentsDir = path.join(rootDir, CODE_AGENTS_SOURCE_DIR);
|
|
24
|
+
try {
|
|
25
|
+
return {
|
|
26
|
+
agentsDir,
|
|
27
|
+
agents: agentDirNames(readdirSync(agentsDir, { withFileTypes: true }))
|
|
28
|
+
};
|
|
29
|
+
} catch {
|
|
30
|
+
return {
|
|
31
|
+
agentsDir,
|
|
32
|
+
agents: []
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
19
37
|
* Discover evals under `<rootDir>/server/agents/<agent>/evals/` — co-located
|
|
20
38
|
* with each agent's `agent.{md,ts}` (same folder-per-agent layout the agents
|
|
21
39
|
* plugin discovers). The agent id is the folder name; the eval id is the file
|
|
22
40
|
* path relative to that evals dir with `.eval.ts` stripped. Sorted + stable.
|
|
23
41
|
*/
|
|
24
42
|
function discoverEvalFiles(rootDir) {
|
|
25
|
-
const agentsDir =
|
|
43
|
+
const { agentsDir, agents } = listAgents(rootDir);
|
|
26
44
|
const out = [];
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
entries = readdirSync(agentsDir, { withFileTypes: true });
|
|
30
|
-
} catch {
|
|
31
|
-
return out;
|
|
32
|
-
}
|
|
33
|
-
for (const agent of agentDirNames(entries)) {
|
|
45
|
+
for (const agent of agents) {
|
|
34
46
|
const evalsDir = path.join(agentsDir, agent, "evals");
|
|
35
47
|
for (const file of evalFilesIn(evalsDir)) {
|
|
36
48
|
const id = path.relative(evalsDir, file).replace(/\.eval\.ts$/, "").split(path.sep).join("/");
|
|
@@ -43,7 +55,25 @@ function discoverEvalFiles(rootDir) {
|
|
|
43
55
|
}
|
|
44
56
|
return out.sort((a, b) => a.agent.localeCompare(b.agent) || a.id.localeCompare(b.id));
|
|
45
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Discover the per-agent `evals.config.ts` (from {@link defineEvalConfig}) at
|
|
60
|
+
* `<rootDir>/server/agents/<agent>/evals/evals.config.ts`. Config is per-agent:
|
|
61
|
+
* each agent's config applies only to that agent's evals. Agents without a
|
|
62
|
+
* config file are omitted. Returns a stable, sorted list.
|
|
63
|
+
*/
|
|
64
|
+
function discoverEvalConfigs(rootDir) {
|
|
65
|
+
const { agentsDir, agents } = listAgents(rootDir);
|
|
66
|
+
const out = [];
|
|
67
|
+
for (const agent of agents) {
|
|
68
|
+
const file = path.join(agentsDir, agent, "evals", "evals.config.ts");
|
|
69
|
+
if (existsSync(file)) out.push({
|
|
70
|
+
file,
|
|
71
|
+
agent
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return out.sort((a, b) => a.agent.localeCompare(b.agent));
|
|
75
|
+
}
|
|
46
76
|
|
|
47
77
|
//#endregion
|
|
48
|
-
export { discoverEvalFiles };
|
|
78
|
+
export { discoverEvalConfigs, discoverEvalFiles };
|
|
49
79
|
//# sourceMappingURL=discover.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discover.js","names":[],"sources":["../../src/evals/discover.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"discover.js","names":[],"sources":["../../src/evals/discover.ts"],"sourcesContent":["import { existsSync, readdirSync } from \"node:fs\";\nimport path from \"node:path\";\n\nimport { agentDirNames } from \"../core/agent/agent-dirs\";\nimport { CODE_AGENTS_SOURCE_DIR } from \"../core/agent/load-code-agents\";\n\n/** An eval file found under `server/agents/<agent>/evals/`. */\nexport interface DiscoveredEval {\n /** Absolute path to the `*.eval.ts` file. */\n file: string;\n /** Id relative to the agent's evals dir, without `.eval.ts` (e.g. `weather/basic`). */\n id: string;\n /** The agent id (the `server/agents/<agent>` directory name). */\n agent: string;\n}\n\n/** A per-agent `evals.config.ts` found under `server/agents/<agent>/evals/`. */\nexport interface DiscoveredEvalConfig {\n /** Absolute path to the `evals.config.ts` file. */\n file: string;\n /** The agent id whose evals this config applies to. */\n agent: string;\n}\n\n/** Recursively collect `*.eval.ts` files under `dir`. Empty when `dir` is absent. */\nfunction evalFilesIn(dir: string): string[] {\n try {\n return readdirSync(dir, { recursive: true, withFileTypes: true })\n .filter((e) => e.isFile() && e.name.endsWith(\".eval.ts\"))\n .map((e) => path.join(e.parentPath, e.name));\n } catch {\n return [];\n }\n}\n\n/**\n * List the agent directory names under `<rootDir>/server/agents/` (empty if the\n * dir is absent). Shared by the eval-file and eval-config discovery below.\n */\nfunction listAgents(rootDir: string): { agentsDir: string; agents: string[] } {\n const agentsDir = path.join(rootDir, CODE_AGENTS_SOURCE_DIR);\n try {\n return {\n agentsDir,\n agents: agentDirNames(readdirSync(agentsDir, { withFileTypes: true })),\n };\n } catch {\n return { agentsDir, agents: [] };\n }\n}\n\n/**\n * Discover evals under `<rootDir>/server/agents/<agent>/evals/` — co-located\n * with each agent's `agent.{md,ts}` (same folder-per-agent layout the agents\n * plugin discovers). The agent id is the folder name; the eval id is the file\n * path relative to that evals dir with `.eval.ts` stripped. Sorted + stable.\n */\nexport function discoverEvalFiles(rootDir: string): DiscoveredEval[] {\n const { agentsDir, agents } = listAgents(rootDir);\n const out: DiscoveredEval[] = [];\n\n for (const agent of agents) {\n const evalsDir = path.join(agentsDir, agent, \"evals\");\n for (const file of evalFilesIn(evalsDir)) {\n const id = path\n .relative(evalsDir, file)\n .replace(/\\.eval\\.ts$/, \"\")\n .split(path.sep)\n .join(\"/\");\n out.push({ file, id, agent });\n }\n }\n\n return out.sort(\n (a, b) => a.agent.localeCompare(b.agent) || a.id.localeCompare(b.id),\n );\n}\n\n/**\n * Discover the per-agent `evals.config.ts` (from {@link defineEvalConfig}) at\n * `<rootDir>/server/agents/<agent>/evals/evals.config.ts`. Config is per-agent:\n * each agent's config applies only to that agent's evals. Agents without a\n * config file are omitted. Returns a stable, sorted list.\n */\nexport function discoverEvalConfigs(rootDir: string): DiscoveredEvalConfig[] {\n const { agentsDir, agents } = listAgents(rootDir);\n const out: DiscoveredEvalConfig[] = [];\n\n for (const agent of agents) {\n const file = path.join(agentsDir, agent, \"evals\", \"evals.config.ts\");\n if (existsSync(file)) out.push({ file, agent });\n }\n\n return out.sort((a, b) => a.agent.localeCompare(b.agent));\n}\n"],"mappings":";;;;;;;AAyBA,SAAS,YAAY,KAAuB;AAC1C,KAAI;AACF,SAAO,YAAY,KAAK;GAAE,WAAW;GAAM,eAAe;GAAM,CAAC,CAC9D,QAAQ,MAAM,EAAE,QAAQ,IAAI,EAAE,KAAK,SAAS,WAAW,CAAC,CACxD,KAAK,MAAM,KAAK,KAAK,EAAE,YAAY,EAAE,KAAK,CAAC;SACxC;AACN,SAAO,EAAE;;;;;;;AAQb,SAAS,WAAW,SAA0D;CAC5E,MAAM,YAAY,KAAK,KAAK,SAAS,uBAAuB;AAC5D,KAAI;AACF,SAAO;GACL;GACA,QAAQ,cAAc,YAAY,WAAW,EAAE,eAAe,MAAM,CAAC,CAAC;GACvE;SACK;AACN,SAAO;GAAE;GAAW,QAAQ,EAAE;GAAE;;;;;;;;;AAUpC,SAAgB,kBAAkB,SAAmC;CACnE,MAAM,EAAE,WAAW,WAAW,WAAW,QAAQ;CACjD,MAAM,MAAwB,EAAE;AAEhC,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,KAAK,KAAK,WAAW,OAAO,QAAQ;AACrD,OAAK,MAAM,QAAQ,YAAY,SAAS,EAAE;GACxC,MAAM,KAAK,KACR,SAAS,UAAU,KAAK,CACxB,QAAQ,eAAe,GAAG,CAC1B,MAAM,KAAK,IAAI,CACf,KAAK,IAAI;AACZ,OAAI,KAAK;IAAE;IAAM;IAAI;IAAO,CAAC;;;AAIjC,QAAO,IAAI,MACR,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,MAAM,IAAI,EAAE,GAAG,cAAc,EAAE,GAAG,CACrE;;;;;;;;AASH,SAAgB,oBAAoB,SAAyC;CAC3E,MAAM,EAAE,WAAW,WAAW,WAAW,QAAQ;CACjD,MAAM,MAA8B,EAAE;AAEtC,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,OAAO,KAAK,KAAK,WAAW,OAAO,SAAS,kBAAkB;AACpE,MAAI,WAAW,KAAK,CAAE,KAAI,KAAK;GAAE;GAAM;GAAO,CAAC;;AAGjD,QAAO,IAAI,MAAM,GAAG,MAAM,EAAE,MAAM,cAAc,EAAE,MAAM,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-driver.d.ts","names":[],"sources":["../../src/evals/http-driver.ts"],"mappings":";;;UAGiB,iBAAA;;EAEf,OAAA;EAFgC;EAIhC,KAAA;EAEgB;EAAhB,OAAA,GAAU,MAAA;EAFV;EAIA,IAAA;EAFU;EAIV,WAAA;EAAA;;;;
|
|
1
|
+
{"version":3,"file":"http-driver.d.ts","names":[],"sources":["../../src/evals/http-driver.ts"],"mappings":";;;UAGiB,iBAAA;;EAEf,OAAA;EAFgC;EAIhC,KAAA;EAEgB;EAAhB,OAAA,GAAU,MAAA;EAFV;EAIA,IAAA;EAFU;EAIV,WAAA;EAAA;;;;AA4IF;;;EAnIE,SAAA;AAAA;;;;;;;iBAmIc,gBAAA,CAAiB,OAAA,EAAS,iBAAA,GAAoB,UAAA"}
|
|
@@ -1,19 +1,37 @@
|
|
|
1
1
|
import { readSseEvents } from "../stream/sse-reader.js";
|
|
2
2
|
|
|
3
3
|
//#region src/evals/http-driver.ts
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Record a `function_call` output item, keyed by `call_id ?? name`, capturing
|
|
6
|
+
* its parsed arguments. A later `done` event's fuller args win over the initial
|
|
7
|
+
* `added` event's (often empty) args.
|
|
8
|
+
*/
|
|
5
9
|
function recordToolCall(item, state) {
|
|
6
10
|
if (item?.type !== "function_call" || !item.name) return;
|
|
7
11
|
const key = item.call_id ?? item.name;
|
|
8
|
-
|
|
9
|
-
state.
|
|
10
|
-
state.toolCalls.
|
|
12
|
+
const args = parseArgs(item.arguments);
|
|
13
|
+
const existing = state.toolCalls.get(key);
|
|
14
|
+
if (!existing) state.toolCalls.set(key, {
|
|
15
|
+
name: item.name,
|
|
16
|
+
args
|
|
17
|
+
});
|
|
18
|
+
else if (Object.keys(args).length > 0) existing.args = args;
|
|
11
19
|
}
|
|
12
20
|
/** Apply an `appkit.metadata` event's thread/trace ids. */
|
|
13
21
|
function applyMetadata(data, state, setThread) {
|
|
14
22
|
if (data?.threadId) setThread(data.threadId);
|
|
15
23
|
if (data?.mlflowTraceId) state.traceId = data.mlflowTraceId;
|
|
16
24
|
}
|
|
25
|
+
/** Parse a function-call `arguments` JSON string; `{}` on missing/invalid. */
|
|
26
|
+
function parseArgs(raw) {
|
|
27
|
+
if (typeof raw !== "string" || raw.trim() === "") return {};
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
17
35
|
/** Parse a single Responses-API SSE `data:` payload into the running totals. */
|
|
18
36
|
function applyEvent(event, state, setThread) {
|
|
19
37
|
const type = event.type;
|
|
@@ -59,8 +77,9 @@ function createHttpDriver(options) {
|
|
|
59
77
|
reset() {
|
|
60
78
|
threadId = void 0;
|
|
61
79
|
},
|
|
62
|
-
async send(message) {
|
|
63
|
-
const
|
|
80
|
+
async send(message, opts) {
|
|
81
|
+
const timeout = AbortSignal.timeout(timeoutMs);
|
|
82
|
+
const signal = opts?.signal ? AbortSignal.any([timeout, opts.signal]) : timeout;
|
|
64
83
|
let res;
|
|
65
84
|
try {
|
|
66
85
|
res = await fetch(`${options.baseUrl}${chatPath}`, {
|
|
@@ -77,19 +96,20 @@ function createHttpDriver(options) {
|
|
|
77
96
|
return {
|
|
78
97
|
reply: "",
|
|
79
98
|
toolCalls: [],
|
|
99
|
+
toolCallDetails: [],
|
|
80
100
|
succeeded: false
|
|
81
101
|
};
|
|
82
102
|
}
|
|
83
103
|
if (!res.ok || !res.body) return {
|
|
84
104
|
reply: "",
|
|
85
105
|
toolCalls: [],
|
|
106
|
+
toolCallDetails: [],
|
|
86
107
|
succeeded: false,
|
|
87
108
|
sessionId: threadId
|
|
88
109
|
};
|
|
89
110
|
const state = {
|
|
90
111
|
reply: "",
|
|
91
|
-
toolCalls:
|
|
92
|
-
seen: /* @__PURE__ */ new Set(),
|
|
112
|
+
toolCalls: /* @__PURE__ */ new Map(),
|
|
93
113
|
ok: true
|
|
94
114
|
};
|
|
95
115
|
const setThread = (id) => {
|
|
@@ -107,9 +127,11 @@ function createHttpDriver(options) {
|
|
|
107
127
|
state.ok = false;
|
|
108
128
|
}
|
|
109
129
|
if (signal.aborted) state.ok = false;
|
|
130
|
+
const toolCallDetails = [...state.toolCalls.values()];
|
|
110
131
|
return {
|
|
111
132
|
reply: state.reply,
|
|
112
|
-
toolCalls:
|
|
133
|
+
toolCalls: toolCallDetails.map((c) => c.name),
|
|
134
|
+
toolCallDetails,
|
|
113
135
|
succeeded: state.ok,
|
|
114
136
|
sessionId: threadId,
|
|
115
137
|
traceId: state.traceId
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-driver.js","names":[],"sources":["../../src/evals/http-driver.ts"],"sourcesContent":["import { readSseEvents } from \"../stream/sse-reader\";\nimport type { DriveResult, EvalDriver } from \"./types\";\n\nexport interface HttpDriverOptions {\n /** Base URL of the running app, e.g. `http://localhost:3000`. */\n baseUrl: string;\n /** Agent alias to target. Omit to use the app's default agent. */\n agent?: string;\n /** Extra request headers (e.g. auth for a deployed app). */\n headers?: Record<string, string>;\n /** Chat endpoint path. Defaults to `/api/agents/chat`. */\n path?: string;\n /** MLflow run id to link each turn's trace to (for evaluation runs). */\n mlflowRunId?: string;\n /**\n * Max wall-clock time for a single turn before it is abandoned as a failed\n * turn (`succeeded: false`). Without this a hung agent — a blocked tool, a\n * stalled model — never ends the SSE stream (heartbeats keep it alive), so\n * the read loop spins forever and wedges the whole sequential suite.\n * Defaults to 120s.\n */\n // ponytail: total per-turn cap; switch to an idle timeout if long legit turns get killed.\n timeoutMs?: number;\n}\n\n/** Mutable running totals accumulated while draining one turn's SSE stream. */\ninterface DriveState {\n reply: string;\n toolCalls: string[];\n seen: Set<string>;\n ok: boolean;\n traceId?: string;\n}\n\n/** Record a `function_call` output item once per call id (deduped). */\nfunction recordToolCall(\n item: { type?: string; name?: string; call_id?: string } | undefined,\n state: DriveState,\n): void {\n if (item?.type !== \"function_call\" || !item.name) return;\n const key = item.call_id ?? item.name;\n if (state.seen.has(key)) return;\n state.seen.add(key);\n state.toolCalls.push(item.name);\n}\n\n/** Apply an `appkit.metadata` event's thread/trace ids. */\nfunction applyMetadata(\n data: { threadId?: string; mlflowTraceId?: string } | undefined,\n state: DriveState,\n setThread: (id: string) => void,\n): void {\n if (data?.threadId) setThread(data.threadId);\n if (data?.mlflowTraceId) state.traceId = data.mlflowTraceId;\n}\n\n/** Parse a single Responses-API SSE `data:` payload into the running totals. */\nfunction applyEvent(\n event: Record<string, unknown>,\n state: DriveState,\n setThread: (id: string) => void,\n): void {\n const type = event.type;\n if (type === \"response.output_text.delta\") {\n if (typeof event.delta === \"string\") state.reply += event.delta;\n return;\n }\n if (\n type === \"response.output_item.added\" ||\n type === \"response.output_item.done\"\n ) {\n const item = event.item as {\n type?: string;\n name?: string;\n call_id?: string;\n content?: Array<{ text?: string }>;\n };\n recordToolCall(item, state);\n // A terminal `message` item carries the full reply text and *replaces*\n // the accumulated deltas — the server emits no delta for a full message\n // (event-translator `handleFullMessage`), so a non-streaming adapter's\n // whole reply arrives only here. Guard on non-empty so a streaming turn's\n // trailing done event can't clobber the deltas with \"\".\n if (type === \"response.output_item.done\" && item.type === \"message\") {\n const text = (item.content ?? []).map((c) => c.text ?? \"\").join(\"\");\n if (text) state.reply = text;\n }\n return;\n }\n if (type === \"error\" || type === \"response.failed\") {\n state.ok = false;\n return;\n }\n if (type === \"appkit.metadata\") {\n applyMetadata(\n event.data as { threadId?: string; mlflowTraceId?: string },\n state,\n setThread,\n );\n }\n}\n\n/** Build the chat request payload, including only the optional fields that are set. */\nfunction buildRequestBody(\n message: string,\n options: HttpDriverOptions,\n threadId: string | undefined,\n): string {\n return JSON.stringify({\n message,\n ...(options.agent ? { agent: options.agent } : {}),\n ...(threadId ? { threadId } : {}),\n ...(options.mlflowRunId ? { mlflowRunId: options.mlflowRunId } : {}),\n });\n}\n\n/**\n * Drives an agent by POSTing to a running app's chat endpoint and parsing the\n * SSE response. Keeps the thread id across `send`s so multi-turn evals share a\n * conversation. Agent/stream errors surface as `succeeded: false` rather than\n * throwing, so `t.succeeded()` can assert on them.\n */\nexport function createHttpDriver(options: HttpDriverOptions): EvalDriver {\n const chatPath = options.path ?? \"/api/agents/chat\";\n const timeoutMs = options.timeoutMs ?? 120_000;\n let threadId: string | undefined;\n\n return {\n reset(): void {\n threadId = undefined;\n },\n async send(message: string): Promise<DriveResult> {\n // Bounds connect + the entire read below. Passed to both the fetch and\n // the SSE reader: on expiry the reader is cancelled and the turn fails.\n const signal = AbortSignal.timeout(timeoutMs);\n let res: Response;\n try {\n res = await fetch(`${options.baseUrl}${chatPath}`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", ...options.headers },\n body: buildRequestBody(message, options, threadId),\n // A chat endpoint never needs a redirect; following one would replay\n // custom auth headers (`options.headers`) to the redirect target.\n // A 3xx becomes an opaque response (res.ok === false), handled as a\n // failed turn by the !res.ok guard below.\n redirect: \"manual\",\n signal,\n });\n } catch {\n return { reply: \"\", toolCalls: [], succeeded: false };\n }\n\n if (!res.ok || !res.body) {\n return {\n reply: \"\",\n toolCalls: [],\n succeeded: false,\n sessionId: threadId,\n };\n }\n\n const state: DriveState = {\n reply: \"\",\n toolCalls: [],\n seen: new Set<string>(),\n ok: true,\n };\n const setThread = (id: string) => {\n threadId = id;\n };\n try {\n for await (const { event, data } of readSseEvents(res.body, signal)) {\n if (data === \"[DONE]\") continue;\n // A stream-level error frame (a thrown exception in the generator)\n // is framed as `event: error` with a payload that carries no `type`,\n // so the SSE event name — not the payload — is the real signal.\n if (event === \"error\") state.ok = false;\n try {\n applyEvent(\n JSON.parse(data) as Record<string, unknown>,\n state,\n setThread,\n );\n } catch {\n // skip malformed event payloads\n }\n }\n } catch {\n // mid-stream transport error or DoS-guard throw: a broken turn.\n state.ok = false;\n }\n // A timeout ends the iterator cleanly (reader cancel) rather than\n // throwing, so mark an aborted turn failed explicitly.\n if (signal.aborted) state.ok = false;\n\n return {\n reply: state.reply,\n toolCalls: state.toolCalls,\n succeeded: state.ok,\n sessionId: threadId,\n traceId: state.traceId,\n };\n },\n };\n}\n"],"mappings":";;;;AAmCA,SAAS,eACP,MACA,OACM;AACN,KAAI,MAAM,SAAS,mBAAmB,CAAC,KAAK,KAAM;CAClD,MAAM,MAAM,KAAK,WAAW,KAAK;AACjC,KAAI,MAAM,KAAK,IAAI,IAAI,CAAE;AACzB,OAAM,KAAK,IAAI,IAAI;AACnB,OAAM,UAAU,KAAK,KAAK,KAAK;;;AAIjC,SAAS,cACP,MACA,OACA,WACM;AACN,KAAI,MAAM,SAAU,WAAU,KAAK,SAAS;AAC5C,KAAI,MAAM,cAAe,OAAM,UAAU,KAAK;;;AAIhD,SAAS,WACP,OACA,OACA,WACM;CACN,MAAM,OAAO,MAAM;AACnB,KAAI,SAAS,8BAA8B;AACzC,MAAI,OAAO,MAAM,UAAU,SAAU,OAAM,SAAS,MAAM;AAC1D;;AAEF,KACE,SAAS,gCACT,SAAS,6BACT;EACA,MAAM,OAAO,MAAM;AAMnB,iBAAe,MAAM,MAAM;AAM3B,MAAI,SAAS,+BAA+B,KAAK,SAAS,WAAW;GACnE,MAAM,QAAQ,KAAK,WAAW,EAAE,EAAE,KAAK,MAAM,EAAE,QAAQ,GAAG,CAAC,KAAK,GAAG;AACnE,OAAI,KAAM,OAAM,QAAQ;;AAE1B;;AAEF,KAAI,SAAS,WAAW,SAAS,mBAAmB;AAClD,QAAM,KAAK;AACX;;AAEF,KAAI,SAAS,kBACX,eACE,MAAM,MACN,OACA,UACD;;;AAKL,SAAS,iBACP,SACA,SACA,UACQ;AACR,QAAO,KAAK,UAAU;EACpB;EACA,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,OAAO,GAAG,EAAE;EACjD,GAAI,WAAW,EAAE,UAAU,GAAG,EAAE;EAChC,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,aAAa,GAAG,EAAE;EACpE,CAAC;;;;;;;;AASJ,SAAgB,iBAAiB,SAAwC;CACvE,MAAM,WAAW,QAAQ,QAAQ;CACjC,MAAM,YAAY,QAAQ,aAAa;CACvC,IAAI;AAEJ,QAAO;EACL,QAAc;AACZ,cAAW;;EAEb,MAAM,KAAK,SAAuC;GAGhD,MAAM,SAAS,YAAY,QAAQ,UAAU;GAC7C,IAAI;AACJ,OAAI;AACF,UAAM,MAAM,MAAM,GAAG,QAAQ,UAAU,YAAY;KACjD,QAAQ;KACR,SAAS;MAAE,gBAAgB;MAAoB,GAAG,QAAQ;MAAS;KACnE,MAAM,iBAAiB,SAAS,SAAS,SAAS;KAKlD,UAAU;KACV;KACD,CAAC;WACI;AACN,WAAO;KAAE,OAAO;KAAI,WAAW,EAAE;KAAE,WAAW;KAAO;;AAGvD,OAAI,CAAC,IAAI,MAAM,CAAC,IAAI,KAClB,QAAO;IACL,OAAO;IACP,WAAW,EAAE;IACb,WAAW;IACX,WAAW;IACZ;GAGH,MAAM,QAAoB;IACxB,OAAO;IACP,WAAW,EAAE;IACb,sBAAM,IAAI,KAAa;IACvB,IAAI;IACL;GACD,MAAM,aAAa,OAAe;AAChC,eAAW;;AAEb,OAAI;AACF,eAAW,MAAM,EAAE,OAAO,UAAU,cAAc,IAAI,MAAM,OAAO,EAAE;AACnE,SAAI,SAAS,SAAU;AAIvB,SAAI,UAAU,QAAS,OAAM,KAAK;AAClC,SAAI;AACF,iBACE,KAAK,MAAM,KAAK,EAChB,OACA,UACD;aACK;;WAIJ;AAEN,UAAM,KAAK;;AAIb,OAAI,OAAO,QAAS,OAAM,KAAK;AAE/B,UAAO;IACL,OAAO,MAAM;IACb,WAAW,MAAM;IACjB,WAAW,MAAM;IACjB,WAAW;IACX,SAAS,MAAM;IAChB;;EAEJ"}
|
|
1
|
+
{"version":3,"file":"http-driver.js","names":[],"sources":["../../src/evals/http-driver.ts"],"sourcesContent":["import { readSseEvents } from \"../stream/sse-reader\";\nimport type { DriveResult, EvalDriver } from \"./types\";\n\nexport interface HttpDriverOptions {\n /** Base URL of the running app, e.g. `http://localhost:3000`. */\n baseUrl: string;\n /** Agent alias to target. Omit to use the app's default agent. */\n agent?: string;\n /** Extra request headers (e.g. auth for a deployed app). */\n headers?: Record<string, string>;\n /** Chat endpoint path. Defaults to `/api/agents/chat`. */\n path?: string;\n /** MLflow run id to link each turn's trace to (for evaluation runs). */\n mlflowRunId?: string;\n /**\n * Max wall-clock time for a single turn before it is abandoned as a failed\n * turn (`succeeded: false`). Without this a hung agent — a blocked tool, a\n * stalled model — never ends the SSE stream (heartbeats keep it alive), so\n * the read loop spins forever and wedges the whole sequential suite.\n * Defaults to 120s.\n */\n // ponytail: total per-turn cap; switch to an idle timeout if long legit turns get killed.\n timeoutMs?: number;\n}\n\n/** Mutable running totals accumulated while draining one turn's SSE stream. */\ninterface DriveState {\n reply: string;\n /** Captured tool calls, keyed by `call_id ?? name` (dedupe). */\n toolCalls: Map<string, ToolCall>;\n ok: boolean;\n traceId?: string;\n}\n\n/**\n * Record a `function_call` output item, keyed by `call_id ?? name`, capturing\n * its parsed arguments. A later `done` event's fuller args win over the initial\n * `added` event's (often empty) args.\n */\nfunction recordToolCall(\n item:\n | { type?: string; name?: string; call_id?: string; arguments?: string }\n | undefined,\n state: DriveState,\n): void {\n if (item?.type !== \"function_call\" || !item.name) return;\n const key = item.call_id ?? item.name;\n const args = parseArgs(item.arguments);\n const existing = state.toolCalls.get(key);\n if (!existing) {\n state.toolCalls.set(key, { name: item.name, args });\n } else if (Object.keys(args).length > 0) {\n // The initial `added` event may carry empty args while the later `done`\n // carries the full arguments — keep the fuller set.\n existing.args = args;\n }\n}\n\n/** Apply an `appkit.metadata` event's thread/trace ids. */\nfunction applyMetadata(\n data: { threadId?: string; mlflowTraceId?: string } | undefined,\n state: DriveState,\n setThread: (id: string) => void,\n): void {\n if (data?.threadId) setThread(data.threadId);\n if (data?.mlflowTraceId) state.traceId = data.mlflowTraceId;\n}\n\n/** A single captured tool call, deduped by `call_id ?? name`. */\ntype ToolCall = { name: string; args: Record<string, unknown> };\n\n/** Parse a function-call `arguments` JSON string; `{}` on missing/invalid. */\nfunction parseArgs(raw: unknown): Record<string, unknown> {\n if (typeof raw !== \"string\" || raw.trim() === \"\") return {};\n try {\n const parsed = JSON.parse(raw);\n return parsed !== null &&\n typeof parsed === \"object\" &&\n !Array.isArray(parsed)\n ? (parsed as Record<string, unknown>)\n : {};\n } catch {\n return {};\n }\n}\n\n/** Parse a single Responses-API SSE `data:` payload into the running totals. */\nfunction applyEvent(\n event: Record<string, unknown>,\n state: DriveState,\n setThread: (id: string) => void,\n): void {\n const type = event.type;\n if (type === \"response.output_text.delta\") {\n if (typeof event.delta === \"string\") state.reply += event.delta;\n return;\n }\n if (\n type === \"response.output_item.added\" ||\n type === \"response.output_item.done\"\n ) {\n const item = event.item as {\n type?: string;\n name?: string;\n call_id?: string;\n arguments?: string;\n content?: Array<{ text?: string }>;\n };\n recordToolCall(item, state);\n // A terminal `message` item carries the full reply text and *replaces*\n // the accumulated deltas — the server emits no delta for a full message\n // (event-translator `handleFullMessage`), so a non-streaming adapter's\n // whole reply arrives only here. Guard on non-empty so a streaming turn's\n // trailing done event can't clobber the deltas with \"\".\n if (type === \"response.output_item.done\" && item.type === \"message\") {\n const text = (item.content ?? []).map((c) => c.text ?? \"\").join(\"\");\n if (text) state.reply = text;\n }\n return;\n }\n if (type === \"error\" || type === \"response.failed\") {\n state.ok = false;\n return;\n }\n if (type === \"appkit.metadata\") {\n applyMetadata(\n event.data as { threadId?: string; mlflowTraceId?: string },\n state,\n setThread,\n );\n }\n}\n\n/** Build the chat request payload, including only the optional fields that are set. */\nfunction buildRequestBody(\n message: string,\n options: HttpDriverOptions,\n threadId: string | undefined,\n): string {\n return JSON.stringify({\n message,\n ...(options.agent ? { agent: options.agent } : {}),\n ...(threadId ? { threadId } : {}),\n ...(options.mlflowRunId ? { mlflowRunId: options.mlflowRunId } : {}),\n });\n}\n\n/**\n * Drives an agent by POSTing to a running app's chat endpoint and parsing the\n * SSE response. Keeps the thread id across `send`s so multi-turn evals share a\n * conversation. Agent/stream errors surface as `succeeded: false` rather than\n * throwing, so `t.succeeded()` can assert on them.\n */\nexport function createHttpDriver(options: HttpDriverOptions): EvalDriver {\n const chatPath = options.path ?? \"/api/agents/chat\";\n const timeoutMs = options.timeoutMs ?? 120_000;\n let threadId: string | undefined;\n\n return {\n reset(): void {\n threadId = undefined;\n },\n async send(\n message: string,\n opts?: { signal?: AbortSignal },\n ): Promise<DriveResult> {\n // Bounds connect + the entire read below. Passed to both the fetch and\n // the SSE reader: on expiry the reader is cancelled and the turn fails.\n // Composed with the caller's signal so a per-eval timeout also aborts this turn.\n const timeout = AbortSignal.timeout(timeoutMs);\n const signal = opts?.signal\n ? AbortSignal.any([timeout, opts.signal])\n : timeout;\n let res: Response;\n try {\n res = await fetch(`${options.baseUrl}${chatPath}`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", ...options.headers },\n body: buildRequestBody(message, options, threadId),\n // A chat endpoint never needs a redirect; following one would replay\n // custom auth headers (`options.headers`) to the redirect target.\n // A 3xx becomes an opaque response (res.ok === false), handled as a\n // failed turn by the !res.ok guard below.\n redirect: \"manual\",\n signal,\n });\n } catch {\n return {\n reply: \"\",\n toolCalls: [],\n toolCallDetails: [],\n succeeded: false,\n };\n }\n\n if (!res.ok || !res.body) {\n return {\n reply: \"\",\n toolCalls: [],\n toolCallDetails: [],\n succeeded: false,\n sessionId: threadId,\n };\n }\n\n const state: DriveState = {\n reply: \"\",\n toolCalls: new Map<string, ToolCall>(),\n ok: true,\n };\n const setThread = (id: string) => {\n threadId = id;\n };\n try {\n for await (const { event, data } of readSseEvents(res.body, signal)) {\n if (data === \"[DONE]\") continue;\n // A stream-level error frame (a thrown exception in the generator)\n // is framed as `event: error` with a payload that carries no `type`,\n // so the SSE event name — not the payload — is the real signal.\n if (event === \"error\") state.ok = false;\n try {\n applyEvent(\n JSON.parse(data) as Record<string, unknown>,\n state,\n setThread,\n );\n } catch {\n // skip malformed event payloads\n }\n }\n } catch {\n // mid-stream transport error or DoS-guard throw: a broken turn.\n state.ok = false;\n }\n // A timeout ends the iterator cleanly (reader cancel) rather than\n // throwing, so mark an aborted turn failed explicitly.\n if (signal.aborted) state.ok = false;\n\n const toolCallDetails = [...state.toolCalls.values()];\n return {\n reply: state.reply,\n toolCalls: toolCallDetails.map((c) => c.name),\n toolCallDetails,\n succeeded: state.ok,\n sessionId: threadId,\n traceId: state.traceId,\n };\n },\n };\n}\n"],"mappings":"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"}
|