langsmith 0.1.23 → 0.1.24
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/client.cjs +46 -5
- package/dist/client.d.ts +17 -3
- package/dist/client.js +46 -5
- package/dist/evaluation/_runner.cjs +25 -7
- package/dist/evaluation/_runner.d.ts +4 -4
- package/dist/evaluation/_runner.js +25 -7
- package/dist/evaluation/evaluate_comparative.cjs +202 -0
- package/dist/evaluation/evaluate_comparative.d.ts +51 -0
- package/dist/evaluation/evaluate_comparative.js +195 -0
- package/dist/evaluation/evaluator.cjs +10 -13
- package/dist/evaluation/evaluator.js +11 -14
- package/dist/evaluation/index.cjs +3 -1
- package/dist/evaluation/index.d.ts +1 -0
- package/dist/evaluation/index.js +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/run_trees.cjs +1 -0
- package/dist/run_trees.js +1 -0
- package/dist/schemas.d.ts +17 -0
- package/dist/traceable.cjs +209 -23
- package/dist/traceable.js +206 -20
- package/dist/utils/shuffle.cjs +15 -0
- package/dist/utils/shuffle.d.ts +1 -0
- package/dist/utils/shuffle.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { v4 as uuid4, validate } from "uuid";
|
|
2
|
+
import { Client } from "../index.js";
|
|
3
|
+
import { shuffle } from "../utils/shuffle.js";
|
|
4
|
+
import { AsyncCaller } from "../utils/async_caller.js";
|
|
5
|
+
import pRetry from "p-retry";
|
|
6
|
+
import { getCurrentRunTree, traceable } from "../traceable.js";
|
|
7
|
+
function isExperimentResultsList(value) {
|
|
8
|
+
return value.some((x) => typeof x !== "string");
|
|
9
|
+
}
|
|
10
|
+
async function loadExperiment(client, experiment) {
|
|
11
|
+
const value = typeof experiment === "string" ? experiment : experiment.experimentName;
|
|
12
|
+
return client.readProject(validate(value) ? { projectId: value } : { projectName: value });
|
|
13
|
+
}
|
|
14
|
+
async function loadTraces(client, experiment, options) {
|
|
15
|
+
const executionOrder = options.loadNested ? undefined : 1;
|
|
16
|
+
const runs = await client.listRuns(validate(experiment)
|
|
17
|
+
? { projectId: experiment, executionOrder }
|
|
18
|
+
: { projectName: experiment, executionOrder });
|
|
19
|
+
const treeMap = {};
|
|
20
|
+
const runIdMap = {};
|
|
21
|
+
const results = [];
|
|
22
|
+
for await (const run of runs) {
|
|
23
|
+
if (run.parent_run_id != null) {
|
|
24
|
+
treeMap[run.parent_run_id] ??= [];
|
|
25
|
+
treeMap[run.parent_run_id].push(run);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
results.push(run);
|
|
29
|
+
}
|
|
30
|
+
runIdMap[run.id] = run;
|
|
31
|
+
}
|
|
32
|
+
for (const [parentRunId, childRuns] of Object.entries(treeMap)) {
|
|
33
|
+
const parentRun = runIdMap[parentRunId];
|
|
34
|
+
parentRun.child_runs = childRuns.sort((a, b) => {
|
|
35
|
+
if (a.dotted_order == null || b.dotted_order == null)
|
|
36
|
+
return 0;
|
|
37
|
+
return a.dotted_order.localeCompare(b.dotted_order);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return results;
|
|
41
|
+
}
|
|
42
|
+
export async function evaluateComparative(experiments, options) {
|
|
43
|
+
if (experiments.length < 2) {
|
|
44
|
+
throw new Error("Comparative evaluation requires at least 2 experiments.");
|
|
45
|
+
}
|
|
46
|
+
if (!options.evaluators.length) {
|
|
47
|
+
throw new Error("At least one evaluator is required for comparative evaluation.");
|
|
48
|
+
}
|
|
49
|
+
if (options.maxConcurrency && options.maxConcurrency < 0) {
|
|
50
|
+
throw new Error("maxConcurrency must be a positive number.");
|
|
51
|
+
}
|
|
52
|
+
const client = options.client ?? new Client();
|
|
53
|
+
const resolvedExperiments = await Promise.all(experiments);
|
|
54
|
+
const projects = await (() => {
|
|
55
|
+
if (!isExperimentResultsList(resolvedExperiments)) {
|
|
56
|
+
return Promise.all(resolvedExperiments.map((experiment) => loadExperiment(client, experiment)));
|
|
57
|
+
}
|
|
58
|
+
// if we know the number of runs beforehand, check if the
|
|
59
|
+
// number of runs in the project matches the expected number of runs
|
|
60
|
+
return Promise.all(resolvedExperiments.map((experiment) => pRetry(async () => {
|
|
61
|
+
const project = await loadExperiment(client, experiment);
|
|
62
|
+
if (project.run_count !== experiment?.results.length) {
|
|
63
|
+
throw new Error("Experiment is missing runs. Retrying.");
|
|
64
|
+
}
|
|
65
|
+
return project;
|
|
66
|
+
}, { factor: 2, minTimeout: 1000, retries: 10 })));
|
|
67
|
+
})();
|
|
68
|
+
if (new Set(projects.map((p) => p.reference_dataset_id)).size > 1) {
|
|
69
|
+
throw new Error("All experiments must have the same reference dataset.");
|
|
70
|
+
}
|
|
71
|
+
const referenceDatasetId = projects.at(0)?.reference_dataset_id;
|
|
72
|
+
if (!referenceDatasetId) {
|
|
73
|
+
throw new Error("Reference dataset is required for comparative evaluation.");
|
|
74
|
+
}
|
|
75
|
+
if (new Set(projects.map((p) => p.extra?.metadata?.dataset_version)).size > 1) {
|
|
76
|
+
console.warn("Detected multiple dataset versions used by experiments, which may lead to inaccurate results.");
|
|
77
|
+
}
|
|
78
|
+
const datasetVersion = projects.at(0)?.extra?.metadata?.dataset_version;
|
|
79
|
+
const id = uuid4();
|
|
80
|
+
const experimentName = (() => {
|
|
81
|
+
if (!options.experimentPrefix) {
|
|
82
|
+
const names = projects
|
|
83
|
+
.map((p) => p.name)
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.join(" vs. ");
|
|
86
|
+
return `${names}-${uuid4().slice(0, 4)}`;
|
|
87
|
+
}
|
|
88
|
+
return `${options.experimentPrefix}-${uuid4().slice(0, 4)}`;
|
|
89
|
+
})();
|
|
90
|
+
// TODO: add URL to the comparative experiment
|
|
91
|
+
console.log(`Starting pairwise evaluation of: ${experimentName}`);
|
|
92
|
+
const comparativeExperiment = await client.createComparativeExperiment({
|
|
93
|
+
id,
|
|
94
|
+
name: experimentName,
|
|
95
|
+
experimentIds: projects.map((p) => p.id),
|
|
96
|
+
description: options.description,
|
|
97
|
+
metadata: options.metadata,
|
|
98
|
+
referenceDatasetId: projects.at(0)?.reference_dataset_id,
|
|
99
|
+
});
|
|
100
|
+
const viewUrl = await (async () => {
|
|
101
|
+
const projectId = projects.at(0)?.id ?? projects.at(1)?.id;
|
|
102
|
+
const datasetId = comparativeExperiment?.reference_dataset_id;
|
|
103
|
+
if (projectId && datasetId) {
|
|
104
|
+
const hostUrl = (await client.getProjectUrl({ projectId }))
|
|
105
|
+
.split("/projects/p/")
|
|
106
|
+
.at(0);
|
|
107
|
+
const result = new URL(`${hostUrl}/datasets/${datasetId}/compare`);
|
|
108
|
+
result.searchParams.set("selectedSessions", projects.map((p) => p.id).join(","));
|
|
109
|
+
result.searchParams.set("comparativeExperiment", comparativeExperiment.id);
|
|
110
|
+
return result.toString();
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
})();
|
|
114
|
+
if (viewUrl != null) {
|
|
115
|
+
console.log(`View results at: ${viewUrl}`);
|
|
116
|
+
}
|
|
117
|
+
const experimentRuns = await Promise.all(projects.map((p) => loadTraces(client, p.id, { loadNested: !!options.loadNested })));
|
|
118
|
+
let exampleIdsIntersect;
|
|
119
|
+
for (const runs of experimentRuns) {
|
|
120
|
+
const exampleIdsSet = new Set(runs
|
|
121
|
+
.map((r) => r.reference_example_id)
|
|
122
|
+
.filter((x) => x != null));
|
|
123
|
+
if (!exampleIdsIntersect) {
|
|
124
|
+
exampleIdsIntersect = exampleIdsSet;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
exampleIdsIntersect = new Set([...exampleIdsIntersect].filter((x) => exampleIdsSet.has(x)));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const exampleIds = [...(exampleIdsIntersect ?? [])];
|
|
131
|
+
if (!exampleIds.length) {
|
|
132
|
+
throw new Error("No examples found in common between experiments.");
|
|
133
|
+
}
|
|
134
|
+
const exampleMap = {};
|
|
135
|
+
for (let start = 0; start < exampleIds.length; start += 99) {
|
|
136
|
+
const exampleIdsChunk = exampleIds.slice(start, start + 99);
|
|
137
|
+
for await (const example of client.listExamples({
|
|
138
|
+
datasetId: referenceDatasetId,
|
|
139
|
+
exampleIds: exampleIdsChunk,
|
|
140
|
+
asOf: datasetVersion,
|
|
141
|
+
})) {
|
|
142
|
+
exampleMap[example.id] = example;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const runMapByExampleId = {};
|
|
146
|
+
for (const runs of experimentRuns) {
|
|
147
|
+
for (const run of runs) {
|
|
148
|
+
if (run.reference_example_id == null ||
|
|
149
|
+
!exampleIds.includes(run.reference_example_id)) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
runMapByExampleId[run.reference_example_id] ??= [];
|
|
153
|
+
runMapByExampleId[run.reference_example_id].push(run);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const caller = new AsyncCaller({ maxConcurrency: options.maxConcurrency });
|
|
157
|
+
async function evaluateAndSubmitFeedback(runs, example, evaluator) {
|
|
158
|
+
const expectedRunIds = new Set(runs.map((r) => r.id));
|
|
159
|
+
const result = await evaluator(options.randomizeOrder ? shuffle(runs) : runs, example);
|
|
160
|
+
for (const [runId, score] of Object.entries(result.scores)) {
|
|
161
|
+
// validate if the run id
|
|
162
|
+
if (!expectedRunIds.has(runId)) {
|
|
163
|
+
throw new Error(`Returning an invalid run id ${runId} from evaluator.`);
|
|
164
|
+
}
|
|
165
|
+
await client.createFeedback(runId, result.key, {
|
|
166
|
+
score,
|
|
167
|
+
sourceRunId: result.source_run_id,
|
|
168
|
+
comparativeExperimentId: comparativeExperiment.id,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
const tracedEvaluators = options.evaluators.map((evaluator) => traceable(async (runs, example) => {
|
|
174
|
+
const evaluatorRun = getCurrentRunTree();
|
|
175
|
+
const result = await evaluator(runs, example);
|
|
176
|
+
// sanitise the payload before sending to LangSmith
|
|
177
|
+
evaluatorRun.inputs = { runs: runs, example: example };
|
|
178
|
+
evaluatorRun.outputs = result;
|
|
179
|
+
return {
|
|
180
|
+
...result,
|
|
181
|
+
source_run_id: result.source_run_id ?? evaluatorRun.id,
|
|
182
|
+
};
|
|
183
|
+
}, {
|
|
184
|
+
project_name: "evaluators",
|
|
185
|
+
name: evaluator.name || "evaluator",
|
|
186
|
+
}));
|
|
187
|
+
const promises = Object.entries(runMapByExampleId).flatMap(([exampleId, runs]) => {
|
|
188
|
+
const example = exampleMap[exampleId];
|
|
189
|
+
if (!example)
|
|
190
|
+
throw new Error(`Example ${exampleId} not found.`);
|
|
191
|
+
return tracedEvaluators.map((evaluator) => caller.call(evaluateAndSubmitFeedback, runs, exampleMap[exampleId], evaluator));
|
|
192
|
+
});
|
|
193
|
+
const results = await Promise.all(promises);
|
|
194
|
+
return { experimentName, results };
|
|
195
|
+
}
|
|
@@ -14,11 +14,10 @@ class DynamicRunEvaluator {
|
|
|
14
14
|
writable: true,
|
|
15
15
|
value: void 0
|
|
16
16
|
});
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
return evaluator(
|
|
20
|
-
};
|
|
21
|
-
this.func = wrappedFunc;
|
|
17
|
+
this.func = ((input) => {
|
|
18
|
+
const { run, example } = input.langSmithRunAndExample;
|
|
19
|
+
return evaluator(run, example);
|
|
20
|
+
});
|
|
22
21
|
}
|
|
23
22
|
coerceEvaluationResults(results, sourceRunId) {
|
|
24
23
|
if ("results" in results) {
|
|
@@ -57,17 +56,15 @@ class DynamicRunEvaluator {
|
|
|
57
56
|
if ("session_id" in run) {
|
|
58
57
|
metadata["experiment"] = run.session_id;
|
|
59
58
|
}
|
|
60
|
-
|
|
59
|
+
if (typeof this.func !== "function") {
|
|
60
|
+
throw new Error("Target must be runnable function");
|
|
61
|
+
}
|
|
62
|
+
const wrappedTraceableFunc = (0, traceable_js_1.traceable)(this.func, { project_name: "evaluators", name: "evaluator", ...options });
|
|
63
|
+
const result = (await wrappedTraceableFunc(
|
|
61
64
|
// Pass data via `langSmithRunAndExample` key to avoid conflicts with other
|
|
62
65
|
// inputs. This key is extracted in the wrapped function, with `run` and
|
|
63
66
|
// `example` passed to evaluator function as arguments.
|
|
64
|
-
|
|
65
|
-
run,
|
|
66
|
-
example,
|
|
67
|
-
};
|
|
68
|
-
const result = (await wrappedTraceableFunc({ langSmithRunAndExample }, {
|
|
69
|
-
metadata,
|
|
70
|
-
}));
|
|
67
|
+
{ langSmithRunAndExample: { run, example } }, { metadata }));
|
|
71
68
|
// Check the one required property of EvaluationResult since 'instanceof' is not possible
|
|
72
69
|
if ("key" in result) {
|
|
73
70
|
if (!result.sourceRunId) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { v4 as uuidv4 } from "uuid";
|
|
2
|
-
import {
|
|
2
|
+
import { traceable } from "../traceable.js";
|
|
3
3
|
/**
|
|
4
4
|
* Wraps an evaluator function + implements the RunEvaluator interface.
|
|
5
5
|
*/
|
|
@@ -11,11 +11,10 @@ export class DynamicRunEvaluator {
|
|
|
11
11
|
writable: true,
|
|
12
12
|
value: void 0
|
|
13
13
|
});
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
return evaluator(
|
|
17
|
-
};
|
|
18
|
-
this.func = wrappedFunc;
|
|
14
|
+
this.func = ((input) => {
|
|
15
|
+
const { run, example } = input.langSmithRunAndExample;
|
|
16
|
+
return evaluator(run, example);
|
|
17
|
+
});
|
|
19
18
|
}
|
|
20
19
|
coerceEvaluationResults(results, sourceRunId) {
|
|
21
20
|
if ("results" in results) {
|
|
@@ -54,17 +53,15 @@ export class DynamicRunEvaluator {
|
|
|
54
53
|
if ("session_id" in run) {
|
|
55
54
|
metadata["experiment"] = run.session_id;
|
|
56
55
|
}
|
|
57
|
-
|
|
56
|
+
if (typeof this.func !== "function") {
|
|
57
|
+
throw new Error("Target must be runnable function");
|
|
58
|
+
}
|
|
59
|
+
const wrappedTraceableFunc = traceable(this.func, { project_name: "evaluators", name: "evaluator", ...options });
|
|
60
|
+
const result = (await wrappedTraceableFunc(
|
|
58
61
|
// Pass data via `langSmithRunAndExample` key to avoid conflicts with other
|
|
59
62
|
// inputs. This key is extracted in the wrapped function, with `run` and
|
|
60
63
|
// `example` passed to evaluator function as arguments.
|
|
61
|
-
|
|
62
|
-
run,
|
|
63
|
-
example,
|
|
64
|
-
};
|
|
65
|
-
const result = (await wrappedTraceableFunc({ langSmithRunAndExample }, {
|
|
66
|
-
metadata,
|
|
67
|
-
}));
|
|
64
|
+
{ langSmithRunAndExample: { run, example } }, { metadata }));
|
|
68
65
|
// Check the one required property of EvaluationResult since 'instanceof' is not possible
|
|
69
66
|
if ("key" in result) {
|
|
70
67
|
if (!result.sourceRunId) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.evaluate = exports.StringEvaluator = void 0;
|
|
3
|
+
exports.evaluateComparative = exports.evaluate = exports.StringEvaluator = void 0;
|
|
4
4
|
var string_evaluator_js_1 = require("./string_evaluator.cjs");
|
|
5
5
|
Object.defineProperty(exports, "StringEvaluator", { enumerable: true, get: function () { return string_evaluator_js_1.StringEvaluator; } });
|
|
6
6
|
var _runner_js_1 = require("./_runner.cjs");
|
|
7
7
|
Object.defineProperty(exports, "evaluate", { enumerable: true, get: function () { return _runner_js_1.evaluate; } });
|
|
8
|
+
var evaluate_comparative_js_1 = require("./evaluate_comparative.cjs");
|
|
9
|
+
Object.defineProperty(exports, "evaluateComparative", { enumerable: true, get: function () { return evaluate_comparative_js_1.evaluateComparative; } });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { RunEvaluator, EvaluationResult } from "./evaluator.js";
|
|
2
2
|
export { StringEvaluator, GradingFunctionParams, GradingFunctionResult, } from "./string_evaluator.js";
|
|
3
3
|
export { evaluate, type EvaluateOptions } from "./_runner.js";
|
|
4
|
+
export { evaluateComparative } from "./evaluate_comparative.js";
|
package/dist/evaluation/index.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -6,4 +6,4 @@ Object.defineProperty(exports, "Client", { enumerable: true, get: function () {
|
|
|
6
6
|
var run_trees_js_1 = require("./run_trees.cjs");
|
|
7
7
|
Object.defineProperty(exports, "RunTree", { enumerable: true, get: function () { return run_trees_js_1.RunTree; } });
|
|
8
8
|
// Update using yarn bump-version
|
|
9
|
-
exports.__version__ = "0.1.
|
|
9
|
+
exports.__version__ = "0.1.24";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { Client } from "./client.js";
|
|
2
2
|
export type { Dataset, Example, TracerSession, Run, Feedback, } from "./schemas.js";
|
|
3
3
|
export { RunTree, type RunTreeConfig } from "./run_trees.js";
|
|
4
|
-
export declare const __version__ = "0.1.
|
|
4
|
+
export declare const __version__ = "0.1.24";
|
package/dist/index.js
CHANGED
package/dist/run_trees.cjs
CHANGED
package/dist/run_trees.js
CHANGED
package/dist/schemas.d.ts
CHANGED
|
@@ -276,4 +276,21 @@ export interface DatasetDiffInfo {
|
|
|
276
276
|
examples_added: string[];
|
|
277
277
|
examples_removed: string[];
|
|
278
278
|
}
|
|
279
|
+
export interface ComparisonEvaluationResult {
|
|
280
|
+
key: string;
|
|
281
|
+
scores: Record<string, ScoreType>;
|
|
282
|
+
source_run_id?: string;
|
|
283
|
+
}
|
|
284
|
+
export interface ComparativeExperiment {
|
|
285
|
+
id: string;
|
|
286
|
+
name: string;
|
|
287
|
+
description: string;
|
|
288
|
+
tenant_id: string;
|
|
289
|
+
created_at: string;
|
|
290
|
+
modified_at: string;
|
|
291
|
+
reference_dataset_id: string;
|
|
292
|
+
extra?: Record<string, unknown>;
|
|
293
|
+
experiments_info?: Array<Record<string, unknown>>;
|
|
294
|
+
feedback_stats?: Record<string, unknown>;
|
|
295
|
+
}
|
|
279
296
|
export {};
|
package/dist/traceable.cjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.wrapFunctionAndEnsureTraceable = exports.isTraceableFunction = exports.getCurrentRunTree = exports.traceable = exports.ROOT = void 0;
|
|
4
|
-
const
|
|
4
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
5
5
|
const run_trees_js_1 = require("./run_trees.cjs");
|
|
6
6
|
const env_js_1 = require("./utils/env.cjs");
|
|
7
7
|
function isPromiseMethod(x) {
|
|
@@ -10,12 +10,28 @@ function isPromiseMethod(x) {
|
|
|
10
10
|
}
|
|
11
11
|
return false;
|
|
12
12
|
}
|
|
13
|
-
const asyncLocalStorage = new
|
|
13
|
+
const asyncLocalStorage = new node_async_hooks_1.AsyncLocalStorage();
|
|
14
14
|
exports.ROOT = Symbol("langsmith:traceable:root");
|
|
15
15
|
const isAsyncIterable = (x) => x != null &&
|
|
16
16
|
typeof x === "object" &&
|
|
17
17
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
18
18
|
typeof x[Symbol.asyncIterator] === "function";
|
|
19
|
+
const GeneratorFunction = function* () { }.constructor;
|
|
20
|
+
const isIteratorLike = (x) => x != null &&
|
|
21
|
+
typeof x === "object" &&
|
|
22
|
+
"next" in x &&
|
|
23
|
+
typeof x.next === "function";
|
|
24
|
+
const isGenerator = (x) =>
|
|
25
|
+
// eslint-disable-next-line no-instanceof/no-instanceof
|
|
26
|
+
x != null && typeof x === "function" && x instanceof GeneratorFunction;
|
|
27
|
+
const isThenable = (x) => x != null &&
|
|
28
|
+
typeof x === "object" &&
|
|
29
|
+
"then" in x &&
|
|
30
|
+
typeof x.then === "function";
|
|
31
|
+
const isReadableStream = (x) => x != null &&
|
|
32
|
+
typeof x === "object" &&
|
|
33
|
+
"getReader" in x &&
|
|
34
|
+
typeof x.getReader === "function";
|
|
19
35
|
const tracingIsEnabled = (tracingEnabled) => {
|
|
20
36
|
if (tracingEnabled !== undefined) {
|
|
21
37
|
return tracingEnabled;
|
|
@@ -55,6 +71,141 @@ const getTracingRunTree = (runTree, inputs) => {
|
|
|
55
71
|
runTree.inputs = handleRunInputs(inputs);
|
|
56
72
|
return runTree;
|
|
57
73
|
};
|
|
74
|
+
// idea: store the state of the promise outside
|
|
75
|
+
// but only when the promise is "consumed"
|
|
76
|
+
const getSerializablePromise = (arg) => {
|
|
77
|
+
const proxyState = { current: undefined };
|
|
78
|
+
const promiseProxy = new Proxy(arg, {
|
|
79
|
+
get(target, prop, receiver) {
|
|
80
|
+
if (prop === "then") {
|
|
81
|
+
const boundThen = arg[prop].bind(arg);
|
|
82
|
+
return (resolve, reject = (x) => {
|
|
83
|
+
throw x;
|
|
84
|
+
}) => {
|
|
85
|
+
return boundThen((value) => {
|
|
86
|
+
proxyState.current = ["resolve", value];
|
|
87
|
+
return resolve(value);
|
|
88
|
+
}, (error) => {
|
|
89
|
+
proxyState.current = ["reject", error];
|
|
90
|
+
return reject(error);
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (prop === "catch") {
|
|
95
|
+
const boundCatch = arg[prop].bind(arg);
|
|
96
|
+
return (reject) => {
|
|
97
|
+
return boundCatch((error) => {
|
|
98
|
+
proxyState.current = ["reject", error];
|
|
99
|
+
return reject(error);
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (prop === "toJSON") {
|
|
104
|
+
return () => {
|
|
105
|
+
if (!proxyState.current)
|
|
106
|
+
return undefined;
|
|
107
|
+
const [type, value] = proxyState.current ?? [];
|
|
108
|
+
if (type === "resolve")
|
|
109
|
+
return value;
|
|
110
|
+
return { error: value };
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return Reflect.get(target, prop, receiver);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
return promiseProxy;
|
|
117
|
+
};
|
|
118
|
+
const convertSerializableArg = (arg) => {
|
|
119
|
+
if (isReadableStream(arg)) {
|
|
120
|
+
const proxyState = [];
|
|
121
|
+
const transform = new TransformStream({
|
|
122
|
+
start: () => void 0,
|
|
123
|
+
transform: (chunk, controller) => {
|
|
124
|
+
proxyState.push(chunk);
|
|
125
|
+
controller.enqueue(chunk);
|
|
126
|
+
},
|
|
127
|
+
flush: () => void 0,
|
|
128
|
+
});
|
|
129
|
+
const pipeThrough = arg.pipeThrough(transform);
|
|
130
|
+
Object.assign(pipeThrough, { toJSON: () => proxyState });
|
|
131
|
+
return pipeThrough;
|
|
132
|
+
}
|
|
133
|
+
if (isAsyncIterable(arg)) {
|
|
134
|
+
const proxyState = { current: [] };
|
|
135
|
+
return new Proxy(arg, {
|
|
136
|
+
get(target, prop, receiver) {
|
|
137
|
+
if (prop === Symbol.asyncIterator) {
|
|
138
|
+
return () => {
|
|
139
|
+
const boundIterator = arg[Symbol.asyncIterator].bind(arg);
|
|
140
|
+
const iterator = boundIterator();
|
|
141
|
+
return new Proxy(iterator, {
|
|
142
|
+
get(target, prop, receiver) {
|
|
143
|
+
if (prop === "next" || prop === "return" || prop === "throw") {
|
|
144
|
+
const bound = iterator.next.bind(iterator);
|
|
145
|
+
return (...args) => {
|
|
146
|
+
// @ts-expect-error TS cannot infer the argument types for the bound function
|
|
147
|
+
const wrapped = getSerializablePromise(bound(...args));
|
|
148
|
+
proxyState.current.push(wrapped);
|
|
149
|
+
return wrapped;
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
if (prop === "return" || prop === "throw") {
|
|
153
|
+
return iterator.next.bind(iterator);
|
|
154
|
+
}
|
|
155
|
+
return Reflect.get(target, prop, receiver);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (prop === "toJSON") {
|
|
161
|
+
return () => {
|
|
162
|
+
const onlyNexts = proxyState.current;
|
|
163
|
+
const serialized = onlyNexts.map((next) => next.toJSON());
|
|
164
|
+
const chunks = serialized.reduce((memo, next) => {
|
|
165
|
+
if (next?.value)
|
|
166
|
+
memo.push(next.value);
|
|
167
|
+
return memo;
|
|
168
|
+
}, []);
|
|
169
|
+
return chunks;
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return Reflect.get(target, prop, receiver);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (!Array.isArray(arg) && isIteratorLike(arg)) {
|
|
177
|
+
const proxyState = [];
|
|
178
|
+
return new Proxy(arg, {
|
|
179
|
+
get(target, prop, receiver) {
|
|
180
|
+
if (prop === "next" || prop === "return" || prop === "throw") {
|
|
181
|
+
const bound = arg[prop]?.bind(arg);
|
|
182
|
+
return (...args) => {
|
|
183
|
+
// @ts-expect-error TS cannot infer the argument types for the bound function
|
|
184
|
+
const next = bound?.(...args);
|
|
185
|
+
if (next != null)
|
|
186
|
+
proxyState.push(next);
|
|
187
|
+
return next;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (prop === "toJSON") {
|
|
191
|
+
return () => {
|
|
192
|
+
const chunks = proxyState.reduce((memo, next) => {
|
|
193
|
+
if (next.value)
|
|
194
|
+
memo.push(next.value);
|
|
195
|
+
return memo;
|
|
196
|
+
}, []);
|
|
197
|
+
return chunks;
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return Reflect.get(target, prop, receiver);
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
if (isThenable(arg)) {
|
|
205
|
+
return getSerializablePromise(arg);
|
|
206
|
+
}
|
|
207
|
+
return arg;
|
|
208
|
+
};
|
|
58
209
|
/**
|
|
59
210
|
* Higher-order function that takes function as input and returns a
|
|
60
211
|
* "TraceableFunction" - a wrapped version of the input that
|
|
@@ -118,8 +269,13 @@ function traceable(wrappedFunc, config) {
|
|
|
118
269
|
...runTreeConfig,
|
|
119
270
|
};
|
|
120
271
|
}
|
|
272
|
+
// TODO: deal with possible nested promises and async iterables
|
|
273
|
+
const processedArgs = args;
|
|
274
|
+
for (let i = 0; i < processedArgs.length; i++) {
|
|
275
|
+
processedArgs[i] = convertSerializableArg(processedArgs[i]);
|
|
276
|
+
}
|
|
121
277
|
const [currentRunTree, rawInputs] = (() => {
|
|
122
|
-
const [firstArg, ...restArgs] =
|
|
278
|
+
const [firstArg, ...restArgs] = processedArgs;
|
|
123
279
|
// used for handoff between LangChain.JS and traceable functions
|
|
124
280
|
if ((0, run_trees_js_1.isRunnableConfigLike)(firstArg)) {
|
|
125
281
|
return [
|
|
@@ -147,12 +303,12 @@ function traceable(wrappedFunc, config) {
|
|
|
147
303
|
const prevRunFromStore = asyncLocalStorage.getStore();
|
|
148
304
|
if (prevRunFromStore) {
|
|
149
305
|
return [
|
|
150
|
-
getTracingRunTree(prevRunFromStore.createChild(ensuredConfig),
|
|
151
|
-
|
|
306
|
+
getTracingRunTree(prevRunFromStore.createChild(ensuredConfig), processedArgs),
|
|
307
|
+
processedArgs,
|
|
152
308
|
];
|
|
153
309
|
}
|
|
154
|
-
const currentRunTree = getTracingRunTree(new run_trees_js_1.RunTree(ensuredConfig),
|
|
155
|
-
return [currentRunTree,
|
|
310
|
+
const currentRunTree = getTracingRunTree(new run_trees_js_1.RunTree(ensuredConfig), processedArgs);
|
|
311
|
+
return [currentRunTree, processedArgs];
|
|
156
312
|
})();
|
|
157
313
|
return asyncLocalStorage.run(currentRunTree, () => {
|
|
158
314
|
const postRunPromise = currentRunTree?.postRun();
|
|
@@ -208,6 +364,17 @@ function traceable(wrappedFunc, config) {
|
|
|
208
364
|
await postRunPromise;
|
|
209
365
|
await currentRunTree?.patchRun();
|
|
210
366
|
}
|
|
367
|
+
function gatherAll(iterator) {
|
|
368
|
+
const chunks = [];
|
|
369
|
+
// eslint-disable-next-line no-constant-condition
|
|
370
|
+
while (true) {
|
|
371
|
+
const next = iterator.next();
|
|
372
|
+
chunks.push(next);
|
|
373
|
+
if (next.done)
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
return chunks;
|
|
377
|
+
}
|
|
211
378
|
let returnValue;
|
|
212
379
|
try {
|
|
213
380
|
returnValue = wrappedFunc(...rawInputs);
|
|
@@ -216,25 +383,40 @@ function traceable(wrappedFunc, config) {
|
|
|
216
383
|
returnValue = Promise.reject(err);
|
|
217
384
|
}
|
|
218
385
|
if (isAsyncIterable(returnValue)) {
|
|
219
|
-
const snapshot =
|
|
386
|
+
const snapshot = node_async_hooks_1.AsyncLocalStorage.snapshot();
|
|
220
387
|
return wrapAsyncGeneratorForTracing(returnValue, snapshot);
|
|
221
388
|
}
|
|
222
389
|
const tracedPromise = new Promise((resolve, reject) => {
|
|
223
390
|
Promise.resolve(returnValue)
|
|
224
391
|
.then(async (rawOutput) => {
|
|
225
392
|
if (isAsyncIterable(rawOutput)) {
|
|
226
|
-
const snapshot =
|
|
393
|
+
const snapshot = node_async_hooks_1.AsyncLocalStorage.snapshot();
|
|
227
394
|
return resolve(wrapAsyncGeneratorForTracing(rawOutput, snapshot));
|
|
228
395
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
396
|
+
if (isGenerator(wrappedFunc) && isIteratorLike(rawOutput)) {
|
|
397
|
+
const chunks = gatherAll(rawOutput);
|
|
398
|
+
await currentRunTree?.end(handleRunOutputs(await handleChunks(chunks.reduce((memo, { value, done }) => {
|
|
399
|
+
if (!done || typeof value !== "undefined") {
|
|
400
|
+
memo.push(value);
|
|
401
|
+
}
|
|
402
|
+
return memo;
|
|
403
|
+
}, []))));
|
|
404
|
+
await handleEnd();
|
|
405
|
+
return (function* () {
|
|
406
|
+
for (const ret of chunks) {
|
|
407
|
+
if (ret.done)
|
|
408
|
+
return ret.value;
|
|
409
|
+
yield ret.value;
|
|
410
|
+
}
|
|
411
|
+
})();
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
await currentRunTree?.end(handleRunOutputs(rawOutput));
|
|
415
|
+
await handleEnd();
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
// eslint-disable-next-line no-unsafe-finally
|
|
419
|
+
return rawOutput;
|
|
238
420
|
}
|
|
239
421
|
}, async (error) => {
|
|
240
422
|
await currentRunTree?.end(undefined, String(error));
|
|
@@ -287,11 +469,15 @@ function isTraceableFunction(x
|
|
|
287
469
|
}
|
|
288
470
|
exports.isTraceableFunction = isTraceableFunction;
|
|
289
471
|
function isKVMap(x) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
472
|
+
if (typeof x !== "object" || x == null) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
const prototype = Object.getPrototypeOf(x);
|
|
476
|
+
return ((prototype === null ||
|
|
477
|
+
prototype === Object.prototype ||
|
|
478
|
+
Object.getPrototypeOf(prototype) === null) &&
|
|
479
|
+
!(Symbol.toStringTag in x) &&
|
|
480
|
+
!(Symbol.iterator in x));
|
|
295
481
|
}
|
|
296
482
|
function wrapFunctionAndEnsureTraceable(target, options, name = "target") {
|
|
297
483
|
if (typeof target === "function") {
|