@crowandraven/evalkeeper 0.0.0-stage → 0.2.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.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/bin/evalkeeper.mjs +9 -0
  4. package/dist/attention.d.ts +49 -0
  5. package/dist/attention.js +49 -0
  6. package/dist/attention.js.map +1 -0
  7. package/dist/board.d.ts +57 -0
  8. package/dist/board.js +2 -0
  9. package/dist/board.js.map +1 -0
  10. package/dist/cli/addPages.d.ts +79 -0
  11. package/dist/cli/addPages.js +181 -0
  12. package/dist/cli/addPages.js.map +1 -0
  13. package/dist/cli/main.d.ts +18 -0
  14. package/dist/cli/main.js +115 -0
  15. package/dist/cli/main.js.map +1 -0
  16. package/dist/common.d.ts +72 -0
  17. package/dist/common.js +276 -0
  18. package/dist/common.js.map +1 -0
  19. package/dist/compare.d.ts +85 -0
  20. package/dist/compare.js +185 -0
  21. package/dist/compare.js.map +1 -0
  22. package/dist/decisions.d.ts +84 -0
  23. package/dist/decisions.js +146 -0
  24. package/dist/decisions.js.map +1 -0
  25. package/dist/feedback.d.ts +114 -0
  26. package/dist/feedback.js +116 -0
  27. package/dist/feedback.js.map +1 -0
  28. package/dist/format.d.ts +6 -0
  29. package/dist/format.js +22 -0
  30. package/dist/format.js.map +1 -0
  31. package/dist/glossary.d.ts +46 -0
  32. package/dist/glossary.js +45 -0
  33. package/dist/glossary.js.map +1 -0
  34. package/dist/health.d.ts +40 -0
  35. package/dist/health.js +101 -0
  36. package/dist/health.js.map +1 -0
  37. package/dist/index.d.ts +31 -0
  38. package/dist/index.js +34 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/labels.d.ts +8 -0
  41. package/dist/labels.js +15 -0
  42. package/dist/labels.js.map +1 -0
  43. package/dist/legs.d.ts +23 -0
  44. package/dist/legs.js +22 -0
  45. package/dist/legs.js.map +1 -0
  46. package/dist/memoryStore.d.ts +21 -0
  47. package/dist/memoryStore.js +228 -0
  48. package/dist/memoryStore.js.map +1 -0
  49. package/dist/models.d.ts +76 -0
  50. package/dist/models.js +56 -0
  51. package/dist/models.js.map +1 -0
  52. package/dist/prune.d.ts +48 -0
  53. package/dist/prune.js +77 -0
  54. package/dist/prune.js.map +1 -0
  55. package/dist/quickSet.d.ts +26 -0
  56. package/dist/quickSet.js +44 -0
  57. package/dist/quickSet.js.map +1 -0
  58. package/dist/runner.d.ts +202 -0
  59. package/dist/runner.js +734 -0
  60. package/dist/runner.js.map +1 -0
  61. package/dist/settings.d.ts +196 -0
  62. package/dist/settings.js +272 -0
  63. package/dist/settings.js.map +1 -0
  64. package/dist/stableJson.d.ts +11 -0
  65. package/dist/stableJson.js +15 -0
  66. package/dist/stableJson.js.map +1 -0
  67. package/dist/stats.d.ts +98 -0
  68. package/dist/stats.js +119 -0
  69. package/dist/stats.js.map +1 -0
  70. package/dist/store.d.ts +151 -0
  71. package/dist/store.js +3 -0
  72. package/dist/store.js.map +1 -0
  73. package/dist/suggest.d.ts +75 -0
  74. package/dist/suggest.js +152 -0
  75. package/dist/suggest.js.map +1 -0
  76. package/dist/summary.d.ts +51 -0
  77. package/dist/summary.js +61 -0
  78. package/dist/summary.js.map +1 -0
  79. package/dist/testing.d.ts +26 -0
  80. package/dist/testing.js +362 -0
  81. package/dist/testing.js.map +1 -0
  82. package/dist/timeout.d.ts +2 -0
  83. package/dist/timeout.js +15 -0
  84. package/dist/timeout.js.map +1 -0
  85. package/dist/types.d.ts +225 -0
  86. package/dist/types.js +46 -0
  87. package/dist/types.js.map +1 -0
  88. package/dist/upkeepRules.d.ts +28 -0
  89. package/dist/upkeepRules.js +40 -0
  90. package/dist/upkeepRules.js.map +1 -0
  91. package/dist/util.d.ts +8 -0
  92. package/dist/util.js +18 -0
  93. package/dist/util.js.map +1 -0
  94. package/dist/weekly.d.ts +80 -0
  95. package/dist/weekly.js +113 -0
  96. package/dist/weekly.js.map +1 -0
  97. package/package.json +81 -4
  98. package/src/attention.ts +89 -0
  99. package/src/board.ts +58 -0
  100. package/src/cli/addPages.ts +256 -0
  101. package/src/cli/main.ts +125 -0
  102. package/src/common.ts +338 -0
  103. package/src/compare.ts +261 -0
  104. package/src/decisions.ts +230 -0
  105. package/src/feedback.ts +225 -0
  106. package/src/format.ts +19 -0
  107. package/src/glossary.ts +75 -0
  108. package/src/health.ts +149 -0
  109. package/src/index.ts +33 -0
  110. package/src/labels.ts +18 -0
  111. package/src/legs.ts +38 -0
  112. package/src/memoryStore.ts +268 -0
  113. package/src/models.ts +127 -0
  114. package/src/prune.ts +95 -0
  115. package/src/quickSet.ts +55 -0
  116. package/src/runner.ts +1010 -0
  117. package/src/settings.ts +441 -0
  118. package/src/stableJson.ts +24 -0
  119. package/src/stats.ts +215 -0
  120. package/src/store.ts +172 -0
  121. package/src/suggest.ts +247 -0
  122. package/src/summary.ts +109 -0
  123. package/src/testing.ts +415 -0
  124. package/src/timeout.ts +14 -0
  125. package/src/types.ts +258 -0
  126. package/src/upkeepRules.ts +46 -0
  127. package/src/util.ts +17 -0
  128. package/src/weekly.ts +169 -0
  129. package/starter/index.ts +86 -0
  130. package/templates/app/api/cron/evals-weekly/route.ts +24 -0
  131. package/templates/app/evals/[suite]/page.tsx +449 -0
  132. package/templates/app/evals/[suite]/runs/[runId]/page.tsx +95 -0
  133. package/templates/app/evals/feedback/page.tsx +219 -0
  134. package/templates/app/evals/page.tsx +313 -0
  135. package/templates/components/evalkeeper/AddCase.tsx +62 -0
  136. package/templates/components/evalkeeper/AnswerKey.tsx +360 -0
  137. package/templates/components/evalkeeper/CaseMenu.tsx +122 -0
  138. package/templates/components/evalkeeper/Fields.tsx +110 -0
  139. package/templates/components/evalkeeper/RefreshWhile.tsx +25 -0
  140. package/templates/components/evalkeeper/SettingsHistory.tsx +191 -0
  141. package/templates/components/evalkeeper/Submit.tsx +58 -0
  142. package/templates/components/evalkeeper/Thumbs.tsx +117 -0
  143. package/templates/components/evalkeeper/Tip.tsx +98 -0
  144. package/templates/components/evalkeeper/WeeklyCheck.tsx +124 -0
  145. package/templates/components/evalkeeper/evalkeeper.css +71 -0
  146. package/templates/components/evalkeeper/ui.tsx +103 -0
  147. package/templates/lib/evalkeeper/actions.ts +263 -0
  148. package/templates/lib/evalkeeper/cronAuth.ts +18 -0
  149. package/templates/lib/evalkeeper/paths.ts +11 -0
  150. package/templates/lib/evalkeeper/time.ts +4 -0
  151. package/templates/lib/evalkeeper/weekly.ts +51 -0
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Rules for the weekly eval check, pure so they're tested: when a quick-set
3
+ * score has drifted, when a model is about to retire, and when people's
4
+ * thumbs-down rate needs a look.
5
+ */
6
+
7
+ /** A drop this big on the quick set is worth a look. */
8
+ export const DRIFT_DROP = 0.15;
9
+ /** Warn about a live or fallback model this long before it retires. */
10
+ export const RETIRE_WARN_DAYS = 60;
11
+ /** Thumbs: at least this many ratings, at least this share down. */
12
+ export const THUMBS_MIN = 5;
13
+ /**
14
+ * The share of thumbs down that flags a feature (with at least THUMBS_MIN ratings).
15
+ */
16
+ export const THUMBS_DOWN_SHARE = 0.2;
17
+
18
+ /** This week's quick-set score against last week's: a drop worth a look. */
19
+ export function drifted(previous: number, latest: number): boolean {
20
+ return previous - latest >= DRIFT_DROP;
21
+ }
22
+
23
+ /**
24
+ * Days until a "YYYY-MM" or "YYYY-MM-DD" retirement; a month counts from
25
+ * its first day. Negative once it's passed.
26
+ */
27
+ export function daysUntil(retiring: string, today: Date): number {
28
+ const [y = 1970, m = 1, d = 1] = retiring.split("-").map(Number);
29
+ const when = Date.UTC(y, (m || 1) - 1, d || 1);
30
+ return Math.floor((when - today.getTime()) / 86_400_000);
31
+ }
32
+
33
+ /** Whether a model retires within the warning window, or already has. */
34
+ export function retiresSoon(retiring: string | undefined, today: Date) {
35
+ return (
36
+ retiring !== undefined && daysUntil(retiring, today) <= RETIRE_WARN_DAYS
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Whether a feature's ratings need a look: enough of them, and enough down.
42
+ */
43
+ export function thumbsAlert(up: number, down: number): boolean {
44
+ const n = up + down;
45
+ return n >= THUMBS_MIN && down / n >= THUMBS_DOWN_SHARE;
46
+ }
package/src/util.ts ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The item at `i`, for indexes the caller has already checked (a loop
3
+ * bound, a non-empty list). Throws instead of returning undefined, so a
4
+ * wrong index fails loudly rather than as NaN or a missing row.
5
+ */
6
+ export function at<T>(items: readonly T[], i: number): T {
7
+ if (i < 0 || i >= items.length)
8
+ throw new RangeError(`Index ${i} is outside 0..${items.length - 1}`);
9
+ return items[i] as T;
10
+ }
11
+
12
+ /** A map's value for a key the caller knows is there. Throws otherwise. */
13
+ export function mustGet<K, V>(map: ReadonlyMap<K, V>, key: K): V {
14
+ const v = map.get(key);
15
+ if (v === undefined) throw new Error(`Missing ${String(key)}`);
16
+ return v;
17
+ }
package/src/weekly.ts ADDED
@@ -0,0 +1,169 @@
1
+ import type { ThumbsSummary } from "./feedback.js";
2
+ import { retiresLabel, type ModelCatalog } from "./models.js";
3
+ import type { RunMetrics, RunRecord } from "./store.js";
4
+ import { daysUntil, drifted, retiresSoon } from "./upkeepRules.js";
5
+
6
+ /**
7
+ * The weekly check: each suite's quick set on its live model, with fresh
8
+ * answers, so a provider quietly changing a model under the same name shows
9
+ * up as drift. The summary also flags models retiring soon and features
10
+ * people rate poorly. It only reports; a person decides what to change.
11
+ * Pure.
12
+ */
13
+
14
+ /** `startedBy` on tests the weekly check starts, so they're told apart. */
15
+ export const SCHEDULED = "schedule";
16
+
17
+ const DAY = 86_400_000;
18
+
19
+ /**
20
+ * What the last weekly check found: drift, unfinished checks, retiring models, and
21
+ * thumbs-down, plus when it ran and what it cost.
22
+ */
23
+ export type WeeklySummary = {
24
+ /** When the last weekly check started; null before the first. */
25
+ lastAt: Date | null;
26
+ /** What last week's quick sets cost (reused answers are free). */
27
+ costUsd: number;
28
+ /** Quick sets still running. */
29
+ running: number;
30
+ drift: {
31
+ suite: string;
32
+ label: string;
33
+ model: string;
34
+ previous: number;
35
+ latest: number;
36
+ }[];
37
+ /** Quick sets that didn't finish, and why. */
38
+ failed: { suite: string; label: string; error: string }[];
39
+ /** Live and fallback models retiring within the warning window. */
40
+ retiring: {
41
+ suite: string;
42
+ label: string;
43
+ model: string;
44
+ role: "live" | "fallback";
45
+ when: string;
46
+ days: number;
47
+ }[];
48
+ /** Features with a high thumbs-down share this week. */
49
+ thumbs: ThumbsSummary[];
50
+ /** Ran, and nothing needs a look. */
51
+ clear: boolean;
52
+ };
53
+
54
+ /** A test's headline score: parts right where it gives partial credit. */
55
+ export function headline(m: RunMetrics | null): number | null {
56
+ if (!m) return null;
57
+ return m.units && m.units.total
58
+ ? m.units.right / m.units.total
59
+ : (m.allPass.rate ?? 0);
60
+ }
61
+
62
+ /**
63
+ * What the last weekly check found, from every suite's tests.
64
+ *
65
+ * @example
66
+ * ```ts
67
+ * const runs = (await Promise.all(SUITES.map((s) => store.listRuns(s.key, { limit: 50 })))).flat();
68
+ * const weekly = weeklySummary({ runs, suites: SUITES, routes, models: MODELS, thumbs });
69
+ * if (!weekly.clear) console.log(weekly.drift, weekly.retiring);
70
+ * ```
71
+ */
72
+ export function weeklySummary(args: {
73
+ /** Every suite's tests, newest first or not. */
74
+ runs: RunRecord[];
75
+ suites: { key: string; label: string; operation: string }[];
76
+ /** Each operation's live and fallback model. */
77
+ routes: Record<string, { model: string; fallbackModel: string | null }>;
78
+ models: ModelCatalog;
79
+ /** From thumbsSummary over the last week; only flagged ones are shown. */
80
+ thumbs?: ThumbsSummary[];
81
+ today?: Date;
82
+ }): WeeklySummary {
83
+ const today = args.today ?? new Date();
84
+ const label = (key: string) =>
85
+ args.suites.find((s) => s.key === key)?.label ?? key;
86
+ const scheduled = args.runs
87
+ .filter((r) => r.startedBy === SCHEDULED && r.set === "SMOKE")
88
+ .sort((a, b) => +b.startedAt - +a.startedAt);
89
+ const lastAt = scheduled[0]?.startedAt ?? null;
90
+ // The last weekly batch: everything started within a day of the newest.
91
+ const batch = lastAt
92
+ ? scheduled.filter((r) => +lastAt - +r.startedAt < DAY)
93
+ : [];
94
+
95
+ const drift: WeeklySummary["drift"] = [];
96
+ const failed: WeeklySummary["failed"] = [];
97
+ for (const r of batch) {
98
+ if (r.status === "FAILED") {
99
+ failed.push({
100
+ suite: r.suite,
101
+ label: label(r.suite),
102
+ error: r.error ?? "unknown error",
103
+ });
104
+ continue;
105
+ }
106
+ if (r.status !== "SUCCEEDED") continue;
107
+ // The last check of the same model, temperature, instructions, and
108
+ // grading, a day or more earlier: anything else changed for a known
109
+ // reason.
110
+ const prior = scheduled.find(
111
+ (p) =>
112
+ p.suite === r.suite &&
113
+ p.status === "SUCCEEDED" &&
114
+ p.model === r.model &&
115
+ (p.temperature ?? null) === (r.temperature ?? null) &&
116
+ p.promptVersion === r.promptVersion &&
117
+ p.graderVersion === r.graderVersion &&
118
+ +r.startedAt - +p.startedAt >= DAY,
119
+ );
120
+ const latest = headline(r.metrics);
121
+ const previous = headline(prior?.metrics ?? null);
122
+ if (latest !== null && previous !== null && drifted(previous, latest))
123
+ drift.push({
124
+ suite: r.suite,
125
+ label: label(r.suite),
126
+ model: args.models[r.model]?.label ?? r.model,
127
+ previous,
128
+ latest,
129
+ });
130
+ }
131
+
132
+ const retiring: WeeklySummary["retiring"] = [];
133
+ for (const s of args.suites) {
134
+ const route = args.routes[s.operation];
135
+ if (!route) continue;
136
+ for (const [model, role] of [
137
+ [route.model, "live"],
138
+ [route.fallbackModel, "fallback"],
139
+ ] as const) {
140
+ const when = model ? args.models[model]?.retiring : undefined;
141
+ if (model && when && retiresSoon(when, today))
142
+ retiring.push({
143
+ suite: s.key,
144
+ label: s.label,
145
+ model: args.models[model]?.label ?? model,
146
+ role,
147
+ when: retiresLabel(args.models, model) ?? when,
148
+ days: daysUntil(when, today),
149
+ });
150
+ }
151
+ }
152
+
153
+ const thumbs = (args.thumbs ?? []).filter((t) => t.alert);
154
+ return {
155
+ lastAt,
156
+ costUsd: batch.reduce((t, r) => t + (r.metrics?.costUsd ?? 0), 0),
157
+ running: batch.filter((r) => r.status === "RUNNING").length,
158
+ drift,
159
+ failed,
160
+ retiring,
161
+ thumbs,
162
+ clear:
163
+ lastAt !== null &&
164
+ !drift.length &&
165
+ !failed.length &&
166
+ !retiring.length &&
167
+ !thumbs.length,
168
+ };
169
+ }
@@ -0,0 +1,86 @@
1
+ // Your app's Evalkeeper wiring: the one file the copied eval pages import.
2
+ // Written by `npx @crowandraven/evalkeeper add pages`; it's yours now. Fill in each TODO.
3
+ import "server-only";
4
+ import {
5
+ createSettings,
6
+ type AnySuite,
7
+ type ModelCatalog,
8
+ type Settings,
9
+ } from "@crowandraven/evalkeeper";
10
+ import { createRunner, type Route } from "@crowandraven/evalkeeper/runner";
11
+ import {
12
+ prismaFeedbackStore,
13
+ prismaSettingsStore,
14
+ prismaStore,
15
+ } from "@crowandraven/evalkeeper-prisma";
16
+ // TODO: your app's Prisma client, with Evalkeeper's models added to the
17
+ // schema (node_modules/@crowandraven/evalkeeper-prisma/schema/evalkeeper.prisma).
18
+ import { prisma } from "@/lib/prisma";
19
+
20
+ /** TODO: the models your app can use, with prices and retirement dates. */
21
+ export const MODELS: ModelCatalog = {};
22
+
23
+ /** Models you can test now, e.g. only those whose API key is set. */
24
+ export function availableModels(): string[] {
25
+ return Object.keys(MODELS);
26
+ }
27
+
28
+ /**
29
+ * The models a feature can be tested on now. TODO: return decision models
30
+ * for features graded as decisions, if you have any.
31
+ */
32
+ export function modelsFor(_operation: string): string[] {
33
+ return availableModels();
34
+ }
35
+
36
+ /** TODO: one suite per AI feature (see the README's "Write a suite"). */
37
+ export const SUITES: AnySuite[] = [];
38
+ export const SUITE_BY_KEY: Record<string, AnySuite> = Object.fromEntries(
39
+ SUITES.map((s) => [s.key, s]),
40
+ );
41
+
42
+ /**
43
+ * TODO: each feature's instructions hash (instructionsHash from
44
+ * @crowandraven/evalkeeper-ai-sdk), so editing a prompt marks older tests out of date.
45
+ */
46
+ const PROMPT_VERSIONS: Record<string, string> = {};
47
+
48
+ /** TODO: each feature's settings in code, used until someone changes them. */
49
+ const DEFAULTS: Record<string, Settings> = {};
50
+
51
+ export const store = prismaStore(prisma);
52
+ export const feedbackStore = prismaFeedbackStore(prisma);
53
+
54
+ export const settings = createSettings({
55
+ store: prismaSettingsStore(prisma),
56
+ defaults: (operation) => {
57
+ const d = DEFAULTS[operation];
58
+ if (!d) throw new Error(`No default settings for ${operation}`);
59
+ return d;
60
+ },
61
+ promptVersion: (operation) => PROMPT_VERSIONS[operation] ?? "unregistered",
62
+ });
63
+
64
+ export const routeFor = (operation: string): Promise<Route> =>
65
+ settings.route(operation);
66
+
67
+ export const runner = createRunner({
68
+ store,
69
+ suites: SUITES,
70
+ models: MODELS,
71
+ promptVersion: (operation) => PROMPT_VERSIONS[operation] ?? "unregistered",
72
+ route: routeFor,
73
+ candidates: (operation) => ({ models: modelsFor(operation) }),
74
+ });
75
+
76
+ /**
77
+ * TODO: who may use the eval pages. Every page and action calls this
78
+ * first. Check your session, throw when the person isn't an admin, and
79
+ * return who they are (recorded as who made each change). Until you do,
80
+ * nobody can open the pages.
81
+ */
82
+ export async function requireAdmin(): Promise<{ id: string; name: string }> {
83
+ throw new Error(
84
+ "Evalkeeper: write requireAdmin in lib/evalkeeper/index.ts so only admins can use the eval pages.",
85
+ );
86
+ }
@@ -0,0 +1,24 @@
1
+ import { after, type NextRequest, NextResponse } from "next/server";
2
+ import { isAuthorizedCron } from "@/lib/evalkeeper/cronAuth";
3
+ import { runWeeklyCheck, startWeeklyCheck } from "@/lib/evalkeeper/weekly";
4
+
5
+ /** The checks run after the response, within this function's time limit. */
6
+ export const maxDuration = 300;
7
+
8
+ /**
9
+ * Mondays (vercel.json): the weekly eval check. Answers at once, then
10
+ * runs each suite's quick set on its live model. Results show on the
11
+ * evals hub. Call it with "Authorization: Bearer $CRON_SECRET".
12
+ */
13
+ export async function GET(request: NextRequest) {
14
+ if (
15
+ !isAuthorizedCron(
16
+ request.headers.get("authorization"),
17
+ process.env.CRON_SECRET,
18
+ )
19
+ )
20
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
21
+ const ids = await startWeeklyCheck();
22
+ after(() => runWeeklyCheck(ids));
23
+ return NextResponse.json({ started: ids.length }, { status: 202 });
24
+ }