@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
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
|
|
|
@@ -309,6 +329,75 @@ in the Node-RED instance) to get a live workflow cockpit without any custom Vue
|
|
|
309
329
|
widget. A starter flow is in [`examples/cockpit.flow.json`](./examples/cockpit.flow.json)
|
|
310
330
|
(inject → `moltnet-workflow-status` → debug; add a `ui-table` to visualize).
|
|
311
331
|
|
|
332
|
+
## Axiom alert triage workflow
|
|
333
|
+
|
|
334
|
+
[`examples/axiom-alert-triage.flow.json`](./examples/axiom-alert-triage.flow.json)
|
|
335
|
+
is an observability triage experiment with two trigger modes:
|
|
336
|
+
|
|
337
|
+
`Axiom monitor history polling` → **parser/enricher task** →
|
|
338
|
+
**codebase triage task** → **judge task**.
|
|
339
|
+
|
|
340
|
+
When Axiom custom webhook notifiers are available, the same triage chain can also
|
|
341
|
+
start from `POST /axiom/monitor`.
|
|
342
|
+
|
|
343
|
+
The polling path calls Axiom monitor history with `AXIOM_MONITORING_TOKEN`
|
|
344
|
+
(`AXIOM_API_TOKEN` is accepted as a fallback for local experiments), dedupes
|
|
345
|
+
events by monitor/state/timestamp in flow context, starts work only for
|
|
346
|
+
trigger-like events, builds task bodies through `task: build`, and chains tasks
|
|
347
|
+
through `task: read` `artifactRef` values so large telemetry artifacts stay in
|
|
348
|
+
MoltNet. Resolution/no-op events go to a debug lane instead of starting codebase
|
|
349
|
+
triage.
|
|
350
|
+
|
|
351
|
+
Polling environment:
|
|
352
|
+
|
|
353
|
+
- `AXIOM_MONITORING_TOKEN`: Axiom token with monitor history/read access and,
|
|
354
|
+
for the current single-token experiment, telemetry query read access.
|
|
355
|
+
- `AXIOM_MONITOR_IDS`: optional comma-separated monitor ids. Defaults to the
|
|
356
|
+
MoltNet monitors created by `infra/axiom/monitors/apply.mjs`.
|
|
357
|
+
- `AXIOM_POLL_WINDOW_MINUTES`: optional lookback window, default `15`.
|
|
358
|
+
|
|
359
|
+
To test the flow without a real Axiom trigger, use the **inject synthetic
|
|
360
|
+
alert** node in the example. It builds the same normalized `msg.axiom` shape as
|
|
361
|
+
the polling/webhook paths, marks it with `synthetic: true`, and routes it through
|
|
362
|
+
the common monitor-events link so the parser, triage, artifact chaining, and
|
|
363
|
+
judge stages all run.
|
|
364
|
+
|
|
365
|
+
Optional Axiom custom webhook setup:
|
|
366
|
+
|
|
367
|
+
- URL: `https://<node-red-host>/axiom/monitor`
|
|
368
|
+
- Method: `POST`
|
|
369
|
+
- Optional secret header: `x-axiom-webhook-secret: <secret>`
|
|
370
|
+
- Node-RED env: `AXIOM_WEBHOOK_SECRET=<secret>` when that header is used
|
|
371
|
+
|
|
372
|
+
Suggested custom webhook body:
|
|
373
|
+
|
|
374
|
+
```json
|
|
375
|
+
{
|
|
376
|
+
"action": "{{.Action}}",
|
|
377
|
+
"body": "{{.Body}}",
|
|
378
|
+
"endTime": "now",
|
|
379
|
+
"monitorId": "{{.MonitorID}}",
|
|
380
|
+
"monitorName": "{{.Title}}",
|
|
381
|
+
"service": "moltnet-rest-api",
|
|
382
|
+
"severity": "warning",
|
|
383
|
+
"startTime": "now-30m",
|
|
384
|
+
"value": "{{.Value}}"
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
The parser/enricher task prompt asks the agent to use the repo-local
|
|
389
|
+
`axiom-alert-triage` skill and produce a JSON artifact titled
|
|
390
|
+
`telemetry-evidence`. The next task consumes that artifact, compares findings
|
|
391
|
+
against the `getlarge/themoltnet` codebase, GitHub issues, and MoltNet entries,
|
|
392
|
+
then writes `triage-report`. The final judge task writes `triage-judgment`.
|
|
393
|
+
|
|
394
|
+
Before enabling live webhooks, refine Axiom monitors so broad auth/client noise
|
|
395
|
+
(401/404) is separated from actionable alerts such as 5xx errors, latency
|
|
396
|
+
regression, event loop pressure, and memory pressure. The companion repo-local
|
|
397
|
+
`axiom-observability` skill and [`infra/axiom`](../../infra/axiom) document the
|
|
398
|
+
monitor/dashboard maintenance conventions and provide idempotent apply scripts
|
|
399
|
+
for the committed Axiom config.
|
|
400
|
+
|
|
312
401
|
## Build
|
|
313
402
|
|
|
314
403
|
```bash
|
|
@@ -369,7 +458,9 @@ export default {
|
|
|
369
458
|
Open the editor, drag in `agent` + the task nodes, or import
|
|
370
459
|
[`examples/issue-lifecycle.flow.json`](./examples/issue-lifecycle.flow.json) or
|
|
371
460
|
[`examples/cockpit.flow.json`](./examples/cockpit.flow.json) or
|
|
372
|
-
[`examples/deep-review-freeform.flow.json`](./examples/deep-review-freeform.flow.json)
|
|
461
|
+
[`examples/deep-review-freeform.flow.json`](./examples/deep-review-freeform.flow.json)
|
|
462
|
+
or
|
|
463
|
+
[`examples/axiom-alert-triage.flow.json`](./examples/axiom-alert-triage.flow.json),
|
|
373
464
|
then fill the agent's `clientId`/`clientSecret`.
|
|
374
465
|
|
|
375
466
|
If Node-RED crashes in `@node-red/editor-api/lib/auth/tokens.js` with
|
package/dist/nodes/src.js
CHANGED
|
@@ -1927,7 +1927,7 @@ var findLatestRuntimeSlotForAttempt = (options) => (options.client ?? client).ge
|
|
|
1927
1927
|
...options
|
|
1928
1928
|
});
|
|
1929
1929
|
/**
|
|
1930
|
-
*
|
|
1930
|
+
* Queue asynchronous deletion of terminal tasks in bulk. By default, live, unauthorized, missing, and protected tasks are skipped. Set force: true with a reason to delete protected terminal tasks.
|
|
1931
1931
|
*/
|
|
1932
1932
|
var batchDeleteTasks = (options) => (options.client ?? client).delete({
|
|
1933
1933
|
security: [
|
|
@@ -14172,6 +14172,86 @@ var PRODUCER_TASK_TYPES = new Set([
|
|
|
14172
14172
|
"render_pack",
|
|
14173
14173
|
"run_eval"
|
|
14174
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
|
+
}
|
|
14175
14255
|
/**
|
|
14176
14256
|
* Fluent, network-free builder for a `tasks.create` body. Encodes the
|
|
14177
14257
|
* non-obvious task schema (context arrays, success-criteria gates,
|
|
@@ -14622,6 +14702,20 @@ function buildJudgeEvalAttempt(input) {
|
|
|
14622
14702
|
return buildTask("judge_eval_attempt", input);
|
|
14623
14703
|
}
|
|
14624
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
|
+
/**
|
|
14625
14719
|
* Build a `pr_review` task. Requires `subject` + `successCriteria`. Note the
|
|
14626
14720
|
* rubric criteria must use `boolean` scoring for this task type.
|
|
14627
14721
|
*
|
|
@@ -14659,7 +14753,9 @@ var TaskResultReader = class {
|
|
|
14659
14753
|
accepted;
|
|
14660
14754
|
/** Token / cost usage for the accepted attempt, if reported. */
|
|
14661
14755
|
usage;
|
|
14756
|
+
/** Task id for the task whose accepted attempt is being read. */
|
|
14662
14757
|
taskId;
|
|
14758
|
+
/** CID of the accepted attempt output. */
|
|
14663
14759
|
outputCid;
|
|
14664
14760
|
constructor(task, attempt) {
|
|
14665
14761
|
const errors = [];
|
|
@@ -14752,6 +14848,19 @@ var TaskResultReader = class {
|
|
|
14752
14848
|
};
|
|
14753
14849
|
}
|
|
14754
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
|
+
/**
|
|
14755
14864
|
* Build a `TaskRef` that anchors a downstream task to this accepted output
|
|
14756
14865
|
* and points at one persistent task artifact by CID.
|
|
14757
14866
|
*
|
|
@@ -14889,6 +14998,7 @@ function createTasksNamespace(context) {
|
|
|
14889
14998
|
buildAssessBrief,
|
|
14890
14999
|
buildJudgePack,
|
|
14891
15000
|
buildJudgeEvalAttempt,
|
|
15001
|
+
buildJudgeEvalAttemptForRunEval,
|
|
14892
15002
|
buildPrReview,
|
|
14893
15003
|
async readResult(taskOrId) {
|
|
14894
15004
|
const task = typeof taskOrId === "string" ? unwrapResult(await getTask({
|
|
@@ -17223,4 +17333,4 @@ if (!etc.sha512Sync) etc.sha512Sync = (...m) => {
|
|
|
17223
17333
|
return hash.digest();
|
|
17224
17334
|
};
|
|
17225
17335
|
//#endregion
|
|
17226
|
-
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 = {
|