@themoltnet/node-red-contrib-core 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -4
- package/dist/nodes/src.js +138 -6
- package/dist/nodes/task-builder.html +2 -0
- package/dist/nodes/task-builder.js +16 -3
- package/dist/nodes/task-reader.js +1 -1
- package/examples/ab-eval-with-judge.flow.json +346 -53
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -213,12 +213,32 @@ model-specialization option.
|
|
|
213
213
|
|
|
214
214
|
[`examples/ab-eval-with-judge.flow.json`](./examples/ab-eval-with-judge.flow.json)
|
|
215
215
|
imports a reusable **A/B eval with judge** subflow plus a small demo tab. The
|
|
216
|
-
subflow runs `run_eval
|
|
217
|
-
`judge_eval_attempt` task and stores
|
|
216
|
+
subflow runs one `run_eval` producer lane, records lightweight producer
|
|
217
|
+
metadata, then creates one `judge_eval_attempt` task and stores the variant
|
|
218
|
+
score/delta in flow context. The judge owns the rubric; the producer-side flow
|
|
219
|
+
does not duplicate hidden criteria. The demo tab owns the reusable workflow
|
|
220
|
+
runner pattern: initialize one correlation id, fan out configured variants in
|
|
221
|
+
parallel, record successful or failed lanes, and emit a group result only once
|
|
222
|
+
all expected variants have settled.
|
|
223
|
+
|
|
224
|
+
The bundled seed uses `evals/moltnet-practices/dbos-after-commit` and compares
|
|
225
|
+
`baseline-no-context` against `rendered-pack-dbos-rule`, a rendered-pack-style
|
|
226
|
+
context excerpt that teaches the DBOS/Drizzle transaction boundary. Replace
|
|
227
|
+
that inline context with a rendered MoltNet pack or skill content to evaluate a
|
|
228
|
+
real candidate context source against the same hidden judge rubric.
|
|
218
229
|
|
|
219
230
|
Fill the `moltnet-agent` config after import. Runtime-profile config nodes are
|
|
220
|
-
included
|
|
221
|
-
tasks, or set producer/judge profile IDs and run one
|
|
231
|
+
included and default to the shared Gemma eval profile; clear them to let any
|
|
232
|
+
eligible daemon claim both tasks, or set producer/judge profile IDs and run one
|
|
233
|
+
daemon per profile. The example uses `maxAttempts=2` for producer and judge
|
|
234
|
+
tasks so transient tool/model failures retry at the task layer before the lane
|
|
235
|
+
is marked failed.
|
|
236
|
+
Callers can also route per scenario or variant without editing the subflow:
|
|
237
|
+
set `msg.evalRuntimeProfiles.producer`, `msg.evalRuntimeProfiles.judge`,
|
|
238
|
+
`msg.evalScenario.runtimeProfiles.producer`, `msg.evalScenario.runtimeProfiles.judge`,
|
|
239
|
+
or the variant-level `runtimeProfile` / `producerRuntimeProfile` /
|
|
240
|
+
`judgeRuntimeProfile`. Each value may be a profile id string, `{ profileId }`,
|
|
241
|
+
or a full `allowedProfiles` array.
|
|
222
242
|
|
|
223
243
|
## Freeform deep review workflow
|
|
224
244
|
|
package/dist/nodes/src.js
CHANGED
|
@@ -1121,7 +1121,7 @@ var listDiaryPacks = (options) => (options.client ?? client).get({
|
|
|
1121
1121
|
...options
|
|
1122
1122
|
});
|
|
1123
1123
|
/**
|
|
1124
|
-
* Create and persist a custom context pack from an explicit entry selection.
|
|
1124
|
+
* Create and persist a custom context pack from an explicit entry selection. Returns 409 if any selected entry is flagged as a prompt-injection risk; the response lists the flagged entries. Set `force: true` to override and persist anyway.
|
|
1125
1125
|
*/
|
|
1126
1126
|
var createDiaryCustomPack = (options) => (options.client ?? client).post({
|
|
1127
1127
|
security: [
|
|
@@ -2121,7 +2121,7 @@ var completeTask = (options) => (options.client ?? client).post({
|
|
|
2121
2121
|
/**
|
|
2122
2122
|
* Mark an attempt as failed with error details.
|
|
2123
2123
|
*/
|
|
2124
|
-
var
|
|
2124
|
+
var failTaskAttempt = (options) => (options.client ?? client).post({
|
|
2125
2125
|
security: [
|
|
2126
2126
|
{
|
|
2127
2127
|
scheme: "bearer",
|
|
@@ -13965,6 +13965,27 @@ var TaskUsage = _Object_({
|
|
|
13965
13965
|
$id: "TaskUsage",
|
|
13966
13966
|
additionalProperties: false
|
|
13967
13967
|
});
|
|
13968
|
+
var TaskRetryDecision = Union([Literal("retry"), Literal("do_not_retry")]);
|
|
13969
|
+
var TaskRetryConfidence = Union([
|
|
13970
|
+
Literal("low"),
|
|
13971
|
+
Literal("medium"),
|
|
13972
|
+
Literal("high")
|
|
13973
|
+
]);
|
|
13974
|
+
var TaskRetryInfo = _Object_({
|
|
13975
|
+
source: Union([
|
|
13976
|
+
Literal("explicit"),
|
|
13977
|
+
Literal("deterministic"),
|
|
13978
|
+
Literal("attempts_exhausted"),
|
|
13979
|
+
Literal("triage"),
|
|
13980
|
+
Literal("triage_failed")
|
|
13981
|
+
]),
|
|
13982
|
+
decision: Optional(TaskRetryDecision),
|
|
13983
|
+
confidence: Optional(TaskRetryConfidence),
|
|
13984
|
+
reason: Optional(String$1())
|
|
13985
|
+
}, {
|
|
13986
|
+
$id: "TaskRetryInfo",
|
|
13987
|
+
additionalProperties: false
|
|
13988
|
+
});
|
|
13968
13989
|
/**
|
|
13969
13990
|
* Structured error returned from a failed attempt.
|
|
13970
13991
|
*/
|
|
@@ -13972,7 +13993,8 @@ var TaskError = _Object_({
|
|
|
13972
13993
|
code: String$1(),
|
|
13973
13994
|
message: String$1(),
|
|
13974
13995
|
stack: Optional(String$1()),
|
|
13975
|
-
retryable: Optional(Boolean$1())
|
|
13996
|
+
retryable: Optional(Boolean$1()),
|
|
13997
|
+
retry: Optional(TaskRetryInfo)
|
|
13976
13998
|
}, {
|
|
13977
13999
|
$id: "TaskError",
|
|
13978
14000
|
additionalProperties: false
|
|
@@ -14150,6 +14172,86 @@ var PRODUCER_TASK_TYPES = new Set([
|
|
|
14150
14172
|
"render_pack",
|
|
14151
14173
|
"run_eval"
|
|
14152
14174
|
]);
|
|
14175
|
+
function isNonEmptyString(value) {
|
|
14176
|
+
return typeof value === "string" && value.length > 0;
|
|
14177
|
+
}
|
|
14178
|
+
function criterionWeight(criterion, index) {
|
|
14179
|
+
if (typeof criterion.weight === "number") return criterion.weight;
|
|
14180
|
+
if (typeof criterion.max_score === "number") return criterion.max_score / 100;
|
|
14181
|
+
if (typeof criterion.maxScore === "number") return criterion.maxScore / 100;
|
|
14182
|
+
throw new TaskBuildError([{
|
|
14183
|
+
field: `successCriteria/rubric/criteria/${index}/weight`,
|
|
14184
|
+
message: "criterion is missing weight or max_score"
|
|
14185
|
+
}]);
|
|
14186
|
+
}
|
|
14187
|
+
/**
|
|
14188
|
+
* Normalize authoring-time rubric criteria to canonical MoltNet rubric
|
|
14189
|
+
* criteria. Accepts `{id,title,description,weight}` and
|
|
14190
|
+
* `{name,description,max_score}` style inputs, strips authoring-only fields,
|
|
14191
|
+
* and fills a default scoring mode.
|
|
14192
|
+
*/
|
|
14193
|
+
function normalizeRubricCriteria(criteria, options) {
|
|
14194
|
+
const errors = [];
|
|
14195
|
+
const normalized = criteria.map((criterion, index) => {
|
|
14196
|
+
const id = criterion.id ?? criterion.name;
|
|
14197
|
+
const description = criterion.description ?? criterion.title;
|
|
14198
|
+
if (!isNonEmptyString(id)) errors.push({
|
|
14199
|
+
field: `successCriteria/rubric/criteria/${index}/id`,
|
|
14200
|
+
message: "criterion is missing id or name"
|
|
14201
|
+
});
|
|
14202
|
+
if (!isNonEmptyString(description)) errors.push({
|
|
14203
|
+
field: `successCriteria/rubric/criteria/${index}/description`,
|
|
14204
|
+
message: "criterion is missing description or title"
|
|
14205
|
+
});
|
|
14206
|
+
return {
|
|
14207
|
+
id: id ?? "",
|
|
14208
|
+
description: description ?? "",
|
|
14209
|
+
weight: criterionWeight(criterion, index),
|
|
14210
|
+
scoring: criterion.scoring ?? options?.scoring ?? "llm_score"
|
|
14211
|
+
};
|
|
14212
|
+
});
|
|
14213
|
+
if (errors.length > 0) throw new TaskBuildError(errors);
|
|
14214
|
+
return normalized;
|
|
14215
|
+
}
|
|
14216
|
+
/**
|
|
14217
|
+
* Build a canonical `SuccessCriteria` envelope from rubric/checklist-style
|
|
14218
|
+
* criteria. This keeps rubrics readable at the authoring boundary while
|
|
14219
|
+
* preserving the strict task schema on the wire.
|
|
14220
|
+
*/
|
|
14221
|
+
function buildRubricSuccessCriteria(options) {
|
|
14222
|
+
const rubric = {
|
|
14223
|
+
rubricId: options.rubricId,
|
|
14224
|
+
version: options.version ?? "v1",
|
|
14225
|
+
criteria: normalizeRubricCriteria(options.criteria, { scoring: options.scoring }),
|
|
14226
|
+
...options.contentHash ? { contentHash: options.contentHash } : {},
|
|
14227
|
+
...options.preamble ? { preamble: options.preamble } : {},
|
|
14228
|
+
...options.scope ? { scope: options.scope } : {}
|
|
14229
|
+
};
|
|
14230
|
+
const weightError = validateRubricWeights(rubric);
|
|
14231
|
+
if (weightError) throw new TaskBuildError([{
|
|
14232
|
+
field: "successCriteria/rubric/criteria",
|
|
14233
|
+
message: weightError
|
|
14234
|
+
}]);
|
|
14235
|
+
return {
|
|
14236
|
+
version: 1,
|
|
14237
|
+
rubric
|
|
14238
|
+
};
|
|
14239
|
+
}
|
|
14240
|
+
function resolveJudgeEvalAttemptTarget(target) {
|
|
14241
|
+
if ("judgeEvalTarget" in target && typeof target.judgeEvalTarget === "function") return target.judgeEvalTarget();
|
|
14242
|
+
if ("targetTaskId" in target) return {
|
|
14243
|
+
targetTaskId: target.targetTaskId,
|
|
14244
|
+
targetAttemptN: target.targetAttemptN
|
|
14245
|
+
};
|
|
14246
|
+
if ("taskId" in target) return {
|
|
14247
|
+
targetTaskId: target.taskId,
|
|
14248
|
+
targetAttemptN: target.accepted?.attemptN ?? target.attemptN ?? 1
|
|
14249
|
+
};
|
|
14250
|
+
throw new TaskBuildError([{
|
|
14251
|
+
field: "target",
|
|
14252
|
+
message: "judge_eval_attempt target is missing task id"
|
|
14253
|
+
}]);
|
|
14254
|
+
}
|
|
14153
14255
|
/**
|
|
14154
14256
|
* Fluent, network-free builder for a `tasks.create` body. Encodes the
|
|
14155
14257
|
* non-obvious task schema (context arrays, success-criteria gates,
|
|
@@ -14600,6 +14702,20 @@ function buildJudgeEvalAttempt(input) {
|
|
|
14600
14702
|
return buildTask("judge_eval_attempt", input);
|
|
14601
14703
|
}
|
|
14602
14704
|
/**
|
|
14705
|
+
* Build a `judge_eval_attempt` task from an accepted `run_eval` result (or a
|
|
14706
|
+
* small target tuple) plus human-friendly rubric criteria.
|
|
14707
|
+
*
|
|
14708
|
+
* @param target - A `TaskResultReader` or `{targetTaskId,targetAttemptN}` tuple.
|
|
14709
|
+
* @param options - Rubric metadata and eval/checklist-style criteria.
|
|
14710
|
+
* @returns A typed {@link TaskBuilder}.
|
|
14711
|
+
*/
|
|
14712
|
+
function buildJudgeEvalAttemptForRunEval(target, options) {
|
|
14713
|
+
return buildJudgeEvalAttempt({
|
|
14714
|
+
...resolveJudgeEvalAttemptTarget(target),
|
|
14715
|
+
successCriteria: buildRubricSuccessCriteria(options)
|
|
14716
|
+
});
|
|
14717
|
+
}
|
|
14718
|
+
/**
|
|
14603
14719
|
* Build a `pr_review` task. Requires `subject` + `successCriteria`. Note the
|
|
14604
14720
|
* rubric criteria must use `boolean` scoring for this task type.
|
|
14605
14721
|
*
|
|
@@ -14637,7 +14753,9 @@ var TaskResultReader = class {
|
|
|
14637
14753
|
accepted;
|
|
14638
14754
|
/** Token / cost usage for the accepted attempt, if reported. */
|
|
14639
14755
|
usage;
|
|
14756
|
+
/** Task id for the task whose accepted attempt is being read. */
|
|
14640
14757
|
taskId;
|
|
14758
|
+
/** CID of the accepted attempt output. */
|
|
14641
14759
|
outputCid;
|
|
14642
14760
|
constructor(task, attempt) {
|
|
14643
14761
|
const errors = [];
|
|
@@ -14730,6 +14848,19 @@ var TaskResultReader = class {
|
|
|
14730
14848
|
};
|
|
14731
14849
|
}
|
|
14732
14850
|
/**
|
|
14851
|
+
* Return the target tuple required by `judge_eval_attempt`.
|
|
14852
|
+
*
|
|
14853
|
+
* This intentionally uses the accepted attempt number, not merely the
|
|
14854
|
+
* attempt object passed to the reader, so a downstream judge is pinned to
|
|
14855
|
+
* the producer output that the task accepted.
|
|
14856
|
+
*/
|
|
14857
|
+
judgeEvalTarget() {
|
|
14858
|
+
return {
|
|
14859
|
+
targetTaskId: this.taskId,
|
|
14860
|
+
targetAttemptN: this.accepted.attemptN
|
|
14861
|
+
};
|
|
14862
|
+
}
|
|
14863
|
+
/**
|
|
14733
14864
|
* Build a `TaskRef` that anchors a downstream task to this accepted output
|
|
14734
14865
|
* and points at one persistent task artifact by CID.
|
|
14735
14866
|
*
|
|
@@ -14867,6 +14998,7 @@ function createTasksNamespace(context) {
|
|
|
14867
14998
|
buildAssessBrief,
|
|
14868
14999
|
buildJudgePack,
|
|
14869
15000
|
buildJudgeEvalAttempt,
|
|
15001
|
+
buildJudgeEvalAttemptForRunEval,
|
|
14870
15002
|
buildPrReview,
|
|
14871
15003
|
async readResult(taskOrId) {
|
|
14872
15004
|
const task = typeof taskOrId === "string" ? unwrapResult(await getTask({
|
|
@@ -14938,8 +15070,8 @@ function createTasksNamespace(context) {
|
|
|
14938
15070
|
body
|
|
14939
15071
|
}));
|
|
14940
15072
|
},
|
|
14941
|
-
async
|
|
14942
|
-
return unwrapResult(await
|
|
15073
|
+
async failAttempt(id, n, body) {
|
|
15074
|
+
return unwrapResult(await failTaskAttempt({
|
|
14943
15075
|
client,
|
|
14944
15076
|
auth,
|
|
14945
15077
|
path: {
|
|
@@ -17201,4 +17333,4 @@ if (!etc.sha512Sync) etc.sha512Sync = (...m) => {
|
|
|
17201
17333
|
return hash.digest();
|
|
17202
17334
|
};
|
|
17203
17335
|
//#endregion
|
|
17204
|
-
export {
|
|
17336
|
+
export { TaskBuildError as a, buildTask as i, createResultReader as n, TaskResultError as o, buildJudgeEvalAttemptForRunEval as r, connect as t };
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as TaskBuildError, i as buildTask, r as buildJudgeEvalAttemptForRunEval } from "./src.js";
|
|
2
2
|
//#region src/nodes/task-builder.ts
|
|
3
3
|
/** Split a comma-separated config string into trimmed, non-empty values. */
|
|
4
4
|
function parseCsv(raw) {
|
|
5
5
|
if (!raw) return [];
|
|
6
6
|
return raw.split(",").map((s) => s.trim()).filter(Boolean);
|
|
7
7
|
}
|
|
8
|
+
function isRuntimeProfileRef(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && typeof value.profileId === "string";
|
|
10
|
+
}
|
|
8
11
|
/** Resolve a context mapping's raw value from the message / context stores / literal. */
|
|
9
12
|
function resolveValue(RED, node, msg, m) {
|
|
10
13
|
switch (m.valueType) {
|
|
@@ -49,8 +52,12 @@ var init = (RED) => {
|
|
|
49
52
|
this.on("input", (msg, send, done) => {
|
|
50
53
|
try {
|
|
51
54
|
const payloadInput = msg.payload && typeof msg.payload === "object" ? msg.payload : {};
|
|
52
|
-
const
|
|
53
|
-
const
|
|
55
|
+
const taskType = typeof payloadInput.taskType === "string" && payloadInput.taskType ? payloadInput.taskType : def.taskType?.trim() || "freeform";
|
|
56
|
+
const inputData = payloadInput.input && typeof payloadInput.input === "object" ? payloadInput.input : taskType === "freeform" || taskType === "fulfill_brief" ? { brief: def.brief ?? "" } : {};
|
|
57
|
+
const builder = taskType === "judge_eval_attempt" && payloadInput.judgeRubric && typeof payloadInput.judgeRubric === "object" ? buildJudgeEvalAttemptForRunEval({
|
|
58
|
+
targetTaskId: inputData.targetTaskId,
|
|
59
|
+
targetAttemptN: inputData.targetAttemptN
|
|
60
|
+
}, payloadInput.judgeRubric) : buildTask(taskType, inputData);
|
|
54
61
|
const teamId = resolveOverride(RED, this, msg, def.teamId, def.teamIdType) ?? payloadInput.teamId ?? agentNode?.teamId;
|
|
55
62
|
const diaryId = resolveOverride(RED, this, msg, def.diaryId, def.diaryIdType) ?? payloadInput.diaryId ?? agentNode?.diaryId;
|
|
56
63
|
if (teamId) builder.team(teamId);
|
|
@@ -81,6 +88,12 @@ var init = (RED) => {
|
|
|
81
88
|
if (title) builder.title(title);
|
|
82
89
|
const tags = Array.isArray(payloadInput.tags) ? payloadInput.tags : parseCsv(def.tags);
|
|
83
90
|
if (tags.length > 0) builder.tags(...tags);
|
|
91
|
+
if (typeof payloadInput.correlationId === "string" && payloadInput.correlationId) builder.correlationId(payloadInput.correlationId);
|
|
92
|
+
if (typeof payloadInput.maxAttempts === "number") builder.maxAttempts(payloadInput.maxAttempts);
|
|
93
|
+
if (Array.isArray(payloadInput.allowedProfiles)) {
|
|
94
|
+
const allowedProfiles = payloadInput.allowedProfiles.filter(isRuntimeProfileRef);
|
|
95
|
+
if (allowedProfiles.length > 0) builder.allowProfiles(...allowedProfiles);
|
|
96
|
+
}
|
|
84
97
|
const built = builder.build();
|
|
85
98
|
const out = RED.util.cloneMessage(msg);
|
|
86
99
|
out.payload = {
|
|
@@ -12,12 +12,16 @@
|
|
|
12
12
|
"id": "sf_ab_build_run_eval"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
|
-
"x":
|
|
16
|
-
"y":
|
|
15
|
+
"x": 90,
|
|
16
|
+
"y": 40
|
|
17
17
|
}
|
|
18
18
|
],
|
|
19
|
-
"info": "Generic run_eval -> local score -> judge_eval_attempt
|
|
20
|
-
"
|
|
19
|
+
"info": "Generic run_eval -> local score -> judge_eval_attempt lane. Parent flow supplies msg.evalScenario, one msg.evalVariant, msg.evalSkillContext, msg.evalJudgeCriteria, and msg.correlationId. The subflow emits one lane result; parent flows own fan-out/fan-in and group completion.",
|
|
20
|
+
"inputLabels": ["eval config"],
|
|
21
|
+
"meta": {
|
|
22
|
+
"author": "ed@getlarge.eu",
|
|
23
|
+
"license": "Apache-2.0"
|
|
24
|
+
},
|
|
21
25
|
"name": "A/B eval with judge",
|
|
22
26
|
"out": [
|
|
23
27
|
{
|
|
@@ -25,21 +29,50 @@
|
|
|
25
29
|
{
|
|
26
30
|
"id": "sf_ab_store_delta",
|
|
27
31
|
"port": 0
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"id": "sf_ab_pack_lane_failure",
|
|
35
|
+
"port": 0
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "sf_ab_pack_node_error",
|
|
39
|
+
"port": 0
|
|
28
40
|
}
|
|
29
41
|
],
|
|
30
|
-
"x":
|
|
31
|
-
"y":
|
|
42
|
+
"x": 1320,
|
|
43
|
+
"y": 360
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"wires": [
|
|
47
|
+
{
|
|
48
|
+
"id": "1f190a60c144f6ec",
|
|
49
|
+
"port": 0
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"x": 1320,
|
|
53
|
+
"y": 80
|
|
32
54
|
}
|
|
33
55
|
],
|
|
56
|
+
"outputLabels": ["workflow state", "task tail events"],
|
|
57
|
+
"status": {
|
|
58
|
+
"wires": [
|
|
59
|
+
{
|
|
60
|
+
"id": "a72acb578ef0ea20",
|
|
61
|
+
"port": 0
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
"x": 1280,
|
|
65
|
+
"y": 720
|
|
66
|
+
},
|
|
34
67
|
"type": "subflow"
|
|
35
68
|
},
|
|
36
69
|
{
|
|
37
|
-
"func": "
|
|
70
|
+
"func": "function allowedProfilesFrom(value) {\n if (!value) return undefined;\n if (Array.isArray(value)) return value;\n if (typeof value === 'string') return [{ profileId: value }];\n if (typeof value === 'object') {\n if (Array.isArray(value.allowedProfiles)) return value.allowedProfiles;\n if (typeof value.profileId === 'string' && value.profileId) return [{ profileId: value.profileId }];\n }\n return undefined;\n}\nconst uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;\nconst scenario = msg.evalScenario;\nif (!scenario || typeof scenario !== 'object') {\n node.error('Missing msg.evalScenario', msg);\n return null;\n}\nconst variant = msg.evalVariant && typeof msg.evalVariant === 'object'\n ? msg.evalVariant\n : { label: msg.evalVariantLabel || msg.payload?.variantLabel || 'with-context' };\nconst variantLabel = variant.label || msg.evalVariantLabel || 'with-context';\nconst correlationId = msg.correlationId || msg.evalGroupCorrelationId || msg.payload?.correlationId;\nif (!uuidRe.test(String(correlationId || ''))) {\n node.error('Missing valid correlationId for eval group', msg);\n return null;\n}\nconst evidence = scenario.evidence || {};\nconst expected = scenario.expected || {};\nconst basePrompt = variant.prompt || scenario.prompt || [\n 'Run this eval scenario.',\n '',\n 'Return ONLY one valid JSON object. Do not use Markdown, headings, prose, tables, or code fences.',\n 'Use only the supplied evidence. Do not browse. Do not invent facts. Mark unknowns explicitly.',\n '',\n 'Scenario: ' + (scenario.title || scenario.id || 'unknown'),\n 'Variant: ' + variantLabel,\n '',\n 'Evidence JSON:',\n JSON.stringify(evidence, null, 2)\n].join('\\n');\nconst taskContexts = [];\nif (Array.isArray(variant.context)) {\n taskContexts.push(...variant.context);\n} else if (variant.includeSkill !== false && msg.evalSkillContext?.content) {\n taskContexts.push({\n slug: msg.evalSkillContext.slug || 'eval-skill-context',\n binding: msg.evalSkillContext.binding || 'skill',\n content: msg.evalSkillContext.content\n });\n}\ntaskContexts.push(\n { slug: 'eval-evidence', binding: 'context_inline', content: JSON.stringify(evidence, null, 2) },\n { slug: 'eval-expectations', binding: 'context_inline', content: JSON.stringify(expected, null, 2) }\n);\nconst producerAllowedProfiles = allowedProfilesFrom(\n variant.allowedProfiles ||\n variant.producerAllowedProfiles ||\n variant.producerRuntimeProfile ||\n variant.runtimeProfile ||\n scenario.runtimeProfiles?.producer ||\n scenario.producerRuntimeProfile ||\n msg.evalProducerAllowedProfiles ||\n msg.evalRuntimeProfiles?.producer\n);\nmsg.correlationId = correlationId;\nmsg.evalGroupCorrelationId = correlationId;\nmsg.evalScenario = scenario;\nmsg.evalVariant = variant;\nmsg.evalVariantLabel = variantLabel;\nmsg.payload = {\n taskType: 'run_eval',\n title: 'Eval: ' + (scenario.id || 'scenario') + ' / ' + variantLabel,\n tags: ['eval', scenario.id || 'scenario', variantLabel],\n correlationId,\n maxAttempts: msg.evalMaxAttempts || msg.payload?.maxAttempts || 2,\n input: {\n scenario: { prompt: basePrompt },\n variantLabel,\n execution: variant.execution || msg.evalExecution || { mode: 'vitro', workspace: 'none' },\n context: taskContexts\n }\n};\nif (producerAllowedProfiles) msg.payload.allowedProfiles = producerAllowedProfiles;\nnode.status({ fill: 'blue', shape: 'dot', text: variantLabel });\nreturn msg;",
|
|
38
71
|
"id": "sf_ab_build_run_eval",
|
|
39
72
|
"name": "build run_eval",
|
|
40
73
|
"outputs": 1,
|
|
41
74
|
"type": "function",
|
|
42
|
-
"wires": [["
|
|
75
|
+
"wires": [["sf_ab_task_builder_run_eval"]],
|
|
43
76
|
"x": 170,
|
|
44
77
|
"y": 120,
|
|
45
78
|
"z": "subflow_ab_eval_with_judge"
|
|
@@ -48,12 +81,12 @@
|
|
|
48
81
|
"agent": "eval_agent_cfg",
|
|
49
82
|
"generateCorrelationId": false,
|
|
50
83
|
"id": "sf_ab_create_run_eval",
|
|
51
|
-
"maxAttempts":
|
|
84
|
+
"maxAttempts": 2,
|
|
52
85
|
"name": "RUN EVAL producer",
|
|
53
86
|
"runtimeProfile": "eval_profile_producer",
|
|
54
87
|
"type": "moltnet-tasks-create",
|
|
55
88
|
"wires": [["sf_ab_wait_run_eval"]],
|
|
56
|
-
"x":
|
|
89
|
+
"x": 600,
|
|
57
90
|
"y": 120,
|
|
58
91
|
"z": "subflow_ab_eval_with_judge"
|
|
59
92
|
},
|
|
@@ -67,20 +100,36 @@
|
|
|
67
100
|
"taskId": "",
|
|
68
101
|
"timeoutSec": 1800,
|
|
69
102
|
"type": "moltnet-task-wait",
|
|
70
|
-
"wires": [[], ["
|
|
71
|
-
"x":
|
|
103
|
+
"wires": [["sf_ab_count_run_eval_tail"], ["sf_ab_gate_run_eval"]],
|
|
104
|
+
"x": 830,
|
|
72
105
|
"y": 120,
|
|
73
106
|
"z": "subflow_ab_eval_with_judge"
|
|
74
107
|
},
|
|
75
108
|
{
|
|
76
|
-
"
|
|
109
|
+
"finalize": "",
|
|
110
|
+
"func": "const correlationId = msg.correlationId || msg.evalGroupCorrelationId || msg.payload?.correlationId || 'unknown';\nconst scenarioId = msg.evalScenario?.id || 'unknown';\nconst variantLabel = msg.evalVariantLabel || msg.evalVariant?.label || 'unknown';\nconst groups = flow.get('abEvalTailMetrics') || {};\nconst group = groups[correlationId] || {};\nconst scenario = group[scenarioId] || {};\nconst lane = scenario[variantLabel] || {};\nconst metrics = lane.producer || { messages: 0, turns: 0, toolCalls: 0 };\nmetrics.messages += 1;\nconst kind = msg.payload?.kind;\nif (kind === 'turn_end') metrics.turns += 1;\nif (kind === 'tool_call_start') metrics.toolCalls += 1;\nmetrics.lastSeq = msg.payload?.seq ?? metrics.lastSeq ?? null;\nmetrics.lastKind = kind || metrics.lastKind || null;\nmetrics.lastAt = msg.payload?.timestamp || new Date().toISOString();\nlane.producer = metrics;\nscenario[variantLabel] = lane;\ngroup[scenarioId] = scenario;\ngroups[correlationId] = group;\nflow.set('abEvalTailMetrics', groups);\nreturn msg;",
|
|
111
|
+
"id": "sf_ab_count_run_eval_tail",
|
|
112
|
+
"initialize": "",
|
|
113
|
+
"libs": [],
|
|
114
|
+
"name": "count RUN EVAL tail",
|
|
115
|
+
"noerr": 0,
|
|
116
|
+
"outputs": 1,
|
|
117
|
+
"timeout": "",
|
|
118
|
+
"type": "function",
|
|
119
|
+
"wires": [["sf_ab_tail_out"]],
|
|
120
|
+
"x": 620,
|
|
121
|
+
"y": 80,
|
|
122
|
+
"z": "subflow_ab_eval_with_judge"
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"func": "msg.evalRunEvalSnapshot = msg.payload;\nmsg.evalTaskId = msg.payload?.taskId || msg.payload?.task?.id || msg.payload?.id || msg.taskId || null;\nmsg.evalAcceptedAttemptN = msg.payload?.acceptedAttemptN || msg.payload?.task?.acceptedAttemptN || 1;\nreturn msg;",
|
|
77
126
|
"id": "sf_ab_stash_run_eval",
|
|
78
127
|
"name": "stash producer task id",
|
|
79
128
|
"outputs": 1,
|
|
80
129
|
"type": "function",
|
|
81
130
|
"wires": [["sf_ab_read_run_eval"]],
|
|
82
|
-
"x":
|
|
83
|
-
"y":
|
|
131
|
+
"x": 210,
|
|
132
|
+
"y": 240,
|
|
84
133
|
"z": "subflow_ab_eval_with_judge"
|
|
85
134
|
},
|
|
86
135
|
{
|
|
@@ -92,43 +141,43 @@
|
|
|
92
141
|
"source": "payload",
|
|
93
142
|
"type": "moltnet-task-reader",
|
|
94
143
|
"wires": [["sf_ab_score_run_eval"]],
|
|
95
|
-
"x":
|
|
96
|
-
"y":
|
|
144
|
+
"x": 460,
|
|
145
|
+
"y": 240,
|
|
97
146
|
"z": "subflow_ab_eval_with_judge"
|
|
98
147
|
},
|
|
99
148
|
{
|
|
100
|
-
"func": "function extractJsonObject(text) {\n if (!text || typeof text !== 'string') return null;\n const fenced = text.match(/```json\\s*([\\s\\S]*?)```/i) || text.match(/```\\s*([\\s\\S]*?)```/);\n const candidates = [];\n if (fenced) candidates.push(fenced[1]);\n const first = text.indexOf('{');\n const last = text.lastIndexOf('}');\n if (first >= 0 && last > first) candidates.push(text.slice(first, last + 1));\n for (const candidate of candidates) {\n try { return JSON.parse(candidate); } catch (_) {}\n }\n return null;\n}\
|
|
149
|
+
"func": "function extractJsonObject(text) {\n if (!text || typeof text !== 'string') return null;\n const fenced = text.match(/```json\\s*([\\s\\S]*?)```/i) || text.match(/```\\s*([\\s\\S]*?)```/);\n const candidates = [];\n if (fenced) candidates.push(fenced[1]);\n const first = text.indexOf('{');\n const last = text.lastIndexOf('}');\n if (first >= 0 && last > first) candidates.push(text.slice(first, last + 1));\n for (const candidate of candidates) {\n try { return JSON.parse(candidate); } catch (_) {}\n }\n return null;\n}\nfunction durationMs(attempt) {\n const started = Date.parse(attempt?.startedAt || attempt?.claimedAt || '');\n const completed = Date.parse(attempt?.completedAt || '');\n return Number.isFinite(started) && Number.isFinite(completed) ? completed - started : null;\n}\nfunction tailMetrics(msg, stage) {\n const groups = flow.get('abEvalTailMetrics') || {};\n const correlationId = msg.correlationId || msg.evalGroupCorrelationId || 'unknown';\n const scenarioId = msg.evalScenario?.id || 'unknown';\n const variantLabel = msg.evalVariantLabel || msg.evalVariant?.label || 'unknown';\n return groups[correlationId]?.[scenarioId]?.[variantLabel]?.[stage] || null;\n}\nfunction taskMetrics(result, snapshot, tail) {\n const attempt = snapshot?.attempt || {};\n const usage = result?.usage || attempt.usage || {};\n const inputTokens = usage.inputTokens ?? null;\n const outputTokens = usage.outputTokens ?? null;\n return {\n taskId: snapshot?.taskId || result?.outputRef?.taskId || null,\n attemptN: result?.accepted?.attemptN || snapshot?.acceptedAttemptN || attempt.attemptN || null,\n status: snapshot?.status || attempt.status || null,\n startedAt: attempt.startedAt || attempt.claimedAt || null,\n completedAt: result?.accepted?.completedAt || attempt.completedAt || null,\n durationMs: durationMs(attempt),\n inputTokens,\n outputTokens,\n totalTokens: inputTokens === null || outputTokens === null ? null : inputTokens + outputTokens,\n cacheReadTokens: usage.cacheReadTokens ?? null,\n cacheWriteTokens: usage.cacheWriteTokens ?? null,\n provider: usage.provider ?? null,\n model: usage.model ?? null,\n toolCalls: usage.toolCalls ?? tail?.toolCalls ?? null,\n turns: tail?.turns ?? null,\n tailMessages: tail?.messages ?? null\n };\n}\nconst output = msg.payload || {};\nconst response = output.response || msg.result?.summary || '';\nconst analysis = extractJsonObject(response);\nmsg.evalProducerResult = msg.result;\nmsg.evalProducerMetrics = taskMetrics(msg.result, msg.evalRunEvalSnapshot, tailMetrics(msg, 'producer'));\nmsg.evalProducerScore = {\n scenarioId: msg.evalScenario?.id,\n variantLabel: msg.evalVariantLabel,\n correlationId: msg.correlationId,\n taskId: msg.evalTaskId || null,\n score0to100: null,\n checks: {},\n parsedJson: Boolean(analysis),\n responseChars: response.length,\n analysis,\n responseRef: msg.result?.outputRef || null,\n metrics: msg.evalProducerMetrics\n};\nreturn msg;",
|
|
101
150
|
"id": "sf_ab_score_run_eval",
|
|
102
151
|
"name": "local score",
|
|
103
152
|
"outputs": 1,
|
|
104
153
|
"type": "function",
|
|
105
154
|
"wires": [["sf_ab_build_judge_eval"]],
|
|
106
|
-
"x":
|
|
107
|
-
"y":
|
|
155
|
+
"x": 670,
|
|
156
|
+
"y": 240,
|
|
108
157
|
"z": "subflow_ab_eval_with_judge"
|
|
109
158
|
},
|
|
110
159
|
{
|
|
111
|
-
"func": "
|
|
160
|
+
"func": "function allowedProfilesFrom(value) {\n if (!value) return undefined;\n if (Array.isArray(value)) return value;\n if (typeof value === 'string') return [{ profileId: value }];\n if (typeof value === 'object') {\n if (Array.isArray(value.allowedProfiles)) return value.allowedProfiles;\n if (typeof value.profileId === 'string' && value.profileId) return [{ profileId: value.profileId }];\n }\n return undefined;\n}\nconst targetTaskId = msg.evalTaskId || msg.payload?.taskId;\nconst targetAttemptN = msg.evalAcceptedAttemptN || msg.payload?.acceptedAttemptN || 1;\nif (!targetTaskId) {\n node.error('Missing target run_eval task id for judge_eval_attempt', msg);\n return null;\n}\nconst criteria = msg.evalJudgeCriteria || [];\nif (!Array.isArray(criteria) || criteria.length === 0) {\n node.error('Missing msg.evalJudgeCriteria', msg);\n return null;\n}\nconst judgeAllowedProfiles = allowedProfilesFrom(\n msg.evalVariant?.judgeAllowedProfiles ||\n msg.evalVariant?.judgeRuntimeProfile ||\n msg.evalScenario?.runtimeProfiles?.judge ||\n msg.evalScenario?.judgeRuntimeProfile ||\n msg.evalJudgeAllowedProfiles ||\n msg.evalRuntimeProfiles?.judge\n);\nmsg.evalJudgeTargetTaskId = targetTaskId;\nmsg.evalJudgeTargetAttemptN = targetAttemptN;\nmsg.payload = {\n taskType: 'judge_eval_attempt',\n title: 'Judge eval: ' + (msg.evalScenario?.id || 'unknown') + ' / ' + (msg.evalVariantLabel || 'unknown'),\n tags: ['eval-judge', msg.evalScenario?.id || 'unknown', msg.evalVariantLabel || 'unknown'],\n correlationId: msg.correlationId,\n maxAttempts: msg.evalMaxAttempts || 2,\n input: {\n targetTaskId,\n targetAttemptN\n },\n judgeRubric: {\n rubricId: msg.evalJudgeRubricId || 'ab-eval-rubric',\n version: msg.evalJudgeRubricVersion || 'v1',\n contentHash: msg.evalJudgeRubricHash || 'node-red-rubric',\n criteria\n }\n};\nif (judgeAllowedProfiles) msg.payload.allowedProfiles = judgeAllowedProfiles;\nreturn msg;",
|
|
112
161
|
"id": "sf_ab_build_judge_eval",
|
|
113
162
|
"name": "build judge_eval_attempt",
|
|
114
163
|
"outputs": 1,
|
|
115
164
|
"type": "function",
|
|
116
|
-
"wires": [["
|
|
117
|
-
"x":
|
|
118
|
-
"y":
|
|
165
|
+
"wires": [["sf_ab_task_builder_judge_eval"]],
|
|
166
|
+
"x": 910,
|
|
167
|
+
"y": 240,
|
|
119
168
|
"z": "subflow_ab_eval_with_judge"
|
|
120
169
|
},
|
|
121
170
|
{
|
|
122
171
|
"agent": "eval_agent_cfg",
|
|
123
172
|
"generateCorrelationId": false,
|
|
124
173
|
"id": "sf_ab_create_judge_eval",
|
|
125
|
-
"maxAttempts":
|
|
174
|
+
"maxAttempts": 2,
|
|
126
175
|
"name": "JUDGE EVAL",
|
|
127
176
|
"runtimeProfile": "eval_profile_judge",
|
|
128
177
|
"type": "moltnet-tasks-create",
|
|
129
178
|
"wires": [["sf_ab_wait_judge_eval"]],
|
|
130
179
|
"x": 220,
|
|
131
|
-
"y":
|
|
180
|
+
"y": 360,
|
|
132
181
|
"z": "subflow_ab_eval_with_judge"
|
|
133
182
|
},
|
|
134
183
|
{
|
|
@@ -141,20 +190,36 @@
|
|
|
141
190
|
"taskId": "",
|
|
142
191
|
"timeoutSec": 1800,
|
|
143
192
|
"type": "moltnet-task-wait",
|
|
144
|
-
"wires": [[], ["
|
|
193
|
+
"wires": [["sf_ab_count_judge_eval_tail"], ["sf_ab_gate_judge_eval"]],
|
|
145
194
|
"x": 430,
|
|
146
|
-
"y":
|
|
195
|
+
"y": 360,
|
|
196
|
+
"z": "subflow_ab_eval_with_judge"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
"finalize": "",
|
|
200
|
+
"func": "const correlationId = msg.correlationId || msg.evalGroupCorrelationId || msg.payload?.correlationId || 'unknown';\nconst scenarioId = msg.evalScenario?.id || 'unknown';\nconst variantLabel = msg.evalVariantLabel || msg.evalVariant?.label || 'unknown';\nconst groups = flow.get('abEvalTailMetrics') || {};\nconst group = groups[correlationId] || {};\nconst scenario = group[scenarioId] || {};\nconst lane = scenario[variantLabel] || {};\nconst metrics = lane.judge || { messages: 0, turns: 0, toolCalls: 0 };\nmetrics.messages += 1;\nconst kind = msg.payload?.kind;\nif (kind === 'turn_end') metrics.turns += 1;\nif (kind === 'tool_call_start') metrics.toolCalls += 1;\nmetrics.lastSeq = msg.payload?.seq ?? metrics.lastSeq ?? null;\nmetrics.lastKind = kind || metrics.lastKind || null;\nmetrics.lastAt = msg.payload?.timestamp || new Date().toISOString();\nlane.judge = metrics;\nscenario[variantLabel] = lane;\ngroup[scenarioId] = scenario;\ngroups[correlationId] = group;\nflow.set('abEvalTailMetrics', groups);\nreturn msg;",
|
|
201
|
+
"id": "sf_ab_count_judge_eval_tail",
|
|
202
|
+
"initialize": "",
|
|
203
|
+
"libs": [],
|
|
204
|
+
"name": "count JUDGE tail",
|
|
205
|
+
"noerr": 0,
|
|
206
|
+
"outputs": 1,
|
|
207
|
+
"timeout": "",
|
|
208
|
+
"type": "function",
|
|
209
|
+
"wires": [["0f5c72247640f952"]],
|
|
210
|
+
"x": 600,
|
|
211
|
+
"y": 320,
|
|
147
212
|
"z": "subflow_ab_eval_with_judge"
|
|
148
213
|
},
|
|
149
214
|
{
|
|
150
|
-
"func": "msg.evalJudgeTaskId = msg.payload?.taskId || msg.payload?.task?.id || msg.payload?.id || msg.taskId || null;\nreturn msg;",
|
|
215
|
+
"func": "msg.evalJudgeSnapshot = msg.payload;\nmsg.evalJudgeTaskId = msg.payload?.taskId || msg.payload?.task?.id || msg.payload?.id || msg.taskId || null;\nreturn msg;",
|
|
151
216
|
"id": "sf_ab_stash_judge_eval",
|
|
152
217
|
"name": "stash judge task id",
|
|
153
218
|
"outputs": 1,
|
|
154
219
|
"type": "function",
|
|
155
220
|
"wires": [["sf_ab_read_judge_eval"]],
|
|
156
|
-
"x":
|
|
157
|
-
"y":
|
|
221
|
+
"x": 210,
|
|
222
|
+
"y": 500,
|
|
158
223
|
"z": "subflow_ab_eval_with_judge"
|
|
159
224
|
},
|
|
160
225
|
{
|
|
@@ -166,19 +231,19 @@
|
|
|
166
231
|
"source": "payload",
|
|
167
232
|
"type": "moltnet-task-reader",
|
|
168
233
|
"wires": [["sf_ab_store_delta"]],
|
|
169
|
-
"x":
|
|
170
|
-
"y":
|
|
234
|
+
"x": 470,
|
|
235
|
+
"y": 500,
|
|
171
236
|
"z": "subflow_ab_eval_with_judge"
|
|
172
237
|
},
|
|
173
238
|
{
|
|
174
|
-
"func": "function scoreFromComposite(judgment) {\n return typeof judgment.composite === 'number' ? Math.round(judgment.composite * 100) : null;\n}\n\nfunction buildVariantRecord(msg, judgment, judgeScore0to100) {\n return {\n correlationId: msg.correlationId || msg.evalGroupCorrelationId || null,\n scenarioId: msg.evalScenario?.id || 'unknown',\n variantLabel: msg.evalVariantLabel || judgment.variantLabel || 'unknown',\n runEvalTaskId: msg.evalJudgeTargetTaskId || msg.evalTaskId || null,\n runEvalAttemptN: msg.evalJudgeTargetAttemptN || msg.evalAcceptedAttemptN || null,\n judgeTaskId: msg.evalJudgeTaskId || null,\n producerScore0to100: msg.evalProducerScore?.score0to100 ?? null,\n judgeScore0to100,\n judgeComposite: typeof judgment.composite === 'number' ? judgment.composite : null,\n verdict: judgment.verdict || null,\n scores: judgment.scores || [],\n updatedAt: new Date().toISOString()\n };\n}\n\nfunction
|
|
239
|
+
"func": "function scoreFromComposite(judgment) {\n return typeof judgment.composite === 'number' ? Math.round(judgment.composite * 100) : null;\n}\nfunction durationMs(attempt) {\n const started = Date.parse(attempt?.startedAt || attempt?.claimedAt || '');\n const completed = Date.parse(attempt?.completedAt || '');\n return Number.isFinite(started) && Number.isFinite(completed) ? completed - started : null;\n}\nfunction tailMetrics(msg, stage) {\n const groups = flow.get('abEvalTailMetrics') || {};\n const correlationId = msg.correlationId || msg.evalGroupCorrelationId || 'unknown';\n const scenarioId = msg.evalScenario?.id || 'unknown';\n const variantLabel = msg.evalVariantLabel || msg.evalVariant?.label || 'unknown';\n return groups[correlationId]?.[scenarioId]?.[variantLabel]?.[stage] || null;\n}\nfunction taskMetrics(result, snapshot, tail) {\n const attempt = snapshot?.attempt || {};\n const usage = result?.usage || attempt.usage || {};\n const inputTokens = usage.inputTokens ?? null;\n const outputTokens = usage.outputTokens ?? null;\n return {\n taskId: snapshot?.taskId || result?.outputRef?.taskId || null,\n attemptN: result?.accepted?.attemptN || snapshot?.acceptedAttemptN || attempt.attemptN || null,\n status: snapshot?.status || attempt.status || null,\n startedAt: attempt.startedAt || attempt.claimedAt || null,\n completedAt: result?.accepted?.completedAt || attempt.completedAt || null,\n durationMs: durationMs(attempt),\n inputTokens,\n outputTokens,\n totalTokens: inputTokens === null || outputTokens === null ? null : inputTokens + outputTokens,\n cacheReadTokens: usage.cacheReadTokens ?? null,\n cacheWriteTokens: usage.cacheWriteTokens ?? null,\n provider: usage.provider ?? null,\n model: usage.model ?? null,\n toolCalls: usage.toolCalls ?? tail?.toolCalls ?? null,\n turns: tail?.turns ?? null,\n tailMessages: tail?.messages ?? null\n };\n}\nfunction metricDelta(baseline, candidate) {\n const keys = ['durationMs', 'inputTokens', 'outputTokens', 'totalTokens', 'turns', 'toolCalls', 'tailMessages'];\n const delta = {};\n for (const key of keys) {\n const base = baseline?.[key];\n const cand = candidate?.[key];\n delta[key] = typeof base === 'number' && typeof cand === 'number' ? cand - base : null;\n }\n return delta;\n}\n\nfunction buildVariantRecord(msg, judgment, judgeScore0to100) {\n const judgeMetrics = taskMetrics(msg.result, msg.evalJudgeSnapshot, tailMetrics(msg, 'judge'));\n return {\n correlationId: msg.correlationId || msg.evalGroupCorrelationId || null,\n scenarioId: msg.evalScenario?.id || 'unknown',\n variantLabel: msg.evalVariantLabel || judgment.variantLabel || 'unknown',\n isBaseline: msg.evalVariant?.baseline === true,\n runEvalTaskId: msg.evalJudgeTargetTaskId || msg.evalTaskId || null,\n runEvalAttemptN: msg.evalJudgeTargetAttemptN || msg.evalAcceptedAttemptN || null,\n judgeTaskId: msg.evalJudgeTaskId || null,\n producerScore0to100: msg.evalProducerScore?.score0to100 ?? null,\n producerMetrics: msg.evalProducerMetrics || null,\n judgeScore0to100,\n judgeComposite: typeof judgment.composite === 'number' ? judgment.composite : null,\n judgeMetrics,\n verdict: judgment.verdict || null,\n scores: judgment.scores || [],\n updatedAt: new Date().toISOString()\n };\n}\n\nfunction expectedFromMessage(msg, scenarioId) {\n const direct = Number(msg.evalExpectedVariants || msg.evalGroup?.expectedVariants || msg.payload?.expectedVariants || 0);\n if (Number.isFinite(direct) && direct > 0) return direct;\n const byScenario = Number(msg.evalGroup?.expectedVariants?.[scenarioId] || 0);\n return Number.isFinite(byScenario) && byScenario > 0 ? byScenario : 0;\n}\nfunction store(record, msg) {\n const groups = flow.get('abEvalResults') || {};\n const group = groups[record.correlationId] || { correlationId: record.correlationId, expectedVariants: {}, scenarios: {}, createdAt: new Date().toISOString() };\n group.expectedVariants = group.expectedVariants || {};\n const expected = expectedFromMessage(msg, record.scenarioId);\n if (expected > 0 && !group.expectedVariants[record.scenarioId]) {\n group.expectedVariants[record.scenarioId] = expected;\n }\n const scenario = group.scenarios[record.scenarioId] || {};\n scenario[record.variantLabel] = record;\n group.scenarios[record.scenarioId] = scenario;\n group.updatedAt = new Date().toISOString();\n groups[record.correlationId] = group;\n flow.set('abEvalResults', groups);\n return { group, scenario };\n}\nfunction expectedFor(group, scenarioId, msg) {\n const expected = Number(group.expectedVariants?.[scenarioId] || expectedFromMessage(msg, scenarioId) || 0);\n return Number.isFinite(expected) ? expected : 0;\n}\n\nfunction rankedVariants(scenario) {\n return Object.values(scenario)\n .filter((item) => typeof item.judgeScore0to100 === 'number')\n .sort((a, b) => b.judgeScore0to100 - a.judgeScore0to100);\n}\n\nfunction buildDelta(scenario) {\n const records = Object.values(scenario);\n const baseline = records.find((item) => item.isBaseline || item.variantLabel === 'baseline-no-skill' || item.variantLabel === 'baseline');\n const candidate = records\n .filter((item) => item !== baseline && typeof item.judgeScore0to100 === 'number')\n .sort((a, b) => b.judgeScore0to100 - a.judgeScore0to100)[0] || null;\n if (!baseline || !candidate) return null;\n return {\n baselineVariant: baseline.variantLabel,\n candidateVariant: candidate.variantLabel,\n baselineScore0to100: baseline.judgeScore0to100,\n candidateScore0to100: candidate.judgeScore0to100,\n judgeDelta0to100: baseline.judgeScore0to100 === null || candidate.judgeScore0to100 === null ? null : candidate.judgeScore0to100 - baseline.judgeScore0to100,\n baselineProducerScore0to100: baseline.producerScore0to100,\n candidateProducerScore0to100: candidate.producerScore0to100,\n producerDelta0to100: baseline.producerScore0to100 === null || candidate.producerScore0to100 === null ? null : candidate.producerScore0to100 - baseline.producerScore0to100,\n producerMetricsDelta: metricDelta(baseline.producerMetrics, candidate.producerMetrics),\n judgeMetricsDelta: metricDelta(baseline.judgeMetrics, candidate.judgeMetrics)\n };\n}\n\nconst judgment = msg.payload || {};\nconst judgeScore0to100 = scoreFromComposite(judgment);\nconst record = buildVariantRecord(msg, judgment, judgeScore0to100);\nconst stored = store(record, msg);\nconst group = stored.group;\nconst scenario = stored.scenario;\nconst expected = expectedFor(group, record.scenarioId, msg);\nconst completed = Object.keys(scenario).length;\nconst winner = rankedVariants(scenario)[0] || null;\nconst delta = buildDelta(scenario);\n\nmsg.payload = {\n laneStatus: 'completed',\n groupComplete: expected > 0 && completed >= expected,\n expectedVariants: expected,\n completedVariants: completed,\n correlationId: record.correlationId,\n scenarioId: record.scenarioId,\n variantLabel: record.variantLabel,\n producerScore0to100: record.producerScore0to100,\n producerMetrics: record.producerMetrics,\n judgeScore0to100,\n judgeMetrics: record.judgeMetrics,\n verdict: judgment.verdict || null,\n scores: judgment.scores || [],\n winner,\n delta,\n metricsDelta: delta ? { producer: delta.producerMetricsDelta, judge: delta.judgeMetricsDelta } : null,\n variants: scenario\n};\nnode.status({ fill: msg.payload.groupComplete ? 'green' : 'blue', shape: 'dot', text: completed + '/' + (expected || '?') + ' variants' });\nreturn msg;",
|
|
175
240
|
"id": "sf_ab_store_delta",
|
|
176
241
|
"name": "store judgment + delta",
|
|
177
242
|
"outputs": 1,
|
|
178
243
|
"type": "function",
|
|
179
244
|
"wires": [[]],
|
|
180
|
-
"x":
|
|
181
|
-
"y":
|
|
245
|
+
"x": 730,
|
|
246
|
+
"y": 500,
|
|
182
247
|
"z": "subflow_ab_eval_with_judge"
|
|
183
248
|
},
|
|
184
249
|
{
|
|
@@ -190,32 +255,32 @@
|
|
|
190
255
|
},
|
|
191
256
|
{
|
|
192
257
|
"apiUrl": "https://api.themolt.net",
|
|
193
|
-
"clientId": "",
|
|
194
|
-
"diaryId": "",
|
|
258
|
+
"clientId": "b46d59c1-0b14-453c-a57b-8be1ced93141",
|
|
259
|
+
"diaryId": "6e4d9948-8ec5-4f59-b82a-3acbc4bbc396",
|
|
195
260
|
"id": "eval_agent_cfg",
|
|
196
261
|
"name": "eval-agent",
|
|
197
|
-
"teamId": "",
|
|
262
|
+
"teamId": "6743b4b1-6b93-46e2-a048-19490f04f91a",
|
|
198
263
|
"type": "moltnet-agent"
|
|
199
264
|
},
|
|
200
265
|
{
|
|
201
266
|
"agent": "eval_agent_cfg",
|
|
202
267
|
"id": "eval_profile_producer",
|
|
203
268
|
"name": "eval-producer",
|
|
204
|
-
"profileId": "",
|
|
205
|
-
"profileName": "",
|
|
269
|
+
"profileId": "50c5ed63-19b9-47a9-aabd-6a3ae3bc68f1",
|
|
270
|
+
"profileName": "il1395-gemma (ollama-cloud/gemma4:31b-cloud)",
|
|
206
271
|
"type": "moltnet-runtime-profile"
|
|
207
272
|
},
|
|
208
273
|
{
|
|
209
274
|
"agent": "eval_agent_cfg",
|
|
210
275
|
"id": "eval_profile_judge",
|
|
211
276
|
"name": "eval-judge",
|
|
212
|
-
"profileId": "",
|
|
213
|
-
"profileName": "",
|
|
277
|
+
"profileId": "50c5ed63-19b9-47a9-aabd-6a3ae3bc68f1",
|
|
278
|
+
"profileName": "il1395-gemma (ollama-cloud/gemma4:31b-cloud)",
|
|
214
279
|
"type": "moltnet-runtime-profile"
|
|
215
280
|
},
|
|
216
281
|
{
|
|
217
282
|
"id": "ab_eval_note",
|
|
218
|
-
"info": "Run at least one agent daemon that can claim run_eval and judge_eval_attempt tasks. If you set runtime profile IDs above, run one daemon per profile. Leave profile IDs blank to let any eligible daemon claim both tasks.",
|
|
283
|
+
"info": "Run at least one agent daemon that can claim run_eval and judge_eval_attempt tasks. The demo seeds both variants under one correlation id and fans them out in parallel. If you set runtime profile IDs above, run one daemon per profile. Leave profile IDs blank to let any eligible daemon claim both tasks.",
|
|
219
284
|
"name": "Daemon setup",
|
|
220
285
|
"type": "comment",
|
|
221
286
|
"wires": [],
|
|
@@ -224,12 +289,12 @@
|
|
|
224
289
|
"z": "ab_eval_demo_tab"
|
|
225
290
|
},
|
|
226
291
|
{
|
|
227
|
-
"func": "function uuid() {\n return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) =>\n (Number(c) ^ Math.floor(Math.random() * 16) >> Number(c) / 4).toString(16)\n );\n}\nmsg.correlationId = uuid();\nmsg.
|
|
292
|
+
"func": "function uuid() {\n return '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) =>\n (Number(c) ^ Math.floor(Math.random() * 16) >> Number(c) / 4).toString(16)\n );\n}\nmsg.correlationId = uuid();\nmsg.evalScenario = {\n id: 'moltnet-practices/dbos-after-commit',\n title: 'DBOS workflow after Drizzle transaction commits',\n prompt: \"# Wire a Keto permission grant to a diary creation\\n\\n## Context\\n\\nMoltNet is a TypeScript backend. When a new diary is created, two\\nthings need to happen atomically from the user's point of view:\\n\\n1. A row is inserted into the `diaries` table via Drizzle ORM.\\n2. A Keto permission tuple is written that grants the creating agent\\n the `owner` relation on the new diary. The Keto write is implemented\\n as a durable workflow called `grantDiaryOwner` (DBOS-backed), and\\n you start it by calling `startGrantDiaryOwnerWorkflow(diaryId,\\nagentId)`.\\n\\nThe existing HTTP handler looks roughly like this:\\n\\n```typescript\\nexport async function createDiaryHandler(request, reply) {\\n const { name, visibility } = request.body;\\n const agentId = request.principal.id;\\n\\n const diary = await runTransaction(async () => {\\n // TODO: insert the diary row here\\n // TODO: trigger the Keto owner grant here\\n return /* the created diary */;\\n });\\n\\n return reply.code(201).send(diary);\\n}\\n```\\n\\n## Task\\n\\nFill in the handler so the diary row is inserted and the owner grant\\nworkflow is started. Follow the existing repository conventions.\\n\\nProduce two files:\\n\\n1. `create-diary-handler.ts` — the completed handler.\\n2. `notes.md` — a short note explaining the implementation choices\\n you made and the reasoning behind them.\\n\\nAssume the following are already imported and available:\\n\\n- `runTransaction` (wraps a Drizzle transaction)\\n- `createDiaryRepository(db)` with a `create(input)` method\\n- `startGrantDiaryOwnerWorkflow(diaryId, agentId)`\",\n source: {\n suite: 'moltnet-practices',\n path: 'evals/moltnet-practices/dbos-after-commit',\n mode: 'vitro'\n },\n runtimeProfiles: {\n producer: '',\n judge: ''\n }\n};\nmsg.evalJudgeCriteria = [\n {\n \"id\": \"workflow-started-after-transaction-commits\",\n \"title\": \"Workflow started after transaction commits\",\n \"weight\": 0.35,\n \"description\": \"`startGrantDiaryOwnerWorkflow(...)` is called AFTER `runTransaction(...)` resolves, NOT inside the transaction callback. The transaction callback returns the created diary, and the workflow is started on the next line using the returned diary id. Any call site where the workflow is started inside the `runTransaction` callback fails this criterion, even if the code otherwise compiles and looks correct.\"\n },\n {\n \"id\": \"notes-name-both-systems-as-separate-backends\",\n \"title\": \"Notes name both systems as separate backends\",\n \"weight\": 0.25,\n \"description\": \"`notes.md` explicitly names the two systems that do not share a transaction — it must mention BOTH DBOS/workflows AND Drizzle/Postgres/runTransaction (or Keto vs Postgres) as separate backends/connections. Generic 'non-atomic' or 'durable' language without naming the two specific systems fails this criterion.\"\n },\n {\n \"id\": \"notes-describe-the-rollback-failure-mode\",\n \"title\": \"Notes describe the rollback failure mode\",\n \"weight\": 0.2,\n \"description\": \"`notes.md` explicitly describes the failure mode if the workflow were started inside the transaction: a rolled-back Postgres transaction would leave a dangling Keto tuple (or equivalent: orphaned grant, inconsistent Keto state). Notes that only say 'the workflow must run after' without explaining the rollback failure mode fail this criterion.\"\n },\n {\n \"id\": \"diary-insert-uses-the-repository\",\n \"title\": \"Diary insert uses the repository\",\n \"weight\": 0.15,\n \"description\": \"The diary insert is performed through the Drizzle repository (e.g. `createDiaryRepository(tx).create(...)` or the equivalent) and the created diary is returned from the transaction callback so the outer scope can read its id.\"\n },\n {\n \"id\": \"grant-not-started-on-rollback-path\",\n \"title\": \"Grant not started on rollback path\",\n \"weight\": 0.05,\n \"description\": \"The handler is structured so that if the diary insert throws, the Keto grant is never started. Starting the workflow BEFORE `runTransaction` resolves (e.g. in a `.then` or before the insert) fails this criterion.\"\n }\n];\nmsg.evalRuntimeProfiles = {\n producer: '',\n judge: ''\n};\nmsg.evalVariants = [\n { label: 'baseline-no-context', baseline: true, includeSkill: false },\n {\n label: 'rendered-pack-dbos-rule',\n context: [\n {\n slug: 'moltnet-rendered-pack-dbos-drizzle',\n binding: 'context_inline',\n content: [\n '# MoltNet DBOS / Drizzle Practice',\n '',\n 'When a code path needs both a Drizzle/Postgres transaction and a DBOS-backed workflow side effect, do not start the DBOS workflow inside the runTransaction callback.',\n '',\n 'Drizzle/Postgres and DBOS/workflows use separate backends/connections and do not share one atomic transaction. Starting the workflow inside the transaction can leave an orphaned or dangling Keto tuple if Postgres later rolls back.',\n '',\n 'Preferred shape: create the diary row through createDiaryRepository(tx).create(...) inside runTransaction, return the created diary, then call startGrantDiaryOwnerWorkflow(diary.id, agentId) after runTransaction resolves.'\n ].join('\\n')\n }\n ]\n }\n];\nreturn msg;",
|
|
228
293
|
"id": "ab_eval_seed",
|
|
229
|
-
"name": "seed
|
|
294
|
+
"name": "seed eval group",
|
|
230
295
|
"outputs": 1,
|
|
231
296
|
"type": "function",
|
|
232
|
-
"wires": [["
|
|
297
|
+
"wires": [["ab_eval_init_group"]],
|
|
233
298
|
"x": 170,
|
|
234
299
|
"y": 120,
|
|
235
300
|
"z": "ab_eval_demo_tab"
|
|
@@ -237,7 +302,7 @@
|
|
|
237
302
|
{
|
|
238
303
|
"crontab": "",
|
|
239
304
|
"id": "ab_eval_inject",
|
|
240
|
-
"name": "run
|
|
305
|
+
"name": "run eval group",
|
|
241
306
|
"once": false,
|
|
242
307
|
"onceDelay": 0.1,
|
|
243
308
|
"payload": "",
|
|
@@ -259,8 +324,8 @@
|
|
|
259
324
|
"id": "ab_eval_subflow",
|
|
260
325
|
"name": "A/B eval with judge",
|
|
261
326
|
"type": "subflow:subflow_ab_eval_with_judge",
|
|
262
|
-
"wires": [["
|
|
263
|
-
"x":
|
|
327
|
+
"wires": [["ab_eval_gate_group"], ["ab_eval_tail_debug"]],
|
|
328
|
+
"x": 670,
|
|
264
329
|
"y": 120,
|
|
265
330
|
"z": "ab_eval_demo_tab"
|
|
266
331
|
},
|
|
@@ -269,7 +334,7 @@
|
|
|
269
334
|
"complete": "payload",
|
|
270
335
|
"console": false,
|
|
271
336
|
"id": "ab_eval_debug",
|
|
272
|
-
"name": "eval result",
|
|
337
|
+
"name": "eval group result",
|
|
273
338
|
"statusType": "auto",
|
|
274
339
|
"statusVal": "",
|
|
275
340
|
"targetType": "msg",
|
|
@@ -277,7 +342,7 @@
|
|
|
277
342
|
"tostatus": false,
|
|
278
343
|
"type": "debug",
|
|
279
344
|
"wires": [],
|
|
280
|
-
"x":
|
|
345
|
+
"x": 1150,
|
|
281
346
|
"y": 120,
|
|
282
347
|
"z": "ab_eval_demo_tab"
|
|
283
348
|
},
|
|
@@ -285,8 +350,236 @@
|
|
|
285
350
|
"env": [],
|
|
286
351
|
"id": "ab_eval_modules",
|
|
287
352
|
"modules": {
|
|
288
|
-
"@themoltnet/node-red-contrib-core": "0.
|
|
353
|
+
"@themoltnet/node-red-contrib-core": "0.6.0"
|
|
289
354
|
},
|
|
290
355
|
"type": "global-config"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"func": "const variants = Array.isArray(msg.evalVariants) && msg.evalVariants.length > 0\n ? msg.evalVariants\n : [{ label: msg.evalVariantLabel || 'with-context', includeSkill: true }];\nconst correlationId = msg.correlationId;\nconst scenarioId = msg.evalScenario?.id || 'unknown';\nconst expectedVariants = variants.length;\nconst groups = flow.get('abEvalResults') || {};\ngroups[correlationId] = {\n correlationId,\n expectedVariants: { [scenarioId]: expectedVariants },\n scenarios: {},\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString()\n};\nflow.set('abEvalResults', groups);\nnode.status({ fill: 'blue', shape: 'dot', text: 'fan out ' + expectedVariants });\nconst messages = variants.map((variant) => ({\n ...msg,\n evalVariant: variant,\n evalVariantLabel: variant.label,\n evalExpectedVariants: expectedVariants,\n evalGroup: { correlationId, scenarioId, expectedVariants },\n payload: { variantLabel: variant.label, correlationId, expectedVariants }\n}));\n// Function nodes use the outer array for outputs. Wrap the message array so\n// every variant is sent through output 1 instead of dropping all but the first.\nreturn [messages];",
|
|
359
|
+
"id": "ab_eval_init_group",
|
|
360
|
+
"name": "init eval group",
|
|
361
|
+
"outputs": 1,
|
|
362
|
+
"type": "function",
|
|
363
|
+
"wires": [["ab_eval_subflow"]],
|
|
364
|
+
"x": 410,
|
|
365
|
+
"y": 120,
|
|
366
|
+
"z": "ab_eval_demo_tab"
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"func": "if (!msg.payload?.groupComplete) {\n node.status({ fill: 'blue', shape: 'dot', text: (msg.payload?.completedVariants || 0) + '/' + (msg.payload?.expectedVariants || '?') + ' variants' });\n return null;\n}\nnode.status({ fill: 'green', shape: 'dot', text: msg.payload.completedVariants + '/' + msg.payload.expectedVariants + ' complete' });\nreturn msg;",
|
|
370
|
+
"id": "ab_eval_gate_group",
|
|
371
|
+
"name": "emit when group complete",
|
|
372
|
+
"outputs": 1,
|
|
373
|
+
"type": "function",
|
|
374
|
+
"wires": [["ab_eval_debug"]],
|
|
375
|
+
"x": 910,
|
|
376
|
+
"y": 120,
|
|
377
|
+
"z": "ab_eval_demo_tab"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
"checkall": "true",
|
|
381
|
+
"id": "sf_ab_gate_run_eval",
|
|
382
|
+
"name": "RUN EVAL accepted?",
|
|
383
|
+
"outputs": 2,
|
|
384
|
+
"property": "payload.accepted",
|
|
385
|
+
"propertyType": "msg",
|
|
386
|
+
"repair": false,
|
|
387
|
+
"rules": [
|
|
388
|
+
{
|
|
389
|
+
"t": "true"
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
"t": "else"
|
|
393
|
+
}
|
|
394
|
+
],
|
|
395
|
+
"type": "switch",
|
|
396
|
+
"wires": [["sf_ab_stash_run_eval"], ["sf_ab_pack_lane_failure"]],
|
|
397
|
+
"x": 1070,
|
|
398
|
+
"y": 120,
|
|
399
|
+
"z": "subflow_ab_eval_with_judge"
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
"checkall": "true",
|
|
403
|
+
"id": "sf_ab_gate_judge_eval",
|
|
404
|
+
"name": "JUDGE accepted?",
|
|
405
|
+
"outputs": 2,
|
|
406
|
+
"property": "payload.accepted",
|
|
407
|
+
"propertyType": "msg",
|
|
408
|
+
"repair": false,
|
|
409
|
+
"rules": [
|
|
410
|
+
{
|
|
411
|
+
"t": "true"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
"t": "else"
|
|
415
|
+
}
|
|
416
|
+
],
|
|
417
|
+
"type": "switch",
|
|
418
|
+
"wires": [["sf_ab_stash_judge_eval"], ["sf_ab_pack_lane_failure"]],
|
|
419
|
+
"x": 650,
|
|
420
|
+
"y": 360,
|
|
421
|
+
"z": "subflow_ab_eval_with_judge"
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
"id": "sf_ab_tail_out",
|
|
425
|
+
"links": ["1f190a60c144f6ec"],
|
|
426
|
+
"mode": "link",
|
|
427
|
+
"name": "tail events",
|
|
428
|
+
"type": "link out",
|
|
429
|
+
"wires": [],
|
|
430
|
+
"x": 805,
|
|
431
|
+
"y": 80,
|
|
432
|
+
"z": "subflow_ab_eval_with_judge"
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
"func": "function expectedFromMessage(msg, scenarioId) {\n const direct = Number(msg.evalExpectedVariants || msg.evalGroup?.expectedVariants || msg.payload?.expectedVariants || 0);\n if (Number.isFinite(direct) && direct > 0) return direct;\n const byScenario = Number(msg.evalGroup?.expectedVariants?.[scenarioId] || 0);\n return Number.isFinite(byScenario) && byScenario > 0 ? byScenario : 0;\n}\nfunction store(record, msg) {\n const groups = flow.get('abEvalResults') || {};\n const group = groups[record.correlationId] || { correlationId: record.correlationId, expectedVariants: {}, scenarios: {}, createdAt: new Date().toISOString() };\n group.expectedVariants = group.expectedVariants || {};\n const expected = expectedFromMessage(msg, record.scenarioId);\n if (expected > 0 && !group.expectedVariants[record.scenarioId]) {\n group.expectedVariants[record.scenarioId] = expected;\n }\n const scenario = group.scenarios[record.scenarioId] || {};\n scenario[record.variantLabel] = record;\n group.scenarios[record.scenarioId] = scenario;\n group.updatedAt = new Date().toISOString();\n groups[record.correlationId] = group;\n flow.set('abEvalResults', groups);\n return { group, scenario };\n}\nfunction expectedFor(group, scenarioId, msg) {\n const expected = Number(group.expectedVariants?.[scenarioId] || expectedFromMessage(msg, scenarioId) || 0);\n return Number.isFinite(expected) ? expected : 0;\n}\nconst snapshot = msg.payload && typeof msg.payload === 'object' ? msg.payload : {};\nconst task = snapshot.task || {};\nconst attempt = snapshot.attempt || {};\nconst stage = task.taskType === 'judge_eval_attempt' ? 'judge_eval_attempt' : 'run_eval';\nconst record = {\n status: 'failed',\n stage,\n correlationId: msg.correlationId || msg.evalGroupCorrelationId || snapshot.correlationId || null,\n scenarioId: msg.evalScenario?.id || 'unknown',\n variantLabel: msg.evalVariantLabel || msg.evalVariant?.label || 'unknown',\n isBaseline: msg.evalVariant?.baseline === true,\n runEvalTaskId: msg.evalTaskId || (stage === 'run_eval' ? task.id : null),\n runEvalAttemptN: msg.evalAcceptedAttemptN || attempt.attemptN || null,\n judgeTaskId: msg.evalJudgeTaskId || (stage === 'judge_eval_attempt' ? task.id : null),\n producerScore0to100: msg.evalProducerScore?.score0to100 ?? null,\n judgeScore0to100: null,\n judgeComposite: null,\n verdict: null,\n scores: [],\n error: snapshot.error || attempt.error || msg.error || { message: stage + ' failed' },\n updatedAt: new Date().toISOString()\n};\nconst stored = store(record, msg);\nconst expected = expectedFor(stored.group, record.scenarioId, msg);\nconst completed = Object.keys(stored.scenario).length;\nmsg.payload = {\n laneStatus: 'failed',\n groupComplete: expected > 0 && completed >= expected,\n expectedVariants: expected,\n completedVariants: completed,\n correlationId: record.correlationId,\n scenarioId: record.scenarioId,\n variantLabel: record.variantLabel,\n error: record.error,\n variants: stored.scenario\n};\nnode.status({ fill: 'yellow', shape: 'ring', text: record.variantLabel + ' failed' });\nreturn msg;",
|
|
436
|
+
"id": "sf_ab_pack_lane_failure",
|
|
437
|
+
"name": "pack lane failure",
|
|
438
|
+
"outputs": 1,
|
|
439
|
+
"type": "function",
|
|
440
|
+
"wires": [[]],
|
|
441
|
+
"x": 930,
|
|
442
|
+
"y": 360,
|
|
443
|
+
"z": "subflow_ab_eval_with_judge"
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
"id": "sf_ab_error_catch",
|
|
447
|
+
"name": "eval lane errors",
|
|
448
|
+
"scope": [
|
|
449
|
+
"sf_ab_create_run_eval",
|
|
450
|
+
"sf_ab_wait_run_eval",
|
|
451
|
+
"sf_ab_read_run_eval",
|
|
452
|
+
"sf_ab_create_judge_eval",
|
|
453
|
+
"sf_ab_wait_judge_eval",
|
|
454
|
+
"sf_ab_read_judge_eval",
|
|
455
|
+
"sf_ab_task_builder_run_eval",
|
|
456
|
+
"sf_ab_task_builder_judge_eval"
|
|
457
|
+
],
|
|
458
|
+
"type": "catch",
|
|
459
|
+
"uncaught": false,
|
|
460
|
+
"wires": [["sf_ab_pack_node_error"]],
|
|
461
|
+
"x": 640,
|
|
462
|
+
"y": 600,
|
|
463
|
+
"z": "subflow_ab_eval_with_judge"
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
"func": "function expectedFromMessage(msg, scenarioId) {\n const direct = Number(msg.evalExpectedVariants || msg.evalGroup?.expectedVariants || msg.payload?.expectedVariants || 0);\n if (Number.isFinite(direct) && direct > 0) return direct;\n const byScenario = Number(msg.evalGroup?.expectedVariants?.[scenarioId] || 0);\n return Number.isFinite(byScenario) && byScenario > 0 ? byScenario : 0;\n}\nfunction store(record, msg) {\n const groups = flow.get('abEvalResults') || {};\n const group = groups[record.correlationId] || { correlationId: record.correlationId, expectedVariants: {}, scenarios: {}, createdAt: new Date().toISOString() };\n group.expectedVariants = group.expectedVariants || {};\n const expected = expectedFromMessage(msg, record.scenarioId);\n if (expected > 0 && !group.expectedVariants[record.scenarioId]) {\n group.expectedVariants[record.scenarioId] = expected;\n }\n const scenario = group.scenarios[record.scenarioId] || {};\n scenario[record.variantLabel] = record;\n group.scenarios[record.scenarioId] = scenario;\n group.updatedAt = new Date().toISOString();\n groups[record.correlationId] = group;\n flow.set('abEvalResults', groups);\n return { group, scenario };\n}\nfunction expectedFor(group, scenarioId, msg) {\n const expected = Number(group.expectedVariants?.[scenarioId] || expectedFromMessage(msg, scenarioId) || 0);\n return Number.isFinite(expected) ? expected : 0;\n}\nconst error = msg.error || {};\nconst record = {\n status: 'failed',\n stage: error.source?.name || 'node-error',\n correlationId: msg.correlationId || msg.evalGroupCorrelationId || null,\n scenarioId: msg.evalScenario?.id || 'unknown',\n variantLabel: msg.evalVariantLabel || msg.evalVariant?.label || 'unknown',\n isBaseline: msg.evalVariant?.baseline === true,\n runEvalTaskId: msg.evalTaskId || null,\n runEvalAttemptN: msg.evalAcceptedAttemptN || null,\n judgeTaskId: msg.evalJudgeTaskId || null,\n producerScore0to100: msg.evalProducerScore?.score0to100 ?? null,\n judgeScore0to100: null,\n judgeComposite: null,\n verdict: null,\n scores: [],\n error: { message: String(error.message || 'eval lane node error'), source: error.source || null },\n updatedAt: new Date().toISOString()\n};\nconst stored = store(record, msg);\nconst expected = expectedFor(stored.group, record.scenarioId, msg);\nconst completed = Object.keys(stored.scenario).length;\nmsg.payload = {\n laneStatus: 'failed',\n groupComplete: expected > 0 && completed >= expected,\n expectedVariants: expected,\n completedVariants: completed,\n correlationId: record.correlationId,\n scenarioId: record.scenarioId,\n variantLabel: record.variantLabel,\n error: record.error,\n variants: stored.scenario\n};\nnode.status({ fill: 'red', shape: 'ring', text: record.variantLabel + ' error' });\nreturn msg;",
|
|
467
|
+
"id": "sf_ab_pack_node_error",
|
|
468
|
+
"name": "pack node error",
|
|
469
|
+
"outputs": 1,
|
|
470
|
+
"type": "function",
|
|
471
|
+
"wires": [[]],
|
|
472
|
+
"x": 920,
|
|
473
|
+
"y": 600,
|
|
474
|
+
"z": "subflow_ab_eval_with_judge"
|
|
475
|
+
},
|
|
476
|
+
{
|
|
477
|
+
"active": true,
|
|
478
|
+
"complete": "payload",
|
|
479
|
+
"console": false,
|
|
480
|
+
"id": "ab_eval_tail_debug",
|
|
481
|
+
"name": "eval task tail",
|
|
482
|
+
"statusType": "auto",
|
|
483
|
+
"statusVal": "",
|
|
484
|
+
"targetType": "msg",
|
|
485
|
+
"tosidebar": true,
|
|
486
|
+
"tostatus": false,
|
|
487
|
+
"type": "debug",
|
|
488
|
+
"wires": [],
|
|
489
|
+
"x": 900,
|
|
490
|
+
"y": 180,
|
|
491
|
+
"z": "ab_eval_demo_tab"
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
"id": "a72acb578ef0ea20",
|
|
495
|
+
"name": "",
|
|
496
|
+
"scope": [
|
|
497
|
+
"sf_ab_wait_judge_eval",
|
|
498
|
+
"sf_ab_wait_run_eval",
|
|
499
|
+
"sf_ab_create_run_eval",
|
|
500
|
+
"sf_ab_read_run_eval",
|
|
501
|
+
"sf_ab_read_judge_eval",
|
|
502
|
+
"sf_ab_create_judge_eval",
|
|
503
|
+
"sf_ab_task_builder_run_eval",
|
|
504
|
+
"sf_ab_task_builder_judge_eval"
|
|
505
|
+
],
|
|
506
|
+
"type": "status",
|
|
507
|
+
"wires": [[]],
|
|
508
|
+
"x": 920,
|
|
509
|
+
"y": 720,
|
|
510
|
+
"z": "subflow_ab_eval_with_judge"
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
"id": "0f5c72247640f952",
|
|
514
|
+
"links": ["1f190a60c144f6ec"],
|
|
515
|
+
"mode": "link",
|
|
516
|
+
"name": "tail events",
|
|
517
|
+
"type": "link out",
|
|
518
|
+
"wires": [],
|
|
519
|
+
"x": 795,
|
|
520
|
+
"y": 320,
|
|
521
|
+
"z": "subflow_ab_eval_with_judge"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"id": "1f190a60c144f6ec",
|
|
525
|
+
"links": ["0f5c72247640f952", "sf_ab_tail_out"],
|
|
526
|
+
"name": "tail events",
|
|
527
|
+
"type": "link in",
|
|
528
|
+
"wires": [[]],
|
|
529
|
+
"x": 1125,
|
|
530
|
+
"y": 80,
|
|
531
|
+
"z": "subflow_ab_eval_with_judge"
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
"agent": "eval_agent_cfg",
|
|
535
|
+
"brief": "",
|
|
536
|
+
"constraints": [],
|
|
537
|
+
"contexts": [],
|
|
538
|
+
"diaryId": "",
|
|
539
|
+
"diaryIdType": "str",
|
|
540
|
+
"expectedOutput": "",
|
|
541
|
+
"id": "sf_ab_task_builder_run_eval",
|
|
542
|
+
"name": "build RUN EVAL body",
|
|
543
|
+
"referencesFrom": "",
|
|
544
|
+
"referencesRole": "context",
|
|
545
|
+
"schemaCid": "",
|
|
546
|
+
"submitOutputGate": false,
|
|
547
|
+
"tags": "",
|
|
548
|
+
"taskType": "run_eval",
|
|
549
|
+
"teamId": "",
|
|
550
|
+
"teamIdType": "str",
|
|
551
|
+
"title": "",
|
|
552
|
+
"type": "moltnet-task-builder",
|
|
553
|
+
"wires": [["sf_ab_create_run_eval"]],
|
|
554
|
+
"workspace": "",
|
|
555
|
+
"x": 390,
|
|
556
|
+
"y": 120,
|
|
557
|
+
"z": "subflow_ab_eval_with_judge"
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
"agent": "eval_agent_cfg",
|
|
561
|
+
"brief": "",
|
|
562
|
+
"constraints": [],
|
|
563
|
+
"contexts": [],
|
|
564
|
+
"diaryId": "",
|
|
565
|
+
"diaryIdType": "str",
|
|
566
|
+
"expectedOutput": "",
|
|
567
|
+
"id": "sf_ab_task_builder_judge_eval",
|
|
568
|
+
"name": "build JUDGE body",
|
|
569
|
+
"referencesFrom": "",
|
|
570
|
+
"referencesRole": "context",
|
|
571
|
+
"schemaCid": "",
|
|
572
|
+
"submitOutputGate": false,
|
|
573
|
+
"tags": "",
|
|
574
|
+
"taskType": "judge_eval_attempt",
|
|
575
|
+
"teamId": "",
|
|
576
|
+
"teamIdType": "str",
|
|
577
|
+
"title": "",
|
|
578
|
+
"type": "moltnet-task-builder",
|
|
579
|
+
"wires": [["sf_ab_create_judge_eval"]],
|
|
580
|
+
"workspace": "",
|
|
581
|
+
"x": 1120,
|
|
582
|
+
"y": 240,
|
|
583
|
+
"z": "subflow_ab_eval_with_judge"
|
|
291
584
|
}
|
|
292
585
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@themoltnet/node-red-contrib-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Node-RED nodes for the MoltNet API",
|
|
6
6
|
"keywords": [
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"main": "dist/nodes/agent.js",
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@themoltnet/sdk": "0.
|
|
44
|
+
"@themoltnet/sdk": "0.118.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^22.19.0",
|