mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-17 16:03:06 +00:00
* feat: make hub integrations usable as examples in global chat Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: keep web search guidance in sync with provider capability Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: resync web search guidance before the request is built Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: carry loop web search availability into every prompt rebuild Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: drop web search guidance on the completions api fallback Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: correct onBeforeIteration contract for the fallback re-entry Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat: give the ai chat hub script descriptions and integration metadata Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: treat authored hub metadata as evidence the scripts were curated Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: treat hub curated as three-state and speak only for a stated true Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: let the benchmark hub serve integration metadata Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: benchmark the hub tool against a real integration's scripts and metadata Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: cover two more real integrations where the scripts already answer Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: report metadata_source from whether the fixture has authored meta Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: parse the hub resource type schema instead of relaying it as a string Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: match integration suggestions on slug words instead of substrings Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: search the integration a query names outright instead of ranking past it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: reject hub integration slugs that would re-target the proxied request Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: add a mentioned integration's hits instead of filtering the search to it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: keep named-integration hits under the content cap and unmangle fixture placeholders Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: cut the middle of a capped hub result instead of repeating its tail Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: cap hub results by keeping the best of the ranked and named hits Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: keep a named integration that ranking placed below the content cap Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: let plan mode use the hub integration lookup Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat: mark which hub integrations carry authored provider knowledge The hub flags the integrations whose document holds hand-written provider knowledge, so a caller can tell before spending a call on the metadata endpoint: 18 of ~216 qualify, and for the rest the endpoint returns what was inferred from the same scripts a search already hands back. Carry the flag onto search results, where the model first meets a slug, so get_hub_integration is aimed at the few instead of guessed at. A hub that predates the flag omits it and nothing is marked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: stop the documented mark reading as a reason not to call The flag says the hub additionally holds provider knowledge checked against the live API. It is not a signal to skip the lookup elsewhere: that call still returns the resource type and the usage-ranked examples, neither of which a search result carries, and neither guessable. The prompt said to read a script instead when the mark is absent, which traded those for a guess on the ~198 undocumented integrations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: keep the benchmark's documented flag true to what it serves The eval hub reported Baremetrics and Holded as undocumented while its metadata endpoint handed back their authored notes, so a case could teach the model the flag means nothing. Derive it from both fixture sources, and pin the agreement. Also trims the documentedIntegrations comment to the four lines AGENTS.md allows, keeping the case-folding constraint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: keep the benchmark hub mostly undocumented, like the real one Adding holded and baremetrics to the documented set resolved the fixture's contradiction the wrong way: it left five of six integrations marked, against the live hub's 18 of ~216, and claimed authored notes for two the hub reports as having none. Drop the notes instead. Their auth and endpoints are in their shipped scripts, which is what the cases that use them are about. Also drops the last two places still describing the flag as a reason to spend or skip the metadata call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: let the model name the integration instead of guessing it from the query Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor: word web search guidance conditionally instead of tracking provider capability Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: open the hub metadata route to job tokens and share one integrations fetch Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: record a copied hub script's source where write_script keeps it Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test: pin that a query narrows to the integration it was given Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: match integration suggestions on the name the hub curates Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: name an integration the hub leaves unnamed from the local word table Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1541 lines
48 KiB
TypeScript
1541 lines
48 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import confluenceFixture from '../../fixtures/frontend/global/hub/confluence.json'
|
|
import servicenowFixture from '../../fixtures/frontend/global/hub/servicenow.json'
|
|
import outreachFixture from '../../fixtures/frontend/global/hub/outreach.json'
|
|
import type {
|
|
AppWithLastVersion,
|
|
CompletedJob,
|
|
Flow,
|
|
Job,
|
|
ListableApp,
|
|
ListableResource,
|
|
ListableVariable,
|
|
Resource,
|
|
Script
|
|
} from '../../../frontend/src/lib/gen'
|
|
import type {
|
|
DataMetric,
|
|
DataTableTables,
|
|
DataTableTableSchema,
|
|
GetDraftForUserResponse,
|
|
GetOwnDraftResponse,
|
|
ListDraftsResponse,
|
|
ScriptLang,
|
|
UpdateDraftResponse,
|
|
UserDraftItemKind
|
|
} from '../../../frontend/src/lib/gen/types.gen'
|
|
import { buildScriptLintResult } from './core/script/preview'
|
|
import { applyDatatableSql, type BenchmarkDatatableSeed } from './datatableSqlEngine'
|
|
|
|
export type { BenchmarkDatatableSeed, BenchmarkDatatableTableSeed } from './datatableSqlEngine'
|
|
|
|
const BENCHMARK_TIMESTAMP = '1970-01-01T00:00:00.000Z'
|
|
|
|
export interface BenchmarkWorkspaceScript {
|
|
path: string
|
|
summary: string
|
|
description?: string
|
|
language: Script['language']
|
|
schema?: Record<string, unknown>
|
|
content: string
|
|
}
|
|
|
|
export interface BenchmarkWorkspaceFlow {
|
|
path: string
|
|
summary: string
|
|
description?: string
|
|
schema?: Record<string, unknown>
|
|
value: Flow['value']
|
|
}
|
|
|
|
export interface BenchmarkWorkspaceApp {
|
|
path: string
|
|
summary: string
|
|
/** Defaults to true. Set false for a drag-and-drop app, which the chat can list
|
|
* and read but has no tool to edit — its value is a grid, not files. */
|
|
rawApp?: boolean
|
|
value: {
|
|
files?: Record<string, string>
|
|
runnables?: Record<string, unknown>
|
|
data?: unknown
|
|
policy?: unknown
|
|
custom_path?: unknown
|
|
[key: string]: unknown
|
|
}
|
|
}
|
|
|
|
export interface BenchmarkWorkspaceVariable {
|
|
path: string
|
|
value: string
|
|
is_secret: boolean
|
|
description?: string
|
|
labels?: string[]
|
|
ws_specific?: boolean
|
|
}
|
|
|
|
/** An AI provider resource of the benchmark workspace, as an AI agent step would reference it.
|
|
* `models` stands in for the provider's model listing, which no eval run can reach. */
|
|
export interface BenchmarkWorkspaceAiProvider {
|
|
path: string
|
|
/** Resource type, which for AI resources is the provider kind (`anthropic`, `openai`, ...). */
|
|
kind: string
|
|
/** What this resource's `/ai/proxy/models` listing returns. */
|
|
models?: string[]
|
|
/** Set to point the resource at a gateway rather than the provider's own API. */
|
|
base_url?: string
|
|
/** Models the workspace AI settings selected for this provider. */
|
|
configuredModels?: string[]
|
|
/** Marks this provider's first configured model as the workspace default. */
|
|
isDefault?: boolean
|
|
}
|
|
|
|
/** A plain (non-AI) resource of the benchmark workspace, for cases about referencing a
|
|
* credential — passing one as a run argument, say. `value` is what `get_resource` returns. */
|
|
export interface BenchmarkWorkspaceResource {
|
|
path: string
|
|
resource_type: string
|
|
value?: Record<string, unknown>
|
|
description?: string
|
|
}
|
|
|
|
export interface BenchmarkWorkspaceJob {
|
|
/** Stable id so a case prompt can reference a specific run (e.g. for get_run). */
|
|
id?: string
|
|
jobKind?: CompletedJob['job_kind']
|
|
scriptPath?: string
|
|
createdBy?: string
|
|
label?: string
|
|
success?: boolean
|
|
logs?: string
|
|
args?: Record<string, unknown>
|
|
result?: unknown
|
|
}
|
|
|
|
export interface BenchmarkWorkspaceRunnables {
|
|
scripts?: BenchmarkWorkspaceScript[]
|
|
flows?: BenchmarkWorkspaceFlow[]
|
|
apps?: BenchmarkWorkspaceApp[]
|
|
variables?: BenchmarkWorkspaceVariable[]
|
|
aiProviders?: BenchmarkWorkspaceAiProvider[]
|
|
resources?: BenchmarkWorkspaceResource[]
|
|
datatables?: BenchmarkDatatableSeed[]
|
|
/** DuckLake catalog names, as `list_ducklakes` reports them. */
|
|
ducklakes?: string[]
|
|
dataMetrics?: DataMetric[]
|
|
jobs?: BenchmarkWorkspaceJob[]
|
|
}
|
|
|
|
type BenchmarkCompletedJob = CompletedJob & { type: 'CompletedJob' }
|
|
|
|
const benchmarkWorkspaces = new Set<string>()
|
|
const benchmarkWorkspaceRunnables = new Map<string, BenchmarkWorkspaceRunnables>()
|
|
// Keyed by `${workspace}::${jobId}` so concurrent attempts (or distinct cases)
|
|
// can seed the same fixed job id without clobbering each other's entry.
|
|
const benchmarkJobs = new Map<string, { workspace: string; job: BenchmarkCompletedJob }>()
|
|
|
|
function benchmarkJobKey(workspace: string, jobId: string): string {
|
|
return `${workspace}::${jobId}`
|
|
}
|
|
|
|
export function resetBenchmarkMockBackend(): void {
|
|
benchmarkWorkspaces.clear()
|
|
benchmarkWorkspaceRunnables.clear()
|
|
benchmarkJobs.clear()
|
|
benchmarkDrafts.clear()
|
|
}
|
|
|
|
// Stand-in for FolderService.createFolder so the global create_folder tool runs in
|
|
// memory instead of mutating the real backend. Folders aren't otherwise modelled
|
|
// (no folder-listing in evals), so this just echoes the created name.
|
|
export function createBenchmarkFolder(_workspace: string, name: string): string {
|
|
return name
|
|
}
|
|
|
|
export function registerBenchmarkWorkspace(workspace: string): void {
|
|
benchmarkWorkspaces.add(workspace)
|
|
}
|
|
|
|
export function registerBenchmarkWorkspaceRunnables(
|
|
workspace: string,
|
|
runnables: BenchmarkWorkspaceRunnables
|
|
): void {
|
|
benchmarkWorkspaces.add(workspace)
|
|
// Fresh case: drop any drafts left from a prior run on this workspace id.
|
|
clearBenchmarkDrafts(workspace)
|
|
// Datatables are mutated in place by exec_datatable_sql (a write must be visible
|
|
// to later reads), so store an isolated deep copy — never mutate the caller's seed.
|
|
benchmarkWorkspaceRunnables.set(workspace, {
|
|
...runnables,
|
|
datatables: runnables.datatables ? structuredClone(runnables.datatables) : undefined
|
|
})
|
|
// Seed any fixture jobs so list_runs / get_run have data to return.
|
|
for (const seed of runnables.jobs ?? []) {
|
|
createBenchmarkCompletedJob({
|
|
workspace,
|
|
id: seed.id,
|
|
jobKind: seed.jobKind ?? 'script',
|
|
success: seed.success,
|
|
scriptPath: seed.scriptPath,
|
|
createdBy: seed.createdBy,
|
|
label: seed.label,
|
|
logs: seed.logs,
|
|
args: seed.args,
|
|
result: seed.result
|
|
})
|
|
}
|
|
}
|
|
|
|
export function unregisterBenchmarkWorkspace(workspace: string): void {
|
|
benchmarkWorkspaces.delete(workspace)
|
|
benchmarkWorkspaceRunnables.delete(workspace)
|
|
clearBenchmarkDrafts(workspace)
|
|
for (const [jobId, entry] of benchmarkJobs.entries()) {
|
|
if (entry.workspace === workspace) {
|
|
benchmarkJobs.delete(jobId)
|
|
}
|
|
}
|
|
}
|
|
|
|
export function unregisterBenchmarkWorkspaceRunnables(workspace: string): void {
|
|
unregisterBenchmarkWorkspace(workspace)
|
|
}
|
|
|
|
export function hasBenchmarkWorkspace(workspace: string): boolean {
|
|
return benchmarkWorkspaces.has(workspace)
|
|
}
|
|
|
|
export function listBenchmarkScripts(workspace: string): Script[] | null {
|
|
const runnables = benchmarkWorkspaceRunnables.get(workspace)
|
|
if (!runnables) {
|
|
return null
|
|
}
|
|
return (runnables.scripts ?? []).map(buildBenchmarkScript)
|
|
}
|
|
|
|
export function listBenchmarkFlows(workspace: string): Flow[] | null {
|
|
const runnables = benchmarkWorkspaceRunnables.get(workspace)
|
|
if (!runnables) {
|
|
return null
|
|
}
|
|
return (runnables.flows ?? []).map(buildBenchmarkFlow)
|
|
}
|
|
|
|
export function getBenchmarkScriptByPath(workspace: string, path: string): Script | null {
|
|
const script = benchmarkWorkspaceRunnables
|
|
.get(workspace)
|
|
?.scripts?.find((entry) => entry.path === path)
|
|
|
|
return script ? buildBenchmarkScript(script) : null
|
|
}
|
|
|
|
export function getBenchmarkScriptByHash(workspace: string, hash: string): Script | null {
|
|
const script = benchmarkWorkspaceRunnables
|
|
.get(workspace)
|
|
?.scripts?.find((entry) => buildBenchmarkScriptHash(entry.path) === hash)
|
|
|
|
return script ? buildBenchmarkScript(script) : null
|
|
}
|
|
|
|
export function getBenchmarkFlowByPath(workspace: string, path: string): Flow | null {
|
|
const flow = benchmarkWorkspaceRunnables
|
|
.get(workspace)
|
|
?.flows?.find((entry) => entry.path === path)
|
|
|
|
return flow ? buildBenchmarkFlow(flow) : null
|
|
}
|
|
|
|
export function listBenchmarkApps(workspace: string): ListableApp[] | null {
|
|
const runnables = benchmarkWorkspaceRunnables.get(workspace)
|
|
if (!runnables) {
|
|
return null
|
|
}
|
|
return (runnables.apps ?? []).map(buildBenchmarkListableApp)
|
|
}
|
|
|
|
export function getBenchmarkAppByPath(workspace: string, path: string): AppWithLastVersion | null {
|
|
const app = benchmarkWorkspaceRunnables.get(workspace)?.apps?.find((entry) => entry.path === path)
|
|
|
|
return app ? buildBenchmarkApp(app) : null
|
|
}
|
|
|
|
function buildBenchmarkVariable(
|
|
workspace: string,
|
|
seed: BenchmarkWorkspaceVariable,
|
|
decryptSecret: boolean
|
|
): ListableVariable {
|
|
return {
|
|
workspace_id: workspace,
|
|
path: seed.path,
|
|
// Mirror `get_variable`: a secret's value is withheld unless decryption was
|
|
// asked for, so a reader genuinely cannot see it.
|
|
value: seed.is_secret && !decryptSecret ? undefined : seed.value,
|
|
is_secret: seed.is_secret,
|
|
description: seed.description,
|
|
labels: seed.labels,
|
|
ws_specific: seed.ws_specific ?? false,
|
|
extra_perms: {},
|
|
edited_at: BENCHMARK_TIMESTAMP
|
|
}
|
|
}
|
|
|
|
export function listBenchmarkVariables(workspace: string): ListableVariable[] | null {
|
|
const runnables = benchmarkWorkspaceRunnables.get(workspace)
|
|
if (!runnables) {
|
|
return null
|
|
}
|
|
// The list route never decrypts.
|
|
return (runnables.variables ?? []).map((seed) => buildBenchmarkVariable(workspace, seed, false))
|
|
}
|
|
|
|
/** AI provider resources of a benchmark workspace, shaped like `ResourceService.listResource`
|
|
* rows (which carry no value). Null when the workspace is not a benchmark one. */
|
|
export function listBenchmarkAiProviderResources(workspace: string): ListableResource[] | null {
|
|
const runnables = benchmarkWorkspaceRunnables.get(workspace)
|
|
if (!runnables) {
|
|
return null
|
|
}
|
|
return (runnables.aiProviders ?? []).map((seed) => ({
|
|
workspace_id: workspace,
|
|
path: seed.path,
|
|
resource_type: seed.kind,
|
|
value: null,
|
|
is_oauth: false,
|
|
is_linked: false,
|
|
is_refreshed: false,
|
|
extra_perms: {},
|
|
edited_at: BENCHMARK_TIMESTAMP
|
|
}))
|
|
}
|
|
|
|
/** Plain seeded resources of a benchmark workspace, shaped like `ResourceService.listResource`
|
|
* rows. Null when the workspace is not a benchmark one. */
|
|
export function listBenchmarkPlainResources(workspace: string): ListableResource[] | null {
|
|
const runnables = benchmarkWorkspaceRunnables.get(workspace)
|
|
if (!runnables) {
|
|
return null
|
|
}
|
|
return (runnables.resources ?? []).map((seed) => ({
|
|
workspace_id: workspace,
|
|
path: seed.path,
|
|
resource_type: seed.resource_type,
|
|
description: seed.description,
|
|
value: null,
|
|
is_oauth: false,
|
|
is_linked: false,
|
|
is_refreshed: false,
|
|
extra_perms: {},
|
|
edited_at: BENCHMARK_TIMESTAMP
|
|
}))
|
|
}
|
|
|
|
/** A seeded resource with its value, as `ResourceService.getResource` returns it. Covers both
|
|
* seed kinds, so it agrees with `existsResource` and `listResource` — both of those report AI
|
|
* providers too, and a case that lists resources and then reads one by path would otherwise get
|
|
* a row it cannot fetch. */
|
|
export function getBenchmarkResource(workspace: string, path: string): Resource | null {
|
|
const runnables = benchmarkWorkspaceRunnables.get(workspace)
|
|
const seed = runnables?.resources?.find((entry) => entry.path === path)
|
|
if (seed) {
|
|
return {
|
|
workspace_id: workspace,
|
|
path: seed.path,
|
|
resource_type: seed.resource_type,
|
|
description: seed.description,
|
|
value: seed.value ?? {},
|
|
is_oauth: false,
|
|
extra_perms: {}
|
|
} as Resource
|
|
}
|
|
const provider = runnables?.aiProviders?.find((entry) => entry.path === path)
|
|
if (!provider) {
|
|
return null
|
|
}
|
|
return {
|
|
workspace_id: workspace,
|
|
path: provider.path,
|
|
resource_type: provider.kind,
|
|
value: getBenchmarkResourceValue(workspace, path) ?? {},
|
|
is_oauth: false,
|
|
extra_perms: {}
|
|
} as Resource
|
|
}
|
|
|
|
/** The value of a seeded resource. For an AI provider only the endpoint fields are modelled — a
|
|
* key is never needed, because no eval run calls the provider through this resource. */
|
|
export function getBenchmarkResourceValue(
|
|
workspace: string,
|
|
path: string
|
|
): Record<string, unknown> | null {
|
|
const runnables = benchmarkWorkspaceRunnables.get(workspace)
|
|
const plain = runnables?.resources?.find((entry) => entry.path === path)
|
|
if (plain) {
|
|
return plain.value ?? {}
|
|
}
|
|
const seed = runnables?.aiProviders?.find((entry) => entry.path === path)
|
|
if (!seed) {
|
|
return null
|
|
}
|
|
return seed.base_url ? { base_url: seed.base_url } : {}
|
|
}
|
|
|
|
/** The AI settings of a benchmark workspace, as `WorkspaceService.getCopilotInfo` returns them. */
|
|
export function getBenchmarkAiConfig(workspace: string): Record<string, unknown> | null {
|
|
const seeds = benchmarkWorkspaceRunnables.get(workspace)?.aiProviders
|
|
if (!seeds) {
|
|
return null
|
|
}
|
|
const providers: Record<string, unknown> = {}
|
|
let defaultModel: { model: string; provider: string } | undefined
|
|
for (const seed of seeds) {
|
|
const models = seed.configuredModels ?? seed.models ?? []
|
|
providers[seed.kind] = { resource_path: seed.path, models }
|
|
if (seed.isDefault && models[0]) {
|
|
defaultModel = { model: models[0], provider: seed.kind }
|
|
}
|
|
}
|
|
return { providers, ...(defaultModel ? { default_model: defaultModel } : {}) }
|
|
}
|
|
|
|
export function getBenchmarkVariableByPath(
|
|
workspace: string,
|
|
path: string,
|
|
decryptSecret = true
|
|
): ListableVariable | null {
|
|
const seed = benchmarkWorkspaceRunnables
|
|
.get(workspace)
|
|
?.variables?.find((entry) => entry.path === path)
|
|
|
|
return seed ? buildBenchmarkVariable(workspace, seed, decryptSecret) : null
|
|
}
|
|
|
|
export function createBenchmarkCompletedJob(input: {
|
|
workspace: string
|
|
jobKind: CompletedJob['job_kind']
|
|
success?: boolean
|
|
result?: unknown
|
|
logs?: string
|
|
scriptPath?: string
|
|
scriptHash?: string
|
|
args?: Record<string, unknown>
|
|
id?: string
|
|
createdBy?: string
|
|
label?: string
|
|
}): string {
|
|
const jobId = input.id ?? `benchmark-job-${randomUUID()}`
|
|
const now = new Date().toISOString()
|
|
const job: BenchmarkCompletedJob = {
|
|
type: 'CompletedJob',
|
|
id: jobId,
|
|
workspace_id: input.workspace,
|
|
created_by: input.createdBy ?? 'ai-evals',
|
|
created_at: now,
|
|
started_at: now,
|
|
completed_at: now,
|
|
duration_ms: 0,
|
|
success: input.success ?? true,
|
|
script_path: input.scriptPath,
|
|
script_hash: input.scriptHash,
|
|
args: input.args,
|
|
result: input.result,
|
|
logs: input.logs,
|
|
canceled: false,
|
|
job_kind: input.jobKind,
|
|
permissioned_as: 'u/ai-evals',
|
|
is_flow_step: false,
|
|
is_skipped: false,
|
|
email: 'ai-evals@local',
|
|
visible_to_owner: true,
|
|
tag: 'benchmark',
|
|
labels: input.label ? [input.label] : undefined
|
|
}
|
|
|
|
benchmarkJobs.set(benchmarkJobKey(input.workspace, jobId), {
|
|
workspace: input.workspace,
|
|
job
|
|
})
|
|
return jobId
|
|
}
|
|
|
|
export function getBenchmarkCompletedJob(
|
|
workspace: string,
|
|
jobId: string
|
|
): BenchmarkCompletedJob | null {
|
|
const entry = benchmarkJobs.get(benchmarkJobKey(workspace, jobId))
|
|
if (!entry) {
|
|
return null
|
|
}
|
|
return structuredClone(entry.job)
|
|
}
|
|
|
|
/**
|
|
* List seeded/recorded jobs for a benchmark workspace, most recent first —
|
|
* the shape `JobService.listJobs` returns. Returns `null` for a non-benchmark
|
|
* workspace so the caller can fall through to the real backend. Server-side
|
|
* filters (path/creator/status/limit) are intentionally not applied: global
|
|
* eval cases assert on the recorded `list_runs` tool call, not on filtering.
|
|
*/
|
|
export function listBenchmarkJobs(workspace: string): Job[] | null {
|
|
if (!hasBenchmarkWorkspace(workspace)) {
|
|
return null
|
|
}
|
|
return [...benchmarkJobs.values()]
|
|
.filter((entry) => entry.workspace === workspace)
|
|
.map((entry) => structuredClone(entry.job) as Job)
|
|
.sort((a, b) => (b.created_at ?? '').localeCompare(a.created_at ?? ''))
|
|
}
|
|
|
|
/**
|
|
* Mirror `JobService.getJobLogs` (response is the raw log string). Throws a
|
|
* "not found" error for an unknown id, matching the backend 404.
|
|
*/
|
|
export function getBenchmarkJobLogs(workspace: string, jobId: string): string {
|
|
const job = getBenchmarkCompletedJob(workspace, jobId)
|
|
if (!job) {
|
|
throw new Error(`Job Logs not found for "${jobId}"`)
|
|
}
|
|
return job.logs ?? ''
|
|
}
|
|
|
|
/**
|
|
* Mirror `JobService.getFlowAllResults`, which get_run calls for the execution
|
|
* tree. Fixture jobs are single runs with no steps, so only the root entry.
|
|
*/
|
|
export function getBenchmarkFlowAllResults(workspace: string, jobId: string) {
|
|
const job = getBenchmarkCompletedJob(workspace, jobId)
|
|
if (!job) {
|
|
throw new Error(`Job "${jobId}" not found in benchmark workspace`)
|
|
}
|
|
return {
|
|
entries: [
|
|
{
|
|
job_id: jobId,
|
|
label: 'Flow',
|
|
kind: job.job_kind ?? 'script',
|
|
depth: 0,
|
|
sibling_index: 1,
|
|
sibling_count: 1,
|
|
status: job.success ? 'success' : 'failure',
|
|
success: job.success
|
|
}
|
|
],
|
|
truncated: false,
|
|
scope_filtered: false
|
|
}
|
|
}
|
|
|
|
// ============= Drafts (per-user, DB-backed in production) =============
|
|
|
|
/**
|
|
* In-memory stand-in for the per-user draft backend (`DraftService`). The global
|
|
* AI chat now persists and reads drafts through the backend DB instead of an
|
|
* in-tab `UserDraft` cell, so the eval mocks the draft endpoints it exercises
|
|
* (`updateDraft` / `getOwnDraft` / `getDraftForUser` / `listDrafts`) and keeps the
|
|
* saved values here, keyed by workspace + draft kind + storage path. Mirrors the
|
|
* semantics of the production unit test's mock in
|
|
* `frontend/src/lib/components/copilot/chat/global/core.test.ts`.
|
|
*/
|
|
const benchmarkDrafts = new Map<
|
|
string,
|
|
{
|
|
workspace: string
|
|
kind: UserDraftItemKind
|
|
path: string
|
|
value: unknown
|
|
createdAt: string
|
|
}
|
|
>()
|
|
|
|
// Counter-based timestamps: deterministic run-to-run (same event order → same
|
|
// values) but MONOTONIC per update, because production bumps a draft row's
|
|
// created_at on every upsert and the diff snapshot cache keys patch reuse on
|
|
// it — a fixed timestamp would serve stale patches after an edit. No eval
|
|
// simulates a concurrent writer, so every save is accepted and the conflict
|
|
// branch is never taken.
|
|
let benchmarkDraftClock = 0
|
|
function nextBenchmarkDraftTimestamp(): string {
|
|
benchmarkDraftClock += 1
|
|
return new Date(benchmarkDraftClock * 1000).toISOString()
|
|
}
|
|
|
|
function benchmarkDraftKey(workspace: string, kind: string, path: string): string {
|
|
return `${workspace}::${kind}::${path}`
|
|
}
|
|
|
|
export function clearBenchmarkDrafts(workspace: string): void {
|
|
for (const [key, entry] of benchmarkDrafts.entries()) {
|
|
if (entry.workspace === workspace) {
|
|
benchmarkDrafts.delete(key)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Seed a draft straight into the store — used by the eval's live-editor draft
|
|
* fixtures, which model "the user already has this draft open/saved". Writing it
|
|
* here (instead of through `UserDraft.save`) keeps it a backend draft row with no
|
|
* shadowing in-tab cell, so a model edit that persists to the backend is what the
|
|
* output read-back captures — not the stale seed.
|
|
*/
|
|
export function seedBenchmarkDraft(
|
|
workspace: string,
|
|
kind: UserDraftItemKind,
|
|
path: string,
|
|
value: unknown
|
|
): void {
|
|
benchmarkDrafts.set(benchmarkDraftKey(workspace, kind, path), {
|
|
workspace,
|
|
kind,
|
|
path,
|
|
value,
|
|
createdAt: nextBenchmarkDraftTimestamp()
|
|
})
|
|
}
|
|
|
|
/** Mirror `DraftService.updateDraft`: a `null`/omitted value deletes the row. */
|
|
export function updateBenchmarkDraft(input: {
|
|
workspace: string
|
|
kind: UserDraftItemKind
|
|
path: string
|
|
requestBody?: { value?: unknown }
|
|
}): UpdateDraftResponse {
|
|
const key = benchmarkDraftKey(input.workspace, input.kind, input.path)
|
|
const value = input.requestBody?.value
|
|
const createdAt = nextBenchmarkDraftTimestamp()
|
|
if (value == null) {
|
|
benchmarkDrafts.delete(key)
|
|
} else {
|
|
benchmarkDrafts.set(key, {
|
|
workspace: input.workspace,
|
|
kind: input.kind,
|
|
path: input.path,
|
|
value,
|
|
createdAt
|
|
})
|
|
}
|
|
return { status: 'saved', current_timestamp: createdAt }
|
|
}
|
|
|
|
/** Mirror `DraftService.getDraftForUser`: 404-shaped throw when absent so the
|
|
* adapter's narrowed catch treats it as "no draft" instead of re-throwing. */
|
|
export function getBenchmarkDraftForUser(input: {
|
|
workspace: string
|
|
kind: UserDraftItemKind
|
|
path: string
|
|
}): GetDraftForUserResponse {
|
|
const entry = benchmarkDrafts.get(benchmarkDraftKey(input.workspace, input.kind, input.path))
|
|
if (!entry) {
|
|
throw Object.assign(new Error(`no draft for "${input.path}"`), {
|
|
status: 404
|
|
})
|
|
}
|
|
return { value: entry.value, created_at: entry.createdAt }
|
|
}
|
|
|
|
/** Mirror `DraftService.getOwnDraft`: `null` (200) when absent — unlike
|
|
* `getDraftForUser`, absence is not an error on this route. */
|
|
export function getBenchmarkOwnDraft(input: {
|
|
workspace: string
|
|
kind: UserDraftItemKind
|
|
path: string
|
|
}): GetOwnDraftResponse {
|
|
const entry = benchmarkDrafts.get(benchmarkDraftKey(input.workspace, input.kind, input.path))
|
|
if (!entry) {
|
|
return null
|
|
}
|
|
return { value: entry.value, created_at: entry.createdAt }
|
|
}
|
|
|
|
/** Whether a deployed benchmark item exists for a draft row's kind+path —
|
|
* drives `draft_only`, which production computes against the deployed tables. */
|
|
function benchmarkDeployedExists(
|
|
workspace: string,
|
|
kind: UserDraftItemKind,
|
|
path: string
|
|
): boolean {
|
|
if (kind === 'script') return Boolean(getBenchmarkScriptByPath(workspace, path))
|
|
if (kind === 'flow') return Boolean(getBenchmarkFlowByPath(workspace, path))
|
|
if (kind === 'app' || kind === 'raw_app') return Boolean(getBenchmarkAppByPath(workspace, path))
|
|
if (kind === 'variable') return Boolean(getBenchmarkVariableByPath(workspace, path))
|
|
// The remaining drawer kinds (resources/schedules/triggers) have no deployed
|
|
// benchmark stores today.
|
|
return false
|
|
}
|
|
|
|
/** Mirror `DraftService.listDrafts`: metadata rows (no value) for a workspace. */
|
|
export function listBenchmarkDrafts(workspace: string): ListDraftsResponse {
|
|
return [...benchmarkDrafts.values()]
|
|
.filter((entry) => entry.workspace === workspace)
|
|
.map((entry) => ({
|
|
kind: entry.kind,
|
|
path: entry.path,
|
|
summary: (entry.value as { summary?: string } | null)?.summary,
|
|
draft_only: !benchmarkDeployedExists(workspace, entry.kind, entry.path),
|
|
legacy_draft: false,
|
|
created_at: entry.createdAt
|
|
}))
|
|
}
|
|
|
|
// ============= Datatables (best-effort in-memory SQL) =============
|
|
|
|
/**
|
|
* Project the seeded datatables down to the `list_datatable_tables` response:
|
|
* `datatable_name` + `schema -> table_names`, with no column detail.
|
|
* Returns `null` for a non-benchmark workspace so callers can fall through to
|
|
* the real backend; an empty seed yields `[]`.
|
|
*/
|
|
export function listBenchmarkDatatables(workspace: string): DataTableTables[] | null {
|
|
const runnables = benchmarkWorkspaceRunnables.get(workspace)
|
|
if (!runnables) {
|
|
return null
|
|
}
|
|
return (runnables.datatables ?? []).map((datatable) => ({
|
|
datatable_name: datatable.datatable_name,
|
|
schemas: Object.fromEntries(
|
|
Object.entries(datatable.schemas).map(([schema, tables]) => [schema, Object.keys(tables)])
|
|
)
|
|
}))
|
|
}
|
|
|
|
// ============= DuckLake catalogs and declared metrics =============
|
|
|
|
/** Seeded DuckLake names, or `null` for a non-benchmark workspace. */
|
|
export function listBenchmarkDucklakes(workspace: string): string[] | null {
|
|
const runnables = benchmarkWorkspaceRunnables.get(workspace)
|
|
return runnables ? (runnables.ducklakes ?? []) : null
|
|
}
|
|
|
|
/**
|
|
* Seeded metric declarations, or `null` for a non-benchmark workspace.
|
|
*
|
|
* The `table` / `path_prefix` filters are ignored: which rows a filter selects is
|
|
* `canonical_table_path`'s business and is pinned by `ducklakeTools.test.ts`.
|
|
* Re-deriving it here would give the eval its own copy of that spec to drift from,
|
|
* and the case this serves measures whether the model reaches for the tool at all.
|
|
*/
|
|
export function listBenchmarkDataMetrics(workspace: string): DataMetric[] | null {
|
|
const runnables = benchmarkWorkspaceRunnables.get(workspace)
|
|
return runnables ? (runnables.dataMetrics ?? []) : null
|
|
}
|
|
|
|
export function getBenchmarkDatatableSchema(input: {
|
|
workspace: string
|
|
datatableName: string
|
|
schemaName: string
|
|
tableName: string
|
|
}): DataTableTableSchema {
|
|
const runnables = benchmarkWorkspaceRunnables.get(input.workspace)
|
|
const datatable = (runnables?.datatables ?? []).find(
|
|
(entry) => entry.datatable_name === input.datatableName
|
|
)
|
|
if (!datatable) {
|
|
// Message MUST match the production `isDatatableNotConfiguredError` regex
|
|
// (/datatable\s+\S+\s+not found/i in datatableTools.ts) so the
|
|
// get_datatable_table_schema not-configured mapping is actually exercised.
|
|
throw new Error(`datatable "${input.datatableName}" not found`)
|
|
}
|
|
const table = datatable.schemas?.[input.schemaName]?.[input.tableName]
|
|
if (!table) {
|
|
throw new Error(
|
|
`table "${input.schemaName}.${input.tableName}" not found in datatable "${input.datatableName}"`
|
|
)
|
|
}
|
|
return {
|
|
datatable_name: input.datatableName,
|
|
schema_name: input.schemaName,
|
|
table_name: input.tableName,
|
|
columns: table.columns
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute SQL against a seeded datatable through the best-effort in-memory engine
|
|
* (`applyDatatableSql`). Writes (CREATE/INSERT/UPDATE/DELETE/DROP) mutate the
|
|
* stored datatable in place so a later list/schema/SELECT reflects them; SELECT
|
|
* (and RETURNING) yield rows, other statements yield `[]`. Creates a benchmark
|
|
* completed job and returns its id, like `runBenchmarkScriptPreview`.
|
|
*/
|
|
export function runBenchmarkDatatableSql(input: {
|
|
workspace: string
|
|
datatableName: string
|
|
sql: string
|
|
}): string {
|
|
const runnables = benchmarkWorkspaceRunnables.get(input.workspace)
|
|
const datatable = (runnables?.datatables ?? []).find(
|
|
(entry) => entry.datatable_name === input.datatableName
|
|
)
|
|
const rows = datatable ? applyDatatableSql(datatable, input.sql).rows : []
|
|
return createBenchmarkCompletedJob({
|
|
workspace: input.workspace,
|
|
jobKind: 'preview',
|
|
success: true,
|
|
args: { database: `datatable://${input.datatableName}` },
|
|
result: rows
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Mirror `JobService.getCompletedJobResultMaybe` for benchmark workspaces — the
|
|
* shape `pollJobResult` consumes. The job is created synchronously before
|
|
* polling, so it is always present and completed.
|
|
*/
|
|
export function getBenchmarkCompletedJobResultMaybe(input: { workspace: string; id: string }): {
|
|
success: boolean
|
|
completed: boolean
|
|
result: unknown
|
|
} {
|
|
const job = getBenchmarkCompletedJob(input.workspace, input.id)
|
|
if (!job) {
|
|
throw new Error(`Job "${input.id}" not found in benchmark workspace`)
|
|
}
|
|
return { success: job.success, completed: true, result: job.result }
|
|
}
|
|
|
|
export function runBenchmarkScriptPreview(input: {
|
|
workspace: string
|
|
requestBody: {
|
|
content?: string
|
|
language?: ScriptLang | 'bunnative'
|
|
args?: Record<string, unknown>
|
|
path?: string
|
|
}
|
|
}): string {
|
|
const content = input.requestBody.content ?? ''
|
|
const language = input.requestBody.language ?? 'bun'
|
|
const lintResult = buildScriptLintResult(content, language)
|
|
const success = lintResult.errorCount === 0
|
|
|
|
return createBenchmarkCompletedJob({
|
|
workspace: input.workspace,
|
|
jobKind: 'preview',
|
|
success,
|
|
scriptPath: input.requestBody.path,
|
|
args: input.requestBody.args,
|
|
result: success
|
|
? {
|
|
path: input.requestBody.path,
|
|
args: input.requestBody.args ?? {},
|
|
validated: true
|
|
}
|
|
: {
|
|
path: input.requestBody.path,
|
|
args: input.requestBody.args ?? {},
|
|
errorCount: lintResult.errorCount,
|
|
errors: lintResult.errors.map((entry) => ({
|
|
line: entry.startLineNumber,
|
|
message: entry.message
|
|
}))
|
|
}
|
|
})
|
|
}
|
|
|
|
export function runBenchmarkScriptByPath(input: {
|
|
workspace: string
|
|
path: string
|
|
args?: Record<string, unknown>
|
|
}): string {
|
|
const script = getBenchmarkScriptByPath(input.workspace, input.path)
|
|
return createBenchmarkCompletedJob({
|
|
workspace: input.workspace,
|
|
jobKind: 'script',
|
|
success: script !== null,
|
|
scriptPath: input.path,
|
|
args: input.args,
|
|
result:
|
|
script !== null
|
|
? {
|
|
path: input.path,
|
|
args: input.args ?? {},
|
|
mocked: true
|
|
}
|
|
: {
|
|
error: `Script "${input.path}" not found in benchmark workspace`
|
|
},
|
|
logs:
|
|
script !== null
|
|
? 'Mock benchmark script run completed successfully.'
|
|
: `Script "${input.path}" not found in benchmark workspace.`
|
|
})
|
|
}
|
|
|
|
export function runBenchmarkFlowByPath(input: {
|
|
workspace: string
|
|
path: string
|
|
args?: Record<string, unknown>
|
|
}): string {
|
|
const flow = getBenchmarkFlowByPath(input.workspace, input.path)
|
|
return createBenchmarkCompletedJob({
|
|
workspace: input.workspace,
|
|
jobKind: 'flowpreview',
|
|
success: flow !== null,
|
|
args: input.args,
|
|
result:
|
|
flow !== null
|
|
? {
|
|
path: input.path,
|
|
args: input.args ?? {},
|
|
mocked: true
|
|
}
|
|
: {
|
|
error: `Flow "${input.path}" not found in benchmark workspace`
|
|
},
|
|
logs:
|
|
flow !== null
|
|
? 'Mock benchmark flow run completed successfully.'
|
|
: `Flow "${input.path}" not found in benchmark workspace.`
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Mirror `JobService.runFlowPreview` for benchmark workspaces, including the server's
|
|
* refusal of a chat-enabled flow run that names no conversation (`memory_id`).
|
|
*/
|
|
export function runBenchmarkFlowPreview(input: {
|
|
workspace: string
|
|
memoryId?: string
|
|
requestBody?: { path?: string; value?: { chat_input_enabled?: boolean }; args?: unknown }
|
|
}): string {
|
|
if (input.requestBody?.value?.chat_input_enabled && !input.memoryId) {
|
|
throw new Error('Bad request: memory_id is required for chat-enabled flows')
|
|
}
|
|
const args = (input.requestBody?.args ?? {}) as Record<string, unknown>
|
|
return createBenchmarkCompletedJob({
|
|
workspace: input.workspace,
|
|
jobKind: 'flowpreview',
|
|
success: true,
|
|
args,
|
|
result: { path: input.requestBody?.path, args, mocked: true },
|
|
logs: 'Mock benchmark flow preview completed successfully.'
|
|
})
|
|
}
|
|
|
|
export function previewBenchmarkSchedule(input: {
|
|
requestBody?: Record<string, unknown>
|
|
}): Record<string, unknown> {
|
|
const schedule = input.requestBody?.schedule
|
|
if (typeof schedule !== 'string' || schedule.trim().split(/\s+/).length !== 6) {
|
|
throw new Error(
|
|
`schedule must use a six-field cron expression, got ${JSON.stringify(schedule)}`
|
|
)
|
|
}
|
|
|
|
return {
|
|
next_runs: ['1970-01-02T00:00:00.000Z']
|
|
}
|
|
}
|
|
|
|
export function createBenchmarkSchedule(input: {
|
|
workspace: string
|
|
requestBody: Record<string, unknown>
|
|
}): Record<string, unknown> {
|
|
assertBenchmarkWorkspacePath('schedule', input.requestBody.path)
|
|
assertBenchmarkWorkspacePath('target', input.requestBody.script_path)
|
|
return {
|
|
path: input.requestBody.path,
|
|
target_path: input.requestBody.script_path,
|
|
is_flow: input.requestBody.is_flow,
|
|
mocked: true
|
|
}
|
|
}
|
|
|
|
export function createBenchmarkHttpTrigger(input: {
|
|
workspace: string
|
|
requestBody: Record<string, unknown>
|
|
}): Record<string, unknown> {
|
|
assertBenchmarkWorkspacePath('trigger', input.requestBody.path)
|
|
assertBenchmarkWorkspacePath('target', input.requestBody.script_path)
|
|
if (
|
|
typeof input.requestBody.route_path === 'string' &&
|
|
input.requestBody.route_path.startsWith('/')
|
|
) {
|
|
throw new Error(
|
|
`HTTP trigger route_path must not start with /, got "${input.requestBody.route_path}"`
|
|
)
|
|
}
|
|
return {
|
|
path: input.requestBody.path,
|
|
target_path: input.requestBody.script_path,
|
|
route_path: input.requestBody.route_path,
|
|
is_flow: input.requestBody.is_flow,
|
|
mocked: true
|
|
}
|
|
}
|
|
|
|
function assertBenchmarkWorkspacePath(label: string, value: unknown): void {
|
|
if (typeof value !== 'string' || (!value.startsWith('f/') && !value.startsWith('u/'))) {
|
|
throw new Error(`${label} path must start with f/ or u/, got ${JSON.stringify(value)}`)
|
|
}
|
|
}
|
|
|
|
function buildBenchmarkScriptHash(path: string): string {
|
|
return `benchmark:${path}`
|
|
}
|
|
|
|
function buildBenchmarkScript(script: BenchmarkWorkspaceScript): Script {
|
|
return {
|
|
workspace_id: 'benchmark',
|
|
hash: buildBenchmarkScriptHash(script.path),
|
|
path: script.path,
|
|
parent_hashes: [],
|
|
summary: script.summary,
|
|
description: script.description ?? '',
|
|
content: script.content,
|
|
created_by: 'benchmark',
|
|
created_at: BENCHMARK_TIMESTAMP,
|
|
archived: false,
|
|
schema: script.schema ?? {},
|
|
deleted: false,
|
|
is_template: false,
|
|
extra_perms: {},
|
|
language: script.language,
|
|
kind: 'script',
|
|
starred: false,
|
|
has_preprocessor: false,
|
|
modules: null
|
|
}
|
|
}
|
|
|
|
function buildBenchmarkFlow(flow: BenchmarkWorkspaceFlow): Flow {
|
|
return {
|
|
path: flow.path,
|
|
summary: flow.summary,
|
|
description: flow.description ?? '',
|
|
value: flow.value,
|
|
schema: flow.schema ?? {},
|
|
edited_by: 'benchmark',
|
|
edited_at: BENCHMARK_TIMESTAMP,
|
|
archived: false,
|
|
extra_perms: {}
|
|
} as Flow
|
|
}
|
|
|
|
function buildBenchmarkListableApp(app: BenchmarkWorkspaceApp): ListableApp {
|
|
return {
|
|
id: 0,
|
|
workspace_id: 'benchmark',
|
|
path: app.path,
|
|
summary: app.summary,
|
|
version: 1,
|
|
extra_perms: {},
|
|
edited_at: BENCHMARK_TIMESTAMP,
|
|
execution_mode: 'viewer',
|
|
raw_app: app.rawApp ?? true
|
|
}
|
|
}
|
|
|
|
function buildBenchmarkApp(app: BenchmarkWorkspaceApp): AppWithLastVersion {
|
|
return {
|
|
id: 0,
|
|
workspace_id: 'benchmark',
|
|
path: app.path,
|
|
summary: app.summary,
|
|
versions: [1],
|
|
created_by: 'benchmark',
|
|
created_at: BENCHMARK_TIMESTAMP,
|
|
value: app.value,
|
|
policy: (app.value.policy ?? {}) as AppWithLastVersion['policy'],
|
|
execution_mode: 'viewer',
|
|
extra_perms: {},
|
|
custom_path: app.value.custom_path as string | undefined,
|
|
raw_app: app.rawApp ?? true
|
|
}
|
|
}
|
|
|
|
/** A stand-in Windmill hub. `search_hub_scripts` and a `hub/` read go out over
|
|
* relative `/api/...` fetches, which have no origin here, so without these the
|
|
* hub tools throw and no case can exercise hub reuse. Serving fixtures rather
|
|
* than the live hub also keeps assertions on script content stable as the real
|
|
* hub republishes new versions. */
|
|
const BENCHMARK_INTEGRATION_META_PATH = /^\/api\/integrations\/hub\/([^/]+)\/meta$/
|
|
|
|
/** One integration lifted verbatim from the content repo — its shipped scripts, its
|
|
* authored meta.json and its resource type. Hand-written fixtures make both routes to
|
|
* an integration's conventions look equally cheap; a real one is the only way to tell
|
|
* whether reading the metadata beats reading a script. */
|
|
const REAL_HUB_INTEGRATIONS = [
|
|
confluenceFixture,
|
|
servicenowFixture,
|
|
outreachFixture
|
|
] as unknown as Array<{
|
|
app: string
|
|
display_name: string
|
|
description: string
|
|
docs_url: string
|
|
curated: boolean | null
|
|
meta: unknown
|
|
resource_type: { name: string; description: string; schema: unknown }
|
|
scripts: Array<{
|
|
version_id: number
|
|
app: string
|
|
summary: string
|
|
description: string
|
|
terms: string
|
|
kind: string
|
|
language: string
|
|
content: string
|
|
schema: unknown
|
|
}>
|
|
}>
|
|
|
|
const BENCHMARK_HUB_SCRIPTS = [
|
|
...REAL_HUB_INTEGRATIONS.flatMap((integration) => integration.scripts),
|
|
{
|
|
version_id: 22235,
|
|
app: 'holded',
|
|
summary: 'Send Document',
|
|
terms: 'holded invoice document send email mail',
|
|
language: 'bun',
|
|
content: `//native
|
|
type Holded = {
|
|
apiKey: string;
|
|
};
|
|
/**
|
|
* Send Document
|
|
* Send a specific document by email.
|
|
*/
|
|
export async function main(
|
|
auth: Holded,
|
|
docType: string,
|
|
documentId: string,
|
|
body: {
|
|
mailTemplateId?: string;
|
|
emails: string;
|
|
subject?: string;
|
|
message?: string;
|
|
docIds?: string;
|
|
},
|
|
) {
|
|
const url = new URL(
|
|
\`https://api.holded.com/api/invoicing/v1/documents/\${docType}/\${documentId}/send\`,
|
|
);
|
|
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
key: auth.apiKey,
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!response.ok) {
|
|
const text = await response.text();
|
|
throw new Error(\`\${response.status} \${text}\`);
|
|
}
|
|
return await response.json();
|
|
}
|
|
`,
|
|
schema: {
|
|
type: 'object',
|
|
required: ['auth', 'docType', 'documentId', 'body'],
|
|
properties: {
|
|
auth: { type: 'object', format: 'resource-holded' },
|
|
docType: { type: 'string' },
|
|
documentId: { type: 'string' },
|
|
body: { type: 'object' }
|
|
}
|
|
}
|
|
},
|
|
{
|
|
version_id: 28294,
|
|
app: 'discord',
|
|
summary: 'Send a message to Discord using Webhook',
|
|
terms: 'discord webhook message send chat channel',
|
|
language: 'bunnative',
|
|
content: `//native
|
|
|
|
type DiscordWebhook = {
|
|
webhook_url: string;
|
|
};
|
|
export async function main(discord_webhook: DiscordWebhook, message: string) {
|
|
const response = await fetch(\`\${discord_webhook.webhook_url}?wait=true\`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ content: message }),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(\`\${response.status} \${await response.text()}\`);
|
|
}
|
|
return await response.json();
|
|
}
|
|
`,
|
|
schema: {
|
|
type: 'object',
|
|
required: ['discord_webhook', 'message'],
|
|
properties: {
|
|
discord_webhook: { type: 'object', format: 'resource-discord_webhook' },
|
|
message: { type: 'string' }
|
|
}
|
|
}
|
|
},
|
|
// Baremetrics carries no annotation script, on purpose: it stands for an
|
|
// integration whose conventions (an `apiKey` resource, bearer auth, the /v1
|
|
// base) are only learnable by reading a script that does something else.
|
|
{
|
|
version_id: 8995,
|
|
app: 'baremetrics',
|
|
summary: 'List Sources',
|
|
description: 'List all the sources attached to a Baremetrics account.',
|
|
terms: 'baremetrics sources list revenue metrics',
|
|
language: 'bunnative',
|
|
content: `//native
|
|
type Baremetrics = {
|
|
apiKey: string;
|
|
};
|
|
export async function main(resource: Baremetrics) {
|
|
const response = await fetch("https://api.baremetrics.com/v1/sources", {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
Authorization: \`Bearer \${resource.apiKey}\`,
|
|
},
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(\`\${response.status} \${await response.text()}\`);
|
|
}
|
|
return await response.json();
|
|
}
|
|
`,
|
|
schema: {
|
|
type: 'object',
|
|
required: ['resource'],
|
|
properties: {
|
|
resource: { type: 'object', format: 'resource-baremetrics' }
|
|
}
|
|
}
|
|
},
|
|
{
|
|
version_id: 8996,
|
|
app: 'baremetrics',
|
|
summary: 'Create Customer',
|
|
description: 'Create a customer on a Baremetrics source.',
|
|
terms: 'baremetrics customer create source',
|
|
language: 'bunnative',
|
|
content: `//native
|
|
type Baremetrics = {
|
|
apiKey: string;
|
|
};
|
|
export async function main(
|
|
resource: Baremetrics,
|
|
sourceId: string,
|
|
body: { oid: string; name?: string; email?: string },
|
|
) {
|
|
const response = await fetch(
|
|
\`https://api.baremetrics.com/v1/\${sourceId}/customers\`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
Authorization: \`Bearer \${resource.apiKey}\`,
|
|
},
|
|
body: JSON.stringify(body),
|
|
},
|
|
);
|
|
if (!response.ok) {
|
|
throw new Error(\`\${response.status} \${await response.text()}\`);
|
|
}
|
|
return await response.json();
|
|
}
|
|
`,
|
|
schema: {
|
|
type: 'object',
|
|
required: ['resource', 'sourceId', 'body'],
|
|
properties: {
|
|
resource: { type: 'object', format: 'resource-baremetrics' },
|
|
sourceId: { type: 'string' },
|
|
body: { type: 'object' }
|
|
}
|
|
}
|
|
}
|
|
]
|
|
|
|
/** Naive whole-word overlap — enough to rank a handful of fixtures for a natural
|
|
* query without pulling an embedding model into the benchmark. Every frontend eval
|
|
* shares this handler, so the bar to match is deliberately high: naming the
|
|
* integration, or overlapping on three meaningful words. A looser bar answers
|
|
* "send a Slack message" with the Discord fixture, handing an unrelated case a
|
|
* plausible-looking wrong integration. */
|
|
function searchBenchmarkHubScripts(text: string, app: string | null, kind: string | null) {
|
|
const tokens = new Set(
|
|
text
|
|
.toLowerCase()
|
|
.split(/[^a-z0-9]+/)
|
|
.filter((token) => token.length > 2)
|
|
)
|
|
// The real endpoint filters by app and kind before ranking, so honour both here —
|
|
// otherwise a narrowed search silently returns other integrations' scripts, and a
|
|
// trigger answers a search production would never have shown it to.
|
|
return BENCHMARK_HUB_SCRIPTS.filter(
|
|
(script) => (!app || script.app === app) && (!kind || (script.kind ?? 'script') === kind)
|
|
)
|
|
.map((script) => {
|
|
const words = new Set(
|
|
`${script.app} ${script.summary} ${script.terms}`.toLowerCase().split(/[^a-z0-9]+/)
|
|
)
|
|
const score = [...tokens].filter((token) => words.has(token)).length
|
|
return { script, score, namesApp: tokens.has(script.app) }
|
|
})
|
|
.filter((entry) => entry.namesApp || entry.score >= 3)
|
|
.sort((a, b) => b.score - a.score)
|
|
.map(({ script }, index) => ({
|
|
ask_id: script.version_id,
|
|
id: script.version_id,
|
|
version_id: script.version_id,
|
|
summary: script.summary,
|
|
description: script.description ?? null,
|
|
app: script.app,
|
|
kind: script.kind ?? 'script',
|
|
score: 1 - index * 0.01
|
|
}))
|
|
}
|
|
|
|
/** Listing an integration is unranked and description-bearing, matching the hub's
|
|
* top-scripts endpoint — that asymmetry with the semantic search is the whole
|
|
* reason the chat browses by app when no script matches the task. */
|
|
function listBenchmarkHubScriptsByApp(app: string | null, kind: string | null) {
|
|
return BENCHMARK_HUB_SCRIPTS.filter(
|
|
(script) => (!app || script.app === app) && (!kind || (script.kind ?? 'script') === kind)
|
|
).map((script) => ({
|
|
id: script.version_id,
|
|
ask_id: script.version_id,
|
|
version_id: script.version_id,
|
|
summary: script.summary,
|
|
description: script.description ?? null,
|
|
app: script.app,
|
|
kind: script.kind ?? 'script',
|
|
views: 0,
|
|
votes: 0
|
|
}))
|
|
}
|
|
|
|
/** What `/integrations/hub/<app>/meta` serves. Carries the integration's conventions
|
|
* and none of the endpoints a case asks the model to write, so the tool shortens the
|
|
* path to that knowledge without supplying answers. */
|
|
/** The integrations a case writes against, none of which the content repo documents —
|
|
* the same majority shape as the live hub, where 18 of ~216 carry a meta.json. Their
|
|
* conventions live in their shipped scripts, which is what these cases exercise. */
|
|
const BENCHMARK_HUB_INTEGRATION_META: Record<
|
|
string,
|
|
{
|
|
display_name: string
|
|
description: string
|
|
docs_url: string
|
|
curated: boolean | null
|
|
meta: unknown
|
|
}
|
|
> = {
|
|
baremetrics: {
|
|
display_name: 'Baremetrics',
|
|
description: 'Subscription analytics for recurring-revenue businesses.',
|
|
docs_url: 'https://developers.baremetrics.com/reference',
|
|
curated: null,
|
|
meta: null
|
|
},
|
|
holded: {
|
|
display_name: 'Holded',
|
|
description: 'Invoicing, accounting and CRM for small businesses.',
|
|
docs_url: 'https://developers.holded.com/reference',
|
|
curated: null,
|
|
meta: null
|
|
}
|
|
}
|
|
|
|
/** Mirrors the hub's own derivation: hosts seen in the shipped scripts, whether they
|
|
* call the provider directly, and how many there are of each kind. */
|
|
function benchmarkDerivedFacts(app: string) {
|
|
const scripts = BENCHMARK_HUB_SCRIPTS.filter((script) => script.app === app)
|
|
const hosts = new Map<string, number>()
|
|
const languages: Record<string, number> = {}
|
|
for (const script of scripts) {
|
|
languages[script.language] = (languages[script.language] ?? 0) + 1
|
|
for (const match of script.content.matchAll(/https?:\/\/([a-zA-Z0-9._-]+)/g)) {
|
|
hosts.set(match[1], (hosts.get(match[1]) ?? 0) + 1)
|
|
}
|
|
}
|
|
return {
|
|
api_hosts: [...hosts.entries()]
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([host, count]) => ({ host, count })),
|
|
style: 'fetch',
|
|
languages,
|
|
script_counts: {
|
|
total: scripts.length,
|
|
by_kind: scripts.reduce<Record<string, number>>((acc, script) => {
|
|
const kind = script.kind ?? 'script'
|
|
acc[kind] = (acc[kind] ?? 0) + 1
|
|
return acc
|
|
}, {})
|
|
},
|
|
top_scripts: scripts.map((script) => ({
|
|
path: `hub/${script.version_id}/${script.app}/${script.summary.toLowerCase().replaceAll(/\s+/g, '_')}`,
|
|
ask_id: script.version_id,
|
|
version_id: script.version_id,
|
|
summary: script.summary,
|
|
description: script.description ?? null,
|
|
kind: script.kind ?? 'script',
|
|
language: script.language,
|
|
views: 0,
|
|
votes: 0
|
|
}))
|
|
}
|
|
}
|
|
|
|
/** The hub keys a script by its version id; the app and slug segments that
|
|
* follow are descriptive, so match on the id exactly as the real hub does. */
|
|
function getBenchmarkHubScript(path: string) {
|
|
const versionId = Number(path.replace(/^\/api\/scripts\/hub\/get_full\/hub\//, '').split('/')[0])
|
|
return BENCHMARK_HUB_SCRIPTS.find((script) => script.version_id === versionId)
|
|
}
|
|
|
|
const BENCHMARK_WORKERS = [
|
|
{
|
|
worker: 'wk-benchmark-1',
|
|
worker_instance: 'benchmark-host',
|
|
last_ping: 2,
|
|
started_at: BENCHMARK_TIMESTAMP,
|
|
jobs_executed: 42,
|
|
custom_tags: null,
|
|
worker_group: 'default',
|
|
wm_version: 'benchmark'
|
|
},
|
|
{
|
|
worker: 'wk-benchmark-2',
|
|
worker_instance: 'benchmark-host',
|
|
last_ping: 5,
|
|
started_at: BENCHMARK_TIMESTAMP,
|
|
jobs_executed: 17,
|
|
custom_tags: null,
|
|
worker_group: 'default',
|
|
wm_version: 'benchmark'
|
|
}
|
|
]
|
|
|
|
/** True when `handleBenchmarkApiFetch` has an answer for this `/api/...` url.
|
|
* Any other relative fetch must keep its normal (non-benchmark) behavior —
|
|
* intercepting it with a synthetic 404 sends the model into retry loops. */
|
|
// Not anchored: the frontend builds this URL from location.origin, so it arrives absolute. The
|
|
// workspace id is greedy because an eval workspace is a temp directory path, slashes and all.
|
|
const BENCHMARK_AI_MODELS_PATH = /\/api\/w\/(.+)\/ai\/proxy\/models$/
|
|
|
|
export function hasBenchmarkApiHandler(url: string): boolean {
|
|
const path = url.split('?')[0]
|
|
return (
|
|
path === '/api/workers/list' ||
|
|
path === '/api/embeddings/query_hub_scripts' ||
|
|
path === '/api/scripts/hub/top' ||
|
|
path === '/api/integrations/hub/list' ||
|
|
BENCHMARK_INTEGRATION_META_PATH.test(path) ||
|
|
path.startsWith('/api/scripts/hub/get_full/') ||
|
|
BENCHMARK_AI_MODELS_PATH.test(path)
|
|
)
|
|
}
|
|
|
|
/** Answer the relative `/api/...` fetches no mocked service covers. */
|
|
export function handleBenchmarkApiFetch(url: string, init?: RequestInit): Response {
|
|
const path = url.split('?')[0]
|
|
if (path === '/api/workers/list') {
|
|
return Response.json(BENCHMARK_WORKERS)
|
|
}
|
|
// The provider's own model listing, which grounds an AI agent step's model id. Keyed by the
|
|
// resource the caller names, so two seeded providers can serve different models.
|
|
const aiModels = BENCHMARK_AI_MODELS_PATH.exec(path)
|
|
if (aiModels) {
|
|
const headers = new Headers(init?.headers)
|
|
const resourcePath = headers.get('X-Resource-Path') ?? ''
|
|
const seed = benchmarkWorkspaceRunnables
|
|
.get(decodeURIComponent(aiModels[1]))
|
|
?.aiProviders?.find((entry) => entry.path === resourcePath)
|
|
return Response.json({ data: (seed?.models ?? []).map((id) => ({ id })) })
|
|
}
|
|
if (path === '/api/embeddings/query_hub_scripts') {
|
|
const params = new URLSearchParams(url.split('?')[1] ?? '')
|
|
return Response.json(
|
|
searchBenchmarkHubScripts(params.get('text') ?? '', params.get('app'), params.get('kind'))
|
|
)
|
|
}
|
|
if (path === '/api/scripts/hub/top') {
|
|
const params = new URLSearchParams(url.split('?')[1] ?? '')
|
|
return Response.json({
|
|
asks: listBenchmarkHubScriptsByApp(params.get('app'), params.get('kind'))
|
|
})
|
|
}
|
|
const integrationMeta = BENCHMARK_INTEGRATION_META_PATH.exec(path)
|
|
if (integrationMeta) {
|
|
const app = decodeURIComponent(integrationMeta[1])
|
|
const real = REAL_HUB_INTEGRATIONS.find((integration) => integration.app === app)
|
|
if (real) {
|
|
return Response.json({
|
|
app,
|
|
display_name: real.display_name,
|
|
description: real.description,
|
|
docs_url: real.docs_url,
|
|
curated: real.curated,
|
|
metadata_source: real.meta ? 'curated' : 'derived',
|
|
meta: real.meta,
|
|
meta_updated_at: null,
|
|
derived: benchmarkDerivedFacts(app),
|
|
resource_types: [{ id: 1, ...real.resource_type }]
|
|
})
|
|
}
|
|
const entry = BENCHMARK_HUB_INTEGRATION_META[app]
|
|
if (!entry) {
|
|
return Response.json({ error: 'integration not found' }, { status: 404 })
|
|
}
|
|
return Response.json({
|
|
app,
|
|
display_name: entry.display_name,
|
|
description: entry.description,
|
|
docs_url: entry.docs_url,
|
|
curated: entry.curated,
|
|
metadata_source: entry.meta ? 'curated' : 'derived',
|
|
meta: entry.meta,
|
|
meta_updated_at: null,
|
|
derived: benchmarkDerivedFacts(app),
|
|
resource_types: [
|
|
{
|
|
id: 1,
|
|
name: app,
|
|
description: `${entry.display_name} credentials`,
|
|
schema: {
|
|
type: 'object',
|
|
required: ['apiKey'],
|
|
properties: {
|
|
apiKey: {
|
|
type: 'string',
|
|
description: `${entry.display_name} API key`
|
|
}
|
|
}
|
|
}
|
|
}
|
|
]
|
|
})
|
|
}
|
|
if (path === '/api/integrations/hub/list') {
|
|
const apps = [...new Set(BENCHMARK_HUB_SCRIPTS.map((script) => script.app))].sort()
|
|
// Read off both fixtures the metadata endpoint serves from, or the list would
|
|
// call an integration undocumented and then hand back its authored notes.
|
|
const documented = new Set([
|
|
...REAL_HUB_INTEGRATIONS.filter((integration) => integration.meta).map(
|
|
(integration) => integration.app
|
|
),
|
|
...Object.entries(BENCHMARK_HUB_INTEGRATION_META)
|
|
.filter(([, entry]) => entry.meta)
|
|
.map(([app]) => app)
|
|
])
|
|
return Response.json(apps.map((name) => ({ name, documented: documented.has(name) })))
|
|
}
|
|
if (path.startsWith('/api/scripts/hub/get_full/')) {
|
|
const script = getBenchmarkHubScript(path)
|
|
if (!script) {
|
|
return Response.json({ error: 'hub script not found' }, { status: 404 })
|
|
}
|
|
return Response.json({
|
|
content: script.content,
|
|
language: script.language,
|
|
schema: script.schema,
|
|
summary: script.summary
|
|
})
|
|
}
|
|
return Response.json({ error: `no benchmark handler for ${path}` }, { status: 404 })
|
|
}
|