@themoltnet/node-red-contrib-core 0.2.0 → 0.3.1
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 +27 -2
- package/dist/nodes/agent.js +1 -1
- package/dist/nodes/entries-search.html +134 -0
- package/dist/nodes/entries-search.js +78 -0
- package/dist/nodes/query-utils.js +43 -0
- package/dist/nodes/src.js +16797 -0
- package/dist/nodes/task-builder.js +1 -1
- package/dist/nodes/task-reader.js +1 -1
- package/dist/nodes/tasks-list.html +174 -0
- package/dist/nodes/tasks-list.js +77 -0
- package/examples/ab-eval-with-judge.flow.json +292 -0
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -15,10 +15,11 @@ Empirically validated against **Node-RED 5.0.0** (Node 22):
|
|
|
15
15
|
package carries no private-package runtime dependency. `@themoltnet/sdk` is
|
|
16
16
|
therefore a **devDependency** (bundled, not installed at runtime).
|
|
17
17
|
- The `.html` editor files are copied to `dist/nodes/` as assets (not compiled).
|
|
18
|
-
- Two **config nodes** (`moltnet-agent`, `moltnet-runtime-profile`) and
|
|
18
|
+
- Two **config nodes** (`moltnet-agent`, `moltnet-runtime-profile`) and eight
|
|
19
19
|
**action nodes** (`moltnet-tasks-create`, `moltnet-task-get`,
|
|
20
20
|
`moltnet-task-wait`, `moltnet-workflow-status`, `moltnet-task-builder`,
|
|
21
|
-
`moltnet-task-reader`)
|
|
21
|
+
`moltnet-task-reader`, `moltnet-tasks-list`, `moltnet-entries-search`)
|
|
22
|
+
register and appear in the palette.
|
|
22
23
|
|
|
23
24
|
## Nodes
|
|
24
25
|
|
|
@@ -38,6 +39,12 @@ Empirically validated against **Node-RED 5.0.0** (Node 22):
|
|
|
38
39
|
wins). The task `input` and advanced fields come from `msg.payload`. See
|
|
39
40
|
[Building the task request](#building-the-task-request). Holds no SDK import —
|
|
40
41
|
the SDK lives only in the config node.
|
|
42
|
+
- **`moltnet-tasks-list`** (palette: _tasks: list_) — lists tasks for the
|
|
43
|
+
referenced agent's team. Supports the server task filters (`status`,
|
|
44
|
+
`statuses`, `taskTypes`, `tags`, `excludeTags`, profile/correlation/diary,
|
|
45
|
+
proposer/claimer ids, attempts, date windows, `limit`, `cursor`). Node fields
|
|
46
|
+
fill the query; an object `msg.payload` overrides them. Emits task rows on
|
|
47
|
+
`msg.payload` and pagination/query metadata on `msg.tasks`.
|
|
41
48
|
- **`moltnet-task-get`** (palette: _task: get_) — one-shot read of a task and its
|
|
42
49
|
attempts (no polling). Emits a normalized **snapshot** on `msg.payload`:
|
|
43
50
|
`{ taskId, status, terminal, accepted, acceptedAttemptN, state, attempt,
|
|
@@ -70,6 +77,13 @@ attempts, error, task }`. `state` is the accepted attempt's output artifact
|
|
|
70
77
|
The pre-computed **`outputRef`** (`{ taskId, outputCid, role }`) chains straight
|
|
71
78
|
into a downstream `task: build`'s **References from**; set an **artifact
|
|
72
79
|
kind/title** to pre-parse a JSON artifact body into `msg.result.artifactBody`.
|
|
80
|
+
- **`moltnet-entries-search`** (palette: _entries: search_) — searches diary
|
|
81
|
+
entries using the SDK hybrid search endpoint. Supports `diaryId`, `query`,
|
|
82
|
+
`tags`, `excludeTags`, `entryTypes`, `excludeSuperseded`, `limit`, `offset`,
|
|
83
|
+
and relevance/recency/importance weights. A string `msg.payload` is treated as
|
|
84
|
+
the query; an object `msg.payload` overrides node fields and may use camelCase
|
|
85
|
+
SDK keys or snake_case MCP-style keys. Emits entries on `msg.payload` and
|
|
86
|
+
search metadata on `msg.entries`.
|
|
73
87
|
|
|
74
88
|
All nodes register a long, collision-safe `type` (`moltnet-*`) but show a short
|
|
75
89
|
`paletteLabel` under the **moltnet** category, so the palette is not crowded by
|
|
@@ -173,6 +187,17 @@ the builder's context rows**, **agent output→input chaining via the reader's
|
|
|
173
187
|
freeform rubric. Runs on one daemon; see the in-flow comment for the
|
|
174
188
|
model-specialization option.
|
|
175
189
|
|
|
190
|
+
## A/B eval with judge subflow
|
|
191
|
+
|
|
192
|
+
[`examples/ab-eval-with-judge.flow.json`](./examples/ab-eval-with-judge.flow.json)
|
|
193
|
+
imports a reusable **A/B eval with judge** subflow plus a small demo tab. The
|
|
194
|
+
subflow runs `run_eval`, locally scores required fields/findings, then creates a
|
|
195
|
+
`judge_eval_attempt` task and stores per-variant scores/deltas in flow context.
|
|
196
|
+
|
|
197
|
+
Fill the `moltnet-agent` config after import. Runtime-profile config nodes are
|
|
198
|
+
included but blank: leave them blank for any eligible daemon to claim both
|
|
199
|
+
tasks, or set producer/judge profile IDs and run one daemon per profile.
|
|
200
|
+
|
|
176
201
|
## Reproducing the issue-lifecycle shape
|
|
177
202
|
|
|
178
203
|
[`examples/issue-lifecycle.flow.json`](./examples/issue-lifecycle.flow.json)
|
package/dist/nodes/agent.js
CHANGED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('moltnet-entries-search', {
|
|
3
|
+
category: 'moltnet',
|
|
4
|
+
color: '#00d4c8',
|
|
5
|
+
paletteLabel: 'entries: search',
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: '' },
|
|
8
|
+
agent: { value: '', type: 'moltnet-agent', required: true },
|
|
9
|
+
diaryId: { value: '' },
|
|
10
|
+
query: { value: '' },
|
|
11
|
+
tags: { value: '' },
|
|
12
|
+
excludeTags: { value: '' },
|
|
13
|
+
entryTypes: { value: '' },
|
|
14
|
+
excludeSuperseded: { value: 'false' },
|
|
15
|
+
limit: { value: 10, validate: RED.validators.number() },
|
|
16
|
+
offset: { value: '', validate: RED.validators.number() },
|
|
17
|
+
wRelevance: { value: 1, validate: RED.validators.number() },
|
|
18
|
+
wRecency: { value: 0, validate: RED.validators.number() },
|
|
19
|
+
wImportance: { value: 0, validate: RED.validators.number() },
|
|
20
|
+
},
|
|
21
|
+
inputs: 1,
|
|
22
|
+
outputs: 1,
|
|
23
|
+
icon: 'font-awesome/fa-search',
|
|
24
|
+
label: function () {
|
|
25
|
+
return this.name || 'entries: search';
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script type="text/html" data-template-name="moltnet-entries-search">
|
|
31
|
+
<div class="form-row">
|
|
32
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
33
|
+
<input type="text" id="node-input-name" />
|
|
34
|
+
</div>
|
|
35
|
+
<div class="form-row">
|
|
36
|
+
<label for="node-input-agent"><i class="fa fa-user"></i> Agent</label>
|
|
37
|
+
<input type="text" id="node-input-agent" />
|
|
38
|
+
</div>
|
|
39
|
+
<div class="form-row">
|
|
40
|
+
<label for="node-input-diaryId"><i class="fa fa-book"></i> Diary</label>
|
|
41
|
+
<input
|
|
42
|
+
type="text"
|
|
43
|
+
id="node-input-diaryId"
|
|
44
|
+
placeholder="agent diary by default"
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="form-row">
|
|
48
|
+
<label for="node-input-query"><i class="fa fa-search"></i> Query</label>
|
|
49
|
+
<input
|
|
50
|
+
type="text"
|
|
51
|
+
id="node-input-query"
|
|
52
|
+
placeholder="msg.payload overrides"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="form-row">
|
|
56
|
+
<label for="node-input-tags"><i class="fa fa-tags"></i> Tags</label>
|
|
57
|
+
<input
|
|
58
|
+
type="text"
|
|
59
|
+
id="node-input-tags"
|
|
60
|
+
placeholder="decision,scope:node-red"
|
|
61
|
+
/>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="form-row">
|
|
64
|
+
<label for="node-input-excludeTags"
|
|
65
|
+
><i class="fa fa-ban"></i> Exclude</label
|
|
66
|
+
>
|
|
67
|
+
<input
|
|
68
|
+
type="text"
|
|
69
|
+
id="node-input-excludeTags"
|
|
70
|
+
placeholder="superseded,noisy"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="form-row">
|
|
74
|
+
<label for="node-input-entryTypes"
|
|
75
|
+
><i class="fa fa-archive"></i> Types</label
|
|
76
|
+
>
|
|
77
|
+
<input
|
|
78
|
+
type="text"
|
|
79
|
+
id="node-input-entryTypes"
|
|
80
|
+
placeholder="semantic,episodic"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
<div class="form-row">
|
|
84
|
+
<label for="node-input-excludeSuperseded"
|
|
85
|
+
><i class="fa fa-filter"></i> Superseded</label
|
|
86
|
+
>
|
|
87
|
+
<select id="node-input-excludeSuperseded">
|
|
88
|
+
<option value="false">Include</option>
|
|
89
|
+
<option value="true">Exclude</option>
|
|
90
|
+
</select>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="form-row">
|
|
93
|
+
<label for="node-input-limit"><i class="fa fa-hashtag"></i> Limit</label>
|
|
94
|
+
<input type="number" id="node-input-limit" />
|
|
95
|
+
</div>
|
|
96
|
+
<div class="form-row">
|
|
97
|
+
<label for="node-input-offset"><i class="fa fa-forward"></i> Offset</label>
|
|
98
|
+
<input type="number" id="node-input-offset" />
|
|
99
|
+
</div>
|
|
100
|
+
<div class="form-row">
|
|
101
|
+
<label for="node-input-wRelevance"
|
|
102
|
+
><i class="fa fa-bullseye"></i> Relevance</label
|
|
103
|
+
>
|
|
104
|
+
<input type="number" step="0.1" id="node-input-wRelevance" />
|
|
105
|
+
</div>
|
|
106
|
+
<div class="form-row">
|
|
107
|
+
<label for="node-input-wRecency"
|
|
108
|
+
><i class="fa fa-clock-o"></i> Recency</label
|
|
109
|
+
>
|
|
110
|
+
<input type="number" step="0.1" id="node-input-wRecency" />
|
|
111
|
+
</div>
|
|
112
|
+
<div class="form-row">
|
|
113
|
+
<label for="node-input-wImportance"
|
|
114
|
+
><i class="fa fa-star"></i> Importance</label
|
|
115
|
+
>
|
|
116
|
+
<input type="number" step="0.1" id="node-input-wImportance" />
|
|
117
|
+
</div>
|
|
118
|
+
</script>
|
|
119
|
+
|
|
120
|
+
<script type="text/html" data-help-name="moltnet-entries-search">
|
|
121
|
+
<p>
|
|
122
|
+
Searches MoltNet diary entries. The configured diary is used first, then the
|
|
123
|
+
agent's diary; set <code>msg.payload.diaryId</code> to override it.
|
|
124
|
+
</p>
|
|
125
|
+
<p>
|
|
126
|
+
A string <code>msg.payload</code> is treated as the search query. An object
|
|
127
|
+
<code>msg.payload</code> overrides node fields and may use either camelCase
|
|
128
|
+
SDK keys or snake_case MCP-style keys.
|
|
129
|
+
</p>
|
|
130
|
+
<p>
|
|
131
|
+
Emits search result items on <code>msg.payload</code> and metadata on
|
|
132
|
+
<code>msg.entries = { total, query }</code>.
|
|
133
|
+
</p>
|
|
134
|
+
</script>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { a as nonEmpty, c as positiveInt, i as finiteNumber, n as compact, o as nonNegativeInt, r as csv, s as normalizeAliases, t as bool } from "./query-utils.js";
|
|
2
|
+
//#region src/nodes/entries-search.ts
|
|
3
|
+
var init = (RED) => {
|
|
4
|
+
function EntriesSearchNode(def) {
|
|
5
|
+
RED.nodes.createNode(this, def);
|
|
6
|
+
const agentNode = def.agent ? RED.nodes.getNode(def.agent) : null;
|
|
7
|
+
this.on("input", (msg, send, done) => {
|
|
8
|
+
const run = async () => {
|
|
9
|
+
try {
|
|
10
|
+
if (!agentNode || typeof agentNode.getAgent !== "function") throw new Error("entries-search: no moltnet-agent configured");
|
|
11
|
+
const body = buildSearchBody(def, msg, agentNode.diaryId);
|
|
12
|
+
if (!body.query) throw new Error("entries-search: query is required");
|
|
13
|
+
this.status({
|
|
14
|
+
fill: "blue",
|
|
15
|
+
shape: "dot",
|
|
16
|
+
text: "searching…"
|
|
17
|
+
});
|
|
18
|
+
const result = await (await agentNode.getAgent()).entries.search(body);
|
|
19
|
+
const out = RED.util.cloneMessage(msg);
|
|
20
|
+
out.payload = result.results;
|
|
21
|
+
out.entries = {
|
|
22
|
+
total: result.total,
|
|
23
|
+
query: body
|
|
24
|
+
};
|
|
25
|
+
this.status({
|
|
26
|
+
fill: "green",
|
|
27
|
+
shape: "dot",
|
|
28
|
+
text: `${result.results.length} entr${result.results.length === 1 ? "y" : "ies"}`
|
|
29
|
+
});
|
|
30
|
+
send(out);
|
|
31
|
+
done();
|
|
32
|
+
} catch (err) {
|
|
33
|
+
this.status({
|
|
34
|
+
fill: "red",
|
|
35
|
+
shape: "ring",
|
|
36
|
+
text: "error"
|
|
37
|
+
});
|
|
38
|
+
done(err instanceof Error ? err : new Error(String(err)));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
run();
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
RED.nodes.registerType("moltnet-entries-search", EntriesSearchNode);
|
|
45
|
+
};
|
|
46
|
+
function buildSearchBody(def, msg, agentDiaryId) {
|
|
47
|
+
const configured = {
|
|
48
|
+
diaryId: nonEmpty(def.diaryId) ?? nonEmpty(agentDiaryId),
|
|
49
|
+
query: nonEmpty(def.query),
|
|
50
|
+
tags: csv(def.tags),
|
|
51
|
+
excludeTags: csv(def.excludeTags),
|
|
52
|
+
entryTypes: csv(def.entryTypes),
|
|
53
|
+
excludeSuperseded: bool(def.excludeSuperseded),
|
|
54
|
+
limit: positiveInt(def.limit),
|
|
55
|
+
offset: nonNegativeInt(def.offset),
|
|
56
|
+
wRelevance: finiteNumber(def.wRelevance),
|
|
57
|
+
wRecency: finiteNumber(def.wRecency),
|
|
58
|
+
wImportance: finiteNumber(def.wImportance)
|
|
59
|
+
};
|
|
60
|
+
const payload = msg.payload && typeof msg.payload === "object" ? normalizePayload(msg.payload) : typeof msg.payload === "string" ? { query: msg.payload } : {};
|
|
61
|
+
return compact({
|
|
62
|
+
...configured,
|
|
63
|
+
...payload
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function normalizePayload(payload) {
|
|
67
|
+
return normalizeAliases(payload, {
|
|
68
|
+
diary_id: "diaryId",
|
|
69
|
+
entry_types: "entryTypes",
|
|
70
|
+
exclude_superseded: "excludeSuperseded",
|
|
71
|
+
exclude_tags: "excludeTags",
|
|
72
|
+
w_importance: "wImportance",
|
|
73
|
+
w_recency: "wRecency",
|
|
74
|
+
w_relevance: "wRelevance"
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
//#endregion
|
|
78
|
+
export { init as default };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
//#region src/nodes/query-utils.ts
|
|
2
|
+
function csv(value) {
|
|
3
|
+
if (Array.isArray(value)) {
|
|
4
|
+
const items = value.filter((item) => typeof item === "string");
|
|
5
|
+
return items.length > 0 ? items : void 0;
|
|
6
|
+
}
|
|
7
|
+
if (typeof value !== "string") return void 0;
|
|
8
|
+
const items = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
9
|
+
return items.length > 0 ? items : void 0;
|
|
10
|
+
}
|
|
11
|
+
function bool(value) {
|
|
12
|
+
if (typeof value === "boolean") return value;
|
|
13
|
+
if (value === "true") return true;
|
|
14
|
+
if (value === "false") return false;
|
|
15
|
+
}
|
|
16
|
+
function positiveInt(value) {
|
|
17
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
18
|
+
return Number.isInteger(n) && n > 0 ? n : void 0;
|
|
19
|
+
}
|
|
20
|
+
function nonNegativeInt(value) {
|
|
21
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
22
|
+
return Number.isInteger(n) && n >= 0 ? n : void 0;
|
|
23
|
+
}
|
|
24
|
+
function finiteNumber(value) {
|
|
25
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
26
|
+
return Number.isFinite(n) ? n : void 0;
|
|
27
|
+
}
|
|
28
|
+
function nonEmpty(value) {
|
|
29
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
30
|
+
}
|
|
31
|
+
function compact(value) {
|
|
32
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
|
|
33
|
+
}
|
|
34
|
+
function normalizeAliases(payload, aliases) {
|
|
35
|
+
const normalized = { ...payload };
|
|
36
|
+
for (const [alias, canonical] of Object.entries(aliases)) if (normalized[alias] !== void 0 && normalized[canonical] === void 0) {
|
|
37
|
+
normalized[canonical] = normalized[alias];
|
|
38
|
+
delete normalized[alias];
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
export { nonEmpty as a, positiveInt as c, finiteNumber as i, compact as n, nonNegativeInt as o, csv as r, normalizeAliases as s, bool as t };
|