Commit 0225519b by YeYuheng Committed by GitHub

fix(agent): normalize empty tool responses (#7171)

parent e4afd59e
...@@ -8,6 +8,13 @@ export const removeDatasetCiteText = (text: string, retainDatasetCite: boolean) ...@@ -8,6 +8,13 @@ export const removeDatasetCiteText = (text: string, retainDatasetCite: boolean)
.replace(/[\[]id[\]]\(CITE\)/g, ''); .replace(/[\[]id[\]]\(CITE\)/g, '');
}; };
/**
* 规范化会写入 LLM tool message 的工具响应。
* OpenAI 兼容接口通常不接受空 tool content;undefined 和空字符串统一兜底为 none。
*/
export const normalizeToolResponseContent = (response?: string) =>
response === '' || response === undefined ? 'none' : response;
export const getLLMSupportParams = (llm?: LLMModelItemType) => { export const getLLMSupportParams = (llm?: LLMModelItemType) => {
return { return {
vision: !!llm?.vision, vision: !!llm?.vision,
......
...@@ -35,4 +35,5 @@ export const SANDBOX_SYSTEM_PROMPT = `## 沙盒能力 ...@@ -35,4 +35,5 @@ export const SANDBOX_SYSTEM_PROMPT = `## 沙盒能力
- 使用 ${SANDBOX_EDIT_FILE_TOOL_NAME} 对已有文件做精确查找替换 - 使用 ${SANDBOX_EDIT_FILE_TOOL_NAME} 对已有文件做精确查找替换
- 使用 ${SANDBOX_SEARCH_TOOL_NAME} 搜索沙盒内的文件路径 - 使用 ${SANDBOX_SEARCH_TOOL_NAME} 搜索沙盒内的文件路径
- 生成的文件内容保存在当前工作区即可 - 生成的文件内容保存在当前工作区即可
- 若需要将生成的文件链接,可使用 ${SANDBOX_GET_FILE_URL_TOOL_NAME} 获取临时访问链接`; - 若需要把沙盒中生成的文件提供给用户下载,必须先使用 ${SANDBOX_GET_FILE_URL_TOOL_NAME} 获取临时访问链接
- 最终回复中不得直接输出 sandbox:/、/workspace/、/home/devbox/workspace/、/home/user/ 等沙盒内部路径`;
...@@ -19,6 +19,7 @@ import type { ...@@ -19,6 +19,7 @@ import type {
ChatCompletionToolMessageParam ChatCompletionToolMessageParam
} from '../ai/llm/type'; } from '../ai/llm/type';
import { ChatCompletionRequestMessageRoleEnum } from '../../core/ai/constants'; import { ChatCompletionRequestMessageRoleEnum } from '../../core/ai/constants';
import { normalizeToolResponseContent } from '../ai/llm/utils';
type FileUrlChatFileType = ChatFileTypeEnum.file | ChatFileTypeEnum.audio | ChatFileTypeEnum.video; type FileUrlChatFileType = ChatFileTypeEnum.file | ChatFileTypeEnum.audio | ChatFileTypeEnum.video;
type FileUrlContentPart = Extract<ChatCompletionContentPart, { type: 'file_url' }>; type FileUrlContentPart = Extract<ChatCompletionContentPart, { type: 'file_url' }>;
...@@ -315,7 +316,9 @@ export const chats2GPTMessages = ({ ...@@ -315,7 +316,9 @@ export const chats2GPTMessages = ({
toolResponse: { toolResponse: {
tool_call_id: id, tool_call_id: id,
role: ChatCompletionRequestMessageRoleEnum.Tool, role: ChatCompletionRequestMessageRoleEnum.Tool,
content: typeof tool.response === 'string' ? tool.response : '' content: normalizeToolResponseContent(
typeof tool.response === 'string' ? tool.response : undefined
)
} }
}; };
}; };
...@@ -451,7 +454,7 @@ export const chats2GPTMessages = ({ ...@@ -451,7 +454,7 @@ export const chats2GPTMessages = ({
dataId, dataId,
role: ChatCompletionRequestMessageRoleEnum.Tool, role: ChatCompletionRequestMessageRoleEnum.Tool,
tool_call_id: id, tool_call_id: id,
content: response content: normalizeToolResponseContent(response)
}); });
}; };
......
...@@ -1289,6 +1289,36 @@ describe('chats2GPTMessages', () => { ...@@ -1289,6 +1289,36 @@ describe('chats2GPTMessages', () => {
expect(result[1].role).toBe(ChatCompletionRequestMessageRoleEnum.Tool); expect(result[1].role).toBe(ChatCompletionRequestMessageRoleEnum.Tool);
}); });
it('should normalize empty runtime tool responses to none when reserveTool is true', () => {
const messages: ChatItemMiniType[] = [
{
obj: ChatRoleEnum.AI,
value: [
{
tools: [
{
id: 'call_empty',
toolName: 'Search',
toolAvatar: '',
functionName: 'search_web',
params: '{"query":"empty"}',
response: ''
}
]
}
]
}
];
const result = chats2GPTMessages({ messages, reserveId: false, reserveTool: true });
expect(result[1]).toMatchObject({
role: ChatCompletionRequestMessageRoleEnum.Tool,
tool_call_id: 'call_empty',
content: 'none'
});
});
it('should handle deprecated single tool when reserveTool is true', () => { it('should handle deprecated single tool when reserveTool is true', () => {
const messages: ChatItemMiniType[] = [ const messages: ChatItemMiniType[] = [
{ {
...@@ -1316,6 +1346,34 @@ describe('chats2GPTMessages', () => { ...@@ -1316,6 +1346,34 @@ describe('chats2GPTMessages', () => {
expect(result[1].role).toBe(ChatCompletionRequestMessageRoleEnum.Tool); expect(result[1].role).toBe(ChatCompletionRequestMessageRoleEnum.Tool);
}); });
it('should normalize empty deprecated single tool responses to none when reserveTool is true', () => {
const messages: ChatItemMiniType[] = [
{
obj: ChatRoleEnum.AI,
value: [
{
tool: {
id: 'call_legacy_empty',
toolName: 'Search',
toolAvatar: '',
functionName: 'search_web',
params: '{"query":"empty"}',
response: ''
}
}
]
}
];
const result = chats2GPTMessages({ messages, reserveId: false, reserveTool: true });
expect(result[1]).toMatchObject({
role: ChatCompletionRequestMessageRoleEnum.Tool,
tool_call_id: 'call_legacy_empty',
content: 'none'
});
});
it('should filter invalid historical tool records when reserveTool is true', () => { it('should filter invalid historical tool records when reserveTool is true', () => {
const messages: ChatItemMiniType[] = [ const messages: ChatItemMiniType[] = [
{ {
...@@ -2151,6 +2209,48 @@ describe('chats2GPTMessages', () => { ...@@ -2151,6 +2209,48 @@ describe('chats2GPTMessages', () => {
]); ]);
}); });
it('should normalize empty agent plan tool response to none when reserving tools', () => {
const messages: ChatItemMiniType[] = [
{
obj: ChatRoleEnum.AI,
value: [
{
agentPlanUpdate: {
id: 'call_plan_empty',
functionName: 'update_plan',
params: '{"updates":[]}',
response: '',
assistantText: 'updating plan'
}
}
]
}
];
expect(chats2GPTMessages({ messages, reserveId: false, reserveTool: true })).toEqual([
{
role: ChatCompletionRequestMessageRoleEnum.Assistant,
content: 'updating plan',
tool_calls: [
{
id: 'call_plan_empty',
type: 'function',
function: {
name: 'update_plan',
arguments: '{"updates":[]}'
}
}
]
},
{
dataId: undefined,
role: ChatCompletionRequestMessageRoleEnum.Tool,
tool_call_id: 'call_plan_empty',
content: 'none'
}
]);
});
it('should restore ask_agent tool response from the matching interactive answer', () => { it('should restore ask_agent tool response from the matching interactive answer', () => {
const messages: ChatItemMiniType[] = [ const messages: ChatItemMiniType[] = [
{ {
......
...@@ -10,4 +10,4 @@ export * from './plan/updateTool'; ...@@ -10,4 +10,4 @@ export * from './plan/updateTool';
export * from './stop'; export * from './stop';
export * from './tools'; export * from './tools';
export * from './loop/unified'; export * from './loop/unified';
export * from './utils'; export { normalizeToolResponseContent } from '@fastgpt/global/core/ai/llm/utils';
...@@ -23,7 +23,7 @@ import type { ...@@ -23,7 +23,7 @@ import type {
import { AgentUsageModuleName } from '../constants'; import { AgentUsageModuleName } from '../constants';
import { getErrText } from '@fastgpt/global/common/error/utils'; import { getErrText } from '@fastgpt/global/common/error/utils';
import { batchRun } from '@fastgpt/global/common/system/utils'; import { batchRun } from '@fastgpt/global/common/system/utils';
import { normalizeToolResponseContent } from '../utils'; import { normalizeToolResponseContent } from '@fastgpt/global/core/ai/llm/utils';
type RunAgentCallProps<TChildrenResponse = unknown> = { type RunAgentCallProps<TChildrenResponse = unknown> = {
maxRunAgentTimes: number; maxRunAgentTimes: number;
......
...@@ -11,7 +11,7 @@ import { parsePlanAskToolCall } from '../plan/parser'; ...@@ -11,7 +11,7 @@ import { parsePlanAskToolCall } from '../plan/parser';
import { applyPlanUpdate } from '../plan/state'; import { applyPlanUpdate } from '../plan/state';
import { runStopGate } from '../stop'; import { runStopGate } from '../stop';
import { getToolsForUnifiedLoop, normalizeToolCatalog } from '../tools'; import { getToolsForUnifiedLoop, normalizeToolCatalog } from '../tools';
import { normalizeToolResponseContent } from '../utils'; import { normalizeToolResponseContent } from '@fastgpt/global/core/ai/llm/utils';
import type { import type {
AgentLoopRuntime, AgentLoopRuntime,
AgentLoopToolExecutionResult, AgentLoopToolExecutionResult,
......
/**
* 规范化会写入 LLM tool message 的工具响应。
* OpenAI 兼容接口通常不接受空 tool content;undefined 和空字符串统一兜底为 none。
*/
export const normalizeToolResponseContent = (response?: string) =>
response === '' || response === undefined ? 'none' : response;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment