openmerit 0.4.0 → 0.4.2
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 +239 -58
- package/data/catalogue.json +6778 -0
- package/dist/catalogue/catalogue.d.ts +41 -0
- package/dist/catalogue/catalogue.js +33 -0
- package/dist/cli/prompt.d.ts +26 -0
- package/dist/cli/prompt.js +114 -0
- package/dist/cli/render.d.ts +12 -0
- package/dist/cli/render.js +104 -0
- package/dist/cli.js +245 -158
- package/dist/connect.d.ts +34 -0
- package/dist/connect.js +158 -0
- package/dist/connectors/langfuse/index.d.ts +39 -112
- package/dist/connectors/langfuse/index.js +95 -246
- package/dist/daemon/daemon.d.ts +81 -0
- package/dist/daemon/daemon.js +408 -0
- package/dist/daemon/registry.d.ts +111 -0
- package/dist/daemon/registry.js +109 -0
- package/dist/eval/proxy.d.ts +54 -0
- package/dist/eval/proxy.js +174 -0
- package/dist/eval/runner.d.ts +29 -0
- package/dist/eval/runner.js +80 -0
- package/dist/harness/claude-bridge.d.ts +21 -0
- package/dist/harness/claude-bridge.js +65 -0
- package/dist/harness/harness.d.ts +60 -0
- package/dist/harness/harness.js +221 -0
- package/dist/index.d.ts +10 -11
- package/dist/index.js +9 -10
- package/dist/local.d.ts +12 -0
- package/dist/local.js +40 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +155 -0
- package/dist/scout/candidates.d.ts +1 -1
- package/dist/scout/pool.d.ts +17 -0
- package/dist/scout/pool.js +56 -0
- package/dist/scout/scan.d.ts +42 -0
- package/dist/scout/scan.js +169 -0
- package/dist/scout/search.d.ts +25 -98
- package/dist/scout/search.js +56 -156
- package/dist/scout/workflow.d.ts +221 -0
- package/dist/scout/workflow.js +941 -0
- package/dist/setup.d.ts +44 -0
- package/dist/setup.js +211 -0
- package/dist/state/config.d.ts +116 -0
- package/dist/state/config.js +60 -0
- package/dist/state/db.d.ts +159 -0
- package/dist/state/db.js +260 -0
- package/dist/state/paths.d.ts +12 -0
- package/dist/state/paths.js +23 -0
- package/dist/update.d.ts +22 -0
- package/dist/update.js +58 -0
- package/package.json +13 -18
- package/dist/connectors/langfuse/otlp.d.ts +0 -60
- package/dist/connectors/langfuse/otlp.js +0 -40
- package/dist/pi/extension.d.ts +0 -13
- package/dist/pi/extension.js +0 -148
- package/dist/pi/harness.d.ts +0 -20
- package/dist/pi/harness.js +0 -49
- package/dist/pi/instructions.d.ts +0 -2
- package/dist/pi/instructions.js +0 -12
- package/dist/scout/evaluators/module.d.ts +0 -24
- package/dist/scout/evaluators/module.js +0 -19
- package/dist/scout/evaluators/pi.d.ts +0 -53
- package/dist/scout/evaluators/pi.js +0 -117
- package/dist/scout/pipeline.d.ts +0 -111
- package/dist/scout/pipeline.js +0 -157
- package/dist/scout/report.d.ts +0 -3
- package/dist/scout/report.js +0 -42
- package/dist/scout/service.d.ts +0 -148
- package/dist/scout/service.js +0 -143
- package/examples/health-declaration-codes/dataset.jsonl +0 -14
- package/examples/sahra-enrollment/evaluator.ts +0 -151
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
// Scout evaluator for the Sahra Health playground's enrollment agent (fictional data only).
|
|
2
|
-
// It uses only what a customer exposes: the playground Worker's HTTP API and Sahra's own Langfuse
|
|
3
|
-
// dataset `sahra-enrollments` (item input `{ application }`, expected output `{ decision,
|
|
4
|
-
// declaredConditions, notDeclared? }`). Each item runs in a fresh, isolated ledger.
|
|
5
|
-
//
|
|
6
|
-
// PLAYGROUND_URL (default http://localhost:8787), PLAYGROUND_TOKEN, LANGFUSE_* for measured cost.
|
|
7
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
8
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
9
|
-
// Copy this file into your project to adapt it; it has no runtime import from OpenMerit.
|
|
10
|
-
// In your project, import the type from the package instead: `import type { EvaluatorPlugin } from "openmerit"`.
|
|
11
|
-
import type { EvaluatorPlugin } from "../../src/scout/evaluators/module.ts";
|
|
12
|
-
|
|
13
|
-
/** Scout skips a candidate that throws this (matched by name) as untested instead of stopping. */
|
|
14
|
-
class CandidateUnavailable extends Error {
|
|
15
|
-
override readonly name = "CandidateUnavailable";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface Expected { decision: "enrolled" | "declined"; declaredConditions: string[]; notDeclared?: string[] }
|
|
19
|
-
interface State {
|
|
20
|
-
enrollment: { decision: "enrolled" | "declined" | "pending" } | null;
|
|
21
|
-
member: { declaredConditions: string[] } | null;
|
|
22
|
-
workflow: { status?: string; error?: { message?: string } } | null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** The agent ended without recording a decision. Under `wrangler dev` the workflow reports only the NonRetryableError. */
|
|
26
|
-
const NO_DECISION = /ended without a decision|threw an? NonRetryableError/;
|
|
27
|
-
|
|
28
|
-
interface Observation { type: string; level?: string; statusMessage?: string | null; endTime?: string | null; totalCost?: number | null; metadata?: { generations?: string | number } }
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Why a run ended without a decision, from the playground's own trace: a provider error on a model
|
|
32
|
-
* call (`402: …`, `404: …`) is not a quality verdict. Returns null when every model call succeeded,
|
|
33
|
-
* which makes it the agent's failure.
|
|
34
|
-
*/
|
|
35
|
-
export function providerError(observations: readonly Observation[]): { status: number; message: string } | null {
|
|
36
|
-
for (const row of observations) {
|
|
37
|
-
if (row.type !== "GENERATION" || row.level !== "ERROR") continue;
|
|
38
|
-
const message = row.statusMessage ?? "";
|
|
39
|
-
const status = Number(/^(\d{3})\b/.exec(message)?.[1] ?? /\b(4\d\d|5\d\d)\b/.exec(message)?.[1] ?? 0);
|
|
40
|
-
return { status, message: message.slice(0, 240) };
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** The playground scenario runner's checks: the decision, then declared condition prefixes. */
|
|
46
|
-
export function enrollmentPass(expected: Expected, state: State): { pass: boolean; comment: string } {
|
|
47
|
-
const decision = state.enrollment?.decision ?? "none";
|
|
48
|
-
if (decision !== expected.decision) return { pass: false, comment: `decision ${decision}, expected ${expected.decision}` };
|
|
49
|
-
if (expected.decision !== "enrolled") return { pass: true, comment: `decision ${decision}` };
|
|
50
|
-
const declared = state.member?.declaredConditions ?? [];
|
|
51
|
-
const missing = expected.declaredConditions.filter(code => !declared.some(item => item.startsWith(code)));
|
|
52
|
-
const wrong = (expected.notDeclared ?? []).filter(code => declared.some(item => item.startsWith(code)));
|
|
53
|
-
return {
|
|
54
|
-
pass: !missing.length && !wrong.length,
|
|
55
|
-
comment: `declared ${declared.join(", ") || "none"}${missing.length ? `; missing ${missing.join(", ")}` : ""}${wrong.length ? `; should not declare ${wrong.join(", ")}` : ""}`,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const plugin: EvaluatorPlugin = {
|
|
60
|
-
name: "sahra-enrollment",
|
|
61
|
-
providers: ["openrouter"],
|
|
62
|
-
tools: true,
|
|
63
|
-
passScore: "enrollment-correct",
|
|
64
|
-
role: "enrollment",
|
|
65
|
-
concurrency: 6,
|
|
66
|
-
create({ env, log }) {
|
|
67
|
-
const url = (env.PLAYGROUND_URL ?? "http://localhost:8787").replace(/\/+$/, "");
|
|
68
|
-
const token = env.PLAYGROUND_TOKEN;
|
|
69
|
-
if (!token) throw new Error("Set PLAYGROUND_TOKEN to the playground Worker's token");
|
|
70
|
-
const langfuse = env.LANGFUSE_PUBLIC_KEY && env.LANGFUSE_SECRET_KEY
|
|
71
|
-
? { base: (env.LANGFUSE_BASE_URL ?? "https://us.cloud.langfuse.com").replace(/\/+$/, ""), auth: `Basic ${btoa(`${env.LANGFUSE_PUBLIC_KEY}:${env.LANGFUSE_SECRET_KEY}`)}` }
|
|
72
|
-
: null;
|
|
73
|
-
|
|
74
|
-
/** The playground's own trace once its agent root has ended and every generation is ingested; null on timeout. */
|
|
75
|
-
async function trace(traceId: string, priced: boolean): Promise<Observation[] | null> {
|
|
76
|
-
if (!langfuse) return null;
|
|
77
|
-
const deadline = Date.now() + 180_000;
|
|
78
|
-
await delay(8_000);
|
|
79
|
-
while (Date.now() < deadline) {
|
|
80
|
-
const response = await fetch(`${langfuse.base}/api/public/v2/observations?traceId=${traceId}&fields=core,basic,usage,metadata&limit=100`, { headers: { authorization: langfuse.auth } });
|
|
81
|
-
if (response.ok) {
|
|
82
|
-
const { data = [] } = await response.json() as { data?: Observation[] };
|
|
83
|
-
const root = data.find(row => row.type === "AGENT");
|
|
84
|
-
const generations = data.filter(row => row.type === "GENERATION");
|
|
85
|
-
const expected = Number(root?.metadata?.generations ?? NaN);
|
|
86
|
-
if (root?.endTime && (generations.length === expected || root.level === "ERROR")
|
|
87
|
-
&& (!priced || generations.every(row => typeof row.totalCost === "number"))) return data;
|
|
88
|
-
}
|
|
89
|
-
await delay(response.status === 429 ? 30_000 : 8_000);
|
|
90
|
-
}
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
const traceCost = async (traceId: string) => {
|
|
94
|
-
const data = await trace(traceId, true);
|
|
95
|
-
return data ? data.filter(row => row.type === "GENERATION").reduce((sum, row) => sum + (row.totalCost ?? 0), 0) : null;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
return async (candidate, item, context) => {
|
|
99
|
-
if (candidate.provider !== "openrouter") throw new Error("The enrollment agent runs OpenRouter models only");
|
|
100
|
-
const input = item.input as { application?: { applicationId?: string } };
|
|
101
|
-
const application = input.application;
|
|
102
|
-
if (!application?.applicationId) throw new Error(`Dataset item ${item.id} has no application`);
|
|
103
|
-
const expected = item.expectedOutput as Expected;
|
|
104
|
-
// Outlast EnrollmentFlow's own 10-minute limit and one retry, so the workflow decides the outcome.
|
|
105
|
-
const signal = AbortSignal.any([AbortSignal.timeout(25 * 60_000), ...(context.signal ? [context.signal] : [])]);
|
|
106
|
-
const call = async <T>(method: string, path: string, body?: unknown): Promise<T> => {
|
|
107
|
-
const response = await fetch(`${url}${path}`, { method, headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: body === undefined ? null : JSON.stringify(body), signal });
|
|
108
|
-
if (!response.ok) throw new Error(`Playground ${method} ${path.split("?")[0]} HTTP ${response.status}`);
|
|
109
|
-
return await response.json() as T;
|
|
110
|
-
};
|
|
111
|
-
const traceIdOf = (runId: string) => createHash("sha256").update(`trace:${application.applicationId}--${runId}/enrollment`).digest("hex").slice(0, 32);
|
|
112
|
-
for (let attempt = 1; ; attempt++) {
|
|
113
|
-
const runId = `scout-${randomUUID()}`;
|
|
114
|
-
log(` ${context.stage} ${candidate.id} ${item.id} run=${runId}`);
|
|
115
|
-
await call("POST", "/admin/seed", { runId });
|
|
116
|
-
await call("POST", "/enrollments", { runId, application, models: { enrollment: { provider: "openrouter", id: candidate.id } } });
|
|
117
|
-
const path = `/enrollments/${encodeURIComponent(application.applicationId)}?runId=${encodeURIComponent(runId)}`;
|
|
118
|
-
let verdict: { pass: boolean; comment: string; output: unknown } | null = null;
|
|
119
|
-
while (!verdict) {
|
|
120
|
-
const state = await call<State>("GET", path);
|
|
121
|
-
const decision = state.enrollment?.decision;
|
|
122
|
-
if (["errored", "terminated"].includes(state.workflow?.status ?? "")) {
|
|
123
|
-
if (!NO_DECISION.test(state.workflow?.error?.message ?? "")) {
|
|
124
|
-
throw new Error(`Enrollment workflow ${state.workflow?.status} (run ${runId}): ${state.workflow?.error?.message ?? "no message"}; no quality verdict`);
|
|
125
|
-
}
|
|
126
|
-
// Only the trace can say whether a model call failed (provider error) or the agent gave up.
|
|
127
|
-
const observations = await trace(traceIdOf(runId), false);
|
|
128
|
-
if (!observations) throw new Error(`Cannot tell a provider error from an agent failure for run ${runId}: its Langfuse trace did not arrive`);
|
|
129
|
-
const failed = providerError(observations);
|
|
130
|
-
if (failed && [400, 402, 404, 413, 422].includes(failed.status)) throw new CandidateUnavailable(`${failed.status} from the provider (run ${runId}): ${failed.message}`);
|
|
131
|
-
if (failed && (failed.status === 429 || failed.status >= 500 || failed.status === 0) && attempt < 2) { log(` retrying ${candidate.id} ${item.id} after ${failed.status || "an unclassified provider error"}`); break; }
|
|
132
|
-
if (failed) throw new Error(`Provider error ${failed.status} for ${candidate.model} (run ${runId}): ${failed.message}`);
|
|
133
|
-
verdict = { pass: false, comment: "the agent ended without recording a decision", output: { decision: null } };
|
|
134
|
-
} else if (decision && decision !== "pending") {
|
|
135
|
-
verdict = { ...enrollmentPass(expected, state), output: { decision, declaredConditions: state.member?.declaredConditions ?? [] } };
|
|
136
|
-
} else if (state.workflow?.status === "complete") {
|
|
137
|
-
throw new Error(`Enrollment workflow completed without a decision (run ${runId})`);
|
|
138
|
-
} else await delay(1500, undefined, { signal });
|
|
139
|
-
}
|
|
140
|
-
if (!verdict) continue;
|
|
141
|
-
// The playground's trace ID for this agent run (see its tracing.ts): sha256("trace:<app>--<runId>/enrollment").
|
|
142
|
-
const traceId = traceIdOf(runId);
|
|
143
|
-
// Cost decides ties between finalists, so wait for it there; screens skip the ingestion wait.
|
|
144
|
-
const costUsd = context.stage === "final" ? await traceCost(traceId) : null;
|
|
145
|
-
return { ...verdict, output: { ...verdict.output as object, playgroundRunId: runId, playgroundTraceId: traceId }, costUsd };
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
},
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
export default plugin;
|