langsmith 0.2.13 → 0.2.14-rc.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/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/jest/globals.cjs +10 -0
- package/dist/jest/globals.d.ts +15 -0
- package/dist/jest/globals.js +6 -0
- package/dist/jest/index.cjs +188 -0
- package/dist/jest/index.d.ts +62 -0
- package/dist/jest/index.js +183 -0
- package/dist/jest/matchers.cjs +149 -0
- package/dist/jest/matchers.d.ts +25 -0
- package/dist/jest/matchers.js +143 -0
- package/dist/jest/vendor/chain.cjs +81 -0
- package/dist/jest/vendor/chain.d.ts +4 -0
- package/dist/jest/vendor/chain.js +78 -0
- package/dist/jest/vendor/gradedBy.cjs +40 -0
- package/dist/jest/vendor/gradedBy.d.ts +7 -0
- package/dist/jest/vendor/gradedBy.js +36 -0
- package/dist/run_trees.d.ts +1 -1
- package/dist/singletons/traceable.cjs +1 -1
- package/dist/singletons/traceable.js +1 -1
- package/jest.cjs +1 -0
- package/jest.d.cts +1 -0
- package/jest.d.ts +1 -0
- package/jest.js +1 -0
- package/package.json +14 -1
package/dist/index.cjs
CHANGED
|
@@ -8,4 +8,4 @@ Object.defineProperty(exports, "RunTree", { enumerable: true, get: function () {
|
|
|
8
8
|
var fetch_js_1 = require("./singletons/fetch.cjs");
|
|
9
9
|
Object.defineProperty(exports, "overrideFetchImplementation", { enumerable: true, get: function () { return fetch_js_1.overrideFetchImplementation; } });
|
|
10
10
|
// Update using yarn bump-version
|
|
11
|
-
exports.__version__ = "0.2.
|
|
11
|
+
exports.__version__ = "0.2.14-rc.1";
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ export { Client, type ClientConfig, type LangSmithTracingClientInterface, } from
|
|
|
2
2
|
export type { Dataset, Example, TracerSession, Run, Feedback, RetrieverOutput, } from "./schemas.js";
|
|
3
3
|
export { RunTree, type RunTreeConfig } from "./run_trees.js";
|
|
4
4
|
export { overrideFetchImplementation } from "./singletons/fetch.js";
|
|
5
|
-
export declare const __version__ = "0.2.
|
|
5
|
+
export declare const __version__ = "0.2.14-rc.1";
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.trackingEnabled = exports.jestAsyncLocalStorageInstance = void 0;
|
|
4
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
5
|
+
const env_js_1 = require("../utils/env.cjs");
|
|
6
|
+
exports.jestAsyncLocalStorageInstance = new node_async_hooks_1.AsyncLocalStorage();
|
|
7
|
+
function trackingEnabled() {
|
|
8
|
+
return (0, env_js_1.getEnvironmentVariable)("LANGSMITH_TEST_TRACKING") === "true";
|
|
9
|
+
}
|
|
10
|
+
exports.trackingEnabled = trackingEnabled;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { Dataset, TracerSession, Example } from "../schemas.js";
|
|
4
|
+
import { Client } from "../client.js";
|
|
5
|
+
export declare const jestAsyncLocalStorageInstance: AsyncLocalStorage<{
|
|
6
|
+
dataset?: Dataset | undefined;
|
|
7
|
+
examples?: (Example & {
|
|
8
|
+
inputHash: string;
|
|
9
|
+
})[] | undefined;
|
|
10
|
+
createdAt: string;
|
|
11
|
+
project?: TracerSession | undefined;
|
|
12
|
+
currentExample?: Partial<Example> | undefined;
|
|
13
|
+
client?: Client | undefined;
|
|
14
|
+
}>;
|
|
15
|
+
export declare function trackingEnabled(): boolean;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { getEnvironmentVariable } from "../utils/env.js";
|
|
3
|
+
export const jestAsyncLocalStorageInstance = new AsyncLocalStorage();
|
|
4
|
+
export function trackingEnabled() {
|
|
5
|
+
return getEnvironmentVariable("LANGSMITH_TEST_TRACKING") === "true";
|
|
6
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const globals_1 = require("@jest/globals");
|
|
7
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
8
|
+
const uuid_1 = require("uuid");
|
|
9
|
+
const traceable_js_1 = require("../traceable.cjs");
|
|
10
|
+
const run_trees_js_1 = require("../run_trees.cjs");
|
|
11
|
+
const _random_name_js_1 = require("../evaluation/_random_name.cjs");
|
|
12
|
+
const matchers_js_1 = require("./matchers.cjs");
|
|
13
|
+
const globals_js_1 = require("./globals.cjs");
|
|
14
|
+
const chain_js_1 = __importDefault(require("./vendor/chain.cjs"));
|
|
15
|
+
globals_1.expect.extend({
|
|
16
|
+
toBeRelativeCloseTo: matchers_js_1.toBeRelativeCloseTo,
|
|
17
|
+
toBeAbsoluteCloseTo: matchers_js_1.toBeAbsoluteCloseTo,
|
|
18
|
+
toBeSemanticCloseTo: matchers_js_1.toBeSemanticCloseTo,
|
|
19
|
+
});
|
|
20
|
+
async function _createProject(client, datasetId) {
|
|
21
|
+
// Create the project, updating the experimentName until we find a unique one.
|
|
22
|
+
let project;
|
|
23
|
+
let experimentName = (0, _random_name_js_1.randomName)();
|
|
24
|
+
for (let i = 0; i < 10; i++) {
|
|
25
|
+
try {
|
|
26
|
+
project = await client.createProject({
|
|
27
|
+
projectName: experimentName,
|
|
28
|
+
referenceDatasetId: datasetId,
|
|
29
|
+
// description: this._description,
|
|
30
|
+
});
|
|
31
|
+
return project;
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
// Naming collision
|
|
35
|
+
if (e?.name === "LangSmithConflictError") {
|
|
36
|
+
const ent = (0, uuid_1.v4)().slice(0, 6);
|
|
37
|
+
experimentName = `${experimentName}-${ent}`;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
throw e;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
throw new Error("Could not generate a unique experiment name within 10 attempts." +
|
|
45
|
+
" Please try again.");
|
|
46
|
+
}
|
|
47
|
+
function wrapDescribeMethod(method) {
|
|
48
|
+
return function (datasetName, fn, config) {
|
|
49
|
+
const testClient = config?.client ?? run_trees_js_1.RunTree.getSharedClient();
|
|
50
|
+
return method(datasetName, () => {
|
|
51
|
+
(0, globals_1.beforeAll)(async () => {
|
|
52
|
+
if (!(0, globals_js_1.trackingEnabled)()) {
|
|
53
|
+
globals_js_1.jestAsyncLocalStorageInstance.enterWith({
|
|
54
|
+
createdAt: new Date().toISOString(),
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
let dataset;
|
|
59
|
+
try {
|
|
60
|
+
dataset = await testClient.readDataset({
|
|
61
|
+
datasetName,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
if (e.message.includes("not found")) {
|
|
66
|
+
dataset = await testClient.createDataset(datasetName, {
|
|
67
|
+
description: `Dataset for unit tests created on ${new Date().toISOString()}`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
throw e;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const examplesList = testClient.listExamples({
|
|
75
|
+
datasetName,
|
|
76
|
+
});
|
|
77
|
+
const examples = [];
|
|
78
|
+
for await (const example of examplesList) {
|
|
79
|
+
const inputHash = crypto_1.default
|
|
80
|
+
.createHash("sha256")
|
|
81
|
+
.update(JSON.stringify(example.inputs))
|
|
82
|
+
.digest("hex");
|
|
83
|
+
examples.push({ ...example, inputHash });
|
|
84
|
+
}
|
|
85
|
+
const project = await _createProject(testClient, dataset.id);
|
|
86
|
+
globals_js_1.jestAsyncLocalStorageInstance.enterWith({
|
|
87
|
+
dataset,
|
|
88
|
+
examples,
|
|
89
|
+
createdAt: new Date().toISOString(),
|
|
90
|
+
project,
|
|
91
|
+
client: testClient,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
fn();
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const lsDescribe = Object.assign(wrapDescribeMethod(globals_1.describe), {
|
|
100
|
+
only: wrapDescribeMethod(globals_1.describe.only),
|
|
101
|
+
skip: wrapDescribeMethod(globals_1.describe.skip),
|
|
102
|
+
});
|
|
103
|
+
function wrapTestMethod(method) {
|
|
104
|
+
return function (params, config) {
|
|
105
|
+
return async function (...args) {
|
|
106
|
+
return method(args[0], async () => {
|
|
107
|
+
const testInput = typeof params === "string" ? {} : params.inputs;
|
|
108
|
+
const testOutput = typeof params === "string" ? {} : params.outputs;
|
|
109
|
+
const inputHash = crypto_1.default
|
|
110
|
+
.createHash("sha256")
|
|
111
|
+
.update(JSON.stringify(testInput))
|
|
112
|
+
.digest("hex");
|
|
113
|
+
const context = globals_js_1.jestAsyncLocalStorageInstance.getStore();
|
|
114
|
+
if (context === undefined) {
|
|
115
|
+
throw new Error(`Could not retrieve test context.\nPlease make sure you have tracing enabled and you are wrapping all of your test cases in an "ls.describe()" function.`);
|
|
116
|
+
}
|
|
117
|
+
const { examples, dataset, createdAt, project, client } = context;
|
|
118
|
+
if ((0, globals_js_1.trackingEnabled)()) {
|
|
119
|
+
if (examples === undefined ||
|
|
120
|
+
dataset === undefined ||
|
|
121
|
+
project === undefined ||
|
|
122
|
+
client === undefined) {
|
|
123
|
+
throw new Error("Failed to initialize test tracking. Please contact us for help.");
|
|
124
|
+
}
|
|
125
|
+
const testClient = config?.client ?? client;
|
|
126
|
+
let example = (examples ?? []).find((example) => {
|
|
127
|
+
return example.inputHash === inputHash;
|
|
128
|
+
});
|
|
129
|
+
if (example === undefined) {
|
|
130
|
+
const newExample = await testClient.createExample(testInput, testOutput, {
|
|
131
|
+
datasetId: dataset?.id,
|
|
132
|
+
createdAt: new Date(createdAt ?? new Date()),
|
|
133
|
+
});
|
|
134
|
+
example = { ...newExample, inputHash };
|
|
135
|
+
}
|
|
136
|
+
globals_js_1.jestAsyncLocalStorageInstance.enterWith({
|
|
137
|
+
...context,
|
|
138
|
+
currentExample: example,
|
|
139
|
+
client: testClient,
|
|
140
|
+
});
|
|
141
|
+
const traceableOptions = {
|
|
142
|
+
reference_example_id: example.id,
|
|
143
|
+
project_name: project.name,
|
|
144
|
+
metadata: {
|
|
145
|
+
...config?.metadata,
|
|
146
|
+
example_version: example.modified_at
|
|
147
|
+
? new Date(example.modified_at).toISOString()
|
|
148
|
+
: new Date(example.created_at).toISOString(),
|
|
149
|
+
},
|
|
150
|
+
client: testClient,
|
|
151
|
+
tracingEnabled: true,
|
|
152
|
+
name: "Unit test",
|
|
153
|
+
};
|
|
154
|
+
// Pass inputs into traceable so tracing works correctly but
|
|
155
|
+
// provide both to the user-defined test function
|
|
156
|
+
const tracedFunction = (0, traceable_js_1.traceable)(async (_) => {
|
|
157
|
+
return args[1]({
|
|
158
|
+
inputs: testInput,
|
|
159
|
+
outputs: testOutput,
|
|
160
|
+
});
|
|
161
|
+
}, { ...traceableOptions, ...config });
|
|
162
|
+
await tracedFunction(testInput);
|
|
163
|
+
await testClient.awaitPendingTraceBatches();
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
globals_js_1.jestAsyncLocalStorageInstance.enterWith({
|
|
167
|
+
...context,
|
|
168
|
+
currentExample: { inputs: testInput, outputs: testOutput },
|
|
169
|
+
});
|
|
170
|
+
await args[1]({
|
|
171
|
+
inputs: testInput,
|
|
172
|
+
outputs: testOutput,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}, ...args.slice(2));
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const lsTest = Object.assign(wrapTestMethod(globals_1.test), {
|
|
180
|
+
only: wrapTestMethod(globals_1.test.only),
|
|
181
|
+
skip: wrapTestMethod(globals_1.test.skip),
|
|
182
|
+
});
|
|
183
|
+
exports.default = {
|
|
184
|
+
test: lsTest,
|
|
185
|
+
it: lsTest,
|
|
186
|
+
describe: lsDescribe,
|
|
187
|
+
expect: chain_js_1.default,
|
|
188
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { RunTreeConfig } from "../run_trees.js";
|
|
2
|
+
import expectWithGradedBy from "./vendor/chain.js";
|
|
3
|
+
import type { SimpleEvaluator } from "./vendor/gradedBy.js";
|
|
4
|
+
declare global {
|
|
5
|
+
namespace jest {
|
|
6
|
+
interface AsymmetricMatchers {
|
|
7
|
+
toBeRelativeCloseTo(expected: string, options: any): void;
|
|
8
|
+
toBeAbsoluteCloseTo(expected: string, options: any): void;
|
|
9
|
+
toBeSemanticCloseTo(expected: string, options: any): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
interface Matchers<R> {
|
|
12
|
+
toBeRelativeCloseTo(expected: string, options: any): R;
|
|
13
|
+
toBeAbsoluteCloseTo(expected: string, options: any): R;
|
|
14
|
+
toBeSemanticCloseTo(expected: string, options: any): Promise<R>;
|
|
15
|
+
gradedBy(evaluator: SimpleEvaluator): jest.Matchers<Promise<R>> & {
|
|
16
|
+
not: jest.Matchers<Promise<R>>;
|
|
17
|
+
resolves: jest.Matchers<Promise<R>>;
|
|
18
|
+
rejects: jest.Matchers<Promise<R>>;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export type LangSmithJestDescribeWrapper = (name: string, fn: () => void | Promise<void>, config?: Partial<RunTreeConfig>) => void;
|
|
24
|
+
export type LangSmithJestTestWrapper<I, O> = (name: string, fn: (params: {
|
|
25
|
+
inputs: I;
|
|
26
|
+
outputs: O;
|
|
27
|
+
}) => unknown | Promise<unknown>, timeout?: number) => void;
|
|
28
|
+
declare const _default: {
|
|
29
|
+
test: (<I extends Record<string, any> = Record<string, any>, O extends Record<string, any> = Record<string, any>>(params: string | {
|
|
30
|
+
inputs: I;
|
|
31
|
+
outputs: O;
|
|
32
|
+
}, config?: Partial<RunTreeConfig> | undefined) => LangSmithJestTestWrapper<I, O>) & {
|
|
33
|
+
only: <I extends Record<string, any> = Record<string, any>, O extends Record<string, any> = Record<string, any>>(params: string | {
|
|
34
|
+
inputs: I;
|
|
35
|
+
outputs: O;
|
|
36
|
+
}, config?: Partial<RunTreeConfig> | undefined) => LangSmithJestTestWrapper<I, O>;
|
|
37
|
+
skip: <I extends Record<string, any> = Record<string, any>, O extends Record<string, any> = Record<string, any>>(params: string | {
|
|
38
|
+
inputs: I;
|
|
39
|
+
outputs: O;
|
|
40
|
+
}, config?: Partial<RunTreeConfig> | undefined) => LangSmithJestTestWrapper<I, O>;
|
|
41
|
+
};
|
|
42
|
+
it: (<I extends Record<string, any> = Record<string, any>, O extends Record<string, any> = Record<string, any>>(params: string | {
|
|
43
|
+
inputs: I;
|
|
44
|
+
outputs: O;
|
|
45
|
+
}, config?: Partial<RunTreeConfig> | undefined) => LangSmithJestTestWrapper<I, O>) & {
|
|
46
|
+
only: <I extends Record<string, any> = Record<string, any>, O extends Record<string, any> = Record<string, any>>(params: string | {
|
|
47
|
+
inputs: I;
|
|
48
|
+
outputs: O;
|
|
49
|
+
}, config?: Partial<RunTreeConfig> | undefined) => LangSmithJestTestWrapper<I, O>;
|
|
50
|
+
skip: <I extends Record<string, any> = Record<string, any>, O extends Record<string, any> = Record<string, any>>(params: string | {
|
|
51
|
+
inputs: I;
|
|
52
|
+
outputs: O;
|
|
53
|
+
}, config?: Partial<RunTreeConfig> | undefined) => LangSmithJestTestWrapper<I, O>;
|
|
54
|
+
};
|
|
55
|
+
describe: LangSmithJestDescribeWrapper & {
|
|
56
|
+
only: LangSmithJestDescribeWrapper;
|
|
57
|
+
skip: LangSmithJestDescribeWrapper;
|
|
58
|
+
};
|
|
59
|
+
expect: typeof expectWithGradedBy;
|
|
60
|
+
};
|
|
61
|
+
export default _default;
|
|
62
|
+
export { type SimpleEvaluator };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { expect, test, describe, beforeAll } from "@jest/globals";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import { v4 } from "uuid";
|
|
4
|
+
import { traceable } from "../traceable.js";
|
|
5
|
+
import { RunTree } from "../run_trees.js";
|
|
6
|
+
import { randomName } from "../evaluation/_random_name.js";
|
|
7
|
+
import { toBeRelativeCloseTo, toBeAbsoluteCloseTo, toBeSemanticCloseTo, } from "./matchers.js";
|
|
8
|
+
import { jestAsyncLocalStorageInstance, trackingEnabled } from "./globals.js";
|
|
9
|
+
import expectWithGradedBy from "./vendor/chain.js";
|
|
10
|
+
expect.extend({
|
|
11
|
+
toBeRelativeCloseTo,
|
|
12
|
+
toBeAbsoluteCloseTo,
|
|
13
|
+
toBeSemanticCloseTo,
|
|
14
|
+
});
|
|
15
|
+
async function _createProject(client, datasetId) {
|
|
16
|
+
// Create the project, updating the experimentName until we find a unique one.
|
|
17
|
+
let project;
|
|
18
|
+
let experimentName = randomName();
|
|
19
|
+
for (let i = 0; i < 10; i++) {
|
|
20
|
+
try {
|
|
21
|
+
project = await client.createProject({
|
|
22
|
+
projectName: experimentName,
|
|
23
|
+
referenceDatasetId: datasetId,
|
|
24
|
+
// description: this._description,
|
|
25
|
+
});
|
|
26
|
+
return project;
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
// Naming collision
|
|
30
|
+
if (e?.name === "LangSmithConflictError") {
|
|
31
|
+
const ent = v4().slice(0, 6);
|
|
32
|
+
experimentName = `${experimentName}-${ent}`;
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
throw e;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new Error("Could not generate a unique experiment name within 10 attempts." +
|
|
40
|
+
" Please try again.");
|
|
41
|
+
}
|
|
42
|
+
function wrapDescribeMethod(method) {
|
|
43
|
+
return function (datasetName, fn, config) {
|
|
44
|
+
const testClient = config?.client ?? RunTree.getSharedClient();
|
|
45
|
+
return method(datasetName, () => {
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
if (!trackingEnabled()) {
|
|
48
|
+
jestAsyncLocalStorageInstance.enterWith({
|
|
49
|
+
createdAt: new Date().toISOString(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
let dataset;
|
|
54
|
+
try {
|
|
55
|
+
dataset = await testClient.readDataset({
|
|
56
|
+
datasetName,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
if (e.message.includes("not found")) {
|
|
61
|
+
dataset = await testClient.createDataset(datasetName, {
|
|
62
|
+
description: `Dataset for unit tests created on ${new Date().toISOString()}`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const examplesList = testClient.listExamples({
|
|
70
|
+
datasetName,
|
|
71
|
+
});
|
|
72
|
+
const examples = [];
|
|
73
|
+
for await (const example of examplesList) {
|
|
74
|
+
const inputHash = crypto
|
|
75
|
+
.createHash("sha256")
|
|
76
|
+
.update(JSON.stringify(example.inputs))
|
|
77
|
+
.digest("hex");
|
|
78
|
+
examples.push({ ...example, inputHash });
|
|
79
|
+
}
|
|
80
|
+
const project = await _createProject(testClient, dataset.id);
|
|
81
|
+
jestAsyncLocalStorageInstance.enterWith({
|
|
82
|
+
dataset,
|
|
83
|
+
examples,
|
|
84
|
+
createdAt: new Date().toISOString(),
|
|
85
|
+
project,
|
|
86
|
+
client: testClient,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
fn();
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
const lsDescribe = Object.assign(wrapDescribeMethod(describe), {
|
|
95
|
+
only: wrapDescribeMethod(describe.only),
|
|
96
|
+
skip: wrapDescribeMethod(describe.skip),
|
|
97
|
+
});
|
|
98
|
+
function wrapTestMethod(method) {
|
|
99
|
+
return function (params, config) {
|
|
100
|
+
return async function (...args) {
|
|
101
|
+
return method(args[0], async () => {
|
|
102
|
+
const testInput = typeof params === "string" ? {} : params.inputs;
|
|
103
|
+
const testOutput = typeof params === "string" ? {} : params.outputs;
|
|
104
|
+
const inputHash = crypto
|
|
105
|
+
.createHash("sha256")
|
|
106
|
+
.update(JSON.stringify(testInput))
|
|
107
|
+
.digest("hex");
|
|
108
|
+
const context = jestAsyncLocalStorageInstance.getStore();
|
|
109
|
+
if (context === undefined) {
|
|
110
|
+
throw new Error(`Could not retrieve test context.\nPlease make sure you have tracing enabled and you are wrapping all of your test cases in an "ls.describe()" function.`);
|
|
111
|
+
}
|
|
112
|
+
const { examples, dataset, createdAt, project, client } = context;
|
|
113
|
+
if (trackingEnabled()) {
|
|
114
|
+
if (examples === undefined ||
|
|
115
|
+
dataset === undefined ||
|
|
116
|
+
project === undefined ||
|
|
117
|
+
client === undefined) {
|
|
118
|
+
throw new Error("Failed to initialize test tracking. Please contact us for help.");
|
|
119
|
+
}
|
|
120
|
+
const testClient = config?.client ?? client;
|
|
121
|
+
let example = (examples ?? []).find((example) => {
|
|
122
|
+
return example.inputHash === inputHash;
|
|
123
|
+
});
|
|
124
|
+
if (example === undefined) {
|
|
125
|
+
const newExample = await testClient.createExample(testInput, testOutput, {
|
|
126
|
+
datasetId: dataset?.id,
|
|
127
|
+
createdAt: new Date(createdAt ?? new Date()),
|
|
128
|
+
});
|
|
129
|
+
example = { ...newExample, inputHash };
|
|
130
|
+
}
|
|
131
|
+
jestAsyncLocalStorageInstance.enterWith({
|
|
132
|
+
...context,
|
|
133
|
+
currentExample: example,
|
|
134
|
+
client: testClient,
|
|
135
|
+
});
|
|
136
|
+
const traceableOptions = {
|
|
137
|
+
reference_example_id: example.id,
|
|
138
|
+
project_name: project.name,
|
|
139
|
+
metadata: {
|
|
140
|
+
...config?.metadata,
|
|
141
|
+
example_version: example.modified_at
|
|
142
|
+
? new Date(example.modified_at).toISOString()
|
|
143
|
+
: new Date(example.created_at).toISOString(),
|
|
144
|
+
},
|
|
145
|
+
client: testClient,
|
|
146
|
+
tracingEnabled: true,
|
|
147
|
+
name: "Unit test",
|
|
148
|
+
};
|
|
149
|
+
// Pass inputs into traceable so tracing works correctly but
|
|
150
|
+
// provide both to the user-defined test function
|
|
151
|
+
const tracedFunction = traceable(async (_) => {
|
|
152
|
+
return args[1]({
|
|
153
|
+
inputs: testInput,
|
|
154
|
+
outputs: testOutput,
|
|
155
|
+
});
|
|
156
|
+
}, { ...traceableOptions, ...config });
|
|
157
|
+
await tracedFunction(testInput);
|
|
158
|
+
await testClient.awaitPendingTraceBatches();
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
jestAsyncLocalStorageInstance.enterWith({
|
|
162
|
+
...context,
|
|
163
|
+
currentExample: { inputs: testInput, outputs: testOutput },
|
|
164
|
+
});
|
|
165
|
+
await args[1]({
|
|
166
|
+
inputs: testInput,
|
|
167
|
+
outputs: testOutput,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}, ...args.slice(2));
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const lsTest = Object.assign(wrapTestMethod(test), {
|
|
175
|
+
only: wrapTestMethod(test.only),
|
|
176
|
+
skip: wrapTestMethod(test.skip),
|
|
177
|
+
});
|
|
178
|
+
export default {
|
|
179
|
+
test: lsTest,
|
|
180
|
+
it: lsTest,
|
|
181
|
+
describe: lsDescribe,
|
|
182
|
+
expect: expectWithGradedBy,
|
|
183
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toBeSemanticCloseTo = exports.toBeAbsoluteCloseTo = exports.toBeRelativeCloseTo = void 0;
|
|
4
|
+
// Levenshtein distance implementation
|
|
5
|
+
function levenshteinDistance(a, b) {
|
|
6
|
+
if (a.length === 0)
|
|
7
|
+
return b.length;
|
|
8
|
+
if (b.length === 0)
|
|
9
|
+
return a.length;
|
|
10
|
+
const matrix = Array(b.length + 1)
|
|
11
|
+
.fill(null)
|
|
12
|
+
.map(() => Array(a.length + 1).fill(null));
|
|
13
|
+
for (let i = 0; i <= a.length; i++)
|
|
14
|
+
matrix[0][i] = i;
|
|
15
|
+
for (let j = 0; j <= b.length; j++)
|
|
16
|
+
matrix[j][0] = j;
|
|
17
|
+
for (let j = 1; j <= b.length; j++) {
|
|
18
|
+
for (let i = 1; i <= a.length; i++) {
|
|
19
|
+
const substitutionCost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
20
|
+
matrix[j][i] = Math.min(matrix[j][i - 1] + 1, matrix[j - 1][i] + 1, matrix[j - 1][i - 1] + substitutionCost);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return matrix[b.length][a.length];
|
|
24
|
+
}
|
|
25
|
+
async function toBeRelativeCloseTo(received, expected, options = {}) {
|
|
26
|
+
const { threshold = 0.1, algorithm = "levenshtein" } = options;
|
|
27
|
+
let distance;
|
|
28
|
+
let maxLength;
|
|
29
|
+
switch (algorithm) {
|
|
30
|
+
case "levenshtein":
|
|
31
|
+
distance = levenshteinDistance(received, expected);
|
|
32
|
+
maxLength = Math.max(received.length, expected.length);
|
|
33
|
+
break;
|
|
34
|
+
default:
|
|
35
|
+
throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
36
|
+
}
|
|
37
|
+
// Calculate relative distance (normalized between 0 and 1)
|
|
38
|
+
const relativeDistance = maxLength === 0 ? 0 : distance / maxLength;
|
|
39
|
+
const pass = relativeDistance <= threshold;
|
|
40
|
+
return {
|
|
41
|
+
pass,
|
|
42
|
+
message: () => pass
|
|
43
|
+
? `Expected "${received}" not to be relatively close to "${expected}" (threshold: ${threshold}, actual distance: ${relativeDistance})`
|
|
44
|
+
: `Expected "${received}" to be relatively close to "${expected}" (threshold: ${threshold}, actual distance: ${relativeDistance})`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
exports.toBeRelativeCloseTo = toBeRelativeCloseTo;
|
|
48
|
+
async function toBeAbsoluteCloseTo(received, expected, options = {}) {
|
|
49
|
+
const { threshold = 3, algorithm = "levenshtein" } = options;
|
|
50
|
+
let distance;
|
|
51
|
+
switch (algorithm) {
|
|
52
|
+
case "levenshtein":
|
|
53
|
+
distance = levenshteinDistance(received, expected);
|
|
54
|
+
break;
|
|
55
|
+
default:
|
|
56
|
+
throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
57
|
+
}
|
|
58
|
+
const pass = distance <= threshold;
|
|
59
|
+
return {
|
|
60
|
+
pass,
|
|
61
|
+
message: () => pass
|
|
62
|
+
? `Expected "${received}" not to be absolutely close to "${expected}" (threshold: ${threshold}, actual distance: ${distance})`
|
|
63
|
+
: `Expected "${received}" to be absolutely close to "${expected}" (threshold: ${threshold}, actual distance: ${distance})`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
exports.toBeAbsoluteCloseTo = toBeAbsoluteCloseTo;
|
|
67
|
+
async function toBeSemanticCloseTo(received, expected, options) {
|
|
68
|
+
const { threshold = 0.2, embedding, algorithm = "cosine" } = options;
|
|
69
|
+
// Get embeddings for both strings
|
|
70
|
+
const [receivedEmbedding, expectedEmbedding] = await Promise.all([
|
|
71
|
+
embedding.embedQuery(received),
|
|
72
|
+
embedding.embedQuery(expected),
|
|
73
|
+
]);
|
|
74
|
+
// Calculate similarity based on chosen algorithm
|
|
75
|
+
let similarity;
|
|
76
|
+
switch (algorithm) {
|
|
77
|
+
case "cosine": {
|
|
78
|
+
// Compute cosine similarity
|
|
79
|
+
const dotProduct = receivedEmbedding.reduce((sum, a, i) => sum + a * expectedEmbedding[i], 0);
|
|
80
|
+
const receivedMagnitude = Math.sqrt(receivedEmbedding.reduce((sum, a) => sum + a * a, 0));
|
|
81
|
+
const expectedMagnitude = Math.sqrt(expectedEmbedding.reduce((sum, a) => sum + a * a, 0));
|
|
82
|
+
similarity = dotProduct / (receivedMagnitude * expectedMagnitude);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "dot-product": {
|
|
86
|
+
// Compute dot product similarity
|
|
87
|
+
similarity = receivedEmbedding.reduce((sum, a, i) => sum + a * expectedEmbedding[i], 0);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
92
|
+
}
|
|
93
|
+
const pass = similarity >= 1 - threshold;
|
|
94
|
+
return {
|
|
95
|
+
pass,
|
|
96
|
+
message: () => pass
|
|
97
|
+
? `Expected "${received}" not to be semantically close to "${expected}" (threshold: ${threshold}, similarity: ${similarity})`
|
|
98
|
+
: `Expected "${received}" to be semantically close to "${expected}" (threshold: ${threshold}, similarity: ${similarity})`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
exports.toBeSemanticCloseTo = toBeSemanticCloseTo;
|
|
102
|
+
// export async function toPassEvaluator(
|
|
103
|
+
// this: MatcherContext,
|
|
104
|
+
// actual: KVMap,
|
|
105
|
+
// evaluator: SimpleEvaluator,
|
|
106
|
+
// _expected?: KVMap
|
|
107
|
+
// ) {
|
|
108
|
+
// const runTree = getCurrentRunTree();
|
|
109
|
+
// const context = localStorage.getStore();
|
|
110
|
+
// if (context === undefined || context.currentExample === undefined) {
|
|
111
|
+
// throw new Error(
|
|
112
|
+
// `Could not identify example context from current context.\nPlease ensure you are calling this matcher within "ls.test()"`
|
|
113
|
+
// );
|
|
114
|
+
// }
|
|
115
|
+
// const wrappedEvaluator = traceable(evaluator, {
|
|
116
|
+
// reference_example_id: context.currentExample.id,
|
|
117
|
+
// metadata: {
|
|
118
|
+
// example_version: context.currentExample.modified_at
|
|
119
|
+
// ? new Date(context.currentExample.modified_at).toISOString()
|
|
120
|
+
// : new Date(context.currentExample.created_at).toISOString(),
|
|
121
|
+
// },
|
|
122
|
+
// client: context.client,
|
|
123
|
+
// tracingEnabled: true,
|
|
124
|
+
// });
|
|
125
|
+
// const evalResult = await wrappedEvaluator({
|
|
126
|
+
// input: runTree.inputs,
|
|
127
|
+
// expected: context.currentExample.outputs ?? {},
|
|
128
|
+
// actual,
|
|
129
|
+
// });
|
|
130
|
+
// await context.client.logEvaluationFeedback(evalResult, runTree);
|
|
131
|
+
// if (!("results" in evalResult) && !evalResult.score) {
|
|
132
|
+
// return {
|
|
133
|
+
// pass: false,
|
|
134
|
+
// message: () =>
|
|
135
|
+
// `expected ${this.utils.printReceived(
|
|
136
|
+
// actual
|
|
137
|
+
// )} to pass evaluator. Failed with ${JSON.stringify(
|
|
138
|
+
// evalResult,
|
|
139
|
+
// null,
|
|
140
|
+
// 2
|
|
141
|
+
// )}`,
|
|
142
|
+
// };
|
|
143
|
+
// }
|
|
144
|
+
// return {
|
|
145
|
+
// pass: true,
|
|
146
|
+
// message: () =>
|
|
147
|
+
// `evaluator passed with score ${JSON.stringify(evalResult, null, 2)}`,
|
|
148
|
+
// };
|
|
149
|
+
// }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MatcherContext } from "expect";
|
|
2
|
+
export declare function toBeRelativeCloseTo(this: MatcherContext, received: string, expected: string, options?: {
|
|
3
|
+
threshold?: number;
|
|
4
|
+
algorithm?: "levenshtein";
|
|
5
|
+
}): Promise<{
|
|
6
|
+
pass: boolean;
|
|
7
|
+
message: () => string;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function toBeAbsoluteCloseTo(this: MatcherContext, received: string, expected: string, options?: {
|
|
10
|
+
threshold?: number;
|
|
11
|
+
algorithm?: "levenshtein";
|
|
12
|
+
}): Promise<{
|
|
13
|
+
pass: boolean;
|
|
14
|
+
message: () => string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function toBeSemanticCloseTo(this: MatcherContext, received: string, expected: string, options: {
|
|
17
|
+
threshold?: number;
|
|
18
|
+
embedding: {
|
|
19
|
+
embedQuery: (query: string) => number[] | Promise<number[]>;
|
|
20
|
+
};
|
|
21
|
+
algorithm?: "cosine" | "dot-product";
|
|
22
|
+
}): Promise<{
|
|
23
|
+
pass: boolean;
|
|
24
|
+
message: () => string;
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Levenshtein distance implementation
|
|
2
|
+
function levenshteinDistance(a, b) {
|
|
3
|
+
if (a.length === 0)
|
|
4
|
+
return b.length;
|
|
5
|
+
if (b.length === 0)
|
|
6
|
+
return a.length;
|
|
7
|
+
const matrix = Array(b.length + 1)
|
|
8
|
+
.fill(null)
|
|
9
|
+
.map(() => Array(a.length + 1).fill(null));
|
|
10
|
+
for (let i = 0; i <= a.length; i++)
|
|
11
|
+
matrix[0][i] = i;
|
|
12
|
+
for (let j = 0; j <= b.length; j++)
|
|
13
|
+
matrix[j][0] = j;
|
|
14
|
+
for (let j = 1; j <= b.length; j++) {
|
|
15
|
+
for (let i = 1; i <= a.length; i++) {
|
|
16
|
+
const substitutionCost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
17
|
+
matrix[j][i] = Math.min(matrix[j][i - 1] + 1, matrix[j - 1][i] + 1, matrix[j - 1][i - 1] + substitutionCost);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return matrix[b.length][a.length];
|
|
21
|
+
}
|
|
22
|
+
export async function toBeRelativeCloseTo(received, expected, options = {}) {
|
|
23
|
+
const { threshold = 0.1, algorithm = "levenshtein" } = options;
|
|
24
|
+
let distance;
|
|
25
|
+
let maxLength;
|
|
26
|
+
switch (algorithm) {
|
|
27
|
+
case "levenshtein":
|
|
28
|
+
distance = levenshteinDistance(received, expected);
|
|
29
|
+
maxLength = Math.max(received.length, expected.length);
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
33
|
+
}
|
|
34
|
+
// Calculate relative distance (normalized between 0 and 1)
|
|
35
|
+
const relativeDistance = maxLength === 0 ? 0 : distance / maxLength;
|
|
36
|
+
const pass = relativeDistance <= threshold;
|
|
37
|
+
return {
|
|
38
|
+
pass,
|
|
39
|
+
message: () => pass
|
|
40
|
+
? `Expected "${received}" not to be relatively close to "${expected}" (threshold: ${threshold}, actual distance: ${relativeDistance})`
|
|
41
|
+
: `Expected "${received}" to be relatively close to "${expected}" (threshold: ${threshold}, actual distance: ${relativeDistance})`,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export async function toBeAbsoluteCloseTo(received, expected, options = {}) {
|
|
45
|
+
const { threshold = 3, algorithm = "levenshtein" } = options;
|
|
46
|
+
let distance;
|
|
47
|
+
switch (algorithm) {
|
|
48
|
+
case "levenshtein":
|
|
49
|
+
distance = levenshteinDistance(received, expected);
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
53
|
+
}
|
|
54
|
+
const pass = distance <= threshold;
|
|
55
|
+
return {
|
|
56
|
+
pass,
|
|
57
|
+
message: () => pass
|
|
58
|
+
? `Expected "${received}" not to be absolutely close to "${expected}" (threshold: ${threshold}, actual distance: ${distance})`
|
|
59
|
+
: `Expected "${received}" to be absolutely close to "${expected}" (threshold: ${threshold}, actual distance: ${distance})`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export async function toBeSemanticCloseTo(received, expected, options) {
|
|
63
|
+
const { threshold = 0.2, embedding, algorithm = "cosine" } = options;
|
|
64
|
+
// Get embeddings for both strings
|
|
65
|
+
const [receivedEmbedding, expectedEmbedding] = await Promise.all([
|
|
66
|
+
embedding.embedQuery(received),
|
|
67
|
+
embedding.embedQuery(expected),
|
|
68
|
+
]);
|
|
69
|
+
// Calculate similarity based on chosen algorithm
|
|
70
|
+
let similarity;
|
|
71
|
+
switch (algorithm) {
|
|
72
|
+
case "cosine": {
|
|
73
|
+
// Compute cosine similarity
|
|
74
|
+
const dotProduct = receivedEmbedding.reduce((sum, a, i) => sum + a * expectedEmbedding[i], 0);
|
|
75
|
+
const receivedMagnitude = Math.sqrt(receivedEmbedding.reduce((sum, a) => sum + a * a, 0));
|
|
76
|
+
const expectedMagnitude = Math.sqrt(expectedEmbedding.reduce((sum, a) => sum + a * a, 0));
|
|
77
|
+
similarity = dotProduct / (receivedMagnitude * expectedMagnitude);
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case "dot-product": {
|
|
81
|
+
// Compute dot product similarity
|
|
82
|
+
similarity = receivedEmbedding.reduce((sum, a, i) => sum + a * expectedEmbedding[i], 0);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
default:
|
|
86
|
+
throw new Error(`Unsupported algorithm: ${algorithm}`);
|
|
87
|
+
}
|
|
88
|
+
const pass = similarity >= 1 - threshold;
|
|
89
|
+
return {
|
|
90
|
+
pass,
|
|
91
|
+
message: () => pass
|
|
92
|
+
? `Expected "${received}" not to be semantically close to "${expected}" (threshold: ${threshold}, similarity: ${similarity})`
|
|
93
|
+
: `Expected "${received}" to be semantically close to "${expected}" (threshold: ${threshold}, similarity: ${similarity})`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
// export async function toPassEvaluator(
|
|
97
|
+
// this: MatcherContext,
|
|
98
|
+
// actual: KVMap,
|
|
99
|
+
// evaluator: SimpleEvaluator,
|
|
100
|
+
// _expected?: KVMap
|
|
101
|
+
// ) {
|
|
102
|
+
// const runTree = getCurrentRunTree();
|
|
103
|
+
// const context = localStorage.getStore();
|
|
104
|
+
// if (context === undefined || context.currentExample === undefined) {
|
|
105
|
+
// throw new Error(
|
|
106
|
+
// `Could not identify example context from current context.\nPlease ensure you are calling this matcher within "ls.test()"`
|
|
107
|
+
// );
|
|
108
|
+
// }
|
|
109
|
+
// const wrappedEvaluator = traceable(evaluator, {
|
|
110
|
+
// reference_example_id: context.currentExample.id,
|
|
111
|
+
// metadata: {
|
|
112
|
+
// example_version: context.currentExample.modified_at
|
|
113
|
+
// ? new Date(context.currentExample.modified_at).toISOString()
|
|
114
|
+
// : new Date(context.currentExample.created_at).toISOString(),
|
|
115
|
+
// },
|
|
116
|
+
// client: context.client,
|
|
117
|
+
// tracingEnabled: true,
|
|
118
|
+
// });
|
|
119
|
+
// const evalResult = await wrappedEvaluator({
|
|
120
|
+
// input: runTree.inputs,
|
|
121
|
+
// expected: context.currentExample.outputs ?? {},
|
|
122
|
+
// actual,
|
|
123
|
+
// });
|
|
124
|
+
// await context.client.logEvaluationFeedback(evalResult, runTree);
|
|
125
|
+
// if (!("results" in evalResult) && !evalResult.score) {
|
|
126
|
+
// return {
|
|
127
|
+
// pass: false,
|
|
128
|
+
// message: () =>
|
|
129
|
+
// `expected ${this.utils.printReceived(
|
|
130
|
+
// actual
|
|
131
|
+
// )} to pass evaluator. Failed with ${JSON.stringify(
|
|
132
|
+
// evalResult,
|
|
133
|
+
// null,
|
|
134
|
+
// 2
|
|
135
|
+
// )}`,
|
|
136
|
+
// };
|
|
137
|
+
// }
|
|
138
|
+
// return {
|
|
139
|
+
// pass: true,
|
|
140
|
+
// message: () =>
|
|
141
|
+
// `evaluator passed with score ${JSON.stringify(evalResult, null, 2)}`,
|
|
142
|
+
// };
|
|
143
|
+
// }
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Adapted from https://github.com/mattphillips/jest-chain/blob/main/src/chain.js
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const gradedBy_js_1 = require("./gradedBy.cjs");
|
|
7
|
+
class JestAssertionError extends Error {
|
|
8
|
+
constructor(result, callsite) {
|
|
9
|
+
super(typeof result.message === "function" ? result.message() : result.message);
|
|
10
|
+
Object.defineProperty(this, "matcherResult", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: void 0
|
|
15
|
+
});
|
|
16
|
+
this.matcherResult = result;
|
|
17
|
+
if (Error.captureStackTrace) {
|
|
18
|
+
Error.captureStackTrace(this, callsite);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const _wrapMatchers = (matchers, evaluator, originalArgs, staticPath = []) => {
|
|
23
|
+
return Object.keys(matchers)
|
|
24
|
+
.filter((name) => typeof matchers[name] === "function")
|
|
25
|
+
.map((name) => {
|
|
26
|
+
const newMatcher = async (...args) => {
|
|
27
|
+
try {
|
|
28
|
+
const score = await (0, gradedBy_js_1.gradedBy)(originalArgs[0], evaluator);
|
|
29
|
+
let result = expect(score);
|
|
30
|
+
for (const pathEntry of staticPath) {
|
|
31
|
+
result = result[pathEntry];
|
|
32
|
+
}
|
|
33
|
+
result = result[name](...args); // run matcher up to current state
|
|
34
|
+
if (result && typeof result.then === "function") {
|
|
35
|
+
return Object.assign(Promise.resolve(result), matchers);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
return matchers;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (!error.matcherResult) {
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
throw new JestAssertionError(error.matcherResult, newMatcher);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
return { [name]: newMatcher };
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
const addGradedBy = (matchers, originalArgs, staticPath = []) => {
|
|
54
|
+
return Object.assign({}, matchers, {
|
|
55
|
+
gradedBy: function (evaluator) {
|
|
56
|
+
const mappedMatchers = _wrapMatchers(matchers, evaluator, originalArgs);
|
|
57
|
+
// .not etc.
|
|
58
|
+
const staticMatchers = Object.keys(matchers)
|
|
59
|
+
.filter((name) => typeof matchers[name] !== "function")
|
|
60
|
+
.map((name) => {
|
|
61
|
+
return {
|
|
62
|
+
[name]: Object.assign({}, ..._wrapMatchers(matchers, evaluator, originalArgs, staticPath.concat(name))),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
return Object.assign({}, ...mappedMatchers, ...staticMatchers);
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
function expectWithGradedBy(expect) {
|
|
70
|
+
// proxy the expect function
|
|
71
|
+
const expectProxy = Object.assign((...args) => addGradedBy(expect(...args), args), // partially apply expect to get all matchers and chain them
|
|
72
|
+
expect // clone additional properties on expect
|
|
73
|
+
);
|
|
74
|
+
// expectProxy.extend = (o: any) => {
|
|
75
|
+
// expect.extend(o); // add new matchers to expect
|
|
76
|
+
// expectProxy = Object.assign(expectProxy, expect); // clone new asymmetric matchers
|
|
77
|
+
// };
|
|
78
|
+
return expectProxy;
|
|
79
|
+
}
|
|
80
|
+
exports.default = expectWithGradedBy;
|
|
81
|
+
globalThis.expect = expectWithGradedBy(globalThis.expect);
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapted from https://github.com/mattphillips/jest-chain/blob/main/src/chain.js
|
|
3
|
+
*/
|
|
4
|
+
import { gradedBy } from "./gradedBy.js";
|
|
5
|
+
class JestAssertionError extends Error {
|
|
6
|
+
constructor(result, callsite) {
|
|
7
|
+
super(typeof result.message === "function" ? result.message() : result.message);
|
|
8
|
+
Object.defineProperty(this, "matcherResult", {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
configurable: true,
|
|
11
|
+
writable: true,
|
|
12
|
+
value: void 0
|
|
13
|
+
});
|
|
14
|
+
this.matcherResult = result;
|
|
15
|
+
if (Error.captureStackTrace) {
|
|
16
|
+
Error.captureStackTrace(this, callsite);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const _wrapMatchers = (matchers, evaluator, originalArgs, staticPath = []) => {
|
|
21
|
+
return Object.keys(matchers)
|
|
22
|
+
.filter((name) => typeof matchers[name] === "function")
|
|
23
|
+
.map((name) => {
|
|
24
|
+
const newMatcher = async (...args) => {
|
|
25
|
+
try {
|
|
26
|
+
const score = await gradedBy(originalArgs[0], evaluator);
|
|
27
|
+
let result = expect(score);
|
|
28
|
+
for (const pathEntry of staticPath) {
|
|
29
|
+
result = result[pathEntry];
|
|
30
|
+
}
|
|
31
|
+
result = result[name](...args); // run matcher up to current state
|
|
32
|
+
if (result && typeof result.then === "function") {
|
|
33
|
+
return Object.assign(Promise.resolve(result), matchers);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
return matchers;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
if (!error.matcherResult) {
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
throw new JestAssertionError(error.matcherResult, newMatcher);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return { [name]: newMatcher };
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const addGradedBy = (matchers, originalArgs, staticPath = []) => {
|
|
52
|
+
return Object.assign({}, matchers, {
|
|
53
|
+
gradedBy: function (evaluator) {
|
|
54
|
+
const mappedMatchers = _wrapMatchers(matchers, evaluator, originalArgs);
|
|
55
|
+
// .not etc.
|
|
56
|
+
const staticMatchers = Object.keys(matchers)
|
|
57
|
+
.filter((name) => typeof matchers[name] !== "function")
|
|
58
|
+
.map((name) => {
|
|
59
|
+
return {
|
|
60
|
+
[name]: Object.assign({}, ..._wrapMatchers(matchers, evaluator, originalArgs, staticPath.concat(name))),
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
return Object.assign({}, ...mappedMatchers, ...staticMatchers);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
export default function expectWithGradedBy(expect) {
|
|
68
|
+
// proxy the expect function
|
|
69
|
+
const expectProxy = Object.assign((...args) => addGradedBy(expect(...args), args), // partially apply expect to get all matchers and chain them
|
|
70
|
+
expect // clone additional properties on expect
|
|
71
|
+
);
|
|
72
|
+
// expectProxy.extend = (o: any) => {
|
|
73
|
+
// expect.extend(o); // add new matchers to expect
|
|
74
|
+
// expectProxy = Object.assign(expectProxy, expect); // clone new asymmetric matchers
|
|
75
|
+
// };
|
|
76
|
+
return expectProxy;
|
|
77
|
+
}
|
|
78
|
+
globalThis.expect = expectWithGradedBy(globalThis.expect);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.gradedBy = void 0;
|
|
4
|
+
const traceable_js_1 = require("../../traceable.cjs");
|
|
5
|
+
const globals_js_1 = require("../globals.cjs");
|
|
6
|
+
async function gradedBy(actual, evaluator) {
|
|
7
|
+
const context = globals_js_1.jestAsyncLocalStorageInstance.getStore();
|
|
8
|
+
if (context === undefined || context.currentExample === undefined) {
|
|
9
|
+
throw new Error(`Could not identify current LangSmith context.\nPlease ensure you are calling this matcher within "ls.test()"`);
|
|
10
|
+
}
|
|
11
|
+
if ((0, globals_js_1.trackingEnabled)()) {
|
|
12
|
+
const runTree = (0, traceable_js_1.getCurrentRunTree)();
|
|
13
|
+
const wrappedEvaluator = (0, traceable_js_1.traceable)(evaluator, {
|
|
14
|
+
reference_example_id: context.currentExample.id,
|
|
15
|
+
metadata: {
|
|
16
|
+
example_version: context.currentExample.modified_at
|
|
17
|
+
? new Date(context.currentExample.modified_at).toISOString()
|
|
18
|
+
: new Date(context.currentExample.created_at ?? new Date()).toISOString(),
|
|
19
|
+
},
|
|
20
|
+
client: context.client,
|
|
21
|
+
tracingEnabled: true,
|
|
22
|
+
});
|
|
23
|
+
const evalResult = await wrappedEvaluator({
|
|
24
|
+
input: runTree.inputs,
|
|
25
|
+
expected: context.currentExample.outputs ?? {},
|
|
26
|
+
actual,
|
|
27
|
+
});
|
|
28
|
+
await context.client?.logEvaluationFeedback(evalResult, runTree);
|
|
29
|
+
return evalResult.score;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const evalResult = await evaluator({
|
|
33
|
+
input: context.currentExample.inputs ?? {},
|
|
34
|
+
expected: context.currentExample.outputs ?? {},
|
|
35
|
+
actual,
|
|
36
|
+
});
|
|
37
|
+
return evalResult.score;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.gradedBy = gradedBy;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { EvaluationResult } from "../../evaluation/evaluator.js";
|
|
2
|
+
export type SimpleEvaluator = (params: {
|
|
3
|
+
input: Record<string, any>;
|
|
4
|
+
actual: Record<string, any>;
|
|
5
|
+
expected: Record<string, any>;
|
|
6
|
+
}) => EvaluationResult | Promise<EvaluationResult>;
|
|
7
|
+
export declare function gradedBy(actual: any, evaluator: SimpleEvaluator): Promise<import("../../schemas.js").ScoreType | undefined>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getCurrentRunTree, traceable } from "../../traceable.js";
|
|
2
|
+
import { jestAsyncLocalStorageInstance, trackingEnabled } from "../globals.js";
|
|
3
|
+
export async function gradedBy(actual, evaluator) {
|
|
4
|
+
const context = jestAsyncLocalStorageInstance.getStore();
|
|
5
|
+
if (context === undefined || context.currentExample === undefined) {
|
|
6
|
+
throw new Error(`Could not identify current LangSmith context.\nPlease ensure you are calling this matcher within "ls.test()"`);
|
|
7
|
+
}
|
|
8
|
+
if (trackingEnabled()) {
|
|
9
|
+
const runTree = getCurrentRunTree();
|
|
10
|
+
const wrappedEvaluator = traceable(evaluator, {
|
|
11
|
+
reference_example_id: context.currentExample.id,
|
|
12
|
+
metadata: {
|
|
13
|
+
example_version: context.currentExample.modified_at
|
|
14
|
+
? new Date(context.currentExample.modified_at).toISOString()
|
|
15
|
+
: new Date(context.currentExample.created_at ?? new Date()).toISOString(),
|
|
16
|
+
},
|
|
17
|
+
client: context.client,
|
|
18
|
+
tracingEnabled: true,
|
|
19
|
+
});
|
|
20
|
+
const evalResult = await wrappedEvaluator({
|
|
21
|
+
input: runTree.inputs,
|
|
22
|
+
expected: context.currentExample.outputs ?? {},
|
|
23
|
+
actual,
|
|
24
|
+
});
|
|
25
|
+
await context.client?.logEvaluationFeedback(evalResult, runTree);
|
|
26
|
+
return evalResult.score;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
const evalResult = await evaluator({
|
|
30
|
+
input: context.currentExample.inputs ?? {},
|
|
31
|
+
expected: context.currentExample.outputs ?? {},
|
|
32
|
+
actual,
|
|
33
|
+
});
|
|
34
|
+
return evalResult.score;
|
|
35
|
+
}
|
|
36
|
+
}
|
package/dist/run_trees.d.ts
CHANGED
|
@@ -80,7 +80,7 @@ export declare class RunTree implements BaseRun {
|
|
|
80
80
|
attachments?: Attachments;
|
|
81
81
|
constructor(originalConfig: RunTreeConfig | RunTree);
|
|
82
82
|
private static getDefaultConfig;
|
|
83
|
-
|
|
83
|
+
static getSharedClient(): Client;
|
|
84
84
|
createChild(config: RunTreeConfig): RunTree;
|
|
85
85
|
end(outputs?: KVMap, error?: string, endTime?: number, metadata?: KVMap): Promise<void>;
|
|
86
86
|
private _convertToCreate;
|
|
@@ -38,7 +38,7 @@ const getCurrentRunTree = () => {
|
|
|
38
38
|
throw new Error([
|
|
39
39
|
"Could not get the current run tree.",
|
|
40
40
|
"",
|
|
41
|
-
"Please make sure you are calling this method within a traceable function
|
|
41
|
+
"Please make sure you are calling this method within a traceable function and that tracing is enabled.",
|
|
42
42
|
].join("\n"));
|
|
43
43
|
}
|
|
44
44
|
return runTree;
|
|
@@ -35,7 +35,7 @@ export const getCurrentRunTree = () => {
|
|
|
35
35
|
throw new Error([
|
|
36
36
|
"Could not get the current run tree.",
|
|
37
37
|
"",
|
|
38
|
-
"Please make sure you are calling this method within a traceable function
|
|
38
|
+
"Please make sure you are calling this method within a traceable function and that tracing is enabled.",
|
|
39
39
|
].join("\n"));
|
|
40
40
|
}
|
|
41
41
|
return runTree;
|
package/jest.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./dist/jest/index.cjs');
|
package/jest.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dist/jest/index.js'
|
package/jest.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dist/jest/index.js'
|
package/jest.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dist/jest/index.js'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "langsmith",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14-rc.1",
|
|
4
4
|
"description": "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform.",
|
|
5
5
|
"packageManager": "yarn@1.22.19",
|
|
6
6
|
"files": [
|
|
@@ -33,6 +33,10 @@
|
|
|
33
33
|
"langchain.js",
|
|
34
34
|
"langchain.d.ts",
|
|
35
35
|
"langchain.d.cts",
|
|
36
|
+
"jest.cjs",
|
|
37
|
+
"jest.js",
|
|
38
|
+
"jest.d.ts",
|
|
39
|
+
"jest.d.cts",
|
|
36
40
|
"vercel.cjs",
|
|
37
41
|
"vercel.js",
|
|
38
42
|
"vercel.d.ts",
|
|
@@ -228,6 +232,15 @@
|
|
|
228
232
|
"import": "./langchain.js",
|
|
229
233
|
"require": "./langchain.cjs"
|
|
230
234
|
},
|
|
235
|
+
"./jest": {
|
|
236
|
+
"types": {
|
|
237
|
+
"import": "./jest.d.ts",
|
|
238
|
+
"require": "./jest.d.cts",
|
|
239
|
+
"default": "./jest.d.ts"
|
|
240
|
+
},
|
|
241
|
+
"import": "./jest.js",
|
|
242
|
+
"require": "./jest.cjs"
|
|
243
|
+
},
|
|
231
244
|
"./vercel": {
|
|
232
245
|
"types": {
|
|
233
246
|
"import": "./vercel.d.ts",
|