Commit 05254ce0 by Archer Committed by GitHub

perf: adapt messages (#6959)

parent 0f55590a
...@@ -15,3 +15,4 @@ description: 'FastGPT V4.14.21 更新说明' ...@@ -15,3 +15,4 @@ description: 'FastGPT V4.14.21 更新说明'
1. 工作流默认选中模型未回传表单值,导致看到的模型和实际运行模型不一致。 1. 工作流默认选中模型未回传表单值,导致看到的模型和实际运行模型不一致。
2. 工作流自动保存时,存在边丢失风险。 2. 工作流自动保存时,存在边丢失风险。
3. admin 修改系统通知弹窗时候报错。 3. admin 修改系统通知弹窗时候报错。
4. 工作流混用思考/非思考模型,可能出现独立 reason 字段上下文,导致模型调用报错。
...@@ -276,7 +276,7 @@ ...@@ -276,7 +276,7 @@
"content/self-host/upgrading/4-14/4149.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/4-14/4149.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-14/4149.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/4-14/4149.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/4-15/4150.mdx": "2026-05-20T17:52:26+08:00", "content/self-host/upgrading/4-15/4150.mdx": "2026-05-20T17:52:26+08:00",
"content/self-host/upgrading/4-15/41502.mdx": "2026-05-18T19:47:26+08:00", "content/self-host/upgrading/4-15/41502.mdx": "2026-05-21T16:33:20+08:00",
"content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/40.en.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/40.mdx": "2026-04-26T21:08:47+08:00",
"content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00", "content/self-host/upgrading/outdated/41.en.mdx": "2026-04-26T21:08:47+08:00",
......
import { ObjectIdSchema } from '../../../common/type/mongo'; import { ObjectIdSchema } from '../../../common/type/mongo';
import z from 'zod'; import z from 'zod';
import { ChatRoleEnum } from '../constants'; import { ChatRoleEnum } from '../constants';
import { UserChatItemSchema, SystemChatItemSchema, ToolModuleResponseItemSchema } from '../type'; import { UserChatItemSchema, SystemChatItemSchema } from '../type';
import { UserInputInteractiveSchema } from '../../workflow/template/system/interactive/type'; import { UserInputInteractiveSchema } from '../../workflow/template/system/interactive/type';
export enum HelperBotTypeEnum { export enum HelperBotTypeEnum {
...@@ -21,18 +21,27 @@ export const HelperBotChatSchema = z.object({ ...@@ -21,18 +21,27 @@ export const HelperBotChatSchema = z.object({
}); });
export type HelperBotChatType = z.infer<typeof HelperBotChatSchema>; export type HelperBotChatType = z.infer<typeof HelperBotChatSchema>;
// AI schema const AIChatContentValueSchema = z
export const AIChatItemValueItemSchema = z.union([ .object({
z.object({ text: z
text: z.object({ .object({
content: z.string() content: z.string()
}) })
}), .optional(),
z.object({ reasoning: z
reasoning: z.object({ .object({
content: z.string() content: z.string()
}) })
}), .optional(),
hideReason: z.boolean().optional()
})
.refine((value) => value.text || value.reasoning, {
message: 'HelperBot AI content value requires text or reasoning'
});
// AI schema
export const AIChatItemValueItemSchema = z.union([
AIChatContentValueSchema,
z.object({ z.object({
collectionForm: UserInputInteractiveSchema collectionForm: UserInputInteractiveSchema
}), }),
......
...@@ -225,6 +225,7 @@ export const AIChatItemValueSchema = z.object({ ...@@ -225,6 +225,7 @@ export const AIChatItemValueSchema = z.object({
agentStopGate: AgentLoopStopGateSchema.nullish(), agentStopGate: AgentLoopStopGateSchema.nullish(),
contextCheckpoint: ContextCheckpointValueSchema.nullish(), contextCheckpoint: ContextCheckpointValueSchema.nullish(),
tool: ToolModuleResponseItemSchema.nullish().meta({ deprecated: true }), tool: ToolModuleResponseItemSchema.nullish().meta({ deprecated: true }),
hideReason: z.boolean().optional(),
hideInUI: z.boolean().optional() hideInUI: z.boolean().optional()
}); });
......
...@@ -152,27 +152,25 @@ export const removeAIResponseCite = <T extends AIChatItemValueItemType[] | strin ...@@ -152,27 +152,25 @@ export const removeAIResponseCite = <T extends AIChatItemValueItemType[] | strin
return removeDatasetCiteText(value, false) as T; return removeDatasetCiteText(value, false) as T;
} }
return value.map<AIChatItemValueItemType>((item) => { return value.map<AIChatItemValueItemType>((item) => ({
if (item.text?.content) {
return {
...item, ...item,
...(item.text?.content
? {
text: { text: {
...item.text, ...item.text,
content: removeDatasetCiteText(item.text.content, false) content: removeDatasetCiteText(item.text.content, false)
} }
};
} }
if (item.reasoning?.content) { : {}),
return { ...(item.reasoning?.content
...item, ? {
reasoning: { reasoning: {
...item.reasoning, ...item.reasoning,
content: removeDatasetCiteText(item.reasoning.content, false) content: removeDatasetCiteText(item.reasoning.content, false)
} }
};
} }
return item; : {})
}) as T; })) as T;
}; };
export const removeEmptyUserInput = (input?: UserChatItemValueItemType[]) => { export const removeEmptyUserInput = (input?: UserChatItemValueItemType[]) => {
......
...@@ -115,6 +115,27 @@ describe('helperChats2GPTMessages', () => { ...@@ -115,6 +115,27 @@ describe('helperChats2GPTMessages', () => {
expect(result[0].content).toBe('Hello, how can I help?'); expect(result[0].content).toBe('Hello, how can I help?');
}); });
it('should convert merged AI reasoning and text value with visible text', () => {
const messages = [
{
obj: ChatRoleEnum.AI,
value: [
{
reasoning: { content: 'Hidden thinking' },
hideReason: true,
text: { content: 'Visible answer' }
}
]
}
] as HelperBotChatItemType[];
const result = helperChats2GPTMessages({ messages });
expect(result).toHaveLength(1);
expect(result[0].role).toBe(ChatCompletionRequestMessageRoleEnum.Assistant);
expect(result[0].content).toBe('Visible answer');
});
it('should concat multiple AI text values', () => { it('should concat multiple AI text values', () => {
const messages = [ const messages = [
{ {
......
...@@ -47,6 +47,22 @@ describe('AIChatItemValueItemSchema', () => { ...@@ -47,6 +47,22 @@ describe('AIChatItemValueItemSchema', () => {
expect(result.success).toBe(true); expect(result.success).toBe(true);
}); });
it('should validate merged text and hidden reasoning content', () => {
const result = AIChatItemValueItemSchema.safeParse({
reasoning: { content: 'Hidden thinking' },
hideReason: true,
text: { content: 'Visible answer' }
});
expect(result.success).toBe(true);
});
it('should reject content value without text or reasoning', () => {
const result = AIChatItemValueItemSchema.safeParse({
hideReason: true
});
expect(result.success).toBe(false);
});
it('should validate planHint', () => { it('should validate planHint', () => {
const result = AIChatItemValueItemSchema.safeParse({ const result = AIChatItemValueItemSchema.safeParse({
planHint: { type: 'generation' } planHint: { type: 'generation' }
......
...@@ -40,7 +40,13 @@ export const computedTemperature = ({ ...@@ -40,7 +40,13 @@ export const computedTemperature = ({
}; };
// LLM utils // LLM utils
// Parse <think></think> tags to think and answer - unstream response const normalizeFirstAnswerAfterReasoning = (answer: string) => answer.trimStart();
/**
* 从非流式模型结果中拆分 <think></think> 思考内容和最终回答。
* </think> 后面的前导空白只用于分隔 reasoning 与 answer,不作为正文保留,
* 避免 reasoning-only 输出被解析成 answerText="\n"。
*/
export const parseReasoningContent = (text: string): [string, string] => { export const parseReasoningContent = (text: string): [string, string] => {
const regex = /<think>([\s\S]*?)<\/think>/; const regex = /<think>([\s\S]*?)<\/think>/;
const match = text.match(regex); const match = text.match(regex);
...@@ -52,7 +58,9 @@ export const parseReasoningContent = (text: string): [string, string] => { ...@@ -52,7 +58,9 @@ export const parseReasoningContent = (text: string): [string, string] => {
const thinkContent = match[1].trim(); const thinkContent = match[1].trim();
// Add answer (remaining text after think tag) // Add answer (remaining text after think tag)
const answerContent = text.slice(match.index! + match[0].length); const answerContent = normalizeFirstAnswerAfterReasoning(
text.slice(match.index! + match[0].length)
);
return [thinkContent, answerContent]; return [thinkContent, answerContent];
}; };
...@@ -75,6 +83,29 @@ export const parseLLMStreamResponse = () => { ...@@ -75,6 +83,29 @@ export const parseLLMStreamResponse = () => {
let buffer_reasoningContent = ''; let buffer_reasoningContent = '';
let buffer_content = ''; let buffer_content = '';
let error: any = undefined; let error: any = undefined;
let shouldNormalizeFirstAnswer = false;
const normalizeContentBoundary = ({
reasoningContent,
content
}: {
reasoningContent: string;
content: string;
}) => {
if (reasoningContent) {
shouldNormalizeFirstAnswer = true;
}
if (!content || !shouldNormalizeFirstAnswer) return content;
const normalizedContent = normalizeFirstAnswerAfterReasoning(content);
if (normalizedContent) {
shouldNormalizeFirstAnswer = false;
}
return normalizedContent;
};
/* /*
parseThinkTag - 只控制是否主动解析 <think></think>,如果接口已经解析了,则不再解析。 parseThinkTag - 只控制是否主动解析 <think></think>,如果接口已经解析了,则不再解析。
...@@ -117,7 +148,7 @@ export const parseLLMStreamResponse = () => { ...@@ -117,7 +148,7 @@ export const parseLLMStreamResponse = () => {
const isStreamEnd = !!buffer_finishReason; const isStreamEnd = !!buffer_finishReason;
// Parse think // Parse think
const { reasoningContent: parsedThinkReasoningContent, content: parsedThinkContent } = const { reasoningContent: parsedThinkReasoningContent, content: rawParsedThinkContent } =
(() => { (() => {
if (reasoningContent || !parseThinkTag) { if (reasoningContent || !parseThinkTag) {
isInThinkTag = false; isInThinkTag = false;
...@@ -234,6 +265,11 @@ export const parseLLMStreamResponse = () => { ...@@ -234,6 +265,11 @@ export const parseLLMStreamResponse = () => {
}; };
})(); })();
const parsedThinkContent = normalizeContentBoundary({
reasoningContent: parsedThinkReasoningContent,
content: rawParsedThinkContent
});
// Parse datset cite // Parse datset cite
if (retainDatasetCite) { if (retainDatasetCite) {
return { return {
......
...@@ -20,15 +20,14 @@ export const formatAIResponse = ({ ...@@ -20,15 +20,14 @@ export const formatAIResponse = ({
}): AIChatItemValueItemType[] => { }): AIChatItemValueItemType[] => {
const result: AIChatItemValueItemType[] = []; const result: AIChatItemValueItemType[] = [];
if (reasoning) {
result.push({ result.push({
...(reasoning
? {
reasoning: { reasoning: {
content: reasoning content: reasoning
} }
});
} }
: {}),
result.push({
text: { text: {
content: text content: text
} }
......
...@@ -192,6 +192,10 @@ export const createWorkflowAgentLoopEventMapper = ({ ...@@ -192,6 +192,10 @@ export const createWorkflowAgentLoopEventMapper = ({
assistantText?: string; assistantText?: string;
reasoningText?: string; reasoningText?: string;
}) => { }) => {
// 没有可见 assistantText 时,不在 request end 阶段回填 reasoning 到已有 tool。
// reasoning 的归属需要按流式顺序附着到后续 content/tool,避免误挂到上一轮工具。
if (!assistantText) return;
const runtimeToolCalls = toolCalls.filter((call) => { const runtimeToolCalls = toolCalls.filter((call) => {
const functionName = call.function.name; const functionName = call.function.name;
return ( return (
...@@ -201,21 +205,22 @@ export const createWorkflowAgentLoopEventMapper = ({ ...@@ -201,21 +205,22 @@ export const createWorkflowAgentLoopEventMapper = ({
!isInternalTool(functionName, internalToolNames) !isInternalTool(functionName, internalToolNames)
); );
}); });
if (!runtimeToolCalls.length || (!assistantText && !reasoningText)) return; if (!runtimeToolCalls.length) return;
const runtimeToolCallIds = new Set(runtimeToolCalls.map((call) => call.id)); const runtimeToolCallIds = new Set(runtimeToolCalls.map((call) => call.id));
const existingIndexes = assistantResponses const existingIndex = assistantResponses.findIndex((item) =>
.map((item, index) => item.tools?.some((tool) => runtimeToolCallIds.has(tool.id))
item.tools?.some((tool) => runtimeToolCallIds.has(tool.id)) ? index : -1 );
) const insertIndex = existingIndex >= 0 ? existingIndex : assistantResponses.length;
.filter((index) => index >= 0);
const insertIndex = existingIndexes.length
? Math.min(...existingIndexes)
: assistantResponses.length;
const assistantValue: AIChatItemValueItemType = { const assistantValue: AIChatItemValueItemType = {
...(assistantText ? { text: { content: assistantText } } : {}), text: { content: assistantText },
...(reasoningText ? { reasoning: { content: reasoningText } } : {}) ...(reasoningText
? {
reasoning: { content: reasoningText },
...(!showReasoning ? { hideReason: true } : {})
}
: {})
}; };
assistantResponses.splice(insertIndex, 0, assistantValue); assistantResponses.splice(insertIndex, 0, assistantValue);
}; };
......
...@@ -332,26 +332,21 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise ...@@ -332,26 +332,21 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
: result.status === 'aborted' : result.status === 'aborted'
? i18nT('chat:completion_finish_error') ? i18nT('chat:completion_finish_error')
: undefined; : undefined;
const reasoningValue = result.reasoningText
if (result.reasoningText) { ? {
assistantResponses.push({
reasoning: { reasoning: {
content: result.reasoningText content: result.reasoningText
}, },
...(aiChatReasoning === false ? { hideInUI: true } : {}) ...(aiChatReasoning === false ? { hideReason: true } : {})
});
} }
: {};
const finalText = result.answerText || errorText;
if (result.answerText) { if (finalText) {
assistantResponses.push({
text: {
content: result.answerText
}
});
} else if (errorText) {
assistantResponses.push({ assistantResponses.push({
...reasoningValue,
text: { text: {
content: errorText content: finalText
} }
}); });
} }
......
...@@ -80,19 +80,17 @@ export const dispatchPiAgent = async (props: DispatchAgentModuleProps): Promise< ...@@ -80,19 +80,17 @@ export const dispatchPiAgent = async (props: DispatchAgentModuleProps): Promise<
const appendFinalAssistantResponses = () => { const appendFinalAssistantResponses = () => {
const reasoningText = piRuntime?.getReasoningText() || ''; const reasoningText = piRuntime?.getReasoningText() || '';
const answerText = piRuntime?.getAnswerText() || ''; const answerText = piRuntime?.getAnswerText() || '';
const showReasoning = aiChatReasoning !== false;
if (reasoningText) { if (answerText) {
assistantResponses.push({ assistantResponses.push({
...(reasoningText
? {
reasoning: { reasoning: {
content: reasoningText content: reasoningText
}, },
...(!showReasoning ? { hideInUI: true } : {}) ...(aiChatReasoning === false ? { hideReason: true } : {})
});
} }
: {}),
if (answerText) {
assistantResponses.push({
text: { text: {
content: answerText content: answerText
} }
......
...@@ -146,7 +146,7 @@ const completions = async ({ ...@@ -146,7 +146,7 @@ const completions = async ({
body: { body: {
model: cqModel.model, model: cqModel.model,
temperature: 0.01, temperature: 0.01,
messages: chats2GPTMessages({ messages, reserveId: false }), messages: chats2GPTMessages({ messages, reserveId: false, reserveReason: false }),
stream: true stream: true
}, },
userKey: externalProvider.openaiAccount userKey: externalProvider.openaiAccount
......
...@@ -209,7 +209,11 @@ const toolChoice = async (props: ActionProps) => { ...@@ -209,7 +209,11 @@ const toolChoice = async (props: ActionProps) => {
] ]
} }
]; ];
const adaptMessages = chats2GPTMessages({ messages, reserveId: false }); const adaptMessages = chats2GPTMessages({
messages,
reserveId: false,
reserveReason: false
});
const filterMessages = await filterGPTMessageByMaxContext({ const filterMessages = await filterGPTMessageByMaxContext({
messages: adaptMessages, messages: adaptMessages,
maxContext: extractModel.maxContext maxContext: extractModel.maxContext
...@@ -317,7 +321,7 @@ const completions = async (props: ActionProps) => { ...@@ -317,7 +321,7 @@ const completions = async (props: ActionProps) => {
body: { body: {
model: extractModel.model, model: extractModel.model,
temperature: 0.01, temperature: 0.01,
messages: chats2GPTMessages({ messages, reserveId: false }), messages: chats2GPTMessages({ messages, reserveId: false, reserveReason: false }),
stream: true stream: true
}, },
userKey: externalProvider.openaiAccount userKey: externalProvider.openaiAccount
......
...@@ -1212,15 +1212,16 @@ export class WorkflowQueue { ...@@ -1212,15 +1212,16 @@ export class WorkflowQueue {
if (assistantResponses) { if (assistantResponses) {
this.chatAssistantResponse = this.chatAssistantResponse.concat(assistantResponses); this.chatAssistantResponse = this.chatAssistantResponse.concat(assistantResponses);
} else { } else {
if (reasoningText) { // reasoning 不能独立落历史;只有存在可见文本时才附着保存。
if (answerText) {
this.chatAssistantResponse.push({ this.chatAssistantResponse.push({
...(reasoningText
? {
reasoning: { reasoning: {
content: reasoningText content: reasoningText
} }
});
} }
if (answerText) { : {}),
this.chatAssistantResponse.push({
text: { text: {
content: answerText content: answerText
} }
......
...@@ -85,6 +85,14 @@ describe('parseReasoningContent', () => { ...@@ -85,6 +85,14 @@ describe('parseReasoningContent', () => {
expect(parseReasoningContent('<think>reasoning</think>')).toEqual(['reasoning', '']); expect(parseReasoningContent('<think>reasoning</think>')).toEqual(['reasoning', '']);
}); });
it('should remove separator whitespace after think tag', () => {
expect(parseReasoningContent('<think>reasoning</think>\n')).toEqual(['reasoning', '']);
expect(parseReasoningContent('<think>reasoning</think>\n\nanswer')).toEqual([
'reasoning',
'answer'
]);
});
it('should handle multiline think content', () => { it('should handle multiline think content', () => {
expect(parseReasoningContent('<think>line1\nline2</think>answer')).toEqual([ expect(parseReasoningContent('<think>line1\nline2</think>answer')).toEqual([
'line1\nline2', 'line1\nline2',
...@@ -191,6 +199,22 @@ describe('parseLLMStreamResponse', () => { ...@@ -191,6 +199,22 @@ describe('parseLLMStreamResponse', () => {
correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' } correct: { answer: '你好1你好2你好3', reasoning: '这是思考过程' }
}, },
{ {
data: [{ content: '<think>这是' }, { content: '思考过程</think>\n' }],
correct: { answer: '', reasoning: '这是思考过程' }
},
{
data: [{ content: '<think>这是' }, { content: '思考过程</think>\n' }, { content: '你好1' }],
correct: { answer: '你好1', reasoning: '这是思考过程' }
},
{
data: [{ content: '<think>这是' }, { content: '思考过程</think>\n\n你好1' }],
correct: { answer: '你好1', reasoning: '这是思考过程' }
},
{
data: [{ reasoning_content: '这是思考过程' }, { content: '\n' }, { content: '你好1' }],
correct: { answer: '你好1', reasoning: '这是思考过程' }
},
{
data: [ data: [
{ content: '<think>这是' }, { content: '<think>这是' },
{ content: '思考' }, { content: '思考' },
......
...@@ -97,6 +97,81 @@ describe('createWorkflowAgentLoopEventMapper', () => { ...@@ -97,6 +97,81 @@ describe('createWorkflowAgentLoopEventMapper', () => {
}); });
}); });
it('hides reasoning stream but persists reasoning with hideReason', () => {
const workflowStreamResponse = vi.fn();
const mapper = createWorkflowAgentLoopEventMapper({
workflowStreamResponse,
getSubAppInfo: (id) => ({
name: id,
avatar: '',
toolDescription: ''
}),
internalToolNames: new Set(),
showReasoning: false
});
mapper.emitEvent({
type: 'reasoning_delta',
text: 'hidden'
});
expect(workflowStreamResponse).not.toHaveBeenCalled();
mapper.emitEvent({
type: 'tool_call',
call: {
id: 'call_search',
type: 'function',
function: {
name: 'search',
arguments: '{}'
}
}
});
mapper.emitEvent({
type: 'llm_request_end',
requestIndex: 1,
modelName: 'GPT-4',
requestId: 'req_search',
finishReason: 'tool_calls',
answerText: 'Need to search.',
reasoningText: 'hidden thinking',
toolCalls: [
{
id: 'call_search',
type: 'function',
function: {
name: 'search',
arguments: '{}'
}
}
]
});
expect(mapper.assistantResponses).toEqual([
{
text: {
content: 'Need to search.'
},
reasoning: {
content: 'hidden thinking'
},
hideReason: true
},
{
id: 'call_search',
tools: [
{
id: 'call_search',
toolName: 'search',
toolAvatar: '',
functionName: 'search',
params: '{}'
}
]
}
]);
});
it('filters internal tool calls and streams runtime tool lifecycle events', () => { it('filters internal tool calls and streams runtime tool lifecycle events', () => {
const workflowStreamResponse = vi.fn(); const workflowStreamResponse = vi.fn();
const mapper = createWorkflowAgentLoopEventMapper({ const mapper = createWorkflowAgentLoopEventMapper({
......
...@@ -228,4 +228,42 @@ describe('dispatchRunAgent user context', () => { ...@@ -228,4 +228,42 @@ describe('dispatchRunAgent user context', () => {
} }
]); ]);
}); });
it('keeps reasoning with hideReason when reasoning display is disabled', async () => {
const { dispatchRunAgent } = await import('@fastgpt/service/core/workflow/dispatch/ai/agent');
const props = createProps();
props.params.aiChatReasoning = false;
runUnifiedAgentLoopMock.mockResolvedValueOnce({
status: 'done',
answerText: 'ok',
reasoningText: 'hidden thinking',
completeMessages: [],
assistantMessages: [],
requestIds: []
});
let resultPromise: Promise<any>;
runWithContext(
{
queryUrlTypeMap: {},
mcpClientMemory: {}
},
() => {
resultPromise = dispatchRunAgent(props);
}
);
const result = await resultPromise!;
expect(result[DispatchNodeResponseKeyEnum.assistantResponses]).toEqual([
{
reasoning: {
content: 'hidden thinking'
},
hideReason: true,
text: {
content: 'ok'
}
}
]);
});
}); });
...@@ -265,4 +265,43 @@ describe('dispatchPiAgent user context', () => { ...@@ -265,4 +265,43 @@ describe('dispatchPiAgent user context', () => {
} }
]); ]);
}); });
it('keeps reasoning with hideReason when reasoning display is disabled', async () => {
const { dispatchPiAgent } =
await import('@fastgpt/service/core/workflow/dispatch/ai/agent/piAgent');
const props = createProps();
props.params.aiChatReasoning = false;
createPiAgentWorkflowRuntimeMock.mockReturnValueOnce({
onPayload: vi.fn(),
handleAgentEvent: vi.fn(),
appendChildNodeResponse: vi.fn(),
getReasoningText: vi.fn(() => 'hidden thinking'),
getAnswerText: vi.fn(() => 'pi answer'),
appendPendingAgentError: vi.fn()
});
let resultPromise: Promise<any>;
runWithContext(
{
queryUrlTypeMap: {},
mcpClientMemory: {}
},
() => {
resultPromise = dispatchPiAgent(props);
}
);
const result = await resultPromise!;
expect(result[DispatchNodeResponseKeyEnum.assistantResponses]).toEqual([
{
reasoning: {
content: 'hidden thinking'
},
hideReason: true,
text: {
content: 'pi answer'
}
}
]);
});
}); });
...@@ -456,17 +456,24 @@ const ChatBox = ({ ...@@ -456,17 +456,24 @@ const ChatBox = ({
}; };
} }
if (event === SseResponseEventEnum.answer || event === SseResponseEventEnum.fastAnswer) { if (event === SseResponseEventEnum.answer || event === SseResponseEventEnum.fastAnswer) {
if (reasoningText) { const replaceUpdateValue = (nextValue: AIChatItemValueItemType) => ({
if (updateValue?.reasoning) {
updateValue.reasoning.content += reasoningText;
return {
...item, ...item,
value: [ value: [
...item.value.slice(0, updateIndex), ...item.value.slice(0, updateIndex),
updateValue, nextValue,
...item.value.slice(updateIndex + 1) ...item.value.slice(updateIndex + 1)
] ]
});
if (reasoningText) {
if (updateValue?.reasoning) {
updateValue.reasoning.content += reasoningText;
return replaceUpdateValue(updateValue);
} else if (updateValue?.text && !updateValue.text.content) {
updateValue.reasoning = {
content: reasoningText
}; };
return replaceUpdateValue(updateValue);
} else { } else {
const val: AIChatItemValueItemType = { const val: AIChatItemValueItemType = {
id: responseValueId, id: responseValueId,
...@@ -483,14 +490,12 @@ const ChatBox = ({ ...@@ -483,14 +490,12 @@ const ChatBox = ({
if (text) { if (text) {
if (updateValue?.text) { if (updateValue?.text) {
updateValue.text.content += text; updateValue.text.content += text;
return { return replaceUpdateValue(updateValue);
...item, } else if (updateValue?.reasoning) {
value: [ updateValue.text = {
...item.value.slice(0, updateIndex), content: text
updateValue,
...item.value.slice(updateIndex + 1)
]
}; };
return replaceUpdateValue(updateValue);
} else { } else {
const newValue: AIChatItemValueItemType = { const newValue: AIChatItemValueItemType = {
id: responseValueId, id: responseValueId,
......
...@@ -173,6 +173,14 @@ const AIItem = ({ ...@@ -173,6 +173,14 @@ const AIItem = ({
onSubmitCollectionForm: (formData: string) => void; onSubmitCollectionForm: (formData: string) => void;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const firstValue = chat.value[0];
const isWaitingForResponse =
chat.value.length === 1 &&
!(
('text' in firstValue && firstValue.text?.content) ||
('reasoning' in firstValue && firstValue.reasoning && !firstValue.hideReason)
);
return ( return (
<Box <Box
_hover={{ _hover={{
...@@ -192,8 +200,7 @@ const AIItem = ({ ...@@ -192,8 +200,7 @@ const AIItem = ({
color={'myGray.900'} color={'myGray.900'}
bg={'myGray.100'} bg={'myGray.100'}
> >
{chat.value.length === 1 && {isWaitingForResponse ? (
(!('text' in chat.value[0]) || !chat.value[0].text?.content) ? (
<RenderText showAnimation={true} text={t('chat:chat.waiting_for_response')} /> <RenderText showAnimation={true} text={t('chat:chat.waiting_for_response')} />
) : ( ) : (
<> <>
...@@ -203,35 +210,34 @@ const AIItem = ({ ...@@ -203,35 +210,34 @@ const AIItem = ({
<RenderText key={i} showAnimation={false} text={t('chat:plan_check_tip')} /> <RenderText key={i} showAnimation={false} text={t('chat:plan_check_tip')} />
); );
} }
if ('text' in value && value.text) { if ('collectionForm' in value && value.collectionForm) {
return ( return (
<RenderText <RenderCollectionForm
key={i} key={i}
showAnimation={isChatting && isLastChild} isLastValue={isLastChild && i === chat.value.length - 1}
text={value.text.content} collectionForm={value.collectionForm}
onSubmit={onSubmitCollectionForm}
/> />
); );
} }
if ('reasoning' in value && value.reasoning) {
return ( return (
<React.Fragment key={i}>
{'reasoning' in value && value.reasoning && !value.hideReason && (
<RenderResoningContent <RenderResoningContent
key={i}
isChatting={isChatting} isChatting={isChatting}
isLastResponseValue={isLastChild} isLastResponseValue={isLastChild}
content={value.reasoning.content} content={value.reasoning.content}
/> />
); )}
} {'text' in value && value.text && (
if ('collectionForm' in value && value.collectionForm) { <RenderText
return ( showAnimation={isChatting && isLastChild}
<RenderCollectionForm text={value.text.content}
key={i}
isLastValue={isLastChild && i === chat.value.length - 1}
collectionForm={value.collectionForm}
onSubmit={onSubmitCollectionForm}
/> />
)}
</React.Fragment>
); );
}
})} })}
</> </>
)} )}
......
...@@ -180,17 +180,31 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro ...@@ -180,17 +180,31 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro
} }
if (event === SseResponseEventEnum.answer || event === SseResponseEventEnum.fastAnswer) { if (event === SseResponseEventEnum.answer || event === SseResponseEventEnum.fastAnswer) {
if (reasoningText) { const replaceUpdateValue = (nextValue: AIChatItemValueItemType) => ({
if ('reasoning' in updateValue && updateValue.reasoning) {
updateValue.reasoning.content += reasoningText;
return {
...item, ...item,
value: [ value: [
...item.value.slice(0, updateIndex), ...item.value.slice(0, updateIndex),
updateValue, nextValue,
...item.value.slice(updateIndex + 1) ...item.value.slice(updateIndex + 1)
] ]
}; });
if (reasoningText) {
if ('reasoning' in updateValue && updateValue.reasoning) {
return replaceUpdateValue({
...updateValue,
reasoning: {
...updateValue.reasoning,
content: updateValue.reasoning.content + reasoningText
}
});
} else if ('text' in updateValue && updateValue.text && !updateValue.text.content) {
return replaceUpdateValue({
...updateValue,
reasoning: {
content: reasoningText
}
});
} else { } else {
const val: AIChatItemValueItemType = { const val: AIChatItemValueItemType = {
reasoning: { reasoning: {
...@@ -205,15 +219,20 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro ...@@ -205,15 +219,20 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro
} }
if (text) { if (text) {
if ('text' in updateValue && updateValue.text) { if ('text' in updateValue && updateValue.text) {
updateValue.text.content += text; return replaceUpdateValue({
return { ...updateValue,
...item, text: {
value: [ ...updateValue.text,
...item.value.slice(0, updateIndex), content: updateValue.text.content + text
updateValue, }
...item.value.slice(updateIndex + 1) });
] } else if ('reasoning' in updateValue && updateValue.reasoning) {
}; return replaceUpdateValue({
...updateValue,
text: {
content: text
}
});
} else { } else {
const newValue: AIChatItemValueItemType = { const newValue: AIChatItemValueItemType = {
text: { text: {
...@@ -258,7 +277,7 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro ...@@ -258,7 +277,7 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro
} }
const chatItemDataId = getNanoid(24); const chatItemDataId = getNanoid(24);
let newChatList: HelperBotChatItemSiteType[] = [ const newChatList: HelperBotChatItemSiteType[] = [
...chatRecords, ...chatRecords,
// 用户消息 // 用户消息
{ {
......
...@@ -43,7 +43,7 @@ const AIResponseBox = ({ ...@@ -43,7 +43,7 @@ const AIResponseBox = ({
const responseBlocks: React.ReactNode[] = []; const responseBlocks: React.ReactNode[] = [];
if ('reasoning' in value && value.reasoning) { if ('reasoning' in value && value.reasoning && !value.hideReason) {
responseBlocks.push( responseBlocks.push(
<RenderReasoningContent <RenderReasoningContent
key="reasoning" key="reasoning"
......
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