Compare commits
7 Commits
ai-chat-su
...
fix/nsjail
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ad2d3e59d | ||
|
|
a3f24aeff8 | ||
|
|
f1e84cb088 | ||
|
|
3aa279cfd7 | ||
|
|
5c179e5448 | ||
|
|
12d0a3de08 | ||
|
|
a98f5b9dfd |
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## [1.684.1](https://github.com/windmill-labs/windmill/compare/v1.684.0...v1.684.1) (2026-04-14)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* stop escalating missing email recipients to critical alert ([#8833](https://github.com/windmill-labs/windmill/issues/8833)) ([6158ff2](https://github.com/windmill-labs/windmill/commit/6158ff2ebe29d6a9a7ff4d524e152bb2f7c24dfc))
|
||||
|
||||
## [1.684.0](https://github.com/windmill-labs/windmill/compare/v1.683.2...v1.684.0) (2026-04-14)
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ bun run cli -- run flow flow-test4-order-processing-loop --model opus
|
||||
bun run cli -- run flow flow-test0-sum-two-numbers --models haiku,opus,4o
|
||||
bun run cli -- run flow flow-test0-sum-two-numbers --runs 3 --verbose
|
||||
bun run cli -- run flow --record
|
||||
WMILL_AI_EVAL_BACKEND_URL=http://127.0.0.1:8000 bun run cli -- run flow --backend-validation preview
|
||||
bun run cli -- run cli bun-hello-script
|
||||
```
|
||||
|
||||
@@ -72,6 +73,7 @@ Public CLI surface:
|
||||
- `--models <a,b,c>`: run the same cases sequentially against several model aliases
|
||||
- `--verbose`: stream assistant output for frontend runs
|
||||
- `--record`: append a compact tracked summary line to `ai_evals/history/<mode>.jsonl` for full-suite runs only
|
||||
- `--backend-validation <mode>`: optional backend smoke validation (`off` or `preview`) for `script` and `flow` evals
|
||||
|
||||
## Models
|
||||
|
||||
@@ -114,6 +116,7 @@ Optional fields:
|
||||
- `initial`: starting state fixture
|
||||
- `expected`: expected artifact fixture
|
||||
- `validate`: extra deterministic validation rules
|
||||
- `runtime.backendPreview`: optional real backend preview config for smoke validation
|
||||
|
||||
For `flow` mode, `validate` can express requirements such as:
|
||||
|
||||
@@ -125,6 +128,23 @@ For `flow` mode, an `initial` fixture can also include a benchmark workspace cat
|
||||
existing scripts and flows. That lets the real `search_workspace` and
|
||||
`get_runnable_details` tools discover reusable workspace runnables during evals.
|
||||
|
||||
If `--backend-validation preview` is enabled:
|
||||
|
||||
- `script` evals run a real backend script preview in an isolated temp workspace
|
||||
- `flow` evals run a real backend flow preview only for cases that define `runtime.backendPreview`
|
||||
- `flow` cases with `initial.workspace` fixtures seed those scripts and flows into the preview workspace before preview
|
||||
- when `WMILL_AI_EVAL_BACKEND_WORKSPACE` is set, `ai_evals` treats that workspace as a dedicated test workspace, clears managed eval assets under `f/evals/*` before each preview run, and then reseeds the current case fixtures
|
||||
|
||||
Supported backend validation env vars:
|
||||
|
||||
- `WMILL_AI_EVAL_BACKEND_VALIDATION=preview`
|
||||
- `WMILL_AI_EVAL_BACKEND_URL=http://127.0.0.1:8000`
|
||||
- `WMILL_AI_EVAL_BACKEND_EMAIL=admin@windmill.dev`
|
||||
- `WMILL_AI_EVAL_BACKEND_PASSWORD=changeme`
|
||||
- `WMILL_AI_EVAL_BACKEND_WORKSPACE=integration-tests` to reuse an existing workspace on CE installs with low workspace limits
|
||||
- `WMILL_AI_EVAL_KEEP_WORKSPACES=1`
|
||||
- `WMILL_AI_EVAL_WORKSPACE_PREFIX=ai-evals`
|
||||
|
||||
## Results And Artifacts
|
||||
|
||||
Every run writes:
|
||||
@@ -158,6 +178,7 @@ Typical artifacts by mode:
|
||||
- `script`: `script.json` plus the generated script file
|
||||
- `app`: `app.json` plus frontend/backend files
|
||||
- `cli`: `assistant-output.txt` plus generated workspace files
|
||||
- backend-validated attempts also include `backend-preview.json`
|
||||
|
||||
## Layout
|
||||
|
||||
|
||||
246
ai_evals/adapters/frontend/backendPreview.test.ts
Normal file
246
ai_evals/adapters/frontend/backendPreview.test.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
import { afterEach, describe, expect, it } from 'bun:test'
|
||||
import type { BackendValidationSettings } from '../../core/backendValidation'
|
||||
import { BackendPreviewClient } from './backendPreview'
|
||||
|
||||
const ORIGINAL_FETCH = globalThis.fetch
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = ORIGINAL_FETCH
|
||||
})
|
||||
|
||||
describe('BackendPreviewClient', () => {
|
||||
it('updates an existing seeded script on path conflict and waits for deployment', async () => {
|
||||
const requests: Array<{ url: string; init?: RequestInit }> = []
|
||||
globalThis.fetch = mockFetch(
|
||||
requests,
|
||||
textResponse(200, 'token'),
|
||||
textResponse(200, ''),
|
||||
textResponse(400, 'Path conflict for f/evals/add_two_numbers with non-archived hash 123'),
|
||||
jsonResponse(200, { hash: '123' }),
|
||||
textResponse(200, '456'),
|
||||
jsonResponse(200, { lock: 'script.lock', lock_error_logs: null })
|
||||
)
|
||||
|
||||
const client = new BackendPreviewClient(
|
||||
buildSettings({ baseUrl: 'http://backend.test/script-upsert' })
|
||||
)
|
||||
|
||||
await client.createScript({
|
||||
workspaceId: 'test',
|
||||
path: 'f/evals/add_two_numbers',
|
||||
summary: 'Add two numbers',
|
||||
content: 'export async function main(a: number, b: number) { return a + b }',
|
||||
language: 'bun'
|
||||
})
|
||||
|
||||
expect(requests.map((entry) => entry.url)).toEqual([
|
||||
'http://backend.test/script-upsert/api/auth/login',
|
||||
'http://backend.test/script-upsert/api/w/test/folders/create',
|
||||
'http://backend.test/script-upsert/api/w/test/scripts/create',
|
||||
'http://backend.test/script-upsert/api/w/test/scripts/get/p/f/evals/add_two_numbers',
|
||||
'http://backend.test/script-upsert/api/w/test/scripts/create',
|
||||
'http://backend.test/script-upsert/api/w/test/scripts/deployment_status/h/456'
|
||||
])
|
||||
|
||||
const updateRequest = requests[4]
|
||||
expect(updateRequest.init?.method).toBe('POST')
|
||||
expect(JSON.parse(String(updateRequest.init?.body))).toMatchObject({
|
||||
path: 'f/evals/add_two_numbers',
|
||||
parent_hash: '123',
|
||||
language: 'bun'
|
||||
})
|
||||
})
|
||||
|
||||
it('updates an existing seeded flow on create conflict', async () => {
|
||||
const requests: Array<{ url: string; init?: RequestInit }> = []
|
||||
globalThis.fetch = mockFetch(
|
||||
requests,
|
||||
textResponse(200, 'token'),
|
||||
textResponse(200, ''),
|
||||
textResponse(400, 'Flow f/evals/add_numbers_flow already exists'),
|
||||
textResponse(200, '')
|
||||
)
|
||||
|
||||
const client = new BackendPreviewClient(
|
||||
buildSettings({ baseUrl: 'http://backend.test/flow-upsert' })
|
||||
)
|
||||
|
||||
await client.createFlow({
|
||||
workspaceId: 'test',
|
||||
path: 'f/evals/add_numbers_flow',
|
||||
summary: 'Add numbers',
|
||||
value: { modules: [] }
|
||||
})
|
||||
|
||||
expect(requests.map((entry) => entry.url)).toEqual([
|
||||
'http://backend.test/flow-upsert/api/auth/login',
|
||||
'http://backend.test/flow-upsert/api/w/test/folders/create',
|
||||
'http://backend.test/flow-upsert/api/w/test/flows/create',
|
||||
'http://backend.test/flow-upsert/api/w/test/flows/update/f/evals/add_numbers_flow'
|
||||
])
|
||||
|
||||
const updateRequest = requests[3]
|
||||
expect(updateRequest.init?.method).toBe('POST')
|
||||
expect(JSON.parse(String(updateRequest.init?.body))).toMatchObject({
|
||||
path: 'f/evals/add_numbers_flow',
|
||||
value: { modules: [] }
|
||||
})
|
||||
})
|
||||
|
||||
it('serializes shared-workspace validations inside the overridden workspace', async () => {
|
||||
globalThis.fetch = async (input) => {
|
||||
const url = String(input)
|
||||
if (url.endsWith('/api/auth/login')) {
|
||||
return textResponse(200, 'token')
|
||||
}
|
||||
if (url.endsWith('/api/workspaces/exists')) {
|
||||
return textResponse(200, 'true')
|
||||
}
|
||||
if (url.endsWith('/api/w/shared-preview/flows/list_paths')) {
|
||||
return jsonResponse(200, [])
|
||||
}
|
||||
if (url.endsWith('/api/w/shared-preview/scripts/list_paths')) {
|
||||
return jsonResponse(200, [])
|
||||
}
|
||||
throw new Error(`Unexpected fetch: ${url}`)
|
||||
}
|
||||
|
||||
const client = new BackendPreviewClient(
|
||||
buildSettings({
|
||||
baseUrl: 'http://backend.test/shared-lock',
|
||||
workspaceOverride: 'shared-preview'
|
||||
})
|
||||
)
|
||||
|
||||
const order: string[] = []
|
||||
let releaseFirst: (() => void) | undefined
|
||||
let notifyFirstStart: (() => void) | undefined
|
||||
const firstStarted = new Promise<void>((resolve) => {
|
||||
notifyFirstStart = resolve
|
||||
})
|
||||
|
||||
const first = client.withWorkspace('flow-test1', 1, async () => {
|
||||
order.push('first:start')
|
||||
notifyFirstStart?.()
|
||||
await new Promise<void>((resolve) => {
|
||||
releaseFirst = resolve
|
||||
})
|
||||
order.push('first:end')
|
||||
})
|
||||
|
||||
const second = client.withWorkspace('flow-test2', 1, async () => {
|
||||
order.push('second:start')
|
||||
order.push('second:end')
|
||||
})
|
||||
|
||||
await firstStarted
|
||||
expect(order).toEqual(['first:start'])
|
||||
|
||||
releaseFirst?.()
|
||||
await Promise.all([first, second])
|
||||
|
||||
expect(order).toEqual(['first:start', 'first:end', 'second:start', 'second:end'])
|
||||
})
|
||||
|
||||
it('clears managed shared-workspace assets before preview runs', async () => {
|
||||
const requests: Array<{ url: string; init?: RequestInit }> = []
|
||||
globalThis.fetch = mockFetch(
|
||||
requests,
|
||||
textResponse(200, 'token'),
|
||||
textResponse(200, 'true'),
|
||||
jsonResponse(200, ['f/evals/old_subflow', 'u/admin/keep_flow']),
|
||||
textResponse(200, ''),
|
||||
jsonResponse(200, ['f/evals/old_script', 'f/shared/keep_script']),
|
||||
textResponse(200, '')
|
||||
)
|
||||
|
||||
const client = new BackendPreviewClient(
|
||||
buildSettings({
|
||||
baseUrl: 'http://backend.test/shared-cleanup',
|
||||
workspaceOverride: 'shared-preview'
|
||||
})
|
||||
)
|
||||
|
||||
await client.withWorkspace('flow-test1', 1, async () => undefined)
|
||||
|
||||
expect(requests.map((entry) => entry.url)).toEqual([
|
||||
'http://backend.test/shared-cleanup/api/auth/login',
|
||||
'http://backend.test/shared-cleanup/api/workspaces/exists',
|
||||
'http://backend.test/shared-cleanup/api/w/shared-preview/flows/list_paths',
|
||||
'http://backend.test/shared-cleanup/api/w/shared-preview/flows/delete/f/evals/old_subflow',
|
||||
'http://backend.test/shared-cleanup/api/w/shared-preview/scripts/list_paths',
|
||||
'http://backend.test/shared-cleanup/api/w/shared-preview/scripts/delete/p/f/evals/old_script'
|
||||
])
|
||||
})
|
||||
|
||||
it('retries login after a cached login failure', async () => {
|
||||
const requests: Array<{ url: string; init?: RequestInit }> = []
|
||||
globalThis.fetch = mockFetch(
|
||||
requests,
|
||||
textResponse(503, 'backend starting'),
|
||||
textResponse(200, 'token'),
|
||||
textResponse(200, 'true'),
|
||||
jsonResponse(200, []),
|
||||
jsonResponse(200, [])
|
||||
)
|
||||
|
||||
const client = new BackendPreviewClient(
|
||||
buildSettings({
|
||||
baseUrl: 'http://backend.test/login-retry',
|
||||
workspaceOverride: 'shared-preview'
|
||||
})
|
||||
)
|
||||
|
||||
await expect(client.withWorkspace('flow-test1', 1, async () => undefined)).rejects.toThrow(
|
||||
'login for backend validation failed'
|
||||
)
|
||||
await expect(client.withWorkspace('flow-test1', 1, async () => 'ok')).resolves.toBe('ok')
|
||||
|
||||
expect(
|
||||
requests.filter((entry) => entry.url === 'http://backend.test/login-retry/api/auth/login')
|
||||
).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
function buildSettings(
|
||||
overrides: Partial<BackendValidationSettings> = {}
|
||||
): BackendValidationSettings {
|
||||
return {
|
||||
mode: 'preview',
|
||||
baseUrl: 'http://backend.test/default',
|
||||
email: 'admin@windmill.dev',
|
||||
password: 'changeme',
|
||||
keepWorkspaces: true,
|
||||
workspacePrefix: 'ai-evals',
|
||||
pollIntervalMs: 1,
|
||||
maxWaitMs: 50,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function mockFetch(
|
||||
requests: Array<{ url: string; init?: RequestInit }>,
|
||||
...responses: Response[]
|
||||
): typeof fetch {
|
||||
const queue = [...responses]
|
||||
return async (input, init) => {
|
||||
const url = String(input)
|
||||
requests.push({ url, init })
|
||||
const next = queue.shift()
|
||||
if (!next) {
|
||||
throw new Error(`Unexpected fetch: ${url}`)
|
||||
}
|
||||
return next
|
||||
}
|
||||
}
|
||||
|
||||
function jsonResponse(status: number, body: unknown): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
}
|
||||
|
||||
function textResponse(status: number, body: string): Response {
|
||||
return new Response(body, { status })
|
||||
}
|
||||
502
ai_evals/adapters/frontend/backendPreview.ts
Normal file
502
ai_evals/adapters/frontend/backendPreview.ts
Normal file
@@ -0,0 +1,502 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { BackendValidationSettings } from '../../core/backendValidation'
|
||||
|
||||
interface CompletedJobResultMaybe {
|
||||
completed: boolean
|
||||
result: unknown
|
||||
success?: boolean
|
||||
started?: boolean
|
||||
}
|
||||
|
||||
interface ScriptDeploymentStatus {
|
||||
lock?: unknown
|
||||
lock_error_logs?: string | null
|
||||
}
|
||||
|
||||
export interface CompletedPreviewJob {
|
||||
id: string
|
||||
success: boolean
|
||||
result: unknown
|
||||
logs?: string | null
|
||||
raw: Record<string, unknown>
|
||||
}
|
||||
|
||||
const tokenCache = new Map<string, Promise<string>>()
|
||||
const sharedWorkspaceQueue = new Map<string, Promise<void>>()
|
||||
const managedSharedWorkspacePrefixes = ['f/evals/']
|
||||
|
||||
export class BackendPreviewClient {
|
||||
constructor(private readonly settings: BackendValidationSettings) {}
|
||||
|
||||
async withWorkspace<T>(
|
||||
caseId: string,
|
||||
attempt: number,
|
||||
body: (workspaceId: string) => Promise<T>
|
||||
): Promise<T> {
|
||||
const workspaceId =
|
||||
this.settings.workspaceOverride ??
|
||||
buildWorkspaceId(this.settings.workspacePrefix, caseId, attempt)
|
||||
|
||||
const run = async () => {
|
||||
await this.ensureWorkspace(workspaceId)
|
||||
if (this.settings.workspaceOverride) {
|
||||
await this.clearManagedSharedWorkspaceAssets(workspaceId)
|
||||
}
|
||||
|
||||
try {
|
||||
return await body(workspaceId)
|
||||
} finally {
|
||||
if (!this.settings.keepWorkspaces && !this.settings.workspaceOverride) {
|
||||
await this.deleteWorkspace(workspaceId).catch(() => undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.settings.workspaceOverride) {
|
||||
return await withSharedWorkspaceLock(workspaceId, run)
|
||||
}
|
||||
|
||||
return await run()
|
||||
}
|
||||
|
||||
async createScript(input: {
|
||||
workspaceId: string
|
||||
path: string
|
||||
summary: string
|
||||
description?: string
|
||||
schema?: Record<string, unknown>
|
||||
content: string
|
||||
language: string
|
||||
}): Promise<void> {
|
||||
await this.ensureFolderForPath(input.workspaceId, input.path)
|
||||
|
||||
const payload = {
|
||||
path: input.path,
|
||||
summary: input.summary,
|
||||
description: input.description ?? '',
|
||||
content: input.content,
|
||||
schema: input.schema ?? { type: 'object', properties: {}, required: [] },
|
||||
is_template: false,
|
||||
language: input.language,
|
||||
kind: 'script'
|
||||
}
|
||||
|
||||
const response = await this.request(`/w/${encodeURIComponent(input.workspaceId)}/scripts/create`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
await this.waitForScriptDeployment(input.workspaceId, input.path, (await response.text()).trim())
|
||||
return
|
||||
}
|
||||
|
||||
const message = await response.text()
|
||||
if (!isConflictMessage(message)) {
|
||||
throw new Error(`create script ${input.path} failed: ${response.status} ${response.statusText} - ${message}`)
|
||||
}
|
||||
|
||||
const currentScript = await this.getScriptByPath(input.workspaceId, input.path)
|
||||
const currentHash = readStringField(currentScript, 'hash', `script ${input.path}`)
|
||||
const updateResponse = await this.request(
|
||||
`/w/${encodeURIComponent(input.workspaceId)}/scripts/create`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
...payload,
|
||||
parent_hash: currentHash
|
||||
})
|
||||
}
|
||||
)
|
||||
await expectOk(updateResponse, `update script ${input.path}`)
|
||||
await this.waitForScriptDeployment(input.workspaceId, input.path, (await updateResponse.text()).trim())
|
||||
}
|
||||
|
||||
async createFlow(input: {
|
||||
workspaceId: string
|
||||
path: string
|
||||
summary: string
|
||||
description?: string
|
||||
schema?: Record<string, unknown>
|
||||
value: Record<string, unknown>
|
||||
}): Promise<void> {
|
||||
await this.ensureFolderForPath(input.workspaceId, input.path)
|
||||
|
||||
const payload = {
|
||||
path: input.path,
|
||||
summary: input.summary,
|
||||
description: input.description ?? '',
|
||||
schema: input.schema ?? { type: 'object', properties: {}, required: [] },
|
||||
value: input.value
|
||||
}
|
||||
|
||||
const response = await this.request(`/w/${encodeURIComponent(input.workspaceId)}/flows/create`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
return
|
||||
}
|
||||
|
||||
const message = await response.text()
|
||||
if (!isConflictMessage(message)) {
|
||||
throw new Error(`create flow ${input.path} failed: ${response.status} ${response.statusText} - ${message}`)
|
||||
}
|
||||
|
||||
const updateResponse = await this.request(
|
||||
`/w/${encodeURIComponent(input.workspaceId)}/flows/update/${input.path}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
}
|
||||
)
|
||||
await expectOk(updateResponse, `update flow ${input.path}`)
|
||||
}
|
||||
|
||||
async runScriptPreview(input: {
|
||||
workspaceId: string
|
||||
content: string
|
||||
args: Record<string, unknown>
|
||||
language: string
|
||||
path?: string
|
||||
timeoutSeconds?: number
|
||||
}): Promise<CompletedPreviewJob> {
|
||||
const response = await this.request(
|
||||
withQuery(`/w/${encodeURIComponent(input.workspaceId)}/jobs/run/preview`, {
|
||||
timeout: input.timeoutSeconds
|
||||
}),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content: input.content,
|
||||
args: input.args,
|
||||
language: input.language,
|
||||
path: input.path
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await expectOk(response, 'start script preview')
|
||||
const jobId = (await response.text()).trim()
|
||||
return await this.waitForCompletedJob(input.workspaceId, jobId)
|
||||
}
|
||||
|
||||
async runFlowPreview(input: {
|
||||
workspaceId: string
|
||||
value: Record<string, unknown>
|
||||
args: Record<string, unknown>
|
||||
timeoutSeconds?: number
|
||||
path?: string
|
||||
}): Promise<CompletedPreviewJob> {
|
||||
const response = await this.request(
|
||||
withQuery(`/w/${encodeURIComponent(input.workspaceId)}/jobs/run/preview_flow`, {
|
||||
timeout: input.timeoutSeconds
|
||||
}),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
value: input.value,
|
||||
args: input.args,
|
||||
path: input.path
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
await expectOk(response, 'start flow preview')
|
||||
const jobId = (await response.text()).trim()
|
||||
return await this.waitForCompletedJob(input.workspaceId, jobId)
|
||||
}
|
||||
|
||||
private async ensureWorkspace(workspaceId: string): Promise<void> {
|
||||
const existsResponse = await this.request('/workspaces/exists', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: workspaceId })
|
||||
})
|
||||
await expectOk(existsResponse, `check workspace ${workspaceId}`)
|
||||
|
||||
if ((await existsResponse.text()).trim() === 'true') {
|
||||
return
|
||||
}
|
||||
|
||||
const createResponse = await this.request('/workspaces/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id: workspaceId, name: workspaceId })
|
||||
})
|
||||
try {
|
||||
await expectOk(createResponse, `create workspace ${workspaceId}`)
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error)
|
||||
if (message.includes('maximum number of workspaces')) {
|
||||
throw new Error(
|
||||
`${message}. Reuse an existing workspace with WMILL_AI_EVAL_BACKEND_WORKSPACE=<workspace-id>.`
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteWorkspace(workspaceId: string): Promise<void> {
|
||||
const response = await this.request(`/workspaces/delete/${encodeURIComponent(workspaceId)}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
await expectOk(response, `delete workspace ${workspaceId}`)
|
||||
}
|
||||
|
||||
private async ensureFolderForPath(workspaceId: string, path: string): Promise<void> {
|
||||
const folderName = extractFolderName(path)
|
||||
if (!folderName) {
|
||||
return
|
||||
}
|
||||
|
||||
const response = await this.request(`/w/${encodeURIComponent(workspaceId)}/folders/create`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: folderName })
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
return
|
||||
}
|
||||
|
||||
const message = await response.text()
|
||||
if (!message.toLowerCase().includes('already exists')) {
|
||||
throw new Error(`Failed to create folder ${folderName}: ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForCompletedJob(
|
||||
workspaceId: string,
|
||||
jobId: string
|
||||
): Promise<CompletedPreviewJob> {
|
||||
const deadline = Date.now() + this.settings.maxWaitMs
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const maybeResponse = await this.request(
|
||||
`/w/${encodeURIComponent(workspaceId)}/jobs_u/completed/get_result_maybe/${encodeURIComponent(jobId)}?get_started=false`
|
||||
)
|
||||
await expectOk(maybeResponse, `poll job ${jobId}`)
|
||||
const maybeResult = (await maybeResponse.json()) as CompletedJobResultMaybe
|
||||
|
||||
if (maybeResult.completed) {
|
||||
const completedResponse = await this.request(
|
||||
`/w/${encodeURIComponent(workspaceId)}/jobs_u/completed/get/${encodeURIComponent(jobId)}`
|
||||
)
|
||||
await expectOk(completedResponse, `get completed job ${jobId}`)
|
||||
const completedJob = (await completedResponse.json()) as Record<string, unknown>
|
||||
return {
|
||||
id: jobId,
|
||||
success: Boolean(maybeResult.success),
|
||||
result: maybeResult.result,
|
||||
logs:
|
||||
typeof completedJob.logs === 'string' || completedJob.logs === null
|
||||
? (completedJob.logs as string | null)
|
||||
: null,
|
||||
raw: completedJob
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, this.settings.pollIntervalMs))
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for preview job ${jobId} to complete`)
|
||||
}
|
||||
|
||||
private async getScriptByPath(workspaceId: string, path: string): Promise<Record<string, unknown>> {
|
||||
const response = await this.request(`/w/${encodeURIComponent(workspaceId)}/scripts/get/p/${path}`)
|
||||
await expectOk(response, `get script ${path}`)
|
||||
return (await response.json()) as Record<string, unknown>
|
||||
}
|
||||
|
||||
private async clearManagedSharedWorkspaceAssets(workspaceId: string): Promise<void> {
|
||||
const flowPaths = await this.listFlowPaths(workspaceId)
|
||||
for (const path of flowPaths.filter(isManagedSharedWorkspacePath)) {
|
||||
await this.deleteFlowByPath(workspaceId, path)
|
||||
}
|
||||
|
||||
const scriptPaths = await this.listScriptPaths(workspaceId)
|
||||
for (const path of scriptPaths.filter(isManagedSharedWorkspacePath)) {
|
||||
await this.deleteScriptByPath(workspaceId, path)
|
||||
}
|
||||
}
|
||||
|
||||
private async listFlowPaths(workspaceId: string): Promise<string[]> {
|
||||
const response = await this.request(`/w/${encodeURIComponent(workspaceId)}/flows/list_paths`)
|
||||
await expectOk(response, `list flows in workspace ${workspaceId}`)
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
private async listScriptPaths(workspaceId: string): Promise<string[]> {
|
||||
const response = await this.request(`/w/${encodeURIComponent(workspaceId)}/scripts/list_paths`)
|
||||
await expectOk(response, `list scripts in workspace ${workspaceId}`)
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
private async deleteFlowByPath(workspaceId: string, path: string): Promise<void> {
|
||||
const response = await this.request(`/w/${encodeURIComponent(workspaceId)}/flows/delete/${path}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
await expectOk(response, `delete flow ${path}`)
|
||||
}
|
||||
|
||||
private async deleteScriptByPath(workspaceId: string, path: string): Promise<void> {
|
||||
const response = await this.request(`/w/${encodeURIComponent(workspaceId)}/scripts/delete/p/${path}`, {
|
||||
method: 'POST'
|
||||
})
|
||||
await expectOk(response, `delete script ${path}`)
|
||||
}
|
||||
|
||||
private async waitForScriptDeployment(
|
||||
workspaceId: string,
|
||||
path: string,
|
||||
hash: string
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + this.settings.maxWaitMs
|
||||
|
||||
while (Date.now() < deadline) {
|
||||
const response = await this.request(
|
||||
`/w/${encodeURIComponent(workspaceId)}/scripts/deployment_status/h/${encodeURIComponent(hash)}`
|
||||
)
|
||||
await expectOk(response, `check deployment status for script ${path}`)
|
||||
const deployment = (await response.json()) as ScriptDeploymentStatus
|
||||
if (deployment.lock != null) {
|
||||
return
|
||||
}
|
||||
if (deployment.lock_error_logs) {
|
||||
throw new Error(`Script deployment failed for ${path}: ${deployment.lock_error_logs}`)
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, this.settings.pollIntervalMs))
|
||||
}
|
||||
|
||||
throw new Error(`Timed out waiting for script ${path} (${hash}) to deploy`)
|
||||
}
|
||||
|
||||
private async request(path: string, init?: RequestInit): Promise<Response> {
|
||||
const token = await this.getToken()
|
||||
return await fetch(`${this.settings.baseUrl}/api${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...(init?.headers ?? {})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async getToken(): Promise<string> {
|
||||
const cacheKey = `${this.settings.baseUrl}|${this.settings.email}`
|
||||
let tokenPromise = tokenCache.get(cacheKey)
|
||||
if (!tokenPromise) {
|
||||
tokenPromise = this.login().catch((error) => {
|
||||
if (tokenCache.get(cacheKey) === tokenPromise) {
|
||||
tokenCache.delete(cacheKey)
|
||||
}
|
||||
throw error
|
||||
})
|
||||
tokenCache.set(cacheKey, tokenPromise)
|
||||
}
|
||||
return await tokenPromise
|
||||
}
|
||||
|
||||
private async login(): Promise<string> {
|
||||
const response = await fetch(`${this.settings.baseUrl}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: this.settings.email,
|
||||
password: this.settings.password
|
||||
})
|
||||
})
|
||||
await expectOk(response, 'login for backend validation')
|
||||
return (await response.text()).trim()
|
||||
}
|
||||
}
|
||||
|
||||
async function withSharedWorkspaceLock<T>(workspaceId: string, body: () => Promise<T>): Promise<T> {
|
||||
const previous = sharedWorkspaceQueue.get(workspaceId) ?? Promise.resolve()
|
||||
let releaseCurrent: (() => void) | undefined
|
||||
const current = new Promise<void>((resolve) => {
|
||||
releaseCurrent = resolve
|
||||
})
|
||||
const tail = previous.catch(() => undefined).then(() => current)
|
||||
sharedWorkspaceQueue.set(workspaceId, tail)
|
||||
|
||||
await previous.catch(() => undefined)
|
||||
|
||||
try {
|
||||
return await body()
|
||||
} finally {
|
||||
releaseCurrent?.()
|
||||
if (sharedWorkspaceQueue.get(workspaceId) === tail) {
|
||||
sharedWorkspaceQueue.delete(workspaceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildWorkspaceId(prefix: string, caseId: string, attempt: number): string {
|
||||
const caseSlug = caseId
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 30)
|
||||
const suffix = randomUUID().slice(0, 8)
|
||||
return `${prefix}-${caseSlug || 'case'}-a${attempt}-${suffix}`
|
||||
}
|
||||
|
||||
function extractFolderName(path: string): string | null {
|
||||
if (!path.startsWith('f/')) {
|
||||
return null
|
||||
}
|
||||
const segments = path.split('/').slice(1, -1)
|
||||
return segments.length > 0 ? segments.join('/') : null
|
||||
}
|
||||
|
||||
function withQuery(
|
||||
path: string,
|
||||
params: Record<string, string | number | undefined>
|
||||
): string {
|
||||
const query = new URLSearchParams()
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === undefined) {
|
||||
continue
|
||||
}
|
||||
query.set(key, String(value))
|
||||
}
|
||||
const suffix = query.toString()
|
||||
return suffix ? `${path}?${suffix}` : path
|
||||
}
|
||||
|
||||
async function expectOk(response: Response, context: string): Promise<void> {
|
||||
if (response.ok) {
|
||||
return
|
||||
}
|
||||
throw new Error(`${context} failed: ${response.status} ${response.statusText} - ${await response.text()}`)
|
||||
}
|
||||
|
||||
function readStringField(
|
||||
value: Record<string, unknown>,
|
||||
field: string,
|
||||
context: string
|
||||
): string {
|
||||
const candidate = value[field]
|
||||
if (typeof candidate === 'string' && candidate.length > 0) {
|
||||
return candidate
|
||||
}
|
||||
throw new Error(`${context} is missing string field ${field}`)
|
||||
}
|
||||
|
||||
function isConflictMessage(message: string): boolean {
|
||||
const normalized = message.toLowerCase()
|
||||
return normalized.includes('already exists') || normalized.includes('path conflict')
|
||||
}
|
||||
|
||||
function isManagedSharedWorkspacePath(path: string): boolean {
|
||||
return managedSharedWorkspacePrefixes.some((prefix) => path.startsWith(prefix))
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { loadSelectedCases } from "../../core/cases";
|
||||
import { resolveBackendValidationSettings } from "../../core/backendValidation";
|
||||
import {
|
||||
formatRunModelLabel,
|
||||
getFrontendEvalModel,
|
||||
@@ -22,9 +23,13 @@ export async function runFrontendBenchmarkFromEnv(): Promise<BenchmarkRunResult>
|
||||
const emitProgress = process.env.WMILL_FRONTEND_AI_EVAL_PROGRESS === "1";
|
||||
const verbose = process.env.WMILL_FRONTEND_AI_EVAL_VERBOSE === "1";
|
||||
const model = resolveEvalModel(mode, process.env.WMILL_FRONTEND_AI_EVAL_MODEL);
|
||||
const backendValidation = resolveBackendValidationSettings({
|
||||
evalMode: mode,
|
||||
requestedMode: process.env.WMILL_FRONTEND_AI_EVAL_BACKEND_VALIDATION,
|
||||
});
|
||||
|
||||
const selectedCases = await loadSelectedCases(mode, caseIds);
|
||||
const modeRunner = getModeRunner(mode, getFrontendEvalModel(model));
|
||||
const modeRunner = getModeRunner(mode, getFrontendEvalModel(model), backendValidation);
|
||||
const runModel = formatRunModelLabel(mode, model);
|
||||
const caseResults = await runSuite({
|
||||
modeRunner,
|
||||
@@ -48,15 +53,16 @@ export async function runFrontendBenchmarkFromEnv(): Promise<BenchmarkRunResult>
|
||||
|
||||
function getModeRunner(
|
||||
mode: FrontendBenchmarkMode,
|
||||
model: ReturnType<typeof getFrontendEvalModel>
|
||||
model: ReturnType<typeof getFrontendEvalModel>,
|
||||
backendValidation: ReturnType<typeof resolveBackendValidationSettings>
|
||||
): ModeRunner<any, any, any> {
|
||||
switch (mode) {
|
||||
case "flow":
|
||||
return createFlowModeRunner(model);
|
||||
return createFlowModeRunner(model, backendValidation);
|
||||
case "app":
|
||||
return createAppModeRunner(model);
|
||||
case "script":
|
||||
return createScriptModeRunner(model);
|
||||
return createScriptModeRunner(model, backendValidation);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ export async function runFrontendBenchmarkAdapter(input: {
|
||||
runs: number
|
||||
model?: string
|
||||
verbose?: boolean
|
||||
backendValidation?: string
|
||||
}): Promise<BenchmarkRunResult> {
|
||||
const tempDir = await mkdtemp(path.join(tmpdir(), 'wmill-frontend-benchmark-'))
|
||||
const outputPath = path.join(tempDir, 'result.json')
|
||||
@@ -48,7 +49,8 @@ export async function runFrontendBenchmarkAdapter(input: {
|
||||
WMILL_FRONTEND_AI_EVAL_RUNS: String(input.runs),
|
||||
WMILL_FRONTEND_AI_EVAL_MODEL: input.model ?? "",
|
||||
WMILL_FRONTEND_AI_EVAL_PROGRESS: '1',
|
||||
WMILL_FRONTEND_AI_EVAL_VERBOSE: input.verbose ? '1' : '0'
|
||||
WMILL_FRONTEND_AI_EVAL_VERBOSE: input.verbose ? '1' : '0',
|
||||
WMILL_FRONTEND_AI_EVAL_BACKEND_VALIDATION: input.backendValidation ?? ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
Create a flow that takes two numbers, `a` and `b`, and returns their sum.
|
||||
Keep it simple and use a single step named `sum_numbers`.
|
||||
expected: ai_evals/fixtures/frontend/flow/expected/test0_sum_two_numbers.json
|
||||
runtime:
|
||||
backendPreview:
|
||||
args:
|
||||
a: 4
|
||||
b: 5
|
||||
judgeChecklist:
|
||||
- "the flow takes `a` and `b` as inputs"
|
||||
- "the main step is named `sum_numbers`"
|
||||
@@ -15,6 +20,11 @@
|
||||
The flow should take `a` and `b` as inputs and use a single step named `sum_numbers`.
|
||||
initial: ai_evals/fixtures/frontend/flow/initial/test1_reuse_existing_script_initial.json
|
||||
expected: ai_evals/fixtures/frontend/flow/expected/test1_reuse_existing_script.json
|
||||
runtime:
|
||||
backendPreview:
|
||||
args:
|
||||
a: 2
|
||||
b: 3
|
||||
judgeChecklist:
|
||||
- "the flow takes `a` and `b` as inputs"
|
||||
- "the main step is named `sum_numbers`"
|
||||
@@ -27,6 +37,11 @@
|
||||
Use a single step named `call_add_numbers`.
|
||||
initial: ai_evals/fixtures/frontend/flow/initial/test2_call_existing_subflow_initial.json
|
||||
expected: ai_evals/fixtures/frontend/flow/expected/test2_call_existing_subflow.json
|
||||
runtime:
|
||||
backendPreview:
|
||||
args:
|
||||
a: 7
|
||||
b: 8
|
||||
judgeChecklist:
|
||||
- "the parent flow takes `a` and `b` as inputs"
|
||||
- "the main step is named `call_add_numbers`"
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
import { Command, InvalidArgumentError } from "commander";
|
||||
import { loadCases, loadSelectedCases } from "../core/cases";
|
||||
import {
|
||||
BACKEND_VALIDATION_MODES,
|
||||
parseBackendValidationMode,
|
||||
} from "../core/backendValidation";
|
||||
import {
|
||||
EVAL_MODELS,
|
||||
type EvalModelSpec,
|
||||
@@ -43,6 +47,7 @@ async function main() {
|
||||
" bun run cli -- run flow --models haiku,opus,4o",
|
||||
" bun run cli -- run flow flow-test0-sum-two-numbers --verbose",
|
||||
" bun run cli -- run flow --record",
|
||||
" bun run cli -- run flow --backend-validation preview",
|
||||
" bun run cli -- run flow flow-test5-simple-modification --runs 3",
|
||||
" bun run cli -- run cli bun-hello-script",
|
||||
"",
|
||||
@@ -77,6 +82,10 @@ async function main() {
|
||||
.option("--models <names>", "comma-separated model aliases to run sequentially")
|
||||
.option("--verbose", "stream assistant output during frontend runs")
|
||||
.option("--record", "append a compact summary line to ai_evals/history/<mode>.jsonl")
|
||||
.option(
|
||||
"--backend-validation <mode>",
|
||||
`backend smoke validation (${BACKEND_VALIDATION_MODES.join(", ")})`
|
||||
)
|
||||
.action(
|
||||
async (
|
||||
mode: EvalMode,
|
||||
@@ -88,6 +97,7 @@ async function main() {
|
||||
models?: string;
|
||||
verbose?: boolean;
|
||||
record?: boolean;
|
||||
backendValidation?: string;
|
||||
}
|
||||
) => {
|
||||
await handleRun({
|
||||
@@ -99,6 +109,7 @@ async function main() {
|
||||
models: options.models,
|
||||
verbose: options.verbose ?? false,
|
||||
record: options.record ?? false,
|
||||
backendValidation: options.backendValidation,
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -143,6 +154,7 @@ async function handleRun(input: {
|
||||
models?: string;
|
||||
verbose: boolean;
|
||||
record: boolean;
|
||||
backendValidation?: string;
|
||||
}) {
|
||||
if (input.record && input.caseIds.length > 0) {
|
||||
throw new Error("--record only supports full-suite runs; omit case ids to record history");
|
||||
@@ -153,9 +165,15 @@ async function handleRun(input: {
|
||||
|
||||
const selectedCases = await loadSelectedCases(input.mode, input.caseIds);
|
||||
const models = resolveRequestedModels(input.mode, input.model, input.models);
|
||||
const backendValidation = parseBackendValidationMode(
|
||||
input.backendValidation ?? process.env.WMILL_AI_EVAL_BACKEND_VALIDATION
|
||||
);
|
||||
if (input.outputPath && models.length > 1) {
|
||||
throw new Error("--output only supports a single model run");
|
||||
}
|
||||
if (backendValidation !== "off" && input.mode !== "flow" && input.mode !== "script") {
|
||||
throw new Error("--backend-validation currently supports only flow and script modes");
|
||||
}
|
||||
|
||||
const summaries: Array<{ label: string; passRate: number; averageDurationMs: number }> = [];
|
||||
|
||||
@@ -177,6 +195,7 @@ async function handleRun(input: {
|
||||
runs: input.runs,
|
||||
model: model.id,
|
||||
verbose: input.verbose,
|
||||
backendValidation,
|
||||
});
|
||||
|
||||
const resolvedOutputPath =
|
||||
|
||||
36
ai_evals/core/backendValidation.test.ts
Normal file
36
ai_evals/core/backendValidation.test.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
parseBackendValidationMode,
|
||||
resolveBackendValidationSettings,
|
||||
} from "./backendValidation";
|
||||
|
||||
describe("parseBackendValidationMode", () => {
|
||||
it("defaults to off", () => {
|
||||
expect(parseBackendValidationMode(undefined)).toBe("off");
|
||||
expect(parseBackendValidationMode("0")).toBe("off");
|
||||
expect(parseBackendValidationMode("false")).toBe("off");
|
||||
});
|
||||
|
||||
it("accepts preview aliases", () => {
|
||||
expect(parseBackendValidationMode("preview")).toBe("preview");
|
||||
expect(parseBackendValidationMode("1")).toBe("preview");
|
||||
expect(parseBackendValidationMode("true")).toBe("preview");
|
||||
});
|
||||
|
||||
it("rejects unknown modes", () => {
|
||||
expect(() => parseBackendValidationMode("maybe")).toThrow(
|
||||
"Unsupported backend validation mode: maybe"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveBackendValidationSettings", () => {
|
||||
it("rejects unsupported eval modes", () => {
|
||||
expect(() =>
|
||||
resolveBackendValidationSettings({
|
||||
evalMode: "app",
|
||||
requestedMode: "preview",
|
||||
})
|
||||
).toThrow('Backend validation mode "preview" is only supported for flow and script evals');
|
||||
});
|
||||
});
|
||||
104
ai_evals/core/backendValidation.ts
Normal file
104
ai_evals/core/backendValidation.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { EvalMode } from "./types";
|
||||
|
||||
export const BACKEND_VALIDATION_MODES = ["off", "preview"] as const;
|
||||
|
||||
export type BackendValidationMode = (typeof BACKEND_VALIDATION_MODES)[number];
|
||||
|
||||
export interface BackendValidationSettings {
|
||||
mode: BackendValidationMode;
|
||||
baseUrl: string;
|
||||
email: string;
|
||||
password: string;
|
||||
keepWorkspaces: boolean;
|
||||
workspaceOverride?: string;
|
||||
workspacePrefix: string;
|
||||
pollIntervalMs: number;
|
||||
maxWaitMs: number;
|
||||
}
|
||||
|
||||
export function parseBackendValidationMode(value?: string | null): BackendValidationMode {
|
||||
const normalized = value?.trim().toLowerCase();
|
||||
|
||||
if (!normalized || normalized === "off" || normalized === "false" || normalized === "0") {
|
||||
return "off";
|
||||
}
|
||||
|
||||
if (normalized === "preview" || normalized === "true" || normalized === "1") {
|
||||
return "preview";
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unsupported backend validation mode: ${value}. Use one of: ${BACKEND_VALIDATION_MODES.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveBackendValidationSettings(input: {
|
||||
evalMode: EvalMode;
|
||||
requestedMode?: string | null;
|
||||
}): BackendValidationSettings {
|
||||
const mode = parseBackendValidationMode(
|
||||
input.requestedMode ?? process.env.WMILL_AI_EVAL_BACKEND_VALIDATION
|
||||
);
|
||||
|
||||
if (mode !== "off" && input.evalMode !== "flow" && input.evalMode !== "script") {
|
||||
throw new Error(
|
||||
`Backend validation mode "${mode}" is only supported for flow and script evals`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
mode,
|
||||
baseUrl: normalizeBaseUrl(
|
||||
process.env.WMILL_AI_EVAL_BACKEND_URL ??
|
||||
process.env.WINDMILL_URL ??
|
||||
process.env.WINDMILL_BASE_URL ??
|
||||
process.env.REMOTE ??
|
||||
"http://127.0.0.1:8000"
|
||||
),
|
||||
email: process.env.WMILL_AI_EVAL_BACKEND_EMAIL ?? "admin@windmill.dev",
|
||||
password: process.env.WMILL_AI_EVAL_BACKEND_PASSWORD ?? "changeme",
|
||||
keepWorkspaces: isTruthy(process.env.WMILL_AI_EVAL_KEEP_WORKSPACES),
|
||||
workspaceOverride: sanitizeOptionalWorkspaceId(process.env.WMILL_AI_EVAL_BACKEND_WORKSPACE),
|
||||
workspacePrefix: sanitizeWorkspacePrefix(
|
||||
process.env.WMILL_AI_EVAL_WORKSPACE_PREFIX ?? "ai-evals"
|
||||
),
|
||||
pollIntervalMs: parsePositiveInteger(
|
||||
process.env.WMILL_AI_EVAL_BACKEND_POLL_INTERVAL_MS,
|
||||
2000
|
||||
),
|
||||
maxWaitMs: parsePositiveInteger(process.env.WMILL_AI_EVAL_BACKEND_MAX_WAIT_MS, 120000),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value: string): string {
|
||||
return value.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function sanitizeWorkspacePrefix(value: string): string {
|
||||
const sanitized = value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9-]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return sanitized.length > 0 ? sanitized : "ai-evals";
|
||||
}
|
||||
|
||||
function sanitizeOptionalWorkspaceId(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function isTruthy(value: string | undefined): boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
||||
}
|
||||
|
||||
function parsePositiveInteger(value: string | undefined, fallback: number): number {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
18
ai_evals/core/cases.test.ts
Normal file
18
ai_evals/core/cases.test.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { loadCases } from "./cases";
|
||||
|
||||
describe("loadCases", () => {
|
||||
it("loads backend preview runtime config for opt-in flow cases", async () => {
|
||||
const flowCases = await loadCases("flow");
|
||||
const caseEntry = flowCases.find((entry) => entry.id === "flow-test1-reuse-existing-script");
|
||||
|
||||
expect(caseEntry?.runtime).toEqual({
|
||||
backendPreview: {
|
||||
args: {
|
||||
a: 2,
|
||||
b: 3,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parse } from "yaml";
|
||||
import type { EvalCase, EvalMode, FlowValidationSpec } from "./types";
|
||||
import type { EvalCase, EvalCaseRuntimeSpec, EvalMode, FlowValidationSpec } from "./types";
|
||||
|
||||
const REPO_ROOT = fileURLToPath(new URL("../../", import.meta.url));
|
||||
const CASES_DIR = path.join(REPO_ROOT, "ai_evals", "cases");
|
||||
@@ -14,6 +14,7 @@ interface RawEvalCase {
|
||||
expected?: string;
|
||||
validate?: FlowValidationSpec;
|
||||
judgeChecklist?: string[];
|
||||
runtime?: EvalCaseRuntimeSpec;
|
||||
}
|
||||
|
||||
export function getRepoRoot(): string {
|
||||
@@ -40,6 +41,7 @@ export async function loadCases(mode: EvalMode): Promise<EvalCase[]> {
|
||||
expectedPath: resolveFixturePath(entry.expected),
|
||||
validate: entry.validate,
|
||||
judgeChecklist: entry.judgeChecklist,
|
||||
runtime: entry.runtime,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -155,6 +155,44 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
|
||||
run,
|
||||
}),
|
||||
];
|
||||
const artifactFiles = input.modeRunner.buildArtifacts?.(run.actual) ?? [];
|
||||
|
||||
if (run.success && input.modeRunner.backendValidate) {
|
||||
try {
|
||||
const backendValidation = await input.modeRunner.backendValidate({
|
||||
evalCase: input.evalCase,
|
||||
prompt: input.evalCase.prompt,
|
||||
initial,
|
||||
expected,
|
||||
actual: run.actual,
|
||||
run,
|
||||
context: {
|
||||
caseId: input.evalCase.id,
|
||||
caseNumber: input.caseIndex + 1,
|
||||
totalCases: input.totalCases,
|
||||
attempt,
|
||||
runs: input.runs,
|
||||
verbose: input.verbose,
|
||||
onAssistantMessageStart: undefined,
|
||||
onAssistantChunk: undefined,
|
||||
onAssistantMessageEnd: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
if (backendValidation) {
|
||||
checks.push(...backendValidation.checks);
|
||||
artifactFiles.push(...(backendValidation.artifactFiles ?? []));
|
||||
}
|
||||
} catch (error) {
|
||||
checks.push(
|
||||
buildCheck(
|
||||
"backend validation succeeded",
|
||||
false,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let judgeScore: number | null = null;
|
||||
let judgeSummary: string | null = null;
|
||||
@@ -182,7 +220,6 @@ async function runCaseAttempts<TInitial, TExpected, TActual>(input: {
|
||||
);
|
||||
}
|
||||
|
||||
const artifactFiles = input.modeRunner.buildArtifacts?.(run.actual) ?? [];
|
||||
const attemptResult: BenchmarkAttemptResult = {
|
||||
attempt,
|
||||
passed: checks.every((check) => check.passed),
|
||||
|
||||
@@ -2,6 +2,15 @@ export const EVAL_MODES = ["cli", "flow", "script", "app"] as const;
|
||||
|
||||
export type EvalMode = (typeof EVAL_MODES)[number];
|
||||
|
||||
export interface EvalCaseRuntimeBackendPreview {
|
||||
args?: Record<string, unknown>;
|
||||
timeoutSeconds?: number;
|
||||
}
|
||||
|
||||
export interface EvalCaseRuntimeSpec {
|
||||
backendPreview?: EvalCaseRuntimeBackendPreview;
|
||||
}
|
||||
|
||||
export interface FlowValidationSpec {
|
||||
schemaRequiredPaths?: string[];
|
||||
schemaAnyOf?: Array<{
|
||||
@@ -23,6 +32,7 @@ export interface EvalCase {
|
||||
expectedPath?: string;
|
||||
validate?: FlowValidationSpec;
|
||||
judgeChecklist?: string[];
|
||||
runtime?: EvalCaseRuntimeSpec;
|
||||
}
|
||||
|
||||
export interface BenchmarkCheck {
|
||||
@@ -43,6 +53,11 @@ export interface BenchmarkArtifactFile {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface BackendValidationResult {
|
||||
checks: BenchmarkCheck[];
|
||||
artifactFiles?: BenchmarkArtifactFile[];
|
||||
}
|
||||
|
||||
export interface BenchmarkTokenUsage {
|
||||
prompt: number;
|
||||
completion: number;
|
||||
@@ -91,6 +106,15 @@ export interface ModeRunner<TInitial, TExpected, TActual> {
|
||||
actual: TActual;
|
||||
run: ModeRunOutput<TActual>;
|
||||
}): BenchmarkCheck[];
|
||||
backendValidate?(input: {
|
||||
evalCase: EvalCase;
|
||||
prompt: string;
|
||||
initial: TInitial | undefined;
|
||||
expected: TExpected | undefined;
|
||||
actual: TActual;
|
||||
run: ModeRunOutput<TActual>;
|
||||
context: ModeRunContext;
|
||||
}): Promise<BackendValidationResult | null>;
|
||||
buildArtifacts?(actual: TActual): BenchmarkArtifactFile[];
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"id": "sum_numbers",
|
||||
"value": {
|
||||
"type": "script",
|
||||
"path": "f/evals/add_two_numbers.ts",
|
||||
"path": "f/evals/add_two_numbers",
|
||||
"input_transforms": {
|
||||
"a": {
|
||||
"type": "javascript",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"workspace": {
|
||||
"scripts": [
|
||||
{
|
||||
"path": "f/evals/add_two_numbers.ts",
|
||||
"path": "f/evals/add_two_numbers",
|
||||
"summary": "Add two numbers",
|
||||
"description": "Returns the sum of two numeric inputs.",
|
||||
"language": "bun",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { readJsonFile } from "../core/files";
|
||||
import type { BackendValidationSettings } from "../core/backendValidation";
|
||||
import type { FrontendEvalModelConfig } from "../core/models";
|
||||
import { validateFlowState, type FlowState } from "../core/validators";
|
||||
import type { BenchmarkArtifactFile, ModeRunner } from "../core/types";
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
type FlowFixture,
|
||||
} from "../adapters/frontend/core/flow/flowEvalRunner";
|
||||
import type { FlowWorkspaceFixtures } from "../adapters/frontend/core/flow/fileHelpers";
|
||||
import { BackendPreviewClient } from "../adapters/frontend/backendPreview";
|
||||
import { DEFAULT_FRONTEND_EVAL_MODEL, getFrontendApiKey } from "./frontendCommon";
|
||||
|
||||
interface FlowInitialFixture {
|
||||
@@ -15,7 +17,8 @@ interface FlowInitialFixture {
|
||||
}
|
||||
|
||||
export function createFlowModeRunner(
|
||||
modelConfig: FrontendEvalModelConfig = DEFAULT_FRONTEND_EVAL_MODEL
|
||||
modelConfig: FrontendEvalModelConfig = DEFAULT_FRONTEND_EVAL_MODEL,
|
||||
backendValidation?: BackendValidationSettings
|
||||
): ModeRunner<FlowInitialFixture, FlowState, FlowState> {
|
||||
return {
|
||||
mode: "flow",
|
||||
@@ -61,6 +64,65 @@ export function createFlowModeRunner(
|
||||
validate: evalCase.validate,
|
||||
});
|
||||
},
|
||||
async backendValidate({ evalCase, initial, actual, context }) {
|
||||
if (backendValidation?.mode !== "preview" || !evalCase.runtime?.backendPreview) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!actual.value) {
|
||||
return {
|
||||
checks: [
|
||||
{
|
||||
name: "backend flow preview succeeded",
|
||||
passed: false,
|
||||
details: "Generated flow is missing value.modules",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const previewClient = new BackendPreviewClient(backendValidation);
|
||||
return await previewClient.withWorkspace(evalCase.id, context.attempt, async (workspaceId) => {
|
||||
await seedWorkspaceFixtures(previewClient, workspaceId, initial?.workspace);
|
||||
|
||||
const completedJob = await previewClient.runFlowPreview({
|
||||
workspaceId,
|
||||
value: actual.value as Record<string, unknown>,
|
||||
args: evalCase.runtime?.backendPreview?.args ?? {},
|
||||
timeoutSeconds: evalCase.runtime?.backendPreview?.timeoutSeconds,
|
||||
});
|
||||
|
||||
return {
|
||||
checks: [
|
||||
{
|
||||
name: "backend flow preview succeeded",
|
||||
passed: completedJob.success,
|
||||
details: completedJob.success
|
||||
? `workspace=${workspaceId}`
|
||||
: `workspace=${workspaceId}; job=${completedJob.id}`,
|
||||
},
|
||||
],
|
||||
artifactFiles: [
|
||||
{
|
||||
path: "backend-preview.json",
|
||||
content:
|
||||
JSON.stringify(
|
||||
{
|
||||
workspaceId,
|
||||
jobId: completedJob.id,
|
||||
success: completedJob.success,
|
||||
result: completedJob.result,
|
||||
logs: completedJob.logs,
|
||||
completedJob: completedJob.raw,
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + "\n",
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
buildArtifacts(actual): BenchmarkArtifactFile[] {
|
||||
return [
|
||||
{
|
||||
@@ -102,3 +164,32 @@ function normalizeFlowStateFixture(value: unknown): FlowState {
|
||||
function isObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
async function seedWorkspaceFixtures(
|
||||
previewClient: BackendPreviewClient,
|
||||
workspaceId: string,
|
||||
fixtures?: FlowWorkspaceFixtures
|
||||
): Promise<void> {
|
||||
for (const script of fixtures?.scripts ?? []) {
|
||||
await previewClient.createScript({
|
||||
workspaceId,
|
||||
path: script.path,
|
||||
summary: script.summary,
|
||||
description: script.description,
|
||||
schema: script.schema,
|
||||
content: script.content,
|
||||
language: script.language,
|
||||
});
|
||||
}
|
||||
|
||||
for (const flow of fixtures?.flows ?? []) {
|
||||
await previewClient.createFlow({
|
||||
workspaceId,
|
||||
path: flow.path,
|
||||
summary: flow.summary,
|
||||
description: flow.description,
|
||||
schema: flow.schema,
|
||||
value: flow.value as Record<string, unknown>,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { readJsonFile } from "../core/files";
|
||||
import type { BackendValidationSettings } from "../core/backendValidation";
|
||||
import type { FrontendEvalModelConfig } from "../core/models";
|
||||
import { validateScriptState } from "../core/validators";
|
||||
import type { BenchmarkArtifactFile, ModeRunner } from "../core/types";
|
||||
import { BackendPreviewClient } from "../adapters/frontend/backendPreview";
|
||||
import { runScriptEval } from "../adapters/frontend/core/script/scriptEvalRunner";
|
||||
import type { ScriptEvalState } from "../adapters/frontend/core/script/fileHelpers";
|
||||
import { DEFAULT_FRONTEND_EVAL_MODEL, getFrontendApiKey } from "./frontendCommon";
|
||||
|
||||
export function createScriptModeRunner(
|
||||
modelConfig: FrontendEvalModelConfig = DEFAULT_FRONTEND_EVAL_MODEL
|
||||
modelConfig: FrontendEvalModelConfig = DEFAULT_FRONTEND_EVAL_MODEL,
|
||||
backendValidation?: BackendValidationSettings
|
||||
): ModeRunner<ScriptEvalState, ScriptEvalState, ScriptEvalState> {
|
||||
return {
|
||||
mode: "script",
|
||||
@@ -45,6 +48,57 @@ export function createScriptModeRunner(
|
||||
validate({ actual, initial, expected }) {
|
||||
return validateScriptState({ actual, initial, expected });
|
||||
},
|
||||
async backendValidate({ evalCase, initial, actual, context }) {
|
||||
if (backendValidation?.mode !== "preview") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const previewClient = new BackendPreviewClient(backendValidation);
|
||||
return await previewClient.withWorkspace(evalCase.id, context.attempt, async (workspaceId) => {
|
||||
const completedJob = await previewClient.runScriptPreview({
|
||||
workspaceId,
|
||||
content: actual.code,
|
||||
args:
|
||||
(evalCase.runtime?.backendPreview?.args as Record<string, unknown> | undefined) ??
|
||||
actual.args ??
|
||||
initial?.args ??
|
||||
{},
|
||||
language: normalizePreviewLanguage(actual.lang),
|
||||
path: toPreviewScriptPath(actual.path),
|
||||
timeoutSeconds: evalCase.runtime?.backendPreview?.timeoutSeconds,
|
||||
});
|
||||
|
||||
return {
|
||||
checks: [
|
||||
{
|
||||
name: "backend script preview succeeded",
|
||||
passed: completedJob.success,
|
||||
details: completedJob.success
|
||||
? `workspace=${workspaceId}`
|
||||
: `workspace=${workspaceId}; job=${completedJob.id}`,
|
||||
},
|
||||
],
|
||||
artifactFiles: [
|
||||
{
|
||||
path: "backend-preview.json",
|
||||
content:
|
||||
JSON.stringify(
|
||||
{
|
||||
workspaceId,
|
||||
jobId: completedJob.id,
|
||||
success: completedJob.success,
|
||||
result: completedJob.result,
|
||||
logs: completedJob.logs,
|
||||
completedJob: completedJob.raw,
|
||||
},
|
||||
null,
|
||||
2
|
||||
) + "\n",
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
buildArtifacts(actual): BenchmarkArtifactFile[] {
|
||||
return [
|
||||
{
|
||||
@@ -59,3 +113,11 @@ export function createScriptModeRunner(
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePreviewLanguage(language: ScriptEvalState["lang"]): string {
|
||||
return language === "bunnative" ? "bun" : language;
|
||||
}
|
||||
|
||||
function toPreviewScriptPath(filePath: string): string {
|
||||
return filePath.replace(/\.[^.\/]+$/, "");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT usage.usage + 1 FROM usage \n WHERE is_workspace IS TRUE AND\n month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
|
||||
"query": "SELECT usage.usage + 1 FROM usage\n WHERE is_workspace IS FALSE AND\n month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -18,5 +18,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "58c0eb36b630d5eba9d12edca672ebd56e13193395701e26328a243055bee6b8"
|
||||
"hash": "0bc5b3483b352770129962139329e28f99450f33865ceb7724fd1f378dacde26"
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n flow_status->>'step' = '0' \n AND (\n jsonb_array_length(flow_status->'modules') = 0 \n OR flow_status->'modules'->0->>'type' = 'WaitingForPriorSteps' \n OR (\n flow_status->'modules'->0->>'type' = 'Failure' \n AND flow_status->'modules'->0->>'job' = $1\n )\n )\n FROM v2_job_completed WHERE id = $2 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "241270e20c751806dece12fbc2de360e389da8c7f653ef8e6bc0d30c823aea51"
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\nWITH RECURSIVE job_tree AS (\n -- Base case: direct children of the given parent job\n SELECT id, parent_job, 1 AS depth\n FROM v2_job_queue \n INNER JOIN v2_job USING (id)\n WHERE parent_job = $1 AND v2_job.workspace_id = $2\n\n UNION ALL\n\n -- Recursive case: fetch children of previously found jobs\n SELECT q.id, j.parent_job, t.depth + 1\n FROM v2_job_queue q\n INNER JOIN v2_job j USING (id)\n INNER JOIN job_tree t ON t.id = j.parent_job\n WHERE j.workspace_id = $2 AND t.depth < 500 -- Limit recursion depth to 500\n)\nSELECT id AS id, depth\nFROM job_tree\nORDER BY depth, id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "depth",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5e8ba1850b2520bd4bf030f53f1f2e6606bfc0f440fbdbbdf4dd3234068c9345"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as, \n j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: JobTriggerKind\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle\n FROM v2_job j LEFT JOIN v2_job_queue q ON j.id = q.id\n WHERE j.id = $1 AND j.workspace_id = $2",
|
||||
"query": "SELECT\n j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as,\n j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: JobTriggerKind\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle\n FROM v2_job j LEFT JOIN v2_job_queue q ON j.id = q.id\n WHERE j.id = $1 AND j.workspace_id = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -231,5 +231,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "a1745a4f525b251d2f5a602ab2b2ede46b4471e21b11f607573a844013911abe"
|
||||
"hash": "67e25a7c19ea0ffaf7ea5303fcd04af5a7eb488c76f783e690af0c2153b1d6a8"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (\n id, -- 1\n workspace_id, -- 2\n raw_code, -- 3\n raw_lock, -- 4\n raw_flow, -- 5\n tag, -- 6\n parent_job, -- 7\n created_by, -- 8\n permissioned_as, -- 9\n runnable_id, -- 10\n runnable_path, -- 11\n args, -- 12\n kind, -- 13\n trigger, -- 14\n script_lang, -- 15\n same_worker, -- 16\n pre_run_error, -- 17 \n permissioned_as_email, -- 18\n visible_to_owner, -- 19\n flow_innermost_root_job, -- 20\n root_job, -- 38\n concurrent_limit, -- 21\n concurrency_time_window_s, -- 22\n timeout, -- 23\n flow_step_id, -- 24\n cache_ttl, -- 25\n priority, -- 26\n trigger_kind, -- 39\n script_entrypoint_override, -- 12\n preprocessed, -- 27,\n labels -- 44\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27, $44)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email) \n values ($1, $32, $33, $34, $35, $36, $37, $2, $41) \n ON CONFLICT (job_id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, is_admin = EXCLUDED.is_admin, is_operator = EXCLUDED.is_operator, folders = EXCLUDED.folders, groups = EXCLUDED.groups, workspace_id = EXCLUDED.workspace_id, end_user_email = EXCLUDED.end_user_email\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority, cache_ignore_s3_path, runnable_settings_handle)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31, $42, $43)",
|
||||
"query": "WITH inserted_job AS (\n INSERT INTO v2_job (\n id, -- 1\n workspace_id, -- 2\n raw_code, -- 3\n raw_lock, -- 4\n raw_flow, -- 5\n tag, -- 6\n parent_job, -- 7\n created_by, -- 8\n permissioned_as, -- 9\n runnable_id, -- 10\n runnable_path, -- 11\n args, -- 12\n kind, -- 13\n trigger, -- 14\n script_lang, -- 15\n same_worker, -- 16\n pre_run_error, -- 17\n permissioned_as_email, -- 18\n visible_to_owner, -- 19\n flow_innermost_root_job, -- 20\n root_job, -- 38\n concurrent_limit, -- 21\n concurrency_time_window_s, -- 22\n timeout, -- 23\n flow_step_id, -- 24\n cache_ttl, -- 25\n priority, -- 26\n trigger_kind, -- 39\n script_entrypoint_override, -- 12\n preprocessed, -- 27,\n labels -- 44\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18,\n $19, $20, $38, $21, $22, $23, $24, $25, $26, $39::job_trigger_kind,\n ($12::JSONB)->>'_ENTRYPOINT_OVERRIDE', $27, $44)\n ),\n inserted_runtime AS (\n INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)\n ),\n inserted_job_perms AS (\n INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email)\n values ($1, $32, $33, $34, $35, $36, $37, $2, $41)\n ON CONFLICT (job_id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, is_admin = EXCLUDED.is_admin, is_operator = EXCLUDED.is_operator, folders = EXCLUDED.folders, groups = EXCLUDED.groups, workspace_id = EXCLUDED.workspace_id, end_user_email = EXCLUDED.end_user_email\n )\n INSERT INTO v2_job_queue\n (workspace_id, id, running, scheduled_for, started_at, tag, priority, cache_ignore_s3_path, runnable_settings_handle)\n VALUES ($2, $1, $28, COALESCE($29, now()), CASE WHEN $27 OR $40 THEN now() END, $30, $31, $42, $43)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -139,5 +139,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5425e2c5e29fc5145dde5ea53d5307ca90d8dd76da2ca560b310b12820be2576"
|
||||
"hash": "756f82b72af07fd690f37b2e16ed2d390604f4fc4cb330842a88d5764cbcf0c6"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT usage.usage + 1 FROM usage \n WHERE is_workspace IS FALSE AND\n month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "94ff696b4d3904e3823ef637fa8f1f0d0bdac01040c81b31514326417eb58cee"
|
||||
}
|
||||
24
backend/.sqlx/query-a7f4b8f5d8d9074ce60c9fb6bc09c8611ffcb833f28b497706f7630dbae33e08.json
generated
Normal file
24
backend/.sqlx/query-a7f4b8f5d8d9074ce60c9fb6bc09c8611ffcb833f28b497706f7630dbae33e08.json
generated
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT\n flow_status->>'step' = '0'\n AND (\n jsonb_array_length(flow_status->'modules') = 0\n OR flow_status->'modules'->0->>'type' = 'WaitingForPriorSteps'\n OR (\n flow_status->'modules'->0->>'type' = 'Failure'\n AND flow_status->'modules'->0->>'job' = $1\n )\n )\n FROM v2_job_completed WHERE id = $2 AND workspace_id = $3",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "a7f4b8f5d8d9074ce60c9fb6bc09c8611ffcb833f28b497706f7630dbae33e08"
|
||||
}
|
||||
29
backend/.sqlx/query-b22e933ea5e9297dd4342310b0561fb43786010f410693e26c9d564e5eff09ff.json
generated
Normal file
29
backend/.sqlx/query-b22e933ea5e9297dd4342310b0561fb43786010f410693e26c9d564e5eff09ff.json
generated
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\nWITH RECURSIVE job_tree AS (\n -- Base case: direct children of the given parent job\n SELECT id, parent_job, 1 AS depth\n FROM v2_job_queue\n INNER JOIN v2_job USING (id)\n WHERE parent_job = $1 AND v2_job.workspace_id = $2\n\n UNION ALL\n\n -- Recursive case: fetch children of previously found jobs\n SELECT q.id, j.parent_job, t.depth + 1\n FROM v2_job_queue q\n INNER JOIN v2_job j USING (id)\n INNER JOIN job_tree t ON t.id = j.parent_job\n WHERE j.workspace_id = $2 AND t.depth < 500 -- Limit recursion depth to 500\n)\nSELECT id AS id, depth\nFROM job_tree\nORDER BY depth, id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "depth",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "b22e933ea5e9297dd4342310b0561fb43786010f410693e26c9d564e5eff09ff"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT \n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n v2_job_queue.runnable_settings_handle,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n cache_ignore_s3_path,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
|
||||
"query": "SELECT\n v2_job_queue.workspace_id,\n v2_job_queue.id,\n v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",\n v2_job.parent_job,\n v2_job.created_by,\n v2_job_queue.started_at,\n v2_job_queue.runnable_settings_handle,\n scheduled_for,\n runnable_path,\n kind as \"kind: JobKind\",\n runnable_id as \"runnable_id: ScriptHash\",\n canceled_reason,\n canceled_by,\n permissioned_as,\n permissioned_as_email,\n flow_status as \"flow_status: sqlx::types::Json<Box<RawValue>>\",\n v2_job.tag,\n script_lang as \"script_lang: ScriptLang\",\n same_worker,\n pre_run_error,\n concurrent_limit,\n concurrency_time_window_s,\n flow_innermost_root_job,\n root_job,\n timeout,\n flow_step_id,\n cache_ttl,\n cache_ignore_s3_path,\n v2_job_queue.priority,\n preprocessed,\n script_entrypoint_override,\n trigger,\n trigger_kind as \"trigger_kind: JobTriggerKind\",\n visible_to_owner,\n NULL as permissioned_as_end_user_email\n FROM v2_job_queue INNER JOIN v2_job ON v2_job.id = v2_job_queue.id LEFT JOIN v2_job_status ON v2_job_status.id = v2_job_queue.id WHERE v2_job_queue.id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -308,5 +308,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "5bf200f2c8db25ddf231b564503c6c70f7f3958564a79bb0c6b3863b1ebb0cbf"
|
||||
"hash": "d4211392e174a0e8f89c7fcebdf120e5b0f629f9f04e08a2982df33ff23ac7a9"
|
||||
}
|
||||
22
backend/.sqlx/query-efc83ffbccd4e354b9afaddd25db2edf3aa5e6a330954ff81c38592c0779df4a.json
generated
Normal file
22
backend/.sqlx/query-efc83ffbccd4e354b9afaddd25db2edf3aa5e6a330954ff81c38592c0779df4a.json
generated
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT usage.usage + 1 FROM usage\n WHERE is_workspace IS TRUE AND\n month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)\n AND id = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "?column?",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "efc83ffbccd4e354b9afaddd25db2edf3aa5e6a330954ff81c38592c0779df4a"
|
||||
}
|
||||
154
backend/Cargo.lock
generated
154
backend/Cargo.lock
generated
@@ -10667,7 +10667,7 @@ version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"multimap",
|
||||
@@ -16013,7 +16013,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -16092,7 +16092,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-alerting"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16105,7 +16105,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argon2",
|
||||
@@ -16246,7 +16246,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-agent-workers"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16269,7 +16269,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-assets"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16282,7 +16282,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-auth"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16308,7 +16308,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-client"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -16318,7 +16318,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-configs"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16335,7 +16335,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-debug"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"base64 0.22.1",
|
||||
@@ -16358,7 +16358,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-embeddings"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16381,7 +16381,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flow-conversations"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16397,7 +16397,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-flows"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16418,7 +16418,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-groups"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16439,7 +16439,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-inputs"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16453,7 +16453,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-integration-tests"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -16484,7 +16484,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-jobs"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16509,7 +16509,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-npm-proxy"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"flate2",
|
||||
@@ -16527,7 +16527,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-openapi"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16549,7 +16549,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-schedule"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16569,7 +16569,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-scripts"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16599,7 +16599,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-settings"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16626,7 +16626,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-sse"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"serde",
|
||||
@@ -16638,7 +16638,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-users"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"axum 0.8.4",
|
||||
@@ -16663,7 +16663,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workers"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16677,7 +16677,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-api-workspaces"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"axum 0.8.4",
|
||||
"chrono",
|
||||
@@ -16710,7 +16710,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-audit"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"lazy_static",
|
||||
@@ -16724,7 +16724,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-autoscaling"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.8.4",
|
||||
@@ -16743,7 +16743,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-common"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"aho-corasick",
|
||||
@@ -16847,7 +16847,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-dep-map"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"itertools 0.14.0",
|
||||
@@ -16866,7 +16866,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-git-sync"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
@@ -16881,7 +16881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-indexer"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"astral-tokio-tar",
|
||||
@@ -16905,7 +16905,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-jseval"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -16922,7 +16922,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-macros"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"lazy_static",
|
||||
@@ -16938,7 +16938,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-mcp"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16959,7 +16959,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-native-triggers"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -16990,7 +16990,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-oauth"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -17015,7 +17015,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-object-store"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@@ -17049,7 +17049,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-operator"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
@@ -17067,7 +17067,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"convert_case 0.6.0",
|
||||
"serde",
|
||||
@@ -17076,7 +17076,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-bash"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17088,7 +17088,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-csharp"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -17100,7 +17100,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-go"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gosyn",
|
||||
@@ -17112,7 +17112,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-graphql"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17124,7 +17124,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-java"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -17136,7 +17136,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-nu"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"nu-parser",
|
||||
@@ -17147,7 +17147,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-php"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -17158,7 +17158,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
@@ -17170,7 +17170,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-asset"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -17181,7 +17181,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-py-imports"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17203,7 +17203,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-r"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde_json",
|
||||
@@ -17215,7 +17215,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ruby"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17229,7 +17229,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-rust"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.6.0",
|
||||
@@ -17246,7 +17246,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17259,7 +17259,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-sql-asset"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -17271,7 +17271,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"lazy_static",
|
||||
@@ -17289,7 +17289,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-ts-asset"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde-wasm-bindgen",
|
||||
@@ -17305,7 +17305,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-wac"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"rustpython-ast",
|
||||
@@ -17321,7 +17321,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-parser-yaml"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -17332,7 +17332,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-queue"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17369,7 +17369,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-runtime-nativets"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"const_format",
|
||||
@@ -17407,7 +17407,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-sql-datatype-parser-wasm"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"wasm-bindgen",
|
||||
@@ -17418,7 +17418,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-store"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-recursion",
|
||||
@@ -17448,7 +17448,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-test-utils"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17472,7 +17472,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17505,7 +17505,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-email"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17525,7 +17525,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-gcp"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17559,7 +17559,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-http"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17595,7 +17595,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-kafka"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17618,7 +17618,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-mqtt"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17642,7 +17642,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-nats"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-nats",
|
||||
@@ -17666,7 +17666,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-postgres"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17701,7 +17701,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-sqs"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17729,7 +17729,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-trigger-websocket"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -17752,7 +17752,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-types"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.9.4",
|
||||
@@ -17771,7 +17771,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-once-cell",
|
||||
@@ -17882,7 +17882,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windmill-worker-volumes"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "windmill"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
authors.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
@@ -85,7 +85,7 @@ members = [
|
||||
exclude = ["./windmill-duckdb-ffi-internal", "./parsers/windmill-parser-wasm"]
|
||||
|
||||
[workspace.package]
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
authors = ["Ruben Fiszel <ruben@windmill.dev>"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: "3.0.3"
|
||||
|
||||
info:
|
||||
version: 1.684.0
|
||||
version: 1.684.1
|
||||
title: Windmill API
|
||||
|
||||
contact:
|
||||
|
||||
@@ -549,15 +549,11 @@ pub fn gemini_response_to_openai(parsed: &GeminiParsedEvent, model: &str) -> ser
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, tc)| {
|
||||
serde_json::json!({
|
||||
"index": i,
|
||||
"id": format!("call_{}", uuid::Uuid::new_v4().simple()),
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tc.name,
|
||||
"arguments": serde_json::to_string(&tc.args).unwrap_or_default()
|
||||
}
|
||||
})
|
||||
openai_tool_call_json(
|
||||
tc,
|
||||
format!("call_{}", uuid::Uuid::new_v4().simple()),
|
||||
Some(i),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -623,7 +619,6 @@ pub fn gemini_event_to_openai_sse_chunks(
|
||||
}
|
||||
|
||||
for tc in &parsed.tool_calls {
|
||||
let args_str = serde_json::to_string(&tc.args).unwrap_or_default();
|
||||
let call_id = format!("call_{}", uuid::Uuid::new_v4().simple());
|
||||
let chunk = serde_json::json!({
|
||||
"id": id,
|
||||
@@ -632,15 +627,7 @@ pub fn gemini_event_to_openai_sse_chunks(
|
||||
"choices": [{
|
||||
"index": 0,
|
||||
"delta": {
|
||||
"tool_calls": [{
|
||||
"index": *tool_call_index,
|
||||
"id": call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tc.name,
|
||||
"arguments": args_str,
|
||||
}
|
||||
}]
|
||||
"tool_calls": [openai_tool_call_json(tc, call_id, Some(*tool_call_index))]
|
||||
},
|
||||
"finish_reason": null,
|
||||
}]
|
||||
@@ -729,3 +716,102 @@ fn extract_candidates_into(candidates: &[GeminiSSECandidate], parsed: &mut Gemin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn openai_tool_call_json(
|
||||
tool_call: &GeminiToolCallEvent,
|
||||
call_id: String,
|
||||
index: Option<usize>,
|
||||
) -> serde_json::Value {
|
||||
let mut value = serde_json::json!({
|
||||
"id": call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_call.name,
|
||||
"arguments": serde_json::to_string(&tool_call.args).unwrap_or_default(),
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(index) = index {
|
||||
value["index"] = serde_json::json!(index);
|
||||
}
|
||||
|
||||
if let Some(extra_content) = tool_call.to_extra_content() {
|
||||
if let Ok(extra_content) = serde_json::to_value(extra_content) {
|
||||
value["extra_content"] = extra_content;
|
||||
}
|
||||
}
|
||||
|
||||
value
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
gemini_event_to_openai_sse_chunks, gemini_response_to_openai, GeminiParsedEvent,
|
||||
GeminiToolCallEvent,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn gemini_response_to_openai_preserves_thought_signature() {
|
||||
let parsed = GeminiParsedEvent {
|
||||
tool_calls: vec![GeminiToolCallEvent {
|
||||
name: "get_instructions_for_code_generation".to_string(),
|
||||
args: serde_json::json!({ "language": "rust" }),
|
||||
thought_signature: Some("test-thought-signature".to_string()),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let response = gemini_response_to_openai(&parsed, "gemini-3.1-pro");
|
||||
let tool_call = &response["choices"][0]["message"]["tool_calls"][0];
|
||||
|
||||
assert_eq!(
|
||||
tool_call["extra_content"]["google"]["thought_signature"],
|
||||
"test-thought-signature"
|
||||
);
|
||||
assert_eq!(
|
||||
tool_call["function"]["name"],
|
||||
"get_instructions_for_code_generation"
|
||||
);
|
||||
assert_eq!(
|
||||
tool_call["function"]["arguments"],
|
||||
"{\"language\":\"rust\"}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gemini_streaming_chunks_preserve_thought_signature() {
|
||||
let parsed = GeminiParsedEvent {
|
||||
tool_calls: vec![GeminiToolCallEvent {
|
||||
name: "get_instructions_for_code_generation".to_string(),
|
||||
args: serde_json::json!({ "language": "rust" }),
|
||||
thought_signature: Some("stream-thought-signature".to_string()),
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut tool_call_index = 0;
|
||||
let chunks = gemini_event_to_openai_sse_chunks(
|
||||
&parsed,
|
||||
"chatcmpl-test",
|
||||
"gemini-3.1-pro",
|
||||
&mut tool_call_index,
|
||||
);
|
||||
|
||||
assert_eq!(chunks.len(), 1);
|
||||
|
||||
let payload = chunks[0]
|
||||
.strip_prefix("data: ")
|
||||
.and_then(|chunk| chunk.strip_suffix("\n\n"))
|
||||
.expect("chunk should be wrapped as SSE data");
|
||||
let parsed_chunk: serde_json::Value =
|
||||
serde_json::from_str(payload).expect("chunk should be valid JSON");
|
||||
let tool_call = &parsed_chunk["choices"][0]["delta"]["tool_calls"][0];
|
||||
|
||||
assert_eq!(
|
||||
tool_call["extra_content"]["google"]["thought_signature"],
|
||||
"stream-thought-signature"
|
||||
);
|
||||
assert_eq!(tool_call["index"], 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +328,7 @@ pub async fn cancel_job<'c>(
|
||||
WITH RECURSIVE job_tree AS (
|
||||
-- Base case: direct children of the given parent job
|
||||
SELECT id, parent_job, 1 AS depth
|
||||
FROM v2_job_queue
|
||||
FROM v2_job_queue
|
||||
INNER JOIN v2_job USING (id)
|
||||
WHERE parent_job = $1 AND v2_job.workspace_id = $2
|
||||
|
||||
@@ -913,8 +913,6 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
) -> windmill_common::error::Result<(Option<Uuid>, i64, bool, Option<serde_json::Value>)> {
|
||||
// let start = std::time::Instant::now();
|
||||
|
||||
let mut tx = db.begin().warn_after_seconds(10).await?;
|
||||
|
||||
let job_id = completed_job.id;
|
||||
// tracing::error!("1 {:?}", start.elapsed());
|
||||
|
||||
@@ -931,6 +929,8 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
return value;
|
||||
}
|
||||
|
||||
let mut tx = db.begin().warn_after_seconds(10).await?;
|
||||
|
||||
let duration = sqlx::query_scalar!(
|
||||
"INSERT INTO v2_job_completed AS cj
|
||||
( workspace_id
|
||||
@@ -1141,13 +1141,13 @@ async fn commit_completed_job<T: Serialize + Send + Sync + ValidableJson>(
|
||||
|| from_cache
|
||||
|| !success
|
||||
&& sqlx::query_scalar!(
|
||||
"SELECT
|
||||
flow_status->>'step' = '0'
|
||||
"SELECT
|
||||
flow_status->>'step' = '0'
|
||||
AND (
|
||||
jsonb_array_length(flow_status->'modules') = 0
|
||||
OR flow_status->'modules'->0->>'type' = 'WaitingForPriorSteps'
|
||||
jsonb_array_length(flow_status->'modules') = 0
|
||||
OR flow_status->'modules'->0->>'type' = 'WaitingForPriorSteps'
|
||||
OR (
|
||||
flow_status->'modules'->0->>'type' = 'Failure'
|
||||
flow_status->'modules'->0->>'type' = 'Failure'
|
||||
AND flow_status->'modules'->0->>'job' = $1
|
||||
)
|
||||
)
|
||||
@@ -2788,7 +2788,7 @@ pub async fn get_mini_pulled_job<'c>(
|
||||
) -> windmill_common::error::Result<Option<MiniPulledJob>> {
|
||||
let job = sqlx::query_as!(
|
||||
MiniPulledJob,
|
||||
"SELECT
|
||||
"SELECT
|
||||
v2_job_queue.workspace_id,
|
||||
v2_job_queue.id,
|
||||
v2_job.args as \"args: sqlx::types::Json<HashMap<String, Box<RawValue>>>\",
|
||||
@@ -3935,7 +3935,7 @@ pub async fn get_result_and_success_by_id_from_flow(
|
||||
r#"WITH modules AS (
|
||||
SELECT jsonb_array_elements(flow_status->'modules') AS module
|
||||
FROM {}
|
||||
WHERE id = $1
|
||||
WHERE id = $1
|
||||
)
|
||||
SELECT module->>'type' = 'Success'
|
||||
FROM modules
|
||||
@@ -4248,8 +4248,8 @@ pub fn get_mini_completed_job<'a, 'e, A: sqlx::Acquire<'e, Database = Postgres>
|
||||
let mut conn = db.acquire().await?;
|
||||
sqlx::query_as!(
|
||||
MiniCompletedJob,
|
||||
"SELECT
|
||||
j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as,
|
||||
"SELECT
|
||||
j.id, j.workspace_id, j.runnable_id AS \"runnable_id: ScriptHash\", q.scheduled_for, q.started_at, j.parent_job, j.flow_innermost_root_job, j.runnable_path, j.kind as \"kind!: JobKind\", j.permissioned_as,
|
||||
j.created_by, j.script_lang AS \"script_lang: ScriptLang\", j.permissioned_as_email, j.flow_step_id, j.trigger_kind AS \"trigger_kind: JobTriggerKind\", j.trigger, j.priority, j.concurrent_limit, j.tag, j.cache_ttl, q.cache_ignore_s3_path, q.runnable_settings_handle
|
||||
FROM v2_job j LEFT JOIN v2_job_queue q ON j.id = q.id
|
||||
WHERE j.id = $1 AND j.workspace_id = $2",
|
||||
@@ -4619,7 +4619,7 @@ async fn push_inner<'c, 'd>(
|
||||
user_usage
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT usage.usage + 1 FROM usage
|
||||
"SELECT usage.usage + 1 FROM usage
|
||||
WHERE is_workspace IS FALSE AND
|
||||
month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)
|
||||
AND id = $1",
|
||||
@@ -4675,7 +4675,7 @@ async fn push_inner<'c, 'd>(
|
||||
workspace_usage
|
||||
} else {
|
||||
sqlx::query_scalar!(
|
||||
"SELECT usage.usage + 1 FROM usage
|
||||
"SELECT usage.usage + 1 FROM usage
|
||||
WHERE is_workspace IS TRUE AND
|
||||
month_ = EXTRACT(YEAR FROM current_date) * 12 + EXTRACT(MONTH FROM current_date)
|
||||
AND id = $1",
|
||||
@@ -5728,7 +5728,7 @@ async fn push_inner<'c, 'd>(
|
||||
trigger, -- 14
|
||||
script_lang, -- 15
|
||||
same_worker, -- 16
|
||||
pre_run_error, -- 17
|
||||
pre_run_error, -- 17
|
||||
permissioned_as_email, -- 18
|
||||
visible_to_owner, -- 19
|
||||
flow_innermost_root_job, -- 20
|
||||
@@ -5751,8 +5751,8 @@ async fn push_inner<'c, 'd>(
|
||||
INSERT INTO v2_job_runtime (id, ping) VALUES ($1, null)
|
||||
),
|
||||
inserted_job_perms AS (
|
||||
INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email)
|
||||
values ($1, $32, $33, $34, $35, $36, $37, $2, $41)
|
||||
INSERT INTO job_perms (job_id, email, username, is_admin, is_operator, folders, groups, workspace_id, end_user_email)
|
||||
values ($1, $32, $33, $34, $35, $36, $37, $2, $41)
|
||||
ON CONFLICT (job_id) DO UPDATE SET email = EXCLUDED.email, username = EXCLUDED.username, is_admin = EXCLUDED.is_admin, is_operator = EXCLUDED.is_operator, folders = EXCLUDED.folders, groups = EXCLUDED.groups, workspace_id = EXCLUDED.workspace_id, end_user_email = EXCLUDED.end_user_email
|
||||
)
|
||||
INSERT INTO v2_job_queue
|
||||
|
||||
@@ -15,17 +15,7 @@ clone_newnet: false
|
||||
clone_newuser: {CLONE_NEWUSER}
|
||||
clone_newcgroup: false
|
||||
|
||||
uidmap {
|
||||
inside_id: "1000"
|
||||
outside_id: ""
|
||||
count: 1
|
||||
}
|
||||
|
||||
gidmap {
|
||||
inside_id: "1000"
|
||||
outside_id: ""
|
||||
count: 1
|
||||
}
|
||||
{UIDGIDMAP}
|
||||
|
||||
skip_setsid: true
|
||||
keep_caps: false
|
||||
|
||||
@@ -2079,6 +2079,9 @@ try {{
|
||||
.replace("{LANG}", if annotation.nodejs { "nodejs" } else { "bun" })
|
||||
.replace("{JOB_DIR}", job_dir)
|
||||
.replace("{CLONE_NEWUSER}", &(!*DISABLE_NUSER).to_string())
|
||||
.replace("{UIDGIDMAP}", if *DISABLE_NUSER { "" } else {
|
||||
"uidmap {\n inside_id: \"1000\"\n outside_id: \"\"\n count: 1\n}\n\ngidmap {\n inside_id: \"1000\"\n outside_id: \"\"\n count: 1\n}"
|
||||
})
|
||||
.replace(
|
||||
"{SHARED_MOUNT}",
|
||||
&shared_mount.replace(
|
||||
|
||||
@@ -2,7 +2,7 @@ import { sleep } from "https://deno.land/x/sleep@v1.2.1/mod.ts";
|
||||
import * as windmill from "https://deno.land/x/windmill@v1.174.0/mod.ts";
|
||||
import * as api from "https://deno.land/x/windmill@v1.174.0/windmill-api/index.ts";
|
||||
|
||||
export const VERSION = "v1.684.0";
|
||||
export const VERSION = "v1.684.1";
|
||||
|
||||
export async function login(email: string, password: string): Promise<string> {
|
||||
return await windmill.UserService.login({
|
||||
|
||||
@@ -78,7 +78,7 @@ export {
|
||||
token,
|
||||
};
|
||||
|
||||
export const VERSION = "1.684.0";
|
||||
export const VERSION = "1.684.1";
|
||||
|
||||
// Re-exported from constants.ts to maintain backwards compatibility
|
||||
export { WM_FORK_PREFIX } from "./core/constants.ts";
|
||||
|
||||
54
frontend/package-lock.json
generated
54
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.684.0",
|
||||
"version": "1.684.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.684.0",
|
||||
"version": "1.684.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@@ -844,6 +844,7 @@
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -855,6 +856,7 @@
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -865,6 +867,7 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1354,6 +1357,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
||||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1510,6 +1514,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1526,6 +1531,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1542,6 +1548,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1558,6 +1565,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1574,6 +1582,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1590,6 +1599,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1606,6 +1616,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1622,6 +1633,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1638,6 +1650,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1654,6 +1667,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1670,6 +1684,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1686,6 +1701,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1702,6 +1718,7 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1718,6 +1735,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1734,6 +1752,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2039,6 +2058,7 @@
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -6814,7 +6834,7 @@
|
||||
"version": "1.21.7",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
@@ -7313,6 +7333,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7333,6 +7354,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7353,6 +7375,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7373,6 +7396,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7393,6 +7417,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7413,6 +7438,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7433,6 +7459,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7453,6 +7480,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7473,6 +7501,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7493,6 +7522,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7513,6 +7543,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -12081,6 +12112,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-eslint-parser": {
|
||||
"version": "0.43.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
|
||||
@@ -12811,7 +12857,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.684.0",
|
||||
"version": "1.684.1",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type Script
|
||||
} from '$lib/gen'
|
||||
import { initialCode } from '$lib/script_helpers'
|
||||
import { inferAssets } from '$lib/infer'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import { getScriptByPath } from '$lib/scripts'
|
||||
import { get } from 'svelte/store'
|
||||
@@ -84,6 +85,11 @@ export async function createInlineScriptModule(
|
||||
): Promise<[FlowModule, FlowModuleState]> {
|
||||
const code = initialCode(language, kind, subkind)
|
||||
|
||||
// Needed when the predefined code has assets in it
|
||||
const inferResult = await inferAssets(language, code)
|
||||
const assets =
|
||||
inferResult.status === 'ok' && inferResult.assets.length > 0 ? inferResult.assets : undefined
|
||||
|
||||
const flowModule: FlowModule = {
|
||||
id,
|
||||
summary,
|
||||
@@ -92,7 +98,8 @@ export async function createInlineScriptModule(
|
||||
content: code,
|
||||
language,
|
||||
input_transforms: {},
|
||||
...(kind === 'trigger' ? { is_trigger: true } : {})
|
||||
...(kind === 'trigger' ? { is_trigger: true } : {}),
|
||||
...(assets ? { assets } : {})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
wmill = ">=1.684.0"
|
||||
wmill = ">=1.684.1"
|
||||
sendgrid = "*"
|
||||
mysql-connector-python = "*"
|
||||
pymongo = "*"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: '3.0.3'
|
||||
|
||||
info:
|
||||
version: 1.684.0
|
||||
version: 1.684.1
|
||||
title: OpenFlow Spec
|
||||
contact:
|
||||
name: Ruben Fiszel
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
RootModule = 'WindmillClient.psm1'
|
||||
|
||||
# Version number of this module.
|
||||
ModuleVersion = '1.684.0'
|
||||
ModuleVersion = '1.684.1'
|
||||
|
||||
# Supported PSEditions
|
||||
# CompatiblePSEditions = @()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "wmill"
|
||||
version = "1.684.0"
|
||||
version = "1.684.1"
|
||||
description = "A client library for accessing Windmill server wrapping the Windmill client API"
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://windmill.dev"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@windmill/windmill",
|
||||
"version": "1.684.0",
|
||||
"version": "1.684.1",
|
||||
"exports": "./src/index.ts",
|
||||
"publish": {
|
||||
"exclude": ["!src", "./s3Types.ts", "./sqlUtils.ts", "./client.ts"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "windmill-client",
|
||||
"description": "Windmill SDK client for browsers and Node.js",
|
||||
"version": "1.684.0",
|
||||
"version": "1.684.1",
|
||||
"author": "Ruben Fiszel",
|
||||
"license": "Apache 2.0",
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.684.0
|
||||
1.684.1
|
||||
|
||||
Reference in New Issue
Block a user