From fb1ec359057f69324f1f5ae94bc4ba5a31899885 Mon Sep 17 00:00:00 2001 From: Lars Grammel Date: Thu, 19 Sep 2024 16:32:26 +0200 Subject: [PATCH 1/5] mv parse tool call into sep file --- .../ai/core/generate-text/generate-text.ts | 5 +- .../ai/core/generate-text/parse-tool-call.ts | 53 +++++++++++++++++++ .../generate-text/run-tools-transformation.ts | 5 +- packages/ai/core/generate-text/tool-call.ts | 50 ----------------- 4 files changed, 59 insertions(+), 54 deletions(-) create mode 100644 packages/ai/core/generate-text/parse-tool-call.ts diff --git a/packages/ai/core/generate-text/generate-text.ts b/packages/ai/core/generate-text/generate-text.ts index d11a61fc6c6..fb2ffc1686e 100644 --- a/packages/ai/core/generate-text/generate-text.ts +++ b/packages/ai/core/generate-text/generate-text.ts @@ -25,10 +25,11 @@ import { calculateLanguageModelUsage, } from '../types/usage'; import { GenerateTextResult } from './generate-text-result'; +import { parseToolCall } from './parse-tool-call'; +import { StepResult } from './step-result'; import { toResponseMessages } from './to-response-messages'; -import { ToToolCallArray, parseToolCall } from './tool-call'; +import { ToToolCallArray } from './tool-call'; import { ToToolResultArray } from './tool-result'; -import { StepResult } from './step-result'; const originalGenerateId = createIdGenerator({ prefix: 'aitxt-', size: 24 }); diff --git a/packages/ai/core/generate-text/parse-tool-call.ts b/packages/ai/core/generate-text/parse-tool-call.ts new file mode 100644 index 00000000000..67baaa0e20d --- /dev/null +++ b/packages/ai/core/generate-text/parse-tool-call.ts @@ -0,0 +1,53 @@ +import { LanguageModelV1FunctionToolCall } from '@ai-sdk/provider'; +import { safeParseJSON } from '@ai-sdk/provider-utils'; +import { Schema, asSchema } from '@ai-sdk/ui-utils'; +import { InvalidToolArgumentsError } from '../../errors/invalid-tool-arguments-error'; +import { NoSuchToolError } from '../../errors/no-such-tool-error'; +import { CoreTool } from '../tool'; +import { inferParameters } from '../tool/tool'; +import { ToToolCall } from './tool-call'; + +export function parseToolCall>({ + toolCall, + tools, +}: { + toolCall: LanguageModelV1FunctionToolCall; + tools?: TOOLS; +}): ToToolCall { + const toolName = toolCall.toolName as keyof TOOLS & string; + + if (tools == null) { + throw new NoSuchToolError({ toolName: toolCall.toolName }); + } + + const tool = tools[toolName]; + + if (tool == null) { + throw new NoSuchToolError({ + toolName: toolCall.toolName, + availableTools: Object.keys(tools), + }); + } + + const parseResult = safeParseJSON({ + text: toolCall.args, + schema: asSchema(tool.parameters) as Schema< + inferParameters + >, + }); + + if (parseResult.success === false) { + throw new InvalidToolArgumentsError({ + toolName, + toolArgs: toolCall.args, + cause: parseResult.error, + }); + } + + return { + type: 'tool-call', + toolCallId: toolCall.toolCallId, + toolName, + args: parseResult.value, + }; +} diff --git a/packages/ai/core/generate-text/run-tools-transformation.ts b/packages/ai/core/generate-text/run-tools-transformation.ts index 67f4169c713..4a54e3f800f 100644 --- a/packages/ai/core/generate-text/run-tools-transformation.ts +++ b/packages/ai/core/generate-text/run-tools-transformation.ts @@ -8,13 +8,14 @@ import { selectTelemetryAttributes } from '../telemetry/select-telemetry-attribu import { TelemetrySettings } from '../telemetry/telemetry-settings'; import { CoreTool } from '../tool'; import { - LanguageModelUsage, FinishReason, + LanguageModelUsage, LogProbs, ProviderMetadata, } from '../types'; import { calculateLanguageModelUsage } from '../types/usage'; -import { parseToolCall, ToToolCall } from './tool-call'; +import { parseToolCall } from './parse-tool-call'; +import { ToToolCall } from './tool-call'; import { ToToolResult } from './tool-result'; export type SingleRequestTextStreamPart< diff --git a/packages/ai/core/generate-text/tool-call.ts b/packages/ai/core/generate-text/tool-call.ts index 24e86ab335e..2c5a3160a5e 100644 --- a/packages/ai/core/generate-text/tool-call.ts +++ b/packages/ai/core/generate-text/tool-call.ts @@ -1,8 +1,3 @@ -import { LanguageModelV1FunctionToolCall } from '@ai-sdk/provider'; -import { safeParseJSON } from '@ai-sdk/provider-utils'; -import { Schema, asSchema } from '@ai-sdk/ui-utils'; -import { InvalidToolArgumentsError } from '../../errors/invalid-tool-arguments-error'; -import { NoSuchToolError } from '../../errors/no-such-tool-error'; import { CoreTool } from '../tool'; import { inferParameters } from '../tool/tool'; import { ValueOf } from '../util/value-of'; @@ -41,48 +36,3 @@ export type ToToolCall> = ValueOf<{ export type ToToolCallArray> = Array< ToToolCall >; - -export function parseToolCall>({ - toolCall, - tools, -}: { - toolCall: LanguageModelV1FunctionToolCall; - tools?: TOOLS; -}): ToToolCall { - const toolName = toolCall.toolName as keyof TOOLS & string; - - if (tools == null) { - throw new NoSuchToolError({ toolName: toolCall.toolName }); - } - - const tool = tools[toolName]; - - if (tool == null) { - throw new NoSuchToolError({ - toolName: toolCall.toolName, - availableTools: Object.keys(tools), - }); - } - - const parseResult = safeParseJSON({ - text: toolCall.args, - schema: asSchema(tool.parameters) as Schema< - inferParameters - >, - }); - - if (parseResult.success === false) { - throw new InvalidToolArgumentsError({ - toolName, - toolArgs: toolCall.args, - cause: parseResult.error, - }); - } - - return { - type: 'tool-call', - toolCallId: toolCall.toolCallId, - toolName, - args: parseResult.value, - }; -} From c1e743be25cb2e40510758dfbd3ea35adf407d48 Mon Sep 17 00:00:00 2001 From: Lars Grammel Date: Thu, 19 Sep 2024 16:38:12 +0200 Subject: [PATCH 2/5] add test --- .../generate-text/parse-tool-call.test.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 packages/ai/core/generate-text/parse-tool-call.test.ts diff --git a/packages/ai/core/generate-text/parse-tool-call.test.ts b/packages/ai/core/generate-text/parse-tool-call.test.ts new file mode 100644 index 00000000000..36b75932b62 --- /dev/null +++ b/packages/ai/core/generate-text/parse-tool-call.test.ts @@ -0,0 +1,72 @@ +import { LanguageModelV1FunctionToolCall } from '@ai-sdk/provider'; +import { InvalidToolArgumentsError } from '../../errors/invalid-tool-arguments-error'; +import { NoSuchToolError } from '../../errors/no-such-tool-error'; +import { parseToolCall } from './parse-tool-call'; +import { tool } from '../tool'; +import { z } from 'zod'; + +const mockTools = { + testTool: tool({ + parameters: z.object({ + param1: z.string(), + param2: z.number(), + }), + }), +} as const; + +it('should successfully parse a valid tool call', () => { + const toolCall: LanguageModelV1FunctionToolCall = { + toolCallType: 'function', + toolName: 'testTool', + toolCallId: '123', + args: '{"param1": "test", "param2": 42}', + }; + + const result = parseToolCall({ toolCall, tools: mockTools }); + + expect(result).toEqual({ + type: 'tool-call', + toolCallId: '123', + toolName: 'testTool', + args: { param1: 'test', param2: 42 }, + }); +}); + +it('should throw NoSuchToolError when tools is null', () => { + const toolCall: LanguageModelV1FunctionToolCall = { + toolCallType: 'function', + toolName: 'testTool', + toolCallId: '123', + args: '{}', + }; + + expect(() => parseToolCall({ toolCall, tools: undefined })).toThrow( + NoSuchToolError, + ); +}); + +it('should throw NoSuchToolError when tool is not found', () => { + const toolCall: LanguageModelV1FunctionToolCall = { + toolCallType: 'function', + toolName: 'nonExistentTool', + toolCallId: '123', + args: '{}', + }; + + expect(() => parseToolCall({ toolCall, tools: mockTools })).toThrow( + NoSuchToolError, + ); +}); + +it('should throw InvalidToolArgumentsError when args are invalid', () => { + const toolCall: LanguageModelV1FunctionToolCall = { + toolCallType: 'function', + toolName: 'testTool', + toolCallId: '123', + args: '{"param1": "test"}', // Missing required param2 + }; + + expect(() => parseToolCall({ toolCall, tools: mockTools })).toThrow( + InvalidToolArgumentsError, + ); +}); From 66e55a8b2676058ba12268b3c99404b3e0fbd641 Mon Sep 17 00:00:00 2001 From: Lars Grammel Date: Thu, 19 Sep 2024 16:39:02 +0200 Subject: [PATCH 3/5] refactor --- .../generate-text/parse-tool-call.test.ts | 110 ++++++++++-------- 1 file changed, 63 insertions(+), 47 deletions(-) diff --git a/packages/ai/core/generate-text/parse-tool-call.test.ts b/packages/ai/core/generate-text/parse-tool-call.test.ts index 36b75932b62..2e854ced2a6 100644 --- a/packages/ai/core/generate-text/parse-tool-call.test.ts +++ b/packages/ai/core/generate-text/parse-tool-call.test.ts @@ -5,24 +5,23 @@ import { parseToolCall } from './parse-tool-call'; import { tool } from '../tool'; import { z } from 'zod'; -const mockTools = { - testTool: tool({ - parameters: z.object({ - param1: z.string(), - param2: z.number(), - }), - }), -} as const; - it('should successfully parse a valid tool call', () => { - const toolCall: LanguageModelV1FunctionToolCall = { - toolCallType: 'function', - toolName: 'testTool', - toolCallId: '123', - args: '{"param1": "test", "param2": 42}', - }; - - const result = parseToolCall({ toolCall, tools: mockTools }); + const result = parseToolCall({ + toolCall: { + toolCallType: 'function', + toolName: 'testTool', + toolCallId: '123', + args: '{"param1": "test", "param2": 42}', + }, + tools: { + testTool: tool({ + parameters: z.object({ + param1: z.string(), + param2: z.number(), + }), + }), + } as const, + }); expect(result).toEqual({ type: 'tool-call', @@ -33,40 +32,57 @@ it('should successfully parse a valid tool call', () => { }); it('should throw NoSuchToolError when tools is null', () => { - const toolCall: LanguageModelV1FunctionToolCall = { - toolCallType: 'function', - toolName: 'testTool', - toolCallId: '123', - args: '{}', - }; - - expect(() => parseToolCall({ toolCall, tools: undefined })).toThrow( - NoSuchToolError, - ); + expect(() => + parseToolCall({ + toolCall: { + toolCallType: 'function', + toolName: 'testTool', + toolCallId: '123', + args: '{}', + }, + tools: undefined, + }), + ).toThrow(NoSuchToolError); }); it('should throw NoSuchToolError when tool is not found', () => { - const toolCall: LanguageModelV1FunctionToolCall = { - toolCallType: 'function', - toolName: 'nonExistentTool', - toolCallId: '123', - args: '{}', - }; - - expect(() => parseToolCall({ toolCall, tools: mockTools })).toThrow( - NoSuchToolError, - ); + expect(() => + parseToolCall({ + toolCall: { + toolCallType: 'function', + toolName: 'nonExistentTool', + toolCallId: '123', + args: '{}', + }, + tools: { + testTool: tool({ + parameters: z.object({ + param1: z.string(), + param2: z.number(), + }), + }), + } as const, + }), + ).toThrow(NoSuchToolError); }); it('should throw InvalidToolArgumentsError when args are invalid', () => { - const toolCall: LanguageModelV1FunctionToolCall = { - toolCallType: 'function', - toolName: 'testTool', - toolCallId: '123', - args: '{"param1": "test"}', // Missing required param2 - }; - - expect(() => parseToolCall({ toolCall, tools: mockTools })).toThrow( - InvalidToolArgumentsError, - ); + expect(() => + parseToolCall({ + toolCall: { + toolCallType: 'function', + toolName: 'testTool', + toolCallId: '123', + args: '{"param1": "test"}', // Missing required param2 + }, + tools: { + testTool: tool({ + parameters: z.object({ + param1: z.string(), + param2: z.number(), + }), + }), + } as const, + }), + ).toThrow(InvalidToolArgumentsError); }); From 6b78d82073a580a9edae4c5dac726febbb8a548d Mon Sep 17 00:00:00 2001 From: Lars Grammel Date: Thu, 19 Sep 2024 16:39:18 +0200 Subject: [PATCH 4/5] refactor --- packages/ai/core/generate-text/parse-tool-call.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/ai/core/generate-text/parse-tool-call.test.ts b/packages/ai/core/generate-text/parse-tool-call.test.ts index 2e854ced2a6..3b6ac6e3297 100644 --- a/packages/ai/core/generate-text/parse-tool-call.test.ts +++ b/packages/ai/core/generate-text/parse-tool-call.test.ts @@ -1,9 +1,8 @@ -import { LanguageModelV1FunctionToolCall } from '@ai-sdk/provider'; +import { z } from 'zod'; import { InvalidToolArgumentsError } from '../../errors/invalid-tool-arguments-error'; import { NoSuchToolError } from '../../errors/no-such-tool-error'; -import { parseToolCall } from './parse-tool-call'; import { tool } from '../tool'; -import { z } from 'zod'; +import { parseToolCall } from './parse-tool-call'; it('should successfully parse a valid tool call', () => { const result = parseToolCall({ From 8a9dc76d7324525f57d6702ff3d84910e3a6aa47 Mon Sep 17 00:00:00 2001 From: Lars Grammel Date: Thu, 19 Sep 2024 16:47:45 +0200 Subject: [PATCH 5/5] fix (ai/core): support tool calls without arguments --- .changeset/popular-suits-jam.md | 5 ++++ .../generate-text/parse-tool-call.test.ts | 23 +++++++++++++++++++ .../ai/core/generate-text/parse-tool-call.ts | 18 +++++++++------ 3 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 .changeset/popular-suits-jam.md diff --git a/.changeset/popular-suits-jam.md b/.changeset/popular-suits-jam.md new file mode 100644 index 00000000000..7c99799d123 --- /dev/null +++ b/.changeset/popular-suits-jam.md @@ -0,0 +1,5 @@ +--- +'ai': patch +--- + +fix (ai/core): support tool calls without arguments diff --git a/packages/ai/core/generate-text/parse-tool-call.test.ts b/packages/ai/core/generate-text/parse-tool-call.test.ts index 3b6ac6e3297..ec45c7cb273 100644 --- a/packages/ai/core/generate-text/parse-tool-call.test.ts +++ b/packages/ai/core/generate-text/parse-tool-call.test.ts @@ -30,6 +30,29 @@ it('should successfully parse a valid tool call', () => { }); }); +it('should successfully process empty calls for tools that have no parameters', () => { + const result = parseToolCall({ + toolCall: { + toolCallType: 'function', + toolName: 'testTool', + toolCallId: '123', + args: '', + }, + tools: { + testTool: tool({ + parameters: z.object({}), + }), + } as const, + }); + + expect(result).toEqual({ + type: 'tool-call', + toolCallId: '123', + toolName: 'testTool', + args: {}, + }); +}); + it('should throw NoSuchToolError when tools is null', () => { expect(() => parseToolCall({ diff --git a/packages/ai/core/generate-text/parse-tool-call.ts b/packages/ai/core/generate-text/parse-tool-call.ts index 67baaa0e20d..ba4755364e9 100644 --- a/packages/ai/core/generate-text/parse-tool-call.ts +++ b/packages/ai/core/generate-text/parse-tool-call.ts @@ -1,5 +1,5 @@ import { LanguageModelV1FunctionToolCall } from '@ai-sdk/provider'; -import { safeParseJSON } from '@ai-sdk/provider-utils'; +import { safeParseJSON, safeValidateTypes } from '@ai-sdk/provider-utils'; import { Schema, asSchema } from '@ai-sdk/ui-utils'; import { InvalidToolArgumentsError } from '../../errors/invalid-tool-arguments-error'; import { NoSuchToolError } from '../../errors/no-such-tool-error'; @@ -29,12 +29,16 @@ export function parseToolCall>({ }); } - const parseResult = safeParseJSON({ - text: toolCall.args, - schema: asSchema(tool.parameters) as Schema< - inferParameters - >, - }); + const schema = asSchema(tool.parameters) as Schema< + inferParameters + >; + + // when the tool call has no arguments, we try passing an empty object to the schema + // (many LLMs generate empty strings for tool calls with no arguments) + const parseResult = + toolCall.args.trim() === '' + ? safeValidateTypes({ value: {}, schema }) + : safeParseJSON({ text: toolCall.args, schema }); if (parseResult.success === false) { throw new InvalidToolArgumentsError({