jev-recipes 0.9.11 → 0.9.13
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 +1 -1
- package/dist/catalog/generated/metadata.js +564 -370
- package/dist/catalog/search.js +45 -11
- package/dist/evaluation/archive.d.ts +3 -3
- package/dist/evaluation/comparison.d.ts +1 -0
- package/dist/evaluation/comparison.js +13 -0
- package/dist/evaluation/dataset.js +7 -3
- package/dist/evaluation/engine.js +6 -2
- package/dist/evaluation/schema.d.ts +2 -2
- package/dist/recipes/game-action/index.js +3 -3
- package/dist/src/create-client.d.ts +1 -0
- package/dist/src/create-client.js +1 -0
- package/package.json +5 -1
package/dist/catalog/search.js
CHANGED
|
@@ -17,6 +17,12 @@ const stopWords = new Set([
|
|
|
17
17
|
'should',
|
|
18
18
|
'with',
|
|
19
19
|
]);
|
|
20
|
+
const EXACT_ID_BONUS = 10_000;
|
|
21
|
+
const FULL_COVERAGE_BONUS = 1_000;
|
|
22
|
+
const PHRASE_BONUS = 50;
|
|
23
|
+
const EXACT_MATCH = 1;
|
|
24
|
+
const SHARED_STEM_MINIMUM_LENGTH = 4;
|
|
25
|
+
const SHARED_STEM_MINIMUM_RATIO = 0.6;
|
|
20
26
|
export function searchRecipes(recipes, filters) {
|
|
21
27
|
const query = normalize(filters.query ?? '');
|
|
22
28
|
const words = query.split(' ').filter(Boolean);
|
|
@@ -33,7 +39,19 @@ export function searchRecipes(recipes, filters) {
|
|
|
33
39
|
function scoreRecipe(recipe, query, terms) {
|
|
34
40
|
if (!query)
|
|
35
41
|
return 0;
|
|
36
|
-
const fields =
|
|
42
|
+
const fields = weightedFields(recipe);
|
|
43
|
+
const matches = terms.map((term) => bestMatch(fields, term));
|
|
44
|
+
if (matches.every((match) => match.quality === 0))
|
|
45
|
+
return -1;
|
|
46
|
+
const coverage = matches.reduce((total, match) => total + match.quality, 0) / terms.length;
|
|
47
|
+
const relevance = matches.reduce((total, match) => total + match.weight, 0);
|
|
48
|
+
return ((normalize(recipe.id) === query ? EXACT_ID_BONUS : 0) +
|
|
49
|
+
(matchesPhrase(recipe, terms) ? PHRASE_BONUS : 0) +
|
|
50
|
+
coverage * FULL_COVERAGE_BONUS +
|
|
51
|
+
relevance);
|
|
52
|
+
}
|
|
53
|
+
function weightedFields(recipe) {
|
|
54
|
+
return [
|
|
37
55
|
[normalize(recipe.id), 40],
|
|
38
56
|
[normalize(recipe.title), 25],
|
|
39
57
|
[normalize(recipe.tags.join(' ')), 20],
|
|
@@ -41,16 +59,32 @@ function scoreRecipe(recipe, query, terms) {
|
|
|
41
59
|
[normalize(recipe.description), 5],
|
|
42
60
|
[normalize(recipe.category), 1],
|
|
43
61
|
];
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
62
|
+
}
|
|
63
|
+
function bestMatch(fields, term) {
|
|
64
|
+
return fields.reduce((best, [text, fieldWeight]) => {
|
|
65
|
+
const quality = fieldMatch(text, term);
|
|
66
|
+
return {
|
|
67
|
+
quality: Math.max(best.quality, quality),
|
|
68
|
+
weight: Math.max(best.weight, fieldWeight * quality),
|
|
69
|
+
};
|
|
70
|
+
}, { quality: 0, weight: 0 });
|
|
71
|
+
}
|
|
72
|
+
function matchesPhrase(recipe, terms) {
|
|
73
|
+
return terms.length > 1 && normalize(recipe.useWhen ?? '').includes(terms.join(' '));
|
|
74
|
+
}
|
|
75
|
+
function fieldMatch(text, term) {
|
|
76
|
+
if (text.includes(term))
|
|
77
|
+
return EXACT_MATCH;
|
|
78
|
+
return text.split(' ').reduce((best, word) => Math.max(best, stemSimilarity(word, term)), 0);
|
|
79
|
+
}
|
|
80
|
+
function stemSimilarity(first, second) {
|
|
81
|
+
const longerLength = Math.max(first.length, second.length);
|
|
82
|
+
let sharedLength = 0;
|
|
83
|
+
while (sharedLength < longerLength && first[sharedLength] === second[sharedLength])
|
|
84
|
+
sharedLength++;
|
|
85
|
+
const sharesStem = sharedLength >= SHARED_STEM_MINIMUM_LENGTH &&
|
|
86
|
+
sharedLength >= Math.ceil(longerLength * SHARED_STEM_MINIMUM_RATIO);
|
|
87
|
+
return sharesStem ? sharedLength / longerLength : 0;
|
|
54
88
|
}
|
|
55
89
|
function normalize(text) {
|
|
56
90
|
return text
|
|
@@ -25,8 +25,8 @@ declare const archiveSchema: z.ZodObject<{
|
|
|
25
25
|
inputFingerprint: z.ZodString;
|
|
26
26
|
answerKeyFingerprint: z.ZodString;
|
|
27
27
|
split: z.ZodEnum<{
|
|
28
|
-
"held-out": "held-out";
|
|
29
28
|
development: "development";
|
|
29
|
+
"held-out": "held-out";
|
|
30
30
|
}>;
|
|
31
31
|
requestedModel: z.ZodString;
|
|
32
32
|
policy: z.ZodObject<{
|
|
@@ -54,8 +54,8 @@ declare const archiveSchema: z.ZodObject<{
|
|
|
54
54
|
rationale: z.ZodString;
|
|
55
55
|
family: z.ZodOptional<z.ZodString>;
|
|
56
56
|
split: z.ZodDefault<z.ZodEnum<{
|
|
57
|
-
"held-out": "held-out";
|
|
58
57
|
development: "development";
|
|
58
|
+
"held-out": "held-out";
|
|
59
59
|
}>>;
|
|
60
60
|
provenance: z.ZodDefault<z.ZodObject<{
|
|
61
61
|
method: z.ZodEnum<{
|
|
@@ -113,7 +113,7 @@ export declare function freezePolicy(run: EvaluationRun): EvaluationPolicy;
|
|
|
113
113
|
export declare function compare(baseline: EvaluationRun, candidate: EvaluationRun): {
|
|
114
114
|
recipe: string;
|
|
115
115
|
cases: number;
|
|
116
|
-
split: "
|
|
116
|
+
split: "development" | "held-out";
|
|
117
117
|
baseline: {
|
|
118
118
|
runId: string;
|
|
119
119
|
packageVersion: string;
|
|
@@ -5,5 +5,6 @@ export declare function comparableDecision(value: unknown, recipe: string): unkn
|
|
|
5
5
|
export declare function matchesExpected(actual: unknown, expected: unknown, recipe: string): boolean;
|
|
6
6
|
export declare function isReady(result: unknown, recipe: string): boolean;
|
|
7
7
|
export declare function isMetadataPath(schema: Schema, segments: string[]): boolean;
|
|
8
|
+
export declare function gatedTwinFor(schema: Schema, segments: string[]): string | undefined;
|
|
8
9
|
export declare function schemaAtPath(schema: Schema, segments: string[]): Schema | undefined;
|
|
9
10
|
export {};
|
|
@@ -76,6 +76,19 @@ export function isMetadataPath(schema, segments) {
|
|
|
76
76
|
const child = schemaAtPath(schema, [first]);
|
|
77
77
|
return child !== undefined && isMetadataPath(child, rest);
|
|
78
78
|
}
|
|
79
|
+
export function gatedTwinFor(schema, segments) {
|
|
80
|
+
const name = segments.at(-1);
|
|
81
|
+
if (name === undefined || !name.length)
|
|
82
|
+
return undefined;
|
|
83
|
+
const twin = `suggested${name[0].toUpperCase()}${name.slice(1)}`;
|
|
84
|
+
const parent = schemaAtPath(schema, segments.slice(0, -1));
|
|
85
|
+
return parent !== undefined && hasProperty(parent, twin) ? twin : undefined;
|
|
86
|
+
}
|
|
87
|
+
function hasProperty(schema, name) {
|
|
88
|
+
if (Array.isArray(schema.anyOf))
|
|
89
|
+
return schema.anyOf.some((branch) => isRecord(branch) && hasProperty(branch, name));
|
|
90
|
+
return isRecord(schema.properties) && Object.hasOwn(schema.properties, name);
|
|
91
|
+
}
|
|
79
92
|
export function schemaAtPath(schema, segments) {
|
|
80
93
|
if (!segments.length)
|
|
81
94
|
return schema;
|
|
@@ -3,7 +3,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { recipeNameSchema } from '../catalog/schema.js';
|
|
5
5
|
import { isRecord } from './decisions.js';
|
|
6
|
-
import { comparableDecision, isMetadataPath, schemaAtPath, resultSchemaFor } from './comparison.js';
|
|
6
|
+
import { comparableDecision, gatedTwinFor, isMetadataPath, schemaAtPath, resultSchemaFor, } from './comparison.js';
|
|
7
7
|
import { evaluationCaseSchema } from './schema.js';
|
|
8
8
|
export async function loadEvaluationRecipe(name) {
|
|
9
9
|
const id = recipeNameSchema.parse(name);
|
|
@@ -45,9 +45,13 @@ export function validateCases(recipe, values) {
|
|
|
45
45
|
}
|
|
46
46
|
recipe.inputSchema.parse(entry.input);
|
|
47
47
|
for (const [path, expected] of Object.entries(entry.expected)) {
|
|
48
|
-
|
|
48
|
+
const segments = path.split('.');
|
|
49
|
+
if (isMetadataPath(resultSchema, segments))
|
|
49
50
|
throw new Error(`${entry.id}: expected must name an ungated decision, not ${path}.`);
|
|
50
|
-
const
|
|
51
|
+
const twin = gatedTwinFor(resultSchema, segments);
|
|
52
|
+
if (twin)
|
|
53
|
+
throw new Error(`${entry.id}: ${path} is gated by the review policy; label ${twin} instead.`);
|
|
54
|
+
const field = schemaAtPath(resultSchema, segments);
|
|
51
55
|
if (!field)
|
|
52
56
|
throw new Error(`${entry.id}: unknown result field ${path}.`);
|
|
53
57
|
if (!acceptsValue(field, expected))
|
|
@@ -81,7 +81,7 @@ async function evaluateCase(recipe, goldenCase, client, options) {
|
|
|
81
81
|
function recordResponses(client, exchanges) {
|
|
82
82
|
return {
|
|
83
83
|
async systemOne(request, options) {
|
|
84
|
-
const exchange = { request:
|
|
84
|
+
const exchange = { request: asArchivedRequest(request), durationMs: 0 };
|
|
85
85
|
exchanges.push(exchange);
|
|
86
86
|
const started = performance.now();
|
|
87
87
|
try {
|
|
@@ -106,7 +106,8 @@ export function replayResponses(exchanges) {
|
|
|
106
106
|
return {
|
|
107
107
|
client: {
|
|
108
108
|
async systemOne(request) {
|
|
109
|
-
const
|
|
109
|
+
const archived = asArchivedRequest(request);
|
|
110
|
+
const index = remaining.findIndex((exchange) => isDeepStrictEqual(exchange.request, archived));
|
|
110
111
|
if (index === -1)
|
|
111
112
|
throw new ReplayMismatch('Replay requested an unrecorded model response. The prompt or model has changed.');
|
|
112
113
|
const [exchange] = remaining.splice(index, 1);
|
|
@@ -121,6 +122,9 @@ export function replayResponses(exchanges) {
|
|
|
121
122
|
},
|
|
122
123
|
};
|
|
123
124
|
}
|
|
125
|
+
function asArchivedRequest(request) {
|
|
126
|
+
return JSON.parse(JSON.stringify(request));
|
|
127
|
+
}
|
|
124
128
|
function safeError(error) {
|
|
125
129
|
let message = error instanceof Error ? error.message : String(error);
|
|
126
130
|
for (const [name, value] of Object.entries(process.env)) {
|
|
@@ -15,8 +15,8 @@ export declare const evaluationCaseSchema: z.ZodObject<{
|
|
|
15
15
|
rationale: z.ZodString;
|
|
16
16
|
family: z.ZodOptional<z.ZodString>;
|
|
17
17
|
split: z.ZodDefault<z.ZodEnum<{
|
|
18
|
-
"held-out": "held-out";
|
|
19
18
|
development: "development";
|
|
19
|
+
"held-out": "held-out";
|
|
20
20
|
}>>;
|
|
21
21
|
provenance: z.ZodDefault<z.ZodObject<{
|
|
22
22
|
method: z.ZodEnum<{
|
|
@@ -54,8 +54,8 @@ export declare const evaluationOptionsSchema: z.ZodObject<{
|
|
|
54
54
|
maxRequests: z.ZodDefault<z.ZodNumber>;
|
|
55
55
|
model: z.ZodOptional<z.ZodString>;
|
|
56
56
|
split: z.ZodDefault<z.ZodEnum<{
|
|
57
|
-
"held-out": "held-out";
|
|
58
57
|
development: "development";
|
|
58
|
+
"held-out": "held-out";
|
|
59
59
|
}>>;
|
|
60
60
|
policy: z.ZodDefault<z.ZodObject<{
|
|
61
61
|
minConfidence: z.ZodOptional<z.ZodNumber>;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { choice } from '@typesafe-ai/sdk';
|
|
2
2
|
import { evaluateWithJev } from '../../src/client.js';
|
|
3
3
|
import { parseChoiceAnswer } from '../../src/answers.js';
|
|
4
|
-
import { parseDecisionState } from '../../src/decisions.js';
|
|
4
|
+
import { asDecisionInstruction, parseDecisionState } from '../../src/decisions.js';
|
|
5
5
|
import { gameActionInputSchema, gameActionResultSchema } from './schema.js';
|
|
6
|
-
const instructions = [
|
|
6
|
+
const instructions = asDecisionInstruction([
|
|
7
7
|
'Which available action should this player take next, given the supplied game state, player state, rules, and objective?',
|
|
8
8
|
...Object.entries(gameActionInputSchema.shape).map(([name, schema]) => `${name}: ${schema.description}`),
|
|
9
|
-
].join('\n');
|
|
9
|
+
].join('\n'));
|
|
10
10
|
export async function gameAction(input, options = {}) {
|
|
11
11
|
const { legalActions, ...state } = gameActionInputSchema.parse(input);
|
|
12
12
|
if (legalActions.length === 0)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createClient } from './client.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createClient } from './client.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jev-recipes",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.13",
|
|
4
4
|
"description": "248 small Jev decisions for JavaScript and TypeScript: route, rerank, gate, grade, compare, and label text for agents, RAG, support, code review, and music. Each returns a typed result with explicit uncertainty. Plug into Vercel AI SDK or LangChain agents, import one recipe, or run the CLI with JSON from any language.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,6 +19,10 @@
|
|
|
19
19
|
"types": "./dist/src/index.d.ts",
|
|
20
20
|
"import": "./dist/src/index.js"
|
|
21
21
|
},
|
|
22
|
+
"./client": {
|
|
23
|
+
"types": "./dist/src/create-client.d.ts",
|
|
24
|
+
"import": "./dist/src/create-client.js"
|
|
25
|
+
},
|
|
22
26
|
"./catalog": {
|
|
23
27
|
"types": "./dist/catalog/index.d.ts",
|
|
24
28
|
"import": "./dist/catalog/index.js"
|