Skip to content

Commit 7b37534

Browse files
committed
fix(jev): validate batch answers against requested questions
1 parent 901ded6 commit 7b37534

4 files changed

Lines changed: 80 additions & 13 deletions

File tree

‎apps/sim/tools/jev/evaluate.ts‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {
22
buildJevBody,
33
JEV_COMMON_PARAMS,
44
JEV_REQUEST,
5-
parseJevJson,
5+
parseJevQuestions,
66
parseJevResponse,
77
} from '@/tools/jev/shared'
88
import type { JevEvaluateParams, JevEvaluateResponse } from '@/tools/jev/types'
@@ -30,10 +30,22 @@ export const jevEvaluateTool: ToolConfig<JevEvaluateParams, JevEvaluateResponse>
3030
mode: 'project',
3131
select: (params) => ({ state: params.state, questions: params.questions }),
3232
},
33-
body: (params) => buildJevBody(params, parseJevJson(params.questions, 'questions')),
33+
body: (params) => buildJevBody(params, params.questions),
3434
},
35-
transformResponse: async (response) => {
35+
transformResponse: async (response, params) => {
3636
const { model, usage, answers } = await parseJevResponse(response)
37+
if (!params) throw new Error('Jev batch response validation requires request parameters')
38+
const questions = parseJevQuestions(params.questions)
39+
if (
40+
Object.keys(answers).length !== Object.keys(questions).length ||
41+
Object.entries(questions).some(
42+
([id, question]) => !Object.hasOwn(answers, id) || answers[id].type !== question.type
43+
)
44+
) {
45+
throw new Error(
46+
'TypeSafe returned Jev answers that do not match the requested question IDs and types'
47+
)
48+
}
3749
return { success: true, output: { model, usage, answers } }
3850
},
3951
outputs: {

‎apps/sim/tools/jev/jev-block.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@ describe('Jev workflow execution', () => {
139139
model: 'jev-1.13.0',
140140
answers,
141141
usage: { input_tokens: 50, output_tokens: 8 },
142-
})
142+
}),
143+
{ apiKey: params.apiKey, state: params.state, questions: params.questions }
143144
)
144145
})
145146
const output = await new GenericBlockHandler().execute(context(), block('jev_evaluate'), {

‎apps/sim/tools/jev/jev.test.ts‎

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it } from 'vitest'
22
import { ResolvedSecretTraceRegistry } from '@/executor/utils/resolved-secret-trace-registry'
33
import { jevChoiceTool, jevEvaluateTool, jevNoulTool, jevScoreTool } from '@/tools/jev'
4+
import type { JevEvaluateParams } from '@/tools/jev/types'
45
import { prepareToolRequest } from '@/tools/request-transport'
56

67
const BASE = { apiKey: 'test-key', state: 'My payouts have been failing for three days.' }
@@ -19,6 +20,22 @@ const SCORE = {
1920
confidence: 0.92,
2021
} as const
2122
const NOUL = { type: 'noul', noul: 0.95 } as const
23+
const BATCH_PARAMS: JevEvaluateParams = {
24+
...BASE,
25+
questions: {
26+
department: {
27+
type: 'choice',
28+
instructions: 'Which team?',
29+
criteria: { billing: null, technical: null },
30+
},
31+
frustration: {
32+
type: 'score',
33+
instructions: 'How frustrated?',
34+
criteria: ['Calm', 'Frustrated', 'Very angry'],
35+
},
36+
is_urgent: { type: 'noul', instructions: 'Is this urgent?' },
37+
},
38+
}
2239

2340
function response(answers: Record<string, unknown>) {
2441
return Response.json({ model: 'jev-1.13.0', answers, usage: USAGE })
@@ -142,12 +159,44 @@ describe('Jev tools', () => {
142159
})
143160
})
144161

145-
it('preserves all mixed batch answer types and question IDs', async () => {
146-
const answers = { department: CHOICE, frustration: SCORE, is_urgent: NOUL }
147-
expect(await jevEvaluateTool.transformResponse!(response(answers))).toEqual({
148-
success: true,
149-
output: { model: 'jev-1.13.0', usage: USAGE, answers },
150-
})
162+
it.each([false, true])(
163+
'matches mixed batch answers regardless of order, serialized=%s',
164+
async (serialized) => {
165+
const answers = { department: CHOICE, frustration: SCORE, is_urgent: NOUL }
166+
const params = {
167+
...BATCH_PARAMS,
168+
questions: serialized ? JSON.stringify(BATCH_PARAMS.questions) : BATCH_PARAMS.questions,
169+
}
170+
expect(
171+
await jevEvaluateTool.transformResponse!(
172+
response({ is_urgent: NOUL, frustration: SCORE, department: CHOICE }),
173+
params
174+
)
175+
).toEqual({
176+
success: true,
177+
output: { model: 'jev-1.13.0', usage: USAGE, answers },
178+
})
179+
}
180+
)
181+
182+
it.each([
183+
['empty', {}],
184+
['partial', { department: CHOICE, frustration: SCORE }],
185+
['mismatched type', { department: NOUL, frustration: SCORE, is_urgent: NOUL }],
186+
['unexpected ID', { department: CHOICE, frustration: SCORE, other: NOUL }],
187+
['extra answer', { department: CHOICE, frustration: SCORE, is_urgent: NOUL, other: NOUL }],
188+
])('rejects a %s batch response', async (_label, answers) => {
189+
await expect(
190+
jevEvaluateTool.transformResponse!(response(answers), BATCH_PARAMS)
191+
).rejects.toThrow(
192+
'TypeSafe returned Jev answers that do not match the requested question IDs and types'
193+
)
194+
})
195+
196+
it('requires request context to validate batch answers', async () => {
197+
await expect(
198+
jevEvaluateTool.transformResponse!(response({ department: CHOICE }))
199+
).rejects.toThrow('Jev batch response validation requires request parameters')
151200
})
152201

153202
it('supports structured Score legends documented by the TypeSafe SDK', async () => {

‎apps/sim/tools/jev/shared.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,19 +115,24 @@ export function parseJevJson(value: unknown, field: string): unknown {
115115
}
116116
}
117117

118-
export function buildJevBody(params: JevBaseParams, questions: unknown) {
119-
const parsedQuestions = questionsSchema.safeParse(questions)
118+
export function parseJevQuestions(questions: unknown) {
119+
const parsedQuestions = questionsSchema.safeParse(parseJevJson(questions, 'questions'))
120120
if (!parsedQuestions.success) {
121121
throw new Error(
122122
'Invalid Jev questions: provide typed questions with instructions, 1–255 Choice options, 2–10 Score levels, or optional true/false Noul criteria'
123123
)
124124
}
125+
return parsedQuestions.data
126+
}
127+
128+
export function buildJevBody(params: JevBaseParams, questions: unknown) {
129+
const parsedQuestions = parseJevQuestions(questions)
125130
const state = contentSchema.safeParse(params.state)
126131
if (!state.success) throw new Error('Jev state must be text, a JSON object, or an array')
127132
return {
128133
model: params.model?.trim() || 'jev-1.13.0',
129134
state: state.data,
130-
questions: parsedQuestions.data,
135+
questions: parsedQuestions,
131136
}
132137
}
133138

0 commit comments

Comments
 (0)