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)
.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) => {
return {
vision: !!llm?.vision,
......
......@@ -35,4 +35,5 @@ export const SANDBOX_SYSTEM_PROMPT = `## 沙盒能力
- 使用 ${SANDBOX_EDIT_FILE_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 {
ChatCompletionToolMessageParam
} from '../ai/llm/type';
import { ChatCompletionRequestMessageRoleEnum } from '../../core/ai/constants';
import { normalizeToolResponseContent } from '../ai/llm/utils';
type FileUrlChatFileType = ChatFileTypeEnum.file | ChatFileTypeEnum.audio | ChatFileTypeEnum.video;
type FileUrlContentPart = Extract<ChatCompletionContentPart, { type: 'file_url' }>;
......@@ -315,7 +316,9 @@ export const chats2GPTMessages = ({
toolResponse: {
tool_call_id: id,
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 = ({
dataId,
role: ChatCompletionRequestMessageRoleEnum.Tool,
tool_call_id: id,
content: response
content: normalizeToolResponseContent(response)
});
};
......
......@@ -1289,6 +1289,36 @@ describe('chats2GPTMessages', () => {
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', () => {
const messages: ChatItemMiniType[] = [
{
......@@ -1316,6 +1346,34 @@ describe('chats2GPTMessages', () => {
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', () => {
const messages: ChatItemMiniType[] = [
{
......@@ -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', () => {
const messages: ChatItemMiniType[] = [
{
......
......@@ -10,4 +10,4 @@ export * from './plan/updateTool';
export * from './stop';
export * from './tools';
export * from './loop/unified';
export * from './utils';
export { normalizeToolResponseContent } from '@fastgpt/global/core/ai/llm/utils';
......@@ -23,7 +23,7 @@ import type {
import { AgentUsageModuleName } from '../constants';
import { getErrText } from '@fastgpt/global/common/error/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> = {
maxRunAgentTimes: number;
......
......@@ -11,7 +11,7 @@ import { parsePlanAskToolCall } from '../plan/parser';
import { applyPlanUpdate } from '../plan/state';
import { runStopGate } from '../stop';
import { getToolsForUnifiedLoop, normalizeToolCatalog } from '../tools';
import { normalizeToolResponseContent } from '../utils';
import { normalizeToolResponseContent } from '@fastgpt/global/core/ai/llm/utils';
import type {
AgentLoopRuntime,
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