trodo-cli 0.3.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/lib/commands/evals.js +144 -1
- package/lib/http.js +24 -6
- package/lib/main.js +2 -1
- package/package.json +1 -1
- package/skills/trodo/SKILL.md +17 -0
package/lib/commands/evals.js
CHANGED
|
@@ -135,4 +135,147 @@ async function steps([ref], flags) {
|
|
|
135
135
|
print({ eval: e.name, steps: e.steps, ...out }, flags);
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
/** A route's condition as words: `verdict = frustrated`, `score < 0.5`. */
|
|
139
|
+
function condText(w) {
|
|
140
|
+
if (!w || typeof w !== 'object') return String(w);
|
|
141
|
+
if (Array.isArray(w.all)) return w.all.map(condText).join(' and ');
|
|
142
|
+
if (Array.isArray(w.any)) return w.any.map(condText).join(' or ');
|
|
143
|
+
const ops = { eq: '=', ne: '!=', neq: '!=', lt: '<', lte: '<=', gt: '>', gte: '>=', in: 'in', contains: 'contains' };
|
|
144
|
+
const v = Array.isArray(w.value) ? w.value.join(', ') : w.value;
|
|
145
|
+
return `${w.field} ${ops[w.op] || w.op} ${v}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const asObj = (v) => { if (v && typeof v === 'object') return v; try { return JSON.parse(v); } catch { return null; } };
|
|
149
|
+
|
|
150
|
+
/** What one step does, from its config, in the terms a person writing a filter needs. */
|
|
151
|
+
function stepWork(n) {
|
|
152
|
+
const c = n.config || {};
|
|
153
|
+
if (n.instrument === 'python') return { runs: 'python', code: c.source };
|
|
154
|
+
if (n.instrument === 'decision_model') {
|
|
155
|
+
const qs = asObj(c.questions) || {};
|
|
156
|
+
return {
|
|
157
|
+
runs: 'decision model (Jev)',
|
|
158
|
+
reads: c.state,
|
|
159
|
+
questions: Object.entries(qs).map(([field, q]) => ({
|
|
160
|
+
field,
|
|
161
|
+
asks: q.instructions,
|
|
162
|
+
type: q.type,
|
|
163
|
+
...(Array.isArray(q.criteria) ? { levels: q.criteria.map((label, i) => ({ value: i, means: label })) } : {}),
|
|
164
|
+
...(Array.isArray(q.options) ? { options: q.options } : {}),
|
|
165
|
+
...(q.type === 'score' && Array.isArray(q.criteria) ? { note: `A weighted score from 0 to ${q.criteria.length - 1}: each level times its probability (1.8 is mostly "${q.criteria[q.criteria.length - 1]}"). Filter with a threshold, e.g. ${field}<${(q.criteria.length - 1) - 0.5}.` } : {}),
|
|
166
|
+
})),
|
|
167
|
+
may_abstain: String(c.allow_abstain) === 'true' || c.allow_abstain === true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (n.instrument === 'llm_judge') {
|
|
171
|
+
const prompt = asObj(c.prompt) || c.prompt;
|
|
172
|
+
return { runs: `LLM judge (${c.model || 'model'})`, prompt };
|
|
173
|
+
}
|
|
174
|
+
return { runs: n.instrument || n.type, config: c };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* trodo evals explain EVAL [--table]
|
|
179
|
+
* The eval as a coding agent needs it to write filters: what it checks, each
|
|
180
|
+
* step in order (what it runs, what it asks or computes, the answers it gives
|
|
181
|
+
* and their options, where each answer leads), the outcomes with pass / fail
|
|
182
|
+
* and reason codes, 30-day numbers, and ready filters.
|
|
183
|
+
*/
|
|
184
|
+
async function explain([ref], flags) {
|
|
185
|
+
const api = client(config.resolve(flags));
|
|
186
|
+
const e = await resolveEval(api, ref);
|
|
187
|
+
const [def, stats] = await Promise.all([
|
|
188
|
+
api.get(`/eval3/definitions/${e.id}`),
|
|
189
|
+
api.get(`/eval3/definitions/${e.id}/stats`).catch(() => null),
|
|
190
|
+
]);
|
|
191
|
+
const L = def.lineage || {};
|
|
192
|
+
const g = (def.current && def.current.graph) || {};
|
|
193
|
+
const nodes = g.nodes || {};
|
|
194
|
+
const nameOf = (id) => (nodes[id] && nodes[id].name) || id;
|
|
195
|
+
// Steps in the order a unit walks them, from the entry.
|
|
196
|
+
const order = [];
|
|
197
|
+
const seen = new Set();
|
|
198
|
+
const queue = [g.entry].filter(Boolean);
|
|
199
|
+
while (queue.length) {
|
|
200
|
+
const id = queue.shift();
|
|
201
|
+
if (seen.has(id) || !nodes[id]) continue;
|
|
202
|
+
seen.add(id);
|
|
203
|
+
order.push(id);
|
|
204
|
+
for (const edge of nodes[id].edges || []) queue.push(edge.to || edge.default);
|
|
205
|
+
}
|
|
206
|
+
for (const id of Object.keys(nodes)) if (!seen.has(id)) order.push(id);
|
|
207
|
+
const outcomes = order.filter((id) => nodes[id].type === 'verdict').map((id) => {
|
|
208
|
+
const c = nodes[id].config || {};
|
|
209
|
+
const sf = asObj(c.score_from);
|
|
210
|
+
return { id, name: nodes[id].name, passed: String(c.passed) === 'true' || c.passed === true, reason_code: c.reason_code || null, ...(sf ? { score_from: `${nameOf(sf.node_id)}.${sf.field}` } : {}) };
|
|
211
|
+
});
|
|
212
|
+
const steps = order.filter((id) => nodes[id].type !== 'verdict').map((id) => {
|
|
213
|
+
const n = nodes[id];
|
|
214
|
+
return {
|
|
215
|
+
id,
|
|
216
|
+
name: n.name,
|
|
217
|
+
kind: n.type,
|
|
218
|
+
...stepWork(n),
|
|
219
|
+
answers: Object.entries(n.output || {}).map(([field, f]) => ({ field, type: f.type, ...(f.values ? { options: f.values } : {}), ...(f.min != null ? { min: f.min, max: f.max } : {}) })),
|
|
220
|
+
next: (n.edges || []).map((edge) => (edge.default ? { otherwise: nameOf(edge.default) } : { when: condText(edge.when), to: nameOf(edge.to) })),
|
|
221
|
+
};
|
|
222
|
+
});
|
|
223
|
+
const t = (stats && stats.totals) || {};
|
|
224
|
+
const q = (s) => (/\s/.test(s) ? `"${s}"` : s);
|
|
225
|
+
const firstEnum = steps.flatMap((s) => s.answers.filter((a) => a.options).map((a) => `${s.name}.${a.field}=${a.options[0]}`))[0];
|
|
226
|
+
// A number worth a threshold: a judgment's weighted score, else a score on a small scale (not a count's open bound).
|
|
227
|
+
const scored = steps.flatMap((s) => (s.questions || []).filter((qq) => qq.levels).map((qq) => `${s.name}.${qq.field}<${qq.levels.length - 1.5}`));
|
|
228
|
+
const small = steps.flatMap((s) => s.answers.filter((a) => a.type === 'number' && a.max != null && Number(a.max) <= 10).map((a) => `${s.name}.${a.field}<${(Number(a.max) + Number(a.min || 0)) / 2}`));
|
|
229
|
+
const firstNum = scored[0] || small[0];
|
|
230
|
+
const codes = outcomes.filter((o) => o.reason_code).map((o) => o.reason_code);
|
|
231
|
+
const out = {
|
|
232
|
+
id: e.id,
|
|
233
|
+
name: e.name,
|
|
234
|
+
checks: L.description || null,
|
|
235
|
+
target: L.target,
|
|
236
|
+
agent: L.agent_name || null,
|
|
237
|
+
enabled: L.enabled,
|
|
238
|
+
production_version: L.production_version,
|
|
239
|
+
sample_pct: L.sample_pct != null ? Number(L.sample_pct) : null,
|
|
240
|
+
last_30_days: t.runs != null ? {
|
|
241
|
+
verdicts: t.runs, passed: t.passed, failed: t.failed, errored: t.errored,
|
|
242
|
+
pass_rate: t.runs ? Number((t.passed / Math.max(1, t.passed + t.failed)).toFixed(3)) : null,
|
|
243
|
+
avg_score: t.avg_score != null ? Number(Number(t.avg_score).toFixed(3)) : null,
|
|
244
|
+
} : null,
|
|
245
|
+
steps,
|
|
246
|
+
outcomes,
|
|
247
|
+
reason_codes: [...new Set(codes)],
|
|
248
|
+
filters: [
|
|
249
|
+
`--eval ${q(`${e.name}:failed`)}`,
|
|
250
|
+
codes[0] ? `--eval ${q(`${e.name}:reason=${codes[0]}`)}` : null,
|
|
251
|
+
firstEnum ? `--eval ${q(`${e.name}:${firstEnum}`)}` : null,
|
|
252
|
+
firstNum ? `--eval ${q(`${e.name}:${firstNum}`)}` : null,
|
|
253
|
+
`--eval ${q(`${e.name}:marked-wrong`)}`,
|
|
254
|
+
].filter(Boolean),
|
|
255
|
+
};
|
|
256
|
+
if (!flags.table) { print(out, flags); return; }
|
|
257
|
+
const lines = [];
|
|
258
|
+
lines.push(`${out.name} (${out.target}${out.agent ? `, agent ${out.agent}` : ''}, v${out.production_version}${out.enabled ? '' : ', off'})`);
|
|
259
|
+
if (out.checks) lines.push(` ${out.checks}`);
|
|
260
|
+
if (out.last_30_days) lines.push(` Last 30 days: ${out.last_30_days.verdicts} verdicts, ${out.last_30_days.passed} passed, ${out.last_30_days.failed} failed, pass rate ${(out.last_30_days.pass_rate * 100).toFixed(1)}%, average score ${out.last_30_days.avg_score}`);
|
|
261
|
+
lines.push('', 'Steps');
|
|
262
|
+
steps.forEach((s, i) => {
|
|
263
|
+
lines.push(` ${i + 1}. ${s.name} [${s.id}] — ${s.runs}`);
|
|
264
|
+
for (const qq of s.questions || []) {
|
|
265
|
+
lines.push(` asks ${qq.field}: ${qq.asks || ''}`);
|
|
266
|
+
if (qq.levels) lines.push(` levels: ${qq.levels.map((l) => `${l.value} = ${l.means}`).join('; ')}`);
|
|
267
|
+
if (qq.options) lines.push(` options: ${qq.options.join(', ')}`);
|
|
268
|
+
if (qq.note) lines.push(` ${qq.note}`);
|
|
269
|
+
}
|
|
270
|
+
if (s.prompt) lines.push(` prompt: ${String(typeof s.prompt === 'string' ? s.prompt : JSON.stringify(s.prompt)).replace(/\s+/g, ' ').slice(0, 220)}…`);
|
|
271
|
+
if (s.code) lines.push(` code: ${String(s.code).split('\n').length} lines of Python (--json shows it)`);
|
|
272
|
+
lines.push(` answers: ${s.answers.map((a) => `${a.field} (${a.options ? a.options.join('|') : a.type}${a.min != null ? ` ${a.min}..${a.max}` : ''})`).join(', ')}`);
|
|
273
|
+
for (const nx of s.next) lines.push(` ${nx.when ? `if ${nx.when} → ${nx.to}` : `then → ${nx.otherwise}`}`);
|
|
274
|
+
});
|
|
275
|
+
lines.push('', 'Outcomes');
|
|
276
|
+
for (const o of outcomes) lines.push(` ${o.passed ? 'pass' : 'FAIL'} ${o.name}${o.reason_code ? ` (${o.reason_code})` : ''}`);
|
|
277
|
+
lines.push('', 'Filters', ...out.filters.map((f) => ` ${f}`));
|
|
278
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = { list: listEvals, get: getEval, explain, verdicts, verdict, mark, queue, review, grade, steps, daysOf, condText };
|
package/lib/http.js
CHANGED
|
@@ -20,14 +20,32 @@ function client({ url, key, fetchImpl = globalThis.fetch }) {
|
|
|
20
20
|
const base = `${url}/api/v1`;
|
|
21
21
|
const headers = { Authorization: `Bearer ${key}`, 'User-Agent': `trodo-cli/${require('../package.json').version}`, Accept: 'application/json' };
|
|
22
22
|
|
|
23
|
+
// Reads are retried on a dropped connection or a gateway hiccup (502/503/504):
|
|
24
|
+
// a script pulling thousands of rows should not die on one blip. A write is
|
|
25
|
+
// never retried, so it can never happen twice.
|
|
26
|
+
const READ_POSTS = /^\/traces\/(count|stats|export)$/;
|
|
27
|
+
const isRead = (method, p) => method === 'GET' || (method === 'POST' && READ_POSTS.test(p));
|
|
28
|
+
const RETRY_STATUS = new Set([502, 503, 504]);
|
|
29
|
+
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
30
|
+
|
|
23
31
|
async function request(method, p, { query, body } = {}) {
|
|
24
32
|
const qs = query ? `?${new URLSearchParams(Object.entries(query).filter(([, v]) => v !== undefined && v !== null && v !== '').map(([k, v]) => [k, String(v)]))}` : '';
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
const attempts = isRead(method, p) ? 4 : 1;
|
|
34
|
+
for (let i = 1; ; i += 1) {
|
|
35
|
+
try {
|
|
36
|
+
// eslint-disable-next-line no-await-in-loop
|
|
37
|
+
const res = await fetchImpl(`${base}${p}${qs}`, {
|
|
38
|
+
method,
|
|
39
|
+
headers: body !== undefined ? { ...headers, 'Content-Type': 'application/json' } : headers,
|
|
40
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
41
|
+
});
|
|
42
|
+
if (i < attempts && RETRY_STATUS.has(res.status)) { await pause(500 * 2 ** (i - 1)); continue; } // eslint-disable-line no-await-in-loop
|
|
43
|
+
return res;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (i >= attempts) throw Object.assign(new Error(`Could not reach ${url} (${e.cause && e.cause.code ? e.cause.code : e.message}).`), { exitCode: 1 });
|
|
46
|
+
await pause(500 * 2 ** (i - 1)); // eslint-disable-line no-await-in-loop
|
|
47
|
+
}
|
|
48
|
+
}
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
async function json(method, p, opts) {
|
package/lib/main.js
CHANGED
|
@@ -35,6 +35,7 @@ Traces
|
|
|
35
35
|
|
|
36
36
|
Evals
|
|
37
37
|
trodo evals list | get EVAL names, steps (STEP.FIELD), 30-day stats
|
|
38
|
+
trodo evals explain EVAL [--table] what it checks, each step, answers and options, outcomes, filters
|
|
38
39
|
trodo evals steps EVAL [--since 30d] [--table] the paths verdicts took through the steps, with outcomes
|
|
39
40
|
trodo evals verdicts EVAL [--failed|--passed] [--since 7d] [--version N]
|
|
40
41
|
trodo evals verdicts --run ID | --span ID | --conversation ID
|
|
@@ -94,7 +95,7 @@ const COMMANDS = {
|
|
|
94
95
|
};
|
|
95
96
|
const ALIASES = { threads: 'conversations', thread: 'conversations', run: 'runs', span: 'spans', dataset: 'datasets', prompt: 'prompts', issue: 'issues', agent: 'agents', eval: 'evals', trace: 'traces' };
|
|
96
97
|
const GROUPS = {
|
|
97
|
-
evals: ['list', 'get', 'steps', 'verdicts', 'verdict', 'mark', 'queue', 'review', 'grade'],
|
|
98
|
+
evals: ['list', 'get', 'explain', 'steps', 'verdicts', 'verdict', 'mark', 'queue', 'review', 'grade'],
|
|
98
99
|
traces: ['count', 'stats', 'export', 'agents', 'fields', 'values'],
|
|
99
100
|
runs: ['list', 'search', 'get', 'tree', 'feedback'],
|
|
100
101
|
spans: ['list', 'search', 'get'],
|
package/package.json
CHANGED
package/skills/trodo/SKILL.md
CHANGED
|
@@ -32,6 +32,18 @@ Everything is per team. `trodo teams` lists the teams this terminal is signed in
|
|
|
32
32
|
to, `trodo switch TEAM` changes the current one, `--team TEAM` picks one for a
|
|
33
33
|
single command. Check `trodo whoami` before writing anything.
|
|
34
34
|
|
|
35
|
+
## Understand the evals first
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
trodo evals list --table
|
|
39
|
+
trodo evals explain "EVAL" --table # what it checks, each step, answer options, outcomes, working filters
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Read `explain` before writing an `--eval` filter. A decision-model question with
|
|
43
|
+
levels answers a weighted score (1.8 is mostly the top level): use a threshold
|
|
44
|
+
such as `STEP.field<1.5`, never `<2`. A filter matches a unit when any of its
|
|
45
|
+
verdicts matches, so a unit re-scored later can appear under both failed and passed.
|
|
46
|
+
|
|
35
47
|
## Find what went wrong
|
|
36
48
|
|
|
37
49
|
```bash
|
|
@@ -96,6 +108,11 @@ trodo evals queue --table ; trodo evals grade ITEM_ID CHOICE
|
|
|
96
108
|
|
|
97
109
|
## Datasets and training data
|
|
98
110
|
|
|
111
|
+
Do the thinking in your own script: export with the filter, process the JSONL
|
|
112
|
+
locally (each line has input, output, and every verdict with each step's
|
|
113
|
+
answer), then write rows back with `datasets create NAME -f rows.jsonl` or
|
|
114
|
+
`datasets update`. Nothing of yours runs on Trodo.
|
|
115
|
+
|
|
99
116
|
```bash
|
|
100
117
|
trodo traces export --kind spans --eval any:passed --eval any:not-failed --format openai -o sft.jsonl
|
|
101
118
|
trodo traces export --kind runs --since 7d --include-trace --dir ./runs # one JSON file per run
|