@zhanla/sdk-ts 0.3.4 → 0.3.6
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/bin/cli.js +20 -2
- package/bin/discover.js +11 -2
- package/bin/run.js +341 -2
- package/dist/manifest.d.ts +1 -3
- package/dist/manifest.js +2 -93
- package/dist/trace_store.d.ts +1 -1
- package/dist/types.d.ts +4 -2
- package/dist/types.js +13 -1
- package/dist/wrap.d.ts +9 -0
- package/dist/wrap.js +14 -5
- package/package.json +3 -2
- package/bin/env.js +0 -72
package/bin/cli.js
CHANGED
|
@@ -29,10 +29,20 @@ if (process.env.BENCH_SDK_TS_LOADER_ACTIVE !== "1") {
|
|
|
29
29
|
|
|
30
30
|
const [, , subcommand, ...args] = process.argv;
|
|
31
31
|
|
|
32
|
+
const INTERNAL_SUBCOMMANDS = ["discover", "run", "eval-only"];
|
|
33
|
+
|
|
32
34
|
if (!subcommand) {
|
|
33
|
-
console.error("Usage: zhanla-sdk-ts <discover|run> <target>");
|
|
35
|
+
console.error("Usage: zhanla-sdk-ts <discover|run|eval-only> <target>");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (INTERNAL_SUBCOMMANDS.includes(subcommand) && process.env.ZHANLA_INTERNAL !== "1") {
|
|
40
|
+
console.error(
|
|
41
|
+
"Use the `zhanla` CLI for the full experience: https://pypi.org/project/zhanla/"
|
|
42
|
+
);
|
|
34
43
|
process.exit(1);
|
|
35
44
|
}
|
|
45
|
+
|
|
36
46
|
if (subcommand === "discover") {
|
|
37
47
|
const [filePath] = args;
|
|
38
48
|
if (!filePath) {
|
|
@@ -61,7 +71,15 @@ if (subcommand === "discover") {
|
|
|
61
71
|
}
|
|
62
72
|
const { runComponent } = await import("./run.js");
|
|
63
73
|
await runComponent(target, evalTarget);
|
|
74
|
+
} else if (subcommand === "eval-only") {
|
|
75
|
+
const [evalTarget] = args;
|
|
76
|
+
if (!evalTarget) {
|
|
77
|
+
console.error("Usage: zhanla-sdk-ts eval-only <file.ts:name>");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
const { runEvalOnly } = await import("./run.js");
|
|
81
|
+
await runEvalOnly(evalTarget);
|
|
64
82
|
} else {
|
|
65
|
-
console.error(`Unknown subcommand: ${subcommand}. Use 'discover' or '
|
|
83
|
+
console.error(`Unknown subcommand: ${subcommand}. Use 'discover', 'run', or 'eval-only'.`);
|
|
66
84
|
process.exit(1);
|
|
67
85
|
}
|
package/bin/discover.js
CHANGED
|
@@ -11,7 +11,17 @@
|
|
|
11
11
|
|
|
12
12
|
import { pathToFileURL } from "url";
|
|
13
13
|
import { resolve } from "path";
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
export function splitFileSpec(fileSpec) {
|
|
16
|
+
const colonIndex = fileSpec.lastIndexOf(":");
|
|
17
|
+
if (colonIndex > 1) {
|
|
18
|
+
return {
|
|
19
|
+
filePath: fileSpec.slice(0, colonIndex),
|
|
20
|
+
symbolName: fileSpec.slice(colonIndex + 1) || null,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
return { filePath: fileSpec, symbolName: null };
|
|
24
|
+
}
|
|
15
25
|
|
|
16
26
|
function isComponentLike(value) {
|
|
17
27
|
return (
|
|
@@ -216,7 +226,6 @@ export async function runDiscover(fileSpec) {
|
|
|
216
226
|
const { filePath, symbolName } = splitFileSpec(fileSpec);
|
|
217
227
|
|
|
218
228
|
const absolutePath = resolve(filePath);
|
|
219
|
-
loadDotenvLocal(absolutePath);
|
|
220
229
|
const fileUrl = pathToFileURL(absolutePath).href;
|
|
221
230
|
|
|
222
231
|
let moduleExports;
|
package/bin/run.js
CHANGED
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { pathToFileURL } from "url";
|
|
12
|
+
import { readFileSync } from "fs";
|
|
12
13
|
import { resolve } from "path";
|
|
14
|
+
import vm from "vm";
|
|
13
15
|
import readline from "readline";
|
|
14
|
-
import
|
|
16
|
+
import ts from "typescript";
|
|
15
17
|
|
|
16
18
|
function redirectConsoleToStderr() {
|
|
17
19
|
const stringifyArg = (arg) => {
|
|
@@ -55,7 +57,6 @@ async function loadTargetComponent(target, collectExportedComponents) {
|
|
|
55
57
|
const { filePath, symbolName } = parseTarget(target);
|
|
56
58
|
|
|
57
59
|
const absolutePath = resolve(filePath);
|
|
58
|
-
loadDotenvLocal(absolutePath);
|
|
59
60
|
const fileUrl = pathToFileURL(absolutePath).href;
|
|
60
61
|
|
|
61
62
|
let moduleExports;
|
|
@@ -78,6 +79,344 @@ async function loadTargetComponent(target, collectExportedComponents) {
|
|
|
78
79
|
return component;
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
function isExportedStatement(statement) {
|
|
83
|
+
return statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getStringLiteralValue(node) {
|
|
87
|
+
if (!node) return null;
|
|
88
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
89
|
+
return node.text;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getManagedCodeEvalName(initializer) {
|
|
95
|
+
if (!initializer || !ts.isNewExpression(initializer)) return null;
|
|
96
|
+
if (!ts.isIdentifier(initializer.expression) || initializer.expression.text !== "CodeEval") {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const [config] = initializer.arguments ?? [];
|
|
100
|
+
if (!config || !ts.isObjectLiteralExpression(config)) return null;
|
|
101
|
+
for (const property of config.properties) {
|
|
102
|
+
if (
|
|
103
|
+
ts.isPropertyAssignment(property) &&
|
|
104
|
+
ts.isIdentifier(property.name) &&
|
|
105
|
+
property.name.text === "name"
|
|
106
|
+
) {
|
|
107
|
+
return getStringLiteralValue(property.initializer);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function buildManagedTsIsolationContext(code) {
|
|
114
|
+
const sourceFile = ts.createSourceFile(
|
|
115
|
+
"managed-eval.ts",
|
|
116
|
+
code,
|
|
117
|
+
ts.ScriptTarget.ES2020,
|
|
118
|
+
true,
|
|
119
|
+
ts.ScriptKind.TS,
|
|
120
|
+
);
|
|
121
|
+
const topLevelDeclarations = new Map();
|
|
122
|
+
const importedBindings = new Map();
|
|
123
|
+
const sdkImportStatements = [];
|
|
124
|
+
|
|
125
|
+
for (const statement of sourceFile.statements) {
|
|
126
|
+
if (ts.isImportDeclaration(statement)) {
|
|
127
|
+
const moduleSpecifier = getStringLiteralValue(statement.moduleSpecifier);
|
|
128
|
+
if (moduleSpecifier === "@zhanla/sdk-ts") {
|
|
129
|
+
sdkImportStatements.push(statement);
|
|
130
|
+
}
|
|
131
|
+
const importClause = statement.importClause;
|
|
132
|
+
if (moduleSpecifier && importClause) {
|
|
133
|
+
if (importClause.name) {
|
|
134
|
+
importedBindings.set(importClause.name.text, moduleSpecifier);
|
|
135
|
+
}
|
|
136
|
+
const namedBindings = importClause.namedBindings;
|
|
137
|
+
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
138
|
+
for (const element of namedBindings.elements) {
|
|
139
|
+
importedBindings.set(element.name.text, moduleSpecifier);
|
|
140
|
+
}
|
|
141
|
+
} else if (namedBindings && ts.isNamespaceImport(namedBindings)) {
|
|
142
|
+
importedBindings.set(namedBindings.name.text, moduleSpecifier);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (
|
|
149
|
+
ts.isFunctionDeclaration(statement) ||
|
|
150
|
+
ts.isClassDeclaration(statement) ||
|
|
151
|
+
ts.isEnumDeclaration(statement)
|
|
152
|
+
) {
|
|
153
|
+
if (statement.name) {
|
|
154
|
+
topLevelDeclarations.set(statement.name.text, statement);
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (ts.isVariableStatement(statement)) {
|
|
160
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
161
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
162
|
+
topLevelDeclarations.set(declaration.name.text, statement);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { sourceFile, topLevelDeclarations, importedBindings, sdkImportStatements };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function scanManagedTsDependencies(statement, context) {
|
|
172
|
+
const referencedDeclarations = new Set();
|
|
173
|
+
const disallowedImports = new Map();
|
|
174
|
+
|
|
175
|
+
const visit = (node) => {
|
|
176
|
+
if (ts.isIdentifier(node)) {
|
|
177
|
+
if (
|
|
178
|
+
(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) ||
|
|
179
|
+
(ts.isPropertyAssignment(node.parent) && node.parent.name === node)
|
|
180
|
+
) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const localStatement = context.topLevelDeclarations.get(node.text);
|
|
185
|
+
if (localStatement && localStatement !== statement) {
|
|
186
|
+
referencedDeclarations.add(localStatement);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const importedFrom = context.importedBindings.get(node.text);
|
|
190
|
+
if (importedFrom && importedFrom !== "@zhanla/sdk-ts") {
|
|
191
|
+
const bindings = disallowedImports.get(importedFrom) ?? new Set();
|
|
192
|
+
bindings.add(node.text);
|
|
193
|
+
disallowedImports.set(importedFrom, bindings);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
ts.forEachChild(node, visit);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
visit(statement);
|
|
201
|
+
return { referencedDeclarations, disallowedImports };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function isolateManagedTypeScriptEvalSource(code, evalName) {
|
|
205
|
+
const context = buildManagedTsIsolationContext(code);
|
|
206
|
+
|
|
207
|
+
let targetStatement = null;
|
|
208
|
+
for (const statement of context.sourceFile.statements) {
|
|
209
|
+
if (!ts.isVariableStatement(statement) || !isExportedStatement(statement)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
213
|
+
if (!ts.isIdentifier(declaration.name)) continue;
|
|
214
|
+
const declaredEvalName = getManagedCodeEvalName(declaration.initializer);
|
|
215
|
+
if (declaredEvalName === evalName || declaration.name.text === evalName) {
|
|
216
|
+
targetStatement = statement;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (targetStatement) break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!targetStatement) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Managed TypeScript eval '${evalName}' was not found in the synced source file.`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const includedStatements = new Set([targetStatement]);
|
|
230
|
+
const pendingStatements = [targetStatement];
|
|
231
|
+
const disallowedImports = new Map();
|
|
232
|
+
|
|
233
|
+
while (pendingStatements.length > 0) {
|
|
234
|
+
const statement = pendingStatements.pop();
|
|
235
|
+
if (!statement) continue;
|
|
236
|
+
|
|
237
|
+
const scan = scanManagedTsDependencies(statement, context);
|
|
238
|
+
for (const dependency of scan.referencedDeclarations) {
|
|
239
|
+
if (!includedStatements.has(dependency)) {
|
|
240
|
+
includedStatements.add(dependency);
|
|
241
|
+
pendingStatements.push(dependency);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const [moduleSpecifier, bindings] of scan.disallowedImports) {
|
|
245
|
+
const existing = disallowedImports.get(moduleSpecifier) ?? new Set();
|
|
246
|
+
for (const binding of bindings) {
|
|
247
|
+
existing.add(binding);
|
|
248
|
+
}
|
|
249
|
+
disallowedImports.set(moduleSpecifier, existing);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (disallowedImports.size > 0) {
|
|
254
|
+
const formattedImports = Array.from(disallowedImports.entries())
|
|
255
|
+
.map(
|
|
256
|
+
([moduleSpecifier, bindings]) =>
|
|
257
|
+
`${moduleSpecifier} (${Array.from(bindings).sort().join(", ")})`,
|
|
258
|
+
)
|
|
259
|
+
.join("; ");
|
|
260
|
+
throw new Error(
|
|
261
|
+
`Managed TypeScript eval '${evalName}' uses unsupported imports: ${formattedImports}. ` +
|
|
262
|
+
"Keep managed evals self-contained and move helpers into the same file.",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return context.sourceFile.statements
|
|
267
|
+
.filter(
|
|
268
|
+
(statement) =>
|
|
269
|
+
context.sdkImportStatements.includes(statement) || includedStatements.has(statement),
|
|
270
|
+
)
|
|
271
|
+
.map((statement) => statement.getFullText(context.sourceFile))
|
|
272
|
+
.join("");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function buildEvalOnlySandbox(code) {
|
|
276
|
+
const transpiled = ts.transpileModule(code, {
|
|
277
|
+
compilerOptions: {
|
|
278
|
+
module: ts.ModuleKind.CommonJS,
|
|
279
|
+
target: ts.ScriptTarget.ES2020,
|
|
280
|
+
esModuleInterop: true,
|
|
281
|
+
},
|
|
282
|
+
}).outputText;
|
|
283
|
+
|
|
284
|
+
const moduleRecord = { exports: {} };
|
|
285
|
+
const sdkStub = class {
|
|
286
|
+
constructor(opts) {
|
|
287
|
+
Object.assign(this, opts);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const sandbox = vm.createContext({
|
|
292
|
+
JSON,
|
|
293
|
+
Math,
|
|
294
|
+
Array,
|
|
295
|
+
Object,
|
|
296
|
+
String,
|
|
297
|
+
Number,
|
|
298
|
+
Boolean,
|
|
299
|
+
Date,
|
|
300
|
+
RegExp,
|
|
301
|
+
Promise,
|
|
302
|
+
Error,
|
|
303
|
+
TypeError,
|
|
304
|
+
RangeError,
|
|
305
|
+
ReferenceError,
|
|
306
|
+
SyntaxError,
|
|
307
|
+
Map,
|
|
308
|
+
Set,
|
|
309
|
+
WeakMap,
|
|
310
|
+
WeakSet,
|
|
311
|
+
Symbol,
|
|
312
|
+
parseInt,
|
|
313
|
+
parseFloat,
|
|
314
|
+
isNaN,
|
|
315
|
+
isFinite,
|
|
316
|
+
encodeURIComponent,
|
|
317
|
+
decodeURIComponent,
|
|
318
|
+
console,
|
|
319
|
+
exports: moduleRecord.exports,
|
|
320
|
+
module: moduleRecord,
|
|
321
|
+
require: (specifier) => {
|
|
322
|
+
if (specifier === "@zhanla/sdk-ts") {
|
|
323
|
+
return {
|
|
324
|
+
CodeEval: sdkStub,
|
|
325
|
+
LLMEval: sdkStub,
|
|
326
|
+
Checklist: sdkStub,
|
|
327
|
+
EvalTree: sdkStub,
|
|
328
|
+
Branch: sdkStub,
|
|
329
|
+
Leaf: sdkStub,
|
|
330
|
+
Edge: sdkStub,
|
|
331
|
+
Tool: sdkStub,
|
|
332
|
+
Skill: sdkStub,
|
|
333
|
+
Agent: sdkStub,
|
|
334
|
+
Orchestration: sdkStub,
|
|
335
|
+
Step: sdkStub,
|
|
336
|
+
ComponentValue: null,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
throw new Error(
|
|
340
|
+
`Cannot import '${specifier}' in the managed TypeScript eval sandbox. ` +
|
|
341
|
+
"Only @zhanla/sdk-ts imports and same-file helpers are supported.",
|
|
342
|
+
);
|
|
343
|
+
},
|
|
344
|
+
});
|
|
345
|
+
sandbox.globalThis = sandbox;
|
|
346
|
+
new vm.Script(transpiled).runInContext(sandbox, { timeout: 10000 });
|
|
347
|
+
return moduleRecord;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function findEvalComponent(moduleRecord, evalName) {
|
|
351
|
+
for (const value of Object.values(moduleRecord.exports)) {
|
|
352
|
+
if (value && typeof value === "object" && value.name === evalName && typeof value.fn === "function") {
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function loadIsolatedEvalComponent(target) {
|
|
360
|
+
const { filePath, symbolName } = parseTarget(target);
|
|
361
|
+
const absolutePath = resolve(filePath);
|
|
362
|
+
const sourceText = readFileSync(absolutePath, "utf8");
|
|
363
|
+
const isolatedSource = isolateManagedTypeScriptEvalSource(sourceText, symbolName);
|
|
364
|
+
const moduleRecord = buildEvalOnlySandbox(isolatedSource);
|
|
365
|
+
const component = findEvalComponent(moduleRecord, symbolName);
|
|
366
|
+
if (!component) {
|
|
367
|
+
throw new Error(`Component '${symbolName}' not found in isolated eval source.`);
|
|
368
|
+
}
|
|
369
|
+
return component;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export async function runEvalOnly(evalTarget) {
|
|
373
|
+
redirectConsoleToStderr();
|
|
374
|
+
let evalComponent;
|
|
375
|
+
try {
|
|
376
|
+
evalComponent = loadIsolatedEvalComponent(evalTarget);
|
|
377
|
+
} catch (err) {
|
|
378
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const rl = readline.createInterface({
|
|
383
|
+
input: process.stdin,
|
|
384
|
+
crlfDelay: Infinity,
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
for await (const line of rl) {
|
|
388
|
+
const trimmed = line.trim();
|
|
389
|
+
if (!trimmed) continue;
|
|
390
|
+
|
|
391
|
+
let context;
|
|
392
|
+
try {
|
|
393
|
+
context = JSON.parse(trimmed);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
process.stdout.write(JSON.stringify({ status: "error", error: `Invalid JSON: ${err.message}` }) + "\n");
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
const evalOutput = evalComponent.fn.length <= 1
|
|
401
|
+
? await Promise.resolve(evalComponent.fn(context))
|
|
402
|
+
: await Promise.resolve(
|
|
403
|
+
evalComponent.fn(
|
|
404
|
+
context.model_response,
|
|
405
|
+
context.expected_output,
|
|
406
|
+
context.model_input,
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
process.stdout.write(
|
|
410
|
+
JSON.stringify({ status: "ok", name: evalComponent.name, eval_output: evalOutput }) + "\n"
|
|
411
|
+
);
|
|
412
|
+
} catch (err) {
|
|
413
|
+
process.stdout.write(
|
|
414
|
+
JSON.stringify({ status: "error", name: evalComponent.name, error: err instanceof Error ? err.message : String(err) }) + "\n"
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
81
420
|
export async function runComponent(target, evalTarget = null) {
|
|
82
421
|
redirectConsoleToStderr();
|
|
83
422
|
// Dynamically import from SDK
|
package/dist/manifest.d.ts
CHANGED
|
@@ -57,10 +57,8 @@ export interface ComponentManifest {
|
|
|
57
57
|
output_schema?: JsonSchema;
|
|
58
58
|
file_path?: string;
|
|
59
59
|
symbol_name?: string;
|
|
60
|
-
source_code?: string;
|
|
61
|
-
source_language?: string;
|
|
62
|
-
source_format?: string;
|
|
63
60
|
model_response_format?: string;
|
|
61
|
+
questions?: string[];
|
|
64
62
|
}
|
|
65
63
|
export declare function toManifest(component: ZhanlaComponent, opts?: {
|
|
66
64
|
filePath?: string;
|
package/dist/manifest.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Manifest emission — serialize SDK component instances into ComponentManifest objects.
|
|
3
3
|
* The manifest shape mirrors the Python CLI's ComponentManifest dataclass.
|
|
4
4
|
*/
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
6
5
|
// ---------------------------------------------------------------------------
|
|
7
6
|
// Tree serialization helpers
|
|
8
7
|
// ---------------------------------------------------------------------------
|
|
@@ -54,87 +53,6 @@ function serializeBranch(branch) {
|
|
|
54
53
|
if_fail: branch.ifFail.map(serializeEdge),
|
|
55
54
|
};
|
|
56
55
|
}
|
|
57
|
-
function escapeRegExp(value) {
|
|
58
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
59
|
-
}
|
|
60
|
-
function findMatchingParen(source, startIndex) {
|
|
61
|
-
let depth = 0;
|
|
62
|
-
let quote = null;
|
|
63
|
-
let inLineComment = false;
|
|
64
|
-
let inBlockComment = false;
|
|
65
|
-
for (let i = startIndex; i < source.length; i += 1) {
|
|
66
|
-
const char = source[i];
|
|
67
|
-
const next = source[i + 1];
|
|
68
|
-
const prev = source[i - 1];
|
|
69
|
-
if (inLineComment) {
|
|
70
|
-
if (char === "\n")
|
|
71
|
-
inLineComment = false;
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (inBlockComment) {
|
|
75
|
-
if (prev === "*" && char === "/")
|
|
76
|
-
inBlockComment = false;
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (quote) {
|
|
80
|
-
if (char === quote && prev !== "\\")
|
|
81
|
-
quote = null;
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
if (char === "/" && next === "/") {
|
|
85
|
-
inLineComment = true;
|
|
86
|
-
i += 1;
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
if (char === "/" && next === "*") {
|
|
90
|
-
inBlockComment = true;
|
|
91
|
-
i += 1;
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
if (char === "'" || char === '"' || char === "`") {
|
|
95
|
-
quote = char;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (char === "(") {
|
|
99
|
-
depth += 1;
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (char === ")") {
|
|
103
|
-
depth -= 1;
|
|
104
|
-
if (depth === 0)
|
|
105
|
-
return i;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return -1;
|
|
109
|
-
}
|
|
110
|
-
function extractExportedComponentSource(filePath, symbolName) {
|
|
111
|
-
if (!filePath || !symbolName)
|
|
112
|
-
return undefined;
|
|
113
|
-
let source = "";
|
|
114
|
-
try {
|
|
115
|
-
source = readFileSync(filePath, "utf8");
|
|
116
|
-
}
|
|
117
|
-
catch {
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
const pattern = new RegExp(String.raw `(?:^|\n)\s*export\s+(?:const|let|var)\s+${escapeRegExp(symbolName)}\s*=\s*new\s+[A-Za-z_$][\w$.]*\s*\(`, "m");
|
|
121
|
-
const match = pattern.exec(source);
|
|
122
|
-
if (!match || match.index == null)
|
|
123
|
-
return undefined;
|
|
124
|
-
const start = source.lastIndexOf("export", match.index + match[0].length);
|
|
125
|
-
const openParenIndex = source.indexOf("(", match.index + match[0].length - 1);
|
|
126
|
-
if (start < 0 || openParenIndex < 0)
|
|
127
|
-
return undefined;
|
|
128
|
-
const closeParenIndex = findMatchingParen(source, openParenIndex);
|
|
129
|
-
if (closeParenIndex < 0)
|
|
130
|
-
return undefined;
|
|
131
|
-
let end = closeParenIndex + 1;
|
|
132
|
-
while (end < source.length && /\s/.test(source[end]))
|
|
133
|
-
end += 1;
|
|
134
|
-
if (source[end] === ";")
|
|
135
|
-
end += 1;
|
|
136
|
-
return source.slice(start, end).trim();
|
|
137
|
-
}
|
|
138
56
|
// ---------------------------------------------------------------------------
|
|
139
57
|
// to_manifest
|
|
140
58
|
// ---------------------------------------------------------------------------
|
|
@@ -150,7 +68,6 @@ const EXECUTION_MODES = {
|
|
|
150
68
|
eval_tree: "composite",
|
|
151
69
|
};
|
|
152
70
|
export function toManifest(component, opts = {}) {
|
|
153
|
-
const componentSource = extractExportedComponentSource(opts.filePath, opts.symbolName);
|
|
154
71
|
const key = component.key;
|
|
155
72
|
const keySource = "explicit";
|
|
156
73
|
const executionMode = EXECUTION_MODES[component.componentType] ?? "unknown";
|
|
@@ -175,9 +92,6 @@ export function toManifest(component, opts = {}) {
|
|
|
175
92
|
fn_present: true,
|
|
176
93
|
is_async: tool.isAsync,
|
|
177
94
|
output_schema: tool.outputSchema,
|
|
178
|
-
source_code: componentSource || tool.fn.toString(),
|
|
179
|
-
source_language: "typescript",
|
|
180
|
-
source_format: componentSource ? "component" : "function",
|
|
181
95
|
};
|
|
182
96
|
}
|
|
183
97
|
case "code_eval": {
|
|
@@ -186,9 +100,6 @@ export function toManifest(component, opts = {}) {
|
|
|
186
100
|
...base,
|
|
187
101
|
fn_present: true,
|
|
188
102
|
is_async: codeEval.isAsync,
|
|
189
|
-
source_code: componentSource || codeEval.fn.toString(),
|
|
190
|
-
source_language: "typescript",
|
|
191
|
-
source_format: componentSource ? "component" : "function",
|
|
192
103
|
model_response_format: codeEval.modelResponseFormat,
|
|
193
104
|
};
|
|
194
105
|
}
|
|
@@ -201,9 +112,6 @@ export function toManifest(component, opts = {}) {
|
|
|
201
112
|
is_async: skill.isAsync,
|
|
202
113
|
tool_refs: skill.tools.length > 0 ? skill.tools.map(resolveComponentRef) : undefined,
|
|
203
114
|
output_schema: skill.outputSchema,
|
|
204
|
-
source_code: componentSource || skill.fn?.toString(),
|
|
205
|
-
source_language: "typescript",
|
|
206
|
-
source_format: componentSource ? "component" : skill.fn ? "function" : undefined,
|
|
207
115
|
};
|
|
208
116
|
}
|
|
209
117
|
case "agent": {
|
|
@@ -233,7 +141,8 @@ export function toManifest(component, opts = {}) {
|
|
|
233
141
|
const llmEval = component;
|
|
234
142
|
return {
|
|
235
143
|
...base,
|
|
236
|
-
instructions: llmEval.instructions,
|
|
144
|
+
...(llmEval.instructions !== undefined ? { instructions: llmEval.instructions } : {}),
|
|
145
|
+
...(llmEval.questions !== undefined ? { questions: llmEval.questions } : {}),
|
|
237
146
|
model: llmEval.model,
|
|
238
147
|
temperature: llmEval.temperature,
|
|
239
148
|
output_schema: llmEval.outputSchema,
|
package/dist/trace_store.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface LLMCall {
|
|
|
12
12
|
sequenceOrder: number;
|
|
13
13
|
autoraterRunId: string | null;
|
|
14
14
|
datasetItemId: string | null;
|
|
15
|
-
provider: "anthropic" | "openai" | "gemini";
|
|
15
|
+
provider: "anthropic" | "openai" | "gemini" | "openrouter";
|
|
16
16
|
model: string;
|
|
17
17
|
inputMessages: unknown[];
|
|
18
18
|
output: unknown | null;
|
package/dist/types.d.ts
CHANGED
|
@@ -209,7 +209,8 @@ export declare class LLMProcessor extends ZhanlaComponent {
|
|
|
209
209
|
export interface LLMEvalOptions {
|
|
210
210
|
name: string;
|
|
211
211
|
description: string;
|
|
212
|
-
instructions
|
|
212
|
+
instructions?: string;
|
|
213
|
+
questions?: string[];
|
|
213
214
|
model: string;
|
|
214
215
|
client?: unknown;
|
|
215
216
|
runner?: Runner;
|
|
@@ -223,7 +224,8 @@ export declare class LLMEval extends ZhanlaComponent {
|
|
|
223
224
|
readonly componentType: "llm_eval";
|
|
224
225
|
readonly name: string;
|
|
225
226
|
readonly description: string;
|
|
226
|
-
readonly instructions
|
|
227
|
+
readonly instructions?: string;
|
|
228
|
+
readonly questions?: string[];
|
|
227
229
|
readonly model: string;
|
|
228
230
|
readonly runner?: Runner;
|
|
229
231
|
readonly outputSchema?: JsonSchema;
|
package/dist/types.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Mirrors the Python SDK's class-based component model.
|
|
4
4
|
*/
|
|
5
5
|
import { parseJsonResponse } from "./json.js";
|
|
6
|
-
import { isAnthropicClient, isGeminiClient, isOpenAIClient, wrap } from "./wrap.js";
|
|
6
|
+
import { isAnthropicClient, isGeminiClient, isOpenAIClient, isOpenRouterClient, wrap } from "./wrap.js";
|
|
7
7
|
export const RUNNABLE_TYPES = new Set([
|
|
8
8
|
"tool",
|
|
9
9
|
"agent",
|
|
@@ -210,6 +210,8 @@ export class Runner {
|
|
|
210
210
|
providerName() {
|
|
211
211
|
if (isAnthropicClient(this.client))
|
|
212
212
|
return "anthropic";
|
|
213
|
+
if (isOpenRouterClient(this.client))
|
|
214
|
+
return "openrouter";
|
|
213
215
|
if (isOpenAIClient(this.client))
|
|
214
216
|
return "openai";
|
|
215
217
|
if (isGeminiClient(this.client))
|
|
@@ -583,6 +585,7 @@ export class LLMEval extends ZhanlaComponent {
|
|
|
583
585
|
name;
|
|
584
586
|
description;
|
|
585
587
|
instructions;
|
|
588
|
+
questions;
|
|
586
589
|
model;
|
|
587
590
|
runner;
|
|
588
591
|
outputSchema;
|
|
@@ -592,12 +595,21 @@ export class LLMEval extends ZhanlaComponent {
|
|
|
592
595
|
key;
|
|
593
596
|
constructor(opts) {
|
|
594
597
|
super();
|
|
598
|
+
const hasInstructions = opts.instructions !== undefined && opts.instructions !== "";
|
|
599
|
+
const hasQuestions = opts.questions !== undefined && opts.questions.length > 0;
|
|
600
|
+
if (hasInstructions && hasQuestions) {
|
|
601
|
+
throw new Error(`LLMEval '${opts.name}': provide exactly one of 'instructions' or 'questions', not both.`);
|
|
602
|
+
}
|
|
603
|
+
if (!hasInstructions && !hasQuestions) {
|
|
604
|
+
throw new Error(`LLMEval '${opts.name}': must provide exactly one of 'instructions' or 'questions'.`);
|
|
605
|
+
}
|
|
595
606
|
if (opts.client !== undefined && opts.runner !== undefined) {
|
|
596
607
|
throw new Error("Specify client or runner, not both.");
|
|
597
608
|
}
|
|
598
609
|
this.name = opts.name;
|
|
599
610
|
this.description = opts.description;
|
|
600
611
|
this.instructions = opts.instructions;
|
|
612
|
+
this.questions = opts.questions;
|
|
601
613
|
this.model = opts.model;
|
|
602
614
|
this.runner = opts.client !== undefined ? new Runner({ client: opts.client }) : opts.runner;
|
|
603
615
|
this.outputSchema = opts.outputSchema;
|
package/dist/wrap.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Supported clients:
|
|
8
8
|
* - Anthropic (from @anthropic-ai/sdk)
|
|
9
9
|
* - OpenAI (from openai)
|
|
10
|
+
* - OpenRouter (OpenAI client with baseURL containing "openrouter")
|
|
10
11
|
* - GoogleGenAI (from @google/genai)
|
|
11
12
|
*/
|
|
12
13
|
export declare function isAnthropicClient(client: unknown): client is {
|
|
@@ -21,6 +22,14 @@ export declare function isOpenAIClient(client: unknown): client is {
|
|
|
21
22
|
};
|
|
22
23
|
};
|
|
23
24
|
};
|
|
25
|
+
export declare function isOpenRouterClient(client: unknown): client is {
|
|
26
|
+
chat: {
|
|
27
|
+
completions: {
|
|
28
|
+
create: (...args: unknown[]) => unknown;
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
baseURL: string;
|
|
32
|
+
};
|
|
24
33
|
export declare function isGeminiClient(client: unknown): client is {
|
|
25
34
|
models: {
|
|
26
35
|
generateContent: (...args: unknown[]) => unknown;
|
package/dist/wrap.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Supported clients:
|
|
8
8
|
* - Anthropic (from @anthropic-ai/sdk)
|
|
9
9
|
* - OpenAI (from openai)
|
|
10
|
+
* - OpenRouter (OpenAI client with baseURL containing "openrouter")
|
|
10
11
|
* - GoogleGenAI (from @google/genai)
|
|
11
12
|
*/
|
|
12
13
|
import { traceStorage } from "./trace_store.js";
|
|
@@ -104,7 +105,7 @@ function wrapAnthropic(client) {
|
|
|
104
105
|
// ---------------------------------------------------------------------------
|
|
105
106
|
// OpenAI
|
|
106
107
|
// ---------------------------------------------------------------------------
|
|
107
|
-
function recordOpenAICall(ctx, kwargs, response, latencyMs) {
|
|
108
|
+
function recordOpenAICall(ctx, kwargs, response, latencyMs, provider = "openai") {
|
|
108
109
|
if (!ctx)
|
|
109
110
|
return;
|
|
110
111
|
const choices = response["choices"] ?? [];
|
|
@@ -119,7 +120,7 @@ function recordOpenAICall(ctx, kwargs, response, latencyMs) {
|
|
|
119
120
|
sequenceOrder: ctx.nextSequence(),
|
|
120
121
|
autoraterRunId: ctx.autoraterRunId,
|
|
121
122
|
datasetItemId: ctx.datasetItemId,
|
|
122
|
-
provider
|
|
123
|
+
provider,
|
|
123
124
|
model: String(response["model"] ?? (kwargs["model"] ?? "")),
|
|
124
125
|
inputMessages: serialize(kwargs["messages"] ?? []),
|
|
125
126
|
output: serialize(message),
|
|
@@ -132,7 +133,7 @@ function recordOpenAICall(ctx, kwargs, response, latencyMs) {
|
|
|
132
133
|
metadata: null,
|
|
133
134
|
});
|
|
134
135
|
}
|
|
135
|
-
function wrapOpenAI(client) {
|
|
136
|
+
function wrapOpenAI(client, provider = "openai") {
|
|
136
137
|
const originalCreate = client.chat.completions.create.bind(client.chat.completions);
|
|
137
138
|
client.chat.completions.create = async function (...args) {
|
|
138
139
|
const ctx = traceStorage.getStore();
|
|
@@ -140,7 +141,7 @@ function wrapOpenAI(client) {
|
|
|
140
141
|
const start = performance.now();
|
|
141
142
|
const response = await originalCreate(...args);
|
|
142
143
|
const latencyMs = Math.round(performance.now() - start);
|
|
143
|
-
recordOpenAICall(ctx, kwargs, response, latencyMs);
|
|
144
|
+
recordOpenAICall(ctx, kwargs, response, latencyMs, provider);
|
|
144
145
|
return response;
|
|
145
146
|
};
|
|
146
147
|
return client;
|
|
@@ -208,6 +209,12 @@ export function isOpenAIClient(client) {
|
|
|
208
209
|
client.chat !== null &&
|
|
209
210
|
typeof client.chat.completions === "object");
|
|
210
211
|
}
|
|
212
|
+
export function isOpenRouterClient(client) {
|
|
213
|
+
if (!isOpenAIClient(client))
|
|
214
|
+
return false;
|
|
215
|
+
const baseURL = client["baseURL"];
|
|
216
|
+
return typeof baseURL === "string" && baseURL.includes("openrouter");
|
|
217
|
+
}
|
|
211
218
|
export function isGeminiClient(client) {
|
|
212
219
|
return (client !== null &&
|
|
213
220
|
typeof client === "object" &&
|
|
@@ -235,13 +242,15 @@ export function wrap(client) {
|
|
|
235
242
|
let wrapped;
|
|
236
243
|
if (isAnthropicClient(client))
|
|
237
244
|
wrapped = wrapAnthropic(client);
|
|
245
|
+
else if (isOpenRouterClient(client))
|
|
246
|
+
wrapped = wrapOpenAI(client, "openrouter");
|
|
238
247
|
else if (isOpenAIClient(client))
|
|
239
248
|
wrapped = wrapOpenAI(client);
|
|
240
249
|
else if (isGeminiClient(client))
|
|
241
250
|
wrapped = wrapGemini(client);
|
|
242
251
|
else {
|
|
243
252
|
throw new TypeError(`bench.wrap() does not support ${Object.getPrototypeOf(client)?.constructor?.name ?? typeof client}. ` +
|
|
244
|
-
"Supported clients: Anthropic, OpenAI, GoogleGenAI.");
|
|
253
|
+
"Supported clients: Anthropic, OpenAI, OpenRouter (OpenAI client with OpenRouter baseURL), GoogleGenAI.");
|
|
245
254
|
}
|
|
246
255
|
if (wrapped != null && typeof wrapped === "object") {
|
|
247
256
|
Object.defineProperty(wrapped, WRAPPED_CLIENT, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhanla/sdk-ts",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "TypeScript SDK for the zhanla CLI — define and run AI components locally",
|
|
5
5
|
"homepage": "https://benchmark-black.vercel.app/",
|
|
6
6
|
"repository": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"postinstall": "node ./bin/postinstall.js"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"tsx": "^4.7.0"
|
|
23
|
+
"tsx": "^4.7.0",
|
|
24
|
+
"typescript": "^5.4.0"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"dist",
|
package/bin/env.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { existsSync, readFileSync, statSync } from "fs";
|
|
4
|
-
import { dirname, join, resolve } from "path";
|
|
5
|
-
|
|
6
|
-
function isDirectory(path) {
|
|
7
|
-
try {
|
|
8
|
-
return statSync(path).isDirectory();
|
|
9
|
-
} catch {
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function splitFileSpec(fileSpec) {
|
|
15
|
-
const colonIndex = fileSpec.lastIndexOf(":");
|
|
16
|
-
// Preserve Windows drive letters (e.g. C:\...) by only splitting on later colons.
|
|
17
|
-
if (colonIndex > 1) {
|
|
18
|
-
return {
|
|
19
|
-
filePath: fileSpec.slice(0, colonIndex),
|
|
20
|
-
symbolName: fileSpec.slice(colonIndex + 1) || null,
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
return { filePath: fileSpec, symbolName: null };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function findDotenvLocal(startPath) {
|
|
27
|
-
let current = resolve(startPath);
|
|
28
|
-
if (!isDirectory(current)) {
|
|
29
|
-
current = dirname(current);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
while (true) {
|
|
33
|
-
const candidate = join(current, ".env.local");
|
|
34
|
-
if (existsSync(candidate)) {
|
|
35
|
-
return candidate;
|
|
36
|
-
}
|
|
37
|
-
const parent = dirname(current);
|
|
38
|
-
if (parent === current) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
current = parent;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function loadDotenvLocal(startPath) {
|
|
46
|
-
const envPath = findDotenvLocal(startPath);
|
|
47
|
-
if (!envPath) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const source = readFileSync(envPath, "utf8");
|
|
52
|
-
for (const line of source.split(/\r?\n/)) {
|
|
53
|
-
const trimmed = line.trim();
|
|
54
|
-
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
55
|
-
const equalsIndex = trimmed.indexOf("=");
|
|
56
|
-
if (equalsIndex === -1) continue;
|
|
57
|
-
|
|
58
|
-
const key = trimmed.slice(0, equalsIndex).trim();
|
|
59
|
-
let value = trimmed.slice(equalsIndex + 1).trim();
|
|
60
|
-
if (
|
|
61
|
-
(value.startsWith('"') && value.endsWith('"'))
|
|
62
|
-
|| (value.startsWith("'") && value.endsWith("'"))
|
|
63
|
-
) {
|
|
64
|
-
value = value.slice(1, -1);
|
|
65
|
-
}
|
|
66
|
-
if (key && process.env[key] === undefined) {
|
|
67
|
-
process.env[key] = value;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return envPath;
|
|
72
|
-
}
|