mirror of
https://github.com/launchdarkly/ai-tooling.git
synced 2026-09-14 20:00:40 +08:00
130 lines
3.9 KiB
JavaScript
130 lines
3.9 KiB
JavaScript
/**
|
|
* Trajectory helpers shared across every suite.
|
|
*
|
|
* The agent provider returns output as a parsed object (via shared/transform.js):
|
|
* {
|
|
* response: "<final agent text>",
|
|
* trajectory: [
|
|
* { tool: "list-ai-configs", arguments: {...}, turn: 1 },
|
|
* { tool: "setup-ai-config", arguments: {...}, turn: 2 },
|
|
* ...
|
|
* ],
|
|
* tools_called: ["list-ai-configs", "setup-ai-config"],
|
|
* turn_count: 3
|
|
* }
|
|
*
|
|
* Convention for "X happens after Y" trajectory checks:
|
|
* - Use the FIRST occurrence of the prerequisite (Y): tools.indexOf(Y)
|
|
* - Use the LAST occurrence of the verifier (X): tools.lastIndexOf(X)
|
|
* - Rationale: agents often call `get-foo` before AND after mutating. Using
|
|
* indexOf for both would silently pass against the pre-mutation call.
|
|
*
|
|
* NOTE: promptfoo evaluates inline `type: javascript` assertions via
|
|
* new Function("output", "context", "process", body)
|
|
* which means `require` is NOT available. So this module is used by scripts
|
|
* (aggregate, diff-changed-skills) — inline YAML assertions implement the
|
|
* same patterns by hand using indexOf / lastIndexOf directly.
|
|
*/
|
|
|
|
function getTools(output) {
|
|
if (!output || typeof output !== "object") return [];
|
|
return Array.isArray(output.tools_called) ? output.tools_called : [];
|
|
}
|
|
|
|
function getTrajectory(output) {
|
|
if (!output || typeof output !== "object") return [];
|
|
return Array.isArray(output.trajectory) ? output.trajectory : [];
|
|
}
|
|
|
|
/** First call of a named tool. Returns { call, idx } — idx is -1 if not found. */
|
|
function firstCallOf(output, name) {
|
|
const trajectory = getTrajectory(output);
|
|
for (let i = 0; i < trajectory.length; i++) {
|
|
if (trajectory[i] && trajectory[i].tool === name) {
|
|
return { call: trajectory[i], idx: i };
|
|
}
|
|
}
|
|
return { call: null, idx: -1 };
|
|
}
|
|
|
|
/** Last call of a named tool. Returns { call, idx } — idx is -1 if not found. */
|
|
function lastCallOf(output, name) {
|
|
const trajectory = getTrajectory(output);
|
|
for (let i = trajectory.length - 1; i >= 0; i--) {
|
|
if (trajectory[i] && trajectory[i].tool === name) {
|
|
return { call: trajectory[i], idx: i };
|
|
}
|
|
}
|
|
return { call: null, idx: -1 };
|
|
}
|
|
|
|
function called(output, name) {
|
|
return getTools(output).includes(name);
|
|
}
|
|
|
|
function calledAny(output, names) {
|
|
const tools = getTools(output);
|
|
return names.some((n) => tools.includes(n));
|
|
}
|
|
|
|
function calledNone(output, names) {
|
|
const tools = getTools(output);
|
|
return names.every((n) => !tools.includes(n));
|
|
}
|
|
|
|
/**
|
|
* Assert that the LAST occurrence of `after` happens after the FIRST
|
|
* occurrence of `before`. Returns a promptfoo-shaped grading result.
|
|
*/
|
|
function expectAfter(output, { before, after }) {
|
|
const beforeIdx = firstCallOf(output, before).idx;
|
|
const afterIdx = lastCallOf(output, after).idx;
|
|
const pass = beforeIdx >= 0 && afterIdx > beforeIdx;
|
|
return {
|
|
pass,
|
|
score: pass ? 1 : 0,
|
|
reason: `${before}@${beforeIdx} ${after}@${afterIdx}`,
|
|
};
|
|
}
|
|
|
|
/** Assert that none of the listed tools were called. */
|
|
function expectNotCalled(output, names) {
|
|
const tools = getTools(output);
|
|
const hits = names.filter((n) => tools.includes(n));
|
|
const pass = hits.length === 0;
|
|
return {
|
|
pass,
|
|
score: pass ? 1 : 0,
|
|
reason: pass
|
|
? `correctly avoided ${names.join(", ")}`
|
|
: `called forbidden tools: ${hits.join(", ")}`,
|
|
};
|
|
}
|
|
|
|
/** Assert that at least one of the listed tools was called. */
|
|
function expectAnyCalled(output, names) {
|
|
const tools = getTools(output);
|
|
const hit = names.find((n) => tools.includes(n));
|
|
const pass = Boolean(hit);
|
|
return {
|
|
pass,
|
|
score: pass ? 1 : 0,
|
|
reason: pass
|
|
? `called ${hit}`
|
|
: `none of [${names.join(", ")}] called; tools: ${tools.join(" -> ") || "(none)"}`,
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
getTools,
|
|
getTrajectory,
|
|
firstCallOf,
|
|
lastCallOf,
|
|
called,
|
|
calledAny,
|
|
calledNone,
|
|
expectAfter,
|
|
expectNotCalled,
|
|
expectAnyCalled,
|
|
};
|