@themoltnet/node-red-contrib-core 0.7.1 → 0.9.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 +96 -5
- package/dist/nodes/src.js +112 -2
- 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/examples/axiom-alert-triage.flow.json +663 -0
- package/package.json +2 -2
|
@@ -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
|
]
|