@flink-app/flink 2.0.0-alpha.65 → 2.0.0-alpha.66
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/CHANGELOG.md +10 -0
- package/dist/src/ai/ToolExecutor.d.ts +11 -1
- package/dist/src/ai/ToolExecutor.js +64 -1
- package/package.json +1 -1
- package/spec/ToolExecutor.spec.ts +113 -0
- package/src/ai/ToolExecutor.ts +69 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @flink-app/flink
|
|
2
2
|
|
|
3
|
+
## 2.0.0-alpha.66
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 5593d26: fix(tools): resolve cross-schema \$ref into \$defs for LLM providers
|
|
8
|
+
|
|
9
|
+
Tools using auto-generated schemas with TypeScript type references (e.g. a shared Canvas.ElementInput type) produced schemas with \$ref values like `"Canvas.ElementInput"`. AJV handled these fine via its schema registry, but OpenAI rejects them with "reference can only point to definitions defined at the top level of the schema".
|
|
10
|
+
|
|
11
|
+
ToolExecutor now resolves all cross-schema refs into a self-contained \$defs block, rewriting \$ref values to the standard #/\$defs/... format before returning the schema to LLM adapters. The openai-adapter sanitizer also now recurses into \$defs entries.
|
|
12
|
+
|
|
3
13
|
## 2.0.0-alpha.65
|
|
4
14
|
|
|
5
15
|
## 2.0.0-alpha.64
|
|
@@ -6,6 +6,7 @@ export declare class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
6
6
|
private toolFn;
|
|
7
7
|
private ctx;
|
|
8
8
|
private autoSchemas?;
|
|
9
|
+
private allSchemas?;
|
|
9
10
|
private ajv;
|
|
10
11
|
private compiledInputValidator?;
|
|
11
12
|
private compiledOutputValidator?;
|
|
@@ -14,7 +15,7 @@ export declare class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
14
15
|
outputSchema?: any;
|
|
15
16
|
inputTypeHint?: "void" | "any" | "named";
|
|
16
17
|
outputTypeHint?: "void" | "any" | "named";
|
|
17
|
-
} | undefined, allSchemas?: Record<string, any>);
|
|
18
|
+
} | undefined, allSchemas?: Record<string, any> | undefined);
|
|
18
19
|
/**
|
|
19
20
|
* Execute the tool with input
|
|
20
21
|
* @param input - Tool input data
|
|
@@ -26,6 +27,15 @@ export declare class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
26
27
|
conversationContext?: any;
|
|
27
28
|
}): Promise<ToolResult<any>>;
|
|
28
29
|
getToolSchema(): FlinkToolSchema;
|
|
30
|
+
/**
|
|
31
|
+
* Resolve cross-schema $ref values into $defs so the schema is self-contained.
|
|
32
|
+
*
|
|
33
|
+
* Flink's schema manifest stores schemas as separate documents with IDs like
|
|
34
|
+
* "Canvas.ElementInput". These are valid for AJV (which uses a schema registry),
|
|
35
|
+
* but LLM providers like OpenAI require a single self-contained schema where all
|
|
36
|
+
* $ref values point to #/$defs/... entries at the top level.
|
|
37
|
+
*/
|
|
38
|
+
private resolveSchemaRefs;
|
|
29
39
|
/**
|
|
30
40
|
* Get tool result for AI consumption
|
|
31
41
|
* Formats ToolResult into string for AI context
|
|
@@ -51,6 +51,7 @@ var ToolExecutor = /** @class */ (function () {
|
|
|
51
51
|
this.toolFn = toolFn;
|
|
52
52
|
this.ctx = ctx;
|
|
53
53
|
this.autoSchemas = autoSchemas;
|
|
54
|
+
this.allSchemas = allSchemas;
|
|
54
55
|
this.ajv = new ajv_1.default({ allErrors: true });
|
|
55
56
|
// Pre-populate AJV with all schemas so $ref references resolve across schema boundaries
|
|
56
57
|
if (allSchemas) {
|
|
@@ -263,7 +264,7 @@ var ToolExecutor = /** @class */ (function () {
|
|
|
263
264
|
return {
|
|
264
265
|
name: this.toolProps.id,
|
|
265
266
|
description: this.toolProps.description,
|
|
266
|
-
inputSchema: this.autoSchemas.inputSchema,
|
|
267
|
+
inputSchema: this.resolveSchemaRefs(this.autoSchemas.inputSchema),
|
|
267
268
|
};
|
|
268
269
|
}
|
|
269
270
|
// No schema provided - return schema based on type hint
|
|
@@ -304,6 +305,68 @@ var ToolExecutor = /** @class */ (function () {
|
|
|
304
305
|
};
|
|
305
306
|
}
|
|
306
307
|
};
|
|
308
|
+
/**
|
|
309
|
+
* Resolve cross-schema $ref values into $defs so the schema is self-contained.
|
|
310
|
+
*
|
|
311
|
+
* Flink's schema manifest stores schemas as separate documents with IDs like
|
|
312
|
+
* "Canvas.ElementInput". These are valid for AJV (which uses a schema registry),
|
|
313
|
+
* but LLM providers like OpenAI require a single self-contained schema where all
|
|
314
|
+
* $ref values point to #/$defs/... entries at the top level.
|
|
315
|
+
*/
|
|
316
|
+
ToolExecutor.prototype.resolveSchemaRefs = function (schema) {
|
|
317
|
+
var _this = this;
|
|
318
|
+
if (!this.allSchemas)
|
|
319
|
+
return schema;
|
|
320
|
+
var defs = {};
|
|
321
|
+
var collectRefs = function (node, visited) {
|
|
322
|
+
if (!node || typeof node !== "object")
|
|
323
|
+
return;
|
|
324
|
+
if (node.$ref && typeof node.$ref === "string" && !node.$ref.startsWith("#")) {
|
|
325
|
+
var refId = node.$ref;
|
|
326
|
+
if (!visited.has(refId) && _this.allSchemas[refId]) {
|
|
327
|
+
visited.add(refId);
|
|
328
|
+
// Clone and strip top-level JSON Schema meta fields not valid inside $defs
|
|
329
|
+
var def = JSON.parse(JSON.stringify(_this.allSchemas[refId]));
|
|
330
|
+
delete def.$id;
|
|
331
|
+
delete def.$schema;
|
|
332
|
+
defs[refId] = def;
|
|
333
|
+
// Recurse into the referenced schema to collect its deps
|
|
334
|
+
collectRefs(def, visited);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
for (var _i = 0, _a = Object.values(node); _i < _a.length; _i++) {
|
|
338
|
+
var value = _a[_i];
|
|
339
|
+
collectRefs(value, visited);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
var visited = new Set();
|
|
343
|
+
collectRefs(schema, visited);
|
|
344
|
+
if (Object.keys(defs).length === 0)
|
|
345
|
+
return schema;
|
|
346
|
+
// Deep clone and rewrite all non-standard $ref values to #/$defs/<id>
|
|
347
|
+
var resolved = JSON.parse(JSON.stringify(schema));
|
|
348
|
+
delete resolved.$id;
|
|
349
|
+
delete resolved.$schema;
|
|
350
|
+
var rewriteRefs = function (node) {
|
|
351
|
+
if (!node || typeof node !== "object")
|
|
352
|
+
return;
|
|
353
|
+
if (node.$ref && typeof node.$ref === "string" && !node.$ref.startsWith("#")) {
|
|
354
|
+
node.$ref = "#/$defs/".concat(node.$ref);
|
|
355
|
+
}
|
|
356
|
+
for (var _i = 0, _a = Object.values(node); _i < _a.length; _i++) {
|
|
357
|
+
var value = _a[_i];
|
|
358
|
+
rewriteRefs(value);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
rewriteRefs(resolved);
|
|
362
|
+
// Also rewrite refs inside the collected defs
|
|
363
|
+
for (var _i = 0, _a = Object.values(defs); _i < _a.length; _i++) {
|
|
364
|
+
var def = _a[_i];
|
|
365
|
+
rewriteRefs(def);
|
|
366
|
+
}
|
|
367
|
+
resolved.$defs = defs;
|
|
368
|
+
return resolved;
|
|
369
|
+
};
|
|
307
370
|
/**
|
|
308
371
|
* Get tool result for AI consumption
|
|
309
372
|
* Formats ToolResult into string for AI context
|
package/package.json
CHANGED
|
@@ -276,6 +276,119 @@ describe("ToolExecutor", () => {
|
|
|
276
276
|
expect(schema.description).toBe("Get weather for a city");
|
|
277
277
|
expect(schema.inputSchema).toBeDefined();
|
|
278
278
|
});
|
|
279
|
+
|
|
280
|
+
describe("cross-schema $ref resolution", () => {
|
|
281
|
+
const allSchemas = {
|
|
282
|
+
"MyTool.Input": {
|
|
283
|
+
$id: "MyTool.Input",
|
|
284
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
285
|
+
type: "object",
|
|
286
|
+
properties: {
|
|
287
|
+
id: { type: "string" },
|
|
288
|
+
item: { $ref: "Shared.Item" },
|
|
289
|
+
},
|
|
290
|
+
required: ["id", "item"],
|
|
291
|
+
},
|
|
292
|
+
"Shared.Item": {
|
|
293
|
+
$id: "Shared.Item",
|
|
294
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
295
|
+
type: "object",
|
|
296
|
+
properties: {
|
|
297
|
+
name: { type: "string" },
|
|
298
|
+
tag: { $ref: "Shared.Tag" },
|
|
299
|
+
},
|
|
300
|
+
required: ["name"],
|
|
301
|
+
},
|
|
302
|
+
"Shared.Tag": {
|
|
303
|
+
$id: "Shared.Tag",
|
|
304
|
+
$schema: "http://json-schema.org/draft-07/schema#",
|
|
305
|
+
type: "string",
|
|
306
|
+
enum: ["foo", "bar"],
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
it("should resolve cross-schema $refs into $defs", () => {
|
|
311
|
+
const toolProps: FlinkToolProps = { id: "my-tool", description: "Test" };
|
|
312
|
+
const toolFn: FlinkTool<any> = async () => ({ success: true, data: {} });
|
|
313
|
+
const executor = new ToolExecutor(
|
|
314
|
+
toolProps,
|
|
315
|
+
toolFn,
|
|
316
|
+
mockCtx,
|
|
317
|
+
{ inputSchema: allSchemas["MyTool.Input"], inputTypeHint: "named" },
|
|
318
|
+
allSchemas
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const { inputSchema } = executor.getToolSchema();
|
|
322
|
+
|
|
323
|
+
// $ref values should use JSON pointer format
|
|
324
|
+
expect(inputSchema.properties.item.$ref).toBe("#/$defs/Shared.Item");
|
|
325
|
+
|
|
326
|
+
// All transitively referenced schemas should be in $defs
|
|
327
|
+
expect(inputSchema.$defs).toBeDefined();
|
|
328
|
+
expect(inputSchema.$defs["Shared.Item"]).toBeDefined();
|
|
329
|
+
expect(inputSchema.$defs["Shared.Tag"]).toBeDefined();
|
|
330
|
+
|
|
331
|
+
// $refs inside $defs should also be rewritten
|
|
332
|
+
expect(inputSchema.$defs["Shared.Item"].properties.tag.$ref).toBe("#/$defs/Shared.Tag");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should strip $id and $schema from inlined $defs", () => {
|
|
336
|
+
const toolProps: FlinkToolProps = { id: "my-tool", description: "Test" };
|
|
337
|
+
const toolFn: FlinkTool<any> = async () => ({ success: true, data: {} });
|
|
338
|
+
const executor = new ToolExecutor(
|
|
339
|
+
toolProps,
|
|
340
|
+
toolFn,
|
|
341
|
+
mockCtx,
|
|
342
|
+
{ inputSchema: allSchemas["MyTool.Input"], inputTypeHint: "named" },
|
|
343
|
+
allSchemas
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const { inputSchema } = executor.getToolSchema();
|
|
347
|
+
|
|
348
|
+
expect(inputSchema.$defs["Shared.Item"].$id).toBeUndefined();
|
|
349
|
+
expect(inputSchema.$defs["Shared.Item"].$schema).toBeUndefined();
|
|
350
|
+
expect(inputSchema.$defs["Shared.Tag"].$id).toBeUndefined();
|
|
351
|
+
expect(inputSchema.$defs["Shared.Tag"].$schema).toBeUndefined();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("should strip $id and $schema from root schema", () => {
|
|
355
|
+
const toolProps: FlinkToolProps = { id: "my-tool", description: "Test" };
|
|
356
|
+
const toolFn: FlinkTool<any> = async () => ({ success: true, data: {} });
|
|
357
|
+
const executor = new ToolExecutor(
|
|
358
|
+
toolProps,
|
|
359
|
+
toolFn,
|
|
360
|
+
mockCtx,
|
|
361
|
+
{ inputSchema: allSchemas["MyTool.Input"], inputTypeHint: "named" },
|
|
362
|
+
allSchemas
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const { inputSchema } = executor.getToolSchema();
|
|
366
|
+
|
|
367
|
+
expect(inputSchema.$id).toBeUndefined();
|
|
368
|
+
expect(inputSchema.$schema).toBeUndefined();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("should return schema unchanged when no cross-schema refs exist", () => {
|
|
372
|
+
const simpleSchema = {
|
|
373
|
+
$id: "Simple.Input",
|
|
374
|
+
type: "object",
|
|
375
|
+
properties: { name: { type: "string" } },
|
|
376
|
+
};
|
|
377
|
+
const toolProps: FlinkToolProps = { id: "simple-tool", description: "Test" };
|
|
378
|
+
const toolFn: FlinkTool<any> = async () => ({ success: true, data: {} });
|
|
379
|
+
const executor = new ToolExecutor(
|
|
380
|
+
toolProps,
|
|
381
|
+
toolFn,
|
|
382
|
+
mockCtx,
|
|
383
|
+
{ inputSchema: simpleSchema, inputTypeHint: "named" },
|
|
384
|
+
allSchemas
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
const { inputSchema } = executor.getToolSchema();
|
|
388
|
+
|
|
389
|
+
expect(inputSchema.$defs).toBeUndefined();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
279
392
|
});
|
|
280
393
|
|
|
281
394
|
describe("Result formatting for AI", () => {
|
package/src/ai/ToolExecutor.ts
CHANGED
|
@@ -23,7 +23,7 @@ export class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
23
23
|
inputTypeHint?: 'void' | 'any' | 'named';
|
|
24
24
|
outputTypeHint?: 'void' | 'any' | 'named';
|
|
25
25
|
},
|
|
26
|
-
allSchemas?: Record<string, any>
|
|
26
|
+
private allSchemas?: Record<string, any>
|
|
27
27
|
) {
|
|
28
28
|
// Pre-populate AJV with all schemas so $ref references resolve across schema boundaries
|
|
29
29
|
if (allSchemas) {
|
|
@@ -238,7 +238,7 @@ export class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
238
238
|
return {
|
|
239
239
|
name: this.toolProps.id,
|
|
240
240
|
description: this.toolProps.description,
|
|
241
|
-
inputSchema: this.autoSchemas.inputSchema,
|
|
241
|
+
inputSchema: this.resolveSchemaRefs(this.autoSchemas.inputSchema),
|
|
242
242
|
};
|
|
243
243
|
}
|
|
244
244
|
|
|
@@ -280,6 +280,73 @@ export class ToolExecutor<Ctx extends FlinkContext> {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Resolve cross-schema $ref values into $defs so the schema is self-contained.
|
|
285
|
+
*
|
|
286
|
+
* Flink's schema manifest stores schemas as separate documents with IDs like
|
|
287
|
+
* "Canvas.ElementInput". These are valid for AJV (which uses a schema registry),
|
|
288
|
+
* but LLM providers like OpenAI require a single self-contained schema where all
|
|
289
|
+
* $ref values point to #/$defs/... entries at the top level.
|
|
290
|
+
*/
|
|
291
|
+
private resolveSchemaRefs(schema: any): any {
|
|
292
|
+
if (!this.allSchemas) return schema;
|
|
293
|
+
|
|
294
|
+
const defs: Record<string, any> = {};
|
|
295
|
+
|
|
296
|
+
const collectRefs = (node: any, visited: Set<string>): void => {
|
|
297
|
+
if (!node || typeof node !== "object") return;
|
|
298
|
+
|
|
299
|
+
if (node.$ref && typeof node.$ref === "string" && !node.$ref.startsWith("#")) {
|
|
300
|
+
const refId = node.$ref;
|
|
301
|
+
if (!visited.has(refId) && this.allSchemas![refId]) {
|
|
302
|
+
visited.add(refId);
|
|
303
|
+
// Clone and strip top-level JSON Schema meta fields not valid inside $defs
|
|
304
|
+
const def = JSON.parse(JSON.stringify(this.allSchemas![refId]));
|
|
305
|
+
delete def.$id;
|
|
306
|
+
delete def.$schema;
|
|
307
|
+
defs[refId] = def;
|
|
308
|
+
// Recurse into the referenced schema to collect its deps
|
|
309
|
+
collectRefs(def, visited);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const value of Object.values(node)) {
|
|
314
|
+
collectRefs(value, visited);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const visited = new Set<string>();
|
|
319
|
+
collectRefs(schema, visited);
|
|
320
|
+
|
|
321
|
+
if (Object.keys(defs).length === 0) return schema;
|
|
322
|
+
|
|
323
|
+
// Deep clone and rewrite all non-standard $ref values to #/$defs/<id>
|
|
324
|
+
const resolved = JSON.parse(JSON.stringify(schema));
|
|
325
|
+
delete resolved.$id;
|
|
326
|
+
delete resolved.$schema;
|
|
327
|
+
|
|
328
|
+
const rewriteRefs = (node: any): void => {
|
|
329
|
+
if (!node || typeof node !== "object") return;
|
|
330
|
+
|
|
331
|
+
if (node.$ref && typeof node.$ref === "string" && !node.$ref.startsWith("#")) {
|
|
332
|
+
node.$ref = `#/$defs/${node.$ref}`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
for (const value of Object.values(node)) {
|
|
336
|
+
rewriteRefs(value);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
rewriteRefs(resolved);
|
|
341
|
+
// Also rewrite refs inside the collected defs
|
|
342
|
+
for (const def of Object.values(defs)) {
|
|
343
|
+
rewriteRefs(def);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
resolved.$defs = defs;
|
|
347
|
+
return resolved;
|
|
348
|
+
}
|
|
349
|
+
|
|
283
350
|
/**
|
|
284
351
|
* Get tool result for AI consumption
|
|
285
352
|
* Formats ToolResult into string for AI context
|