mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
feat: migrate tools to use parametersJsonSchema. (#5330)
This commit is contained in:
@@ -15,19 +15,9 @@ import {
|
||||
Mocked,
|
||||
} from 'vitest';
|
||||
import { Config, ConfigParameters, ApprovalMode } from '../config/config.js';
|
||||
import {
|
||||
ToolRegistry,
|
||||
DiscoveredTool,
|
||||
sanitizeParameters,
|
||||
} from './tool-registry.js';
|
||||
import { ToolRegistry, DiscoveredTool } from './tool-registry.js';
|
||||
import { DiscoveredMCPTool } from './mcp-tool.js';
|
||||
import {
|
||||
FunctionDeclaration,
|
||||
CallableTool,
|
||||
mcpToTool,
|
||||
Type,
|
||||
Schema,
|
||||
} from '@google/genai';
|
||||
import { FunctionDeclaration, CallableTool, mcpToTool } from '@google/genai';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
import fs from 'node:fs';
|
||||
@@ -254,18 +244,18 @@ describe('ToolRegistry', () => {
|
||||
});
|
||||
|
||||
describe('discoverTools', () => {
|
||||
it('should sanitize tool parameters during discovery from command', async () => {
|
||||
it('should will preserve tool parametersJsonSchema during discovery from command', async () => {
|
||||
const discoveryCommand = 'my-discovery-command';
|
||||
mockConfigGetToolDiscoveryCommand.mockReturnValue(discoveryCommand);
|
||||
|
||||
const unsanitizedToolDeclaration: FunctionDeclaration = {
|
||||
name: 'tool-with-bad-format',
|
||||
description: 'A tool with an invalid format property',
|
||||
parameters: {
|
||||
type: Type.OBJECT,
|
||||
parametersJsonSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
some_string: {
|
||||
type: Type.STRING,
|
||||
type: 'string',
|
||||
format: 'uuid', // This is an unsupported format
|
||||
},
|
||||
},
|
||||
@@ -308,12 +298,16 @@ describe('ToolRegistry', () => {
|
||||
expect(discoveredTool).toBeDefined();
|
||||
|
||||
const registeredParams = (discoveredTool as DiscoveredTool).schema
|
||||
.parameters as Schema;
|
||||
expect(registeredParams.properties?.['some_string']).toBeDefined();
|
||||
expect(registeredParams.properties?.['some_string']).toHaveProperty(
|
||||
'format',
|
||||
undefined,
|
||||
);
|
||||
.parametersJsonSchema;
|
||||
expect(registeredParams).toStrictEqual({
|
||||
type: 'object',
|
||||
properties: {
|
||||
some_string: {
|
||||
type: 'string',
|
||||
format: 'uuid',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should discover tools using MCP servers defined in getMcpServers', async () => {
|
||||
@@ -365,187 +359,3 @@ describe('ToolRegistry', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeParameters', () => {
|
||||
it('should remove default when anyOf is present', () => {
|
||||
const schema: Schema = {
|
||||
anyOf: [{ type: Type.STRING }, { type: Type.NUMBER }],
|
||||
default: 'hello',
|
||||
};
|
||||
sanitizeParameters(schema);
|
||||
expect(schema.default).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should recursively sanitize items in anyOf', () => {
|
||||
const schema: Schema = {
|
||||
anyOf: [
|
||||
{
|
||||
anyOf: [{ type: Type.STRING }],
|
||||
default: 'world',
|
||||
},
|
||||
{ type: Type.NUMBER },
|
||||
],
|
||||
};
|
||||
sanitizeParameters(schema);
|
||||
expect(schema.anyOf![0].default).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should recursively sanitize items in items', () => {
|
||||
const schema: Schema = {
|
||||
items: {
|
||||
anyOf: [{ type: Type.STRING }],
|
||||
default: 'world',
|
||||
},
|
||||
};
|
||||
sanitizeParameters(schema);
|
||||
expect(schema.items!.default).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should recursively sanitize items in properties', () => {
|
||||
const schema: Schema = {
|
||||
properties: {
|
||||
prop1: {
|
||||
anyOf: [{ type: Type.STRING }],
|
||||
default: 'world',
|
||||
},
|
||||
},
|
||||
};
|
||||
sanitizeParameters(schema);
|
||||
expect(schema.properties!.prop1.default).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle complex nested schemas', () => {
|
||||
const schema: Schema = {
|
||||
properties: {
|
||||
prop1: {
|
||||
items: {
|
||||
anyOf: [{ type: Type.STRING }],
|
||||
default: 'world',
|
||||
},
|
||||
},
|
||||
prop2: {
|
||||
anyOf: [
|
||||
{
|
||||
properties: {
|
||||
nestedProp: {
|
||||
anyOf: [{ type: Type.NUMBER }],
|
||||
default: 123,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
sanitizeParameters(schema);
|
||||
expect(schema.properties!.prop1.items!.default).toBeUndefined();
|
||||
const nestedProp =
|
||||
schema.properties!.prop2.anyOf![0].properties!.nestedProp;
|
||||
expect(nestedProp?.default).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should remove unsupported format from a simple string property', () => {
|
||||
const schema: Schema = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
name: { type: Type.STRING },
|
||||
id: { type: Type.STRING, format: 'uuid' },
|
||||
},
|
||||
};
|
||||
sanitizeParameters(schema);
|
||||
expect(schema.properties?.['id']).toHaveProperty('format', undefined);
|
||||
expect(schema.properties?.['name']).not.toHaveProperty('format');
|
||||
});
|
||||
|
||||
it('should NOT remove supported format values', () => {
|
||||
const schema: Schema = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
date: { type: Type.STRING, format: 'date-time' },
|
||||
role: {
|
||||
type: Type.STRING,
|
||||
format: 'enum',
|
||||
enum: ['admin', 'user'],
|
||||
},
|
||||
},
|
||||
};
|
||||
const originalSchema = JSON.parse(JSON.stringify(schema));
|
||||
sanitizeParameters(schema);
|
||||
expect(schema).toEqual(originalSchema);
|
||||
});
|
||||
|
||||
it('should handle arrays of objects', () => {
|
||||
const schema: Schema = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
items: {
|
||||
type: Type.ARRAY,
|
||||
items: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
itemId: { type: Type.STRING, format: 'uuid' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
sanitizeParameters(schema);
|
||||
expect(
|
||||
(schema.properties?.['items']?.items as Schema)?.properties?.['itemId'],
|
||||
).toHaveProperty('format', undefined);
|
||||
});
|
||||
|
||||
it('should handle schemas with no properties to sanitize', () => {
|
||||
const schema: Schema = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
count: { type: Type.NUMBER },
|
||||
isActive: { type: Type.BOOLEAN },
|
||||
},
|
||||
};
|
||||
const originalSchema = JSON.parse(JSON.stringify(schema));
|
||||
sanitizeParameters(schema);
|
||||
expect(schema).toEqual(originalSchema);
|
||||
});
|
||||
|
||||
it('should not crash on an empty or undefined schema', () => {
|
||||
expect(() => sanitizeParameters({})).not.toThrow();
|
||||
expect(() => sanitizeParameters(undefined)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle complex nested schemas with cycles', () => {
|
||||
const userNode: any = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
id: { type: Type.STRING, format: 'uuid' },
|
||||
name: { type: Type.STRING },
|
||||
manager: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
id: { type: Type.STRING, format: 'uuid' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
userNode.properties.reports = {
|
||||
type: Type.ARRAY,
|
||||
items: userNode,
|
||||
};
|
||||
|
||||
const schema: Schema = {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
ceo: userNode,
|
||||
},
|
||||
};
|
||||
|
||||
expect(() => sanitizeParameters(schema)).not.toThrow();
|
||||
expect(schema.properties?.['ceo']?.properties?.['id']).toHaveProperty(
|
||||
'format',
|
||||
undefined,
|
||||
);
|
||||
expect(
|
||||
schema.properties?.['ceo']?.properties?.['manager']?.properties?.['id'],
|
||||
).toHaveProperty('format', undefined);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user