Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
赵月辉
/
fastgpt-migrated
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
05254ce0
authored
May 21, 2026
by
Archer
Committed by
GitHub
May 21, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
perf: adapt messages (#6959)
parent
0f55590a
Hide whitespace changes
Inline
Side-by-side
Showing
25 changed files
with
1398 additions
and
257 deletions
+1398
-257
document/content/self-host/upgrading/4-14/41421.mdx
+1
-0
document/data/doc-last-modified.json
+1
-1
packages/global/core/chat/adapt.ts
+130
-57
packages/global/core/chat/helperBot/type.ts
+20
-11
packages/global/core/chat/type.ts
+1
-0
packages/global/core/chat/utils.ts
+17
-19
packages/global/test/core/chat/adapt.test.ts
+833
-55
packages/global/test/core/chat/helperBot/adaptor.test.ts
+21
-0
packages/global/test/core/chat/helperBot/type.test.ts
+16
-0
packages/service/core/ai/utils.ts
+39
-3
packages/service/core/chat/HelperBot/dispatch/utils.ts
+7
-8
packages/service/core/workflow/dispatch/ai/agent/adapter/eventMapper.ts
+16
-11
packages/service/core/workflow/dispatch/ai/agent/index.ts
+12
-17
packages/service/core/workflow/dispatch/ai/agent/piAgent/index.ts
+8
-10
packages/service/core/workflow/dispatch/ai/classifyQuestion.ts
+1
-1
packages/service/core/workflow/dispatch/ai/extract.ts
+6
-2
packages/service/core/workflow/dispatch/index.ts
+8
-7
packages/service/test/core/ai/utils.test.ts
+24
-0
packages/service/test/core/workflow/dispatch/ai/agent/adapter/eventMapper.test.ts
+75
-0
packages/service/test/core/workflow/dispatch/ai/agent/index.test.ts
+38
-0
packages/service/test/core/workflow/dispatch/ai/agent/piAgent/index.test.ts
+39
-0
projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx
+19
-14
projects/app/src/components/core/chat/HelperBot/components/AIItem.tsx
+27
-21
projects/app/src/components/core/chat/HelperBot/index.tsx
+38
-19
projects/app/src/components/core/chat/components/AIResponseBox/index.tsx
+1
-1
No files found.
document/content/self-host/upgrading/4-14/41421.mdx
View file @
05254ce0
...
...
@@ -15,3 +15,4 @@ description: 'FastGPT V4.14.21 更新说明'
1. 工作流默认选中模型未回传表单值,导致看到的模型和实际运行模型不一致。
2. 工作流自动保存时,存在边丢失风险。
3. admin 修改系统通知弹窗时候报错。
4. 工作流混用思考/非思考模型,可能出现独立 reason 字段上下文,导致模型调用报错。
document/data/doc-last-modified.json
View file @
05254ce0
...
...
@@ -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.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/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.mdx"
:
"2026-04-26T21:08:47+08:00"
,
"content/self-host/upgrading/outdated/41.en.mdx"
:
"2026-04-26T21:08:47+08:00"
,
...
...
packages/global/core/chat/adapt.ts
View file @
05254ce0
...
...
@@ -29,10 +29,18 @@ export const GPT2Chat = {
[
ChatCompletionRequestMessageRoleEnum
.
Tool
]:
ChatRoleEnum
.
AI
};
/**
* 将 OpenAI/GPT message role 映射为 FastGPT 内部聊天角色。
* function/tool message 本质上属于 AI 轮次的工具上下文,因此统一归到 AI。
*/
export
function
adaptRole_Message2Chat
(
role
:
`
${
ChatCompletionRequestMessageRoleEnum
}
`
)
{
return
GPT2Chat
[
role
];
}
/**
* 压缩用户 content part:单纯文本保持旧版 string 结构,多模态或多段内容保留数组。
* 这样既兼容历史文本模型上下文,又不会丢失图片/文件等结构化输入。
*/
export
const
simpleUserContentPart
=
(
content
:
ChatCompletionContentPart
[])
=>
{
if
(
content
.
length
===
1
&&
content
[
0
].
type
===
'text'
)
{
return
content
[
0
].
text
;
...
...
@@ -40,7 +48,7 @@ export const simpleUserContentPart = (content: ChatCompletionContentPart[]) => {
return
content
;
};
// 获取最后一个压缩
的 messages
// 获取最后一个压缩
检查点的位置。检查点会替代它之前的普通上下文。
const
getLatestCheckpointPosition
=
(
messages
:
ChatItemMiniType
[])
=>
{
for
(
let
index
=
messages
.
length
-
1
;
index
>=
0
;
index
--
)
{
const
item
=
messages
[
index
];
...
...
@@ -59,7 +67,10 @@ const getLatestCheckpointPosition = (messages: ChatItemMiniType[]) => {
return
;
};
// 找到最后一个包含压缩的 messages,且只取 compressIndex 后面的 value
/**
* 根据最后一个 contextCheckpoint 重建待请求的历史。
* checkpoint 前只保留 System 历史,避免压缩后的上下文又叠加旧对话导致重复计入。
*/
const
getCheckpointAwareMessages
=
(
messages
:
ChatItemMiniType
[])
=>
{
const
checkpointPosition
=
getLatestCheckpointPosition
(
messages
);
if
(
!
checkpointPosition
)
return
messages
;
...
...
@@ -83,6 +94,16 @@ const getCheckpointAwareMessages = (messages: ChatItemMiniType[]) => {
return
[...
systemMessages
,
...
checkpointAndRecentMessages
];
};
/**
* 规整 assistant 拆分字段消息。
*
* FastGPT 历史为了 UI 展示会把 reasoning、text、tools 拆成多个 value;转成 GPT message
* 时需要合并为 provider 能接受的 assistant message:
* - reasoning 不能作为孤立 assistant item 发送,必须附着到后续 text/tool/function payload。
* - 连续 reasoning 可以合并,reasoning 后接 text/tool_calls 时附着到同一个 assistant message。
* - dataId/hideInUI 不同表示来自不同轮次或不同可见性上下文,不能跨边界合并。
* - 没有可附着目标的孤立 reasoning 会被丢弃,避免生成只有 reasoning_content 的非法上下文。
*/
export
const
mergeAssistantFieldMessages
=
(
messages
:
ChatCompletionMessageParam
[])
=>
{
type
AssistantToolCallMessage
=
Extract
<
ChatCompletionMessageParam
,
{
role
:
'assistant'
}
>
&
{
tool_calls
:
ChatCompletionMessageToolCall
[];
...
...
@@ -92,6 +113,21 @@ export const mergeAssistantFieldMessages = (messages: ChatCompletionMessageParam
tool_call_id
:
string
;
};
// 多段 reasoning 代表同一次 assistant payload 的连续思考,使用空行拼接便于 UI 和上下文阅读。
const
appendSeparatedText
=
(
current
:
string
|
undefined
,
next
:
string
)
=>
{
if
(
!
next
)
return
current
;
return
current
?
`
${
current
}
\n\n
${
next
}
`
:
next
;
};
const
hasAssistantToolCalls
=
(
message
:
ChatCompletionMessageParam
)
=>
message
.
role
===
ChatCompletionRequestMessageRoleEnum
.
Assistant
&&
Array
.
isArray
(
message
.
tool_calls
)
&&
message
.
tool_calls
.
length
>
0
;
const
hasAssistantFunctionCall
=
(
message
:
ChatCompletionMessageParam
)
=>
message
.
role
===
ChatCompletionRequestMessageRoleEnum
.
Assistant
&&
Boolean
(
message
.
function_call
);
const
isAssistantFieldMessage
=
(
message
?:
ChatCompletionMessageParam
)
=>
{
if
(
message
?.
role
!==
ChatCompletionRequestMessageRoleEnum
.
Assistant
)
return
false
;
...
...
@@ -107,8 +143,7 @@ export const mergeAssistantFieldMessages = (messages: ChatCompletionMessageParam
if
(
message
?.
role
!==
ChatCompletionRequestMessageRoleEnum
.
Assistant
)
return
false
;
return
(
Array
.
isArray
(
message
.
tool_calls
)
&&
message
.
tool_calls
.
length
>
0
&&
hasAssistantToolCalls
(
message
)
&&
typeof
message
.
content
!==
'string'
&&
typeof
message
.
reasoning_content
!==
'string'
);
...
...
@@ -121,10 +156,12 @@ export const mergeAssistantFieldMessages = (messages: ChatCompletionMessageParam
message
?.
role
===
ChatCompletionRequestMessageRoleEnum
.
Tool
&&
toolCallIds
.
has
(
message
.
tool_call_id
||
''
);
const
hasSame
Visibility
=
(
const
hasSame
AssistantContext
=
(
message
:
ChatCompletionMessageParam
|
undefined
,
assistantMessage
:
ChatCompletionMessageParam
)
=>
(
message
?.
hideInUI
??
false
)
===
(
assistantMessage
.
hideInUI
??
false
);
)
=>
(
message
?.
hideInUI
??
false
)
===
(
assistantMessage
.
hideInUI
??
false
)
&&
message
?.
dataId
===
assistantMessage
.
dataId
;
const
mergedMessages
:
ChatCompletionMessageParam
[]
=
[];
...
...
@@ -138,24 +175,27 @@ export const mergeAssistantFieldMessages = (messages: ChatCompletionMessageParam
const
assistantMessage
:
ChatCompletionMessageParam
=
{
...
currentMessage
};
let
cursor
=
index
+
1
;
// 先吞掉同一轮连续的 assistant 字段消息,例如 reasoning -> reasoning -> text。
while
(
isAssistantFieldMessage
(
messages
[
cursor
]))
{
const
nextMessage
=
messages
[
cursor
];
if
(
!
hasSame
Visibility
(
nextMessage
,
assistantMessage
))
break
;
if
(
!
hasSame
AssistantContext
(
nextMessage
,
assistantMessage
))
break
;
const
nextReasoning
=
typeof
nextMessage
.
reasoning_content
===
'string'
?
nextMessage
.
reasoning_content
:
''
;
const
nextContent
=
typeof
nextMessage
.
content
===
'string'
?
nextMessage
.
content
:
''
;
if
(
nextReasoning
&&
(
typeof
assistantMessage
.
reasoning_content
===
'string'
||
typeof
assistantMessage
.
content
===
'string'
)
)
{
// text 后再次出现 reasoning 通常已经是下一段思考,不能挂回前一个 answer。
if
(
nextReasoning
&&
typeof
assistantMessage
.
content
===
'string'
)
{
break
;
}
if
(
nextReasoning
)
{
assistantMessage
.
reasoning_content
=
nextReasoning
;
assistantMessage
.
reasoning_content
=
appendSeparatedText
(
typeof
assistantMessage
.
reasoning_content
===
'string'
?
assistantMessage
.
reasoning_content
:
undefined
,
nextReasoning
);
}
if
(
typeof
nextMessage
.
content
===
'string'
)
{
...
...
@@ -172,22 +212,22 @@ export const mergeAssistantFieldMessages = (messages: ChatCompletionMessageParam
const
toolCalls
:
ChatCompletionMessageToolCall
[]
=
[];
const
toolResponses
:
ChatCompletionMessageParam
[]
=
[];
// 再把紧随其后的 tool_calls 与它们的 tool response 合并到同一个 assistant payload。
let
assistantToolMessage
:
ChatCompletionMessageParam
|
undefined
=
messages
[
cursor
];
while
(
isAssistantToolCallMessage
(
assistantToolMessage
))
{
if
(
!
hasSame
Visibility
(
assistantToolMessage
,
assistantMessage
))
break
;
if
(
!
hasSame
AssistantContext
(
assistantToolMessage
,
assistantMessage
))
break
;
const
currentToolCalls
=
assistantToolMessage
.
tool_calls
;
const
currentToolCallIds
=
new
Set
(
currentToolCalls
.
map
((
toolCall
)
=>
toolCall
.
id
));
const
currentToolResponses
:
ChatCompletionMessageParam
[]
=
[];
let
responseCursor
=
cursor
+
1
;
// 只消费属于当前 tool_calls 的 response,防止把下一轮工具结果串进来。
while
(
isToolResponseForToolCalls
(
messages
[
responseCursor
],
currentToolCallIds
))
{
currentToolResponses
.
push
(
messages
[
responseCursor
]);
responseCursor
++
;
}
if
(
!
currentToolResponses
.
length
)
break
;
toolCalls
.
push
(...
currentToolCalls
);
toolResponses
.
push
(...
currentToolResponses
);
cursor
=
responseCursor
;
...
...
@@ -195,7 +235,13 @@ export const mergeAssistantFieldMessages = (messages: ChatCompletionMessageParam
}
if
(
!
toolCalls
.
length
)
{
mergedMessages
.
push
(
assistantMessage
);
// 孤立 reasoning 没有可发给 provider 的合法载体;只有 text/function_call 才保留。
if
(
typeof
assistantMessage
.
content
===
'string'
||
hasAssistantFunctionCall
(
assistantMessage
)
)
{
mergedMessages
.
push
(
assistantMessage
);
}
index
=
cursor
-
1
;
continue
;
}
...
...
@@ -211,14 +257,25 @@ export const mergeAssistantFieldMessages = (messages: ChatCompletionMessageParam
return
mergedMessages
;
};
/**
* 将 FastGPT 内部 ChatItem 历史转换为 GPT request messages。
*
* 关键约定:
* - reserveTool=false 时只保留自然语言上下文,不把历史工具调用带入分类/普通对话。
* - reserveReason=false 时去掉 reasoning_content,适用于问题分类等不需要思考过程的节点。
* - reserveId=true 时保留 dataId,供需要按轮次追踪的调用方使用。
* - 输出前会调用 mergeAssistantFieldMessages,保证 reasoning 不以独立 assistant message 出现。
*/
export
const
chats2GPTMessages
=
({
messages
,
reserveId
,
reserveTool
=
false
reserveTool
=
false
,
reserveReason
=
true
}:
{
messages
:
ChatItemMiniType
[];
reserveId
:
boolean
;
reserveTool
?:
boolean
;
reserveReason
?:
boolean
;
}):
ChatCompletionMessageParam
[]
=>
{
let
results
:
ChatCompletionMessageParam
[]
=
[];
const
sourceMessages
=
getCheckpointAwareMessages
(
messages
);
...
...
@@ -272,7 +329,6 @@ export const chats2GPTMessages = ({
}
};
};
sourceMessages
.
forEach
((
item
)
=>
{
const
dataId
=
reserveId
?
item
.
dataId
:
undefined
;
if
(
item
.
obj
===
ChatRoleEnum
.
System
)
{
...
...
@@ -328,6 +384,7 @@ export const chats2GPTMessages = ({
}
else
{
const
aiResults
:
ChatCompletionMessageParam
[]
=
[];
const
agentAskAnswerMap
=
new
Map
<
string
,
string
>
();
// agentAsk 的用户回答以交互记录形式存在,需要按 planId 恢复为 ask_agent tool response。
item
.
value
.
forEach
((
value
)
=>
{
if
(
value
.
interactive
?.
type
===
'agentPlanAskQuery'
&&
...
...
@@ -363,6 +420,7 @@ export const chats2GPTMessages = ({
if
(
reasoningText
)
appendAssistantReasoning
(
reasoningText
,
hideInUI
);
if
(
assistantText
)
appendAssistantText
(
assistantText
,
hideInUI
);
if
(
!
normalizedToolContext
)
{
// tool 元数据不完整时,保留前置 assistantText/reasoning,丢弃非法 tool_call。
return
false
;
}
...
...
@@ -376,6 +434,8 @@ export const chats2GPTMessages = ({
};
const
appendAssistantReasoning
=
(
content
:
string
,
hideInUI
?:
boolean
)
=>
{
if
(
!
reserveReason
||
!
content
)
return
;
aiResults
.
push
({
dataId
,
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
...
...
@@ -406,7 +466,7 @@ export const chats2GPTMessages = ({
item
.
value
.
forEach
((
value
)
=>
{
if
(
value
.
contextCheckpoint
)
{
//
A checkpoint value replaces everything before it; fields on the same value are ignored.
//
checkpoint 会重置之前累积的 AI 字段;同一个 value 上的其他字段不再参与上下文。
results
=
results
.
concat
(
mergeAssistantFieldMessages
(
aiResults
));
aiResults
.
length
=
0
;
results
.
push
({
...
...
@@ -504,7 +564,7 @@ export const chats2GPTMessages = ({
}
});
// A
uto add empty assistant message
// A
I value 遍历结束后统一合并,处理 reasoning/text/tools 分散存储的兼容格式。
results
=
results
.
concat
(
mergeAssistantFieldMessages
(
aiResults
));
}
});
...
...
@@ -512,6 +572,13 @@ export const chats2GPTMessages = ({
return
results
;
};
/**
* 将 GPT messages 转回 FastGPT ChatItem。
*
* GPTMessages2Chats 只恢复当前 message 已经聚合好的结构,不再跨 message 追踪 reasoning。
* reasoning_content 必须和 content/tool_calls/function_call 在同一个 assistant message 上才会恢复;
* 孤立 reasoning 会被过滤,避免把下一轮或未知归属的思考过程误挂到后续消息。
*/
export
const
GPTMessages2Chats
=
({
messages
,
reserveTool
=
true
,
...
...
@@ -607,30 +674,18 @@ export const GPTMessages2Chats = ({
)
{
const
value
:
AIChatItemValueItemType
[]
=
[];
const
valueVisibility
=
item
.
hideInUI
?
{
hideInUI
:
item
.
hideInUI
}
:
{};
const
assistantValue
:
AIChatItemValueItemType
=
{
...
valueVisibility
};
if
(
typeof
item
.
reasoning_content
===
'string'
&&
item
.
reasoning_content
&&
reserveReason
)
{
value
.
push
({
reasoning
:
{
content
:
item
.
reasoning_content
},
...
valueVisibility
});
}
// text、tools、function_call 和 reasoning 归在同一个 value,避免 UI/历史再产生独立 reason item。
if
(
typeof
item
.
content
===
'string'
&&
item
.
content
)
{
const
lastValue
=
value
[
value
.
length
-
1
];
if
(
lastValue
&&
lastValue
.
text
)
{
lastValue
.
text
.
content
+=
item
.
content
;
}
else
{
value
.
push
({
text
:
{
content
:
item
.
content
},
...
valueVisibility
});
}
assistantValue
.
text
=
{
content
:
item
.
content
};
}
if
(
item
.
tool_calls
&&
reserveTool
)
{
//
save tool calls
//
tool response 存在于独立 tool message 中,这里按 tool_call_id 回查并折回 ChatItem.tools。
const
toolCalls
=
item
.
tool_calls
as
ChatCompletionMessageToolCall
[];
const
tools
=
toolCalls
.
flatMap
<
ToolModuleResponseItemType
>
((
tool
)
=>
{
...
...
@@ -656,10 +711,9 @@ export const GPTMessages2Chats = ({
}
];
});
value
.
push
({
tools
,
...
valueVisibility
});
if
(
tools
.
length
)
{
assistantValue
.
tools
=
tools
;
}
}
if
(
item
.
function_call
&&
reserveTool
)
{
const
functionCall
=
item
.
function_call
as
ChatCompletionMessageFunctionCall
;
...
...
@@ -670,19 +724,29 @@ export const GPTMessages2Chats = ({
)
as
ChatCompletionFunctionMessageParam
;
if
(
functionResponse
)
{
value
.
push
({
tool
:
{
id
:
functionCall
.
id
||
''
,
toolName
:
functionCall
.
toolName
||
''
,
toolAvatar
:
functionCall
.
toolAvatar
||
''
,
functionName
:
functionCall
.
name
,
params
:
functionCall
.
arguments
,
response
:
functionResponse
.
content
||
''
},
...
valueVisibility
});
assistantValue
.
tool
=
{
id
:
functionCall
.
id
||
''
,
toolName
:
functionCall
.
toolName
||
''
,
toolAvatar
:
functionCall
.
toolAvatar
||
''
,
functionName
:
functionCall
.
name
,
params
:
functionCall
.
arguments
,
response
:
functionResponse
.
content
||
''
};
}
}
if
(
typeof
item
.
reasoning_content
===
'string'
&&
item
.
reasoning_content
&&
reserveReason
&&
(
assistantValue
.
text
||
assistantValue
.
tools
||
assistantValue
.
tool
)
)
{
assistantValue
.
reasoning
=
{
content
:
item
.
reasoning_content
};
}
if
(
assistantValue
.
text
||
assistantValue
.
tools
||
assistantValue
.
tool
)
{
value
.
push
(
assistantValue
);
}
if
(
item
.
interactive
)
{
value
.
push
({
interactive
:
item
.
interactive
,
...
...
@@ -706,7 +770,7 @@ export const GPTMessages2Chats = ({
})
.
filter
((
item
)
=>
item
.
value
.
length
>
0
);
//
Merge data with the same dataId(Sequential obj merging)
//
相邻同 dataId/obj 的记录归并,保持一轮 AI 多个 value 在同一个 ChatItem 中展示。
const
result
=
chatMessages
.
reduce
((
result
:
ChatItemMiniType
[],
currentItem
)
=>
{
const
lastItem
=
result
[
result
.
length
-
1
];
...
...
@@ -723,6 +787,9 @@ export const GPTMessages2Chats = ({
return
result
;
};
/**
* 将聊天 value 提取成运行时用户输入。文件保留结构,文本按展示顺序拼接。
*/
export
const
chatValue2RuntimePrompt
=
(
value
:
ChatItemValueItemType
[]):
RuntimeUserPromptType
=>
{
const
prompt
:
RuntimeUserPromptType
=
{
files
:
[],
...
...
@@ -738,6 +805,9 @@ export const chatValue2RuntimePrompt = (value: ChatItemValueItemType[]): Runtime
return
prompt
;
};
/**
* 将运行时 prompt 恢复为用户聊天 value,主要用于调试/重放入口。
*/
export
const
runtimePrompt2ChatsValue
=
(
prompt
:
{
files
?:
UserChatItemFileItemType
[];
text
?:
string
;
...
...
@@ -760,6 +830,9 @@ export const runtimePrompt2ChatsValue = (prompt: {
return
value
;
};
/**
* 用一条 System ChatItem 包装系统提示词,便于统一走 ChatItem -> GPT message 转换链。
*/
export
const
getSystemPrompt_ChatItemType
=
(
prompt
?:
string
):
ChatItemMiniType
[]
=>
{
if
(
!
prompt
)
return
[];
return
[
...
...
packages/global/core/chat/helperBot/type.ts
View file @
05254ce0
import
{
ObjectIdSchema
}
from
'../../../common/type/mongo'
;
import
z
from
'zod'
;
import
{
ChatRoleEnum
}
from
'../constants'
;
import
{
UserChatItemSchema
,
SystemChatItemSchema
,
ToolModuleResponseItemSchema
}
from
'../type'
;
import
{
UserChatItemSchema
,
SystemChatItemSchema
}
from
'../type'
;
import
{
UserInputInteractiveSchema
}
from
'../../workflow/template/system/interactive/type'
;
export
enum
HelperBotTypeEnum
{
...
...
@@ -21,18 +21,27 @@ export const HelperBotChatSchema = z.object({
});
export
type
HelperBotChatType
=
z
.
infer
<
typeof
HelperBotChatSchema
>
;
const
AIChatContentValueSchema
=
z
.
object
({
text
:
z
.
object
({
content
:
z
.
string
()
})
.
optional
(),
reasoning
:
z
.
object
({
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
([
z
.
object
({
text
:
z
.
object
({
content
:
z
.
string
()
})
}),
z
.
object
({
reasoning
:
z
.
object
({
content
:
z
.
string
()
})
}),
AIChatContentValueSchema
,
z
.
object
({
collectionForm
:
UserInputInteractiveSchema
}),
...
...
packages/global/core/chat/type.ts
View file @
05254ce0
...
...
@@ -225,6 +225,7 @@ export const AIChatItemValueSchema = z.object({
agentStopGate
:
AgentLoopStopGateSchema
.
nullish
(),
contextCheckpoint
:
ContextCheckpointValueSchema
.
nullish
(),
tool
:
ToolModuleResponseItemSchema
.
nullish
().
meta
({
deprecated
:
true
}),
hideReason
:
z
.
boolean
().
optional
(),
hideInUI
:
z
.
boolean
().
optional
()
});
...
...
packages/global/core/chat/utils.ts
View file @
05254ce0
...
...
@@ -152,27 +152,25 @@ export const removeAIResponseCite = <T extends AIChatItemValueItemType[] | strin
return
removeDatasetCiteText
(
value
,
false
)
as
T
;
}
return
value
.
map
<
AIChatItemValueItemType
>
((
item
)
=>
{
if
(
item
.
text
?.
content
)
{
return
{
...
item
,
text
:
{
...
item
.
text
,
content
:
removeDatasetCiteText
(
item
.
text
.
content
,
false
)
return
value
.
map
<
AIChatItemValueItemType
>
((
item
)
=>
({
...
item
,
...(
item
.
text
?.
content
?
{
text
:
{
...
item
.
text
,
content
:
removeDatasetCiteText
(
item
.
text
.
content
,
false
)
}
}
};
}
if
(
item
.
reasoning
?.
content
)
{
return
{
...
item
,
reasoning
:
{
...
item
.
reasoning
,
content
:
removeDatasetCiteText
(
item
.
reasoning
.
content
,
false
)
:
{}),
...(
item
.
reasoning
?.
content
?
{
reasoning
:
{
...
item
.
reasoning
,
content
:
removeDatasetCiteText
(
item
.
reasoning
.
content
,
false
)
}
}
};
}
return
item
;
})
as
T
;
:
{})
}))
as
T
;
};
export
const
removeEmptyUserInput
=
(
input
?:
UserChatItemValueItemType
[])
=>
{
...
...
packages/global/test/core/chat/adapt.test.ts
View file @
05254ce0
...
...
@@ -142,7 +142,36 @@ describe('mergeAssistantFieldMessages', () => {
]);
});
it
(
'should not merge assistant messages with different hideInUI visibility'
,
()
=>
{
it
(
'should start a new assistant payload when reasoning appears after content'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Draft answer.'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Second step thinking.'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Final answer.'
}
];
expect
(
mergeAssistantFieldMessages
(
messages
)).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Draft answer.'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Second step thinking.'
,
content
:
'Final answer.'
}
]);
});
it
(
'should drop orphan reasoning when the next assistant message has different visibility'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
...
...
@@ -155,7 +184,124 @@ describe('mergeAssistantFieldMessages', () => {
}
];
expect
(
mergeAssistantFieldMessages
(
messages
)).
toEqual
(
messages
);
expect
(
mergeAssistantFieldMessages
(
messages
)).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Visible answer.'
}
]);
});
it
(
'should drop orphan reasoning when the next assistant message has different dataId'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
dataId
:
'old-ai'
,
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Reason from another response.'
},
{
dataId
:
'current-ai'
,
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Current answer.'
}
];
expect
(
mergeAssistantFieldMessages
(
messages
)).
toEqual
([
{
dataId
:
'current-ai'
,
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Current answer.'
}
]);
});
it
(
'should not attach orphan reasoning to tool calls with different dataId'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
dataId
:
'old-ai'
,
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Reason from another response.'
},
{
dataId
:
'current-ai'
,
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
tool_calls
:
[
{
id
:
'call_search'
,
type
:
'function'
,
function
:
{
name
:
'search_web'
,
arguments
:
'{}'
}
}
]
},
{
dataId
:
'current-ai'
,
role
:
ChatCompletionRequestMessageRoleEnum
.
Tool
,
tool_call_id
:
'call_search'
,
content
:
'{}'
}
];
expect
(
mergeAssistantFieldMessages
(
messages
)).
toEqual
([
{
dataId
:
'current-ai'
,
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
tool_calls
:
[
{
id
:
'call_search'
,
type
:
'function'
,
function
:
{
name
:
'search_web'
,
arguments
:
'{}'
}
}
]
},
{
dataId
:
'current-ai'
,
role
:
ChatCompletionRequestMessageRoleEnum
.
Tool
,
tool_call_id
:
'call_search'
,
content
:
'{}'
}
]);
});
it
(
'should merge consecutive assistant reasoning fields into the next assistant payload'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'reason 1'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'reason 2'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Final answer.'
}
];
expect
(
mergeAssistantFieldMessages
(
messages
)).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'reason 1\n\nreason 2'
,
content
:
'Final answer.'
}
]);
});
it
(
'should drop orphan assistant reasoning fields'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'reason only'
}
];
expect
(
mergeAssistantFieldMessages
(
messages
)).
toEqual
([]);
});
it
(
'should keep DeepSeek reasoning on tool call message when there is no answer text'
,
()
=>
{
...
...
@@ -292,7 +438,7 @@ describe('mergeAssistantFieldMessages', () => {
]);
});
it
(
'should
keep tool call message separat
e when tool response is missing'
,
()
=>
{
it
(
'should
attach reasoning to tool call messag
e when tool response is missing'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
...
...
@@ -313,6 +459,47 @@ describe('mergeAssistantFieldMessages', () => {
}
];
expect
(
mergeAssistantFieldMessages
(
messages
)).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Need a tool.'
,
tool_calls
:
[
{
id
:
'call_search'
,
type
:
'function'
,
function
:
{
name
:
'search_web'
,
arguments
:
'{}'
}
}
]
}
]);
});
it
(
'should keep already merged reasoning and tool calls idempotent'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Need a tool.'
,
tool_calls
:
[
{
id
:
'call_search'
,
type
:
'function'
,
function
:
{
name
:
'search_web'
,
arguments
:
'{}'
}
}
]
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Tool
,
tool_call_id
:
'call_search'
,
content
:
'{}'
}
];
expect
(
mergeAssistantFieldMessages
(
messages
)).
toEqual
(
messages
);
});
});
...
...
@@ -697,6 +884,42 @@ describe('chats2GPTMessages', () => {
});
});
it
(
'should not attach reasoning that appears before the latest checkpoint'
,
()
=>
{
const
messages
:
ChatItemMiniType
[]
=
[
{
obj
:
ChatRoleEnum
.
AI
,
value
:
[
{
reasoning
:
{
content
:
'Old reasoning before checkpoint'
}
},
{
contextCheckpoint
:
'<context_checkpoint>compressed state</context_checkpoint>'
,
hideInUI
:
true
},
{
text
:
{
content
:
'Answer after checkpoint'
}
}
]
}
];
expect
(
chats2GPTMessages
({
messages
,
reserveId
:
false
})).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
User
,
content
:
'<context_checkpoint>compressed state</context_checkpoint>'
,
hideInUI
:
true
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Answer after checkpoint'
}
]);
});
it
(
'should handle AI message with tool calls when reserveTool is true'
,
()
=>
{
const
messages
:
ChatItemMiniType
[]
=
[
{
...
...
@@ -834,12 +1057,7 @@ describe('chats2GPTMessages', () => {
const
result
=
chats2GPTMessages
({
messages
,
reserveId
:
false
,
reserveTool
:
true
});
expect
(
result
).
toHaveLength
(
1
);
expect
(
result
[
0
]).
toMatchObject
({
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Need a tool'
});
expect
((
result
[
0
]
as
any
).
tool_calls
).
toBeUndefined
();
expect
(
result
).
toHaveLength
(
0
);
});
it
(
'should normalize non-string historical tool params before restoring tool_calls'
,
()
=>
{
...
...
@@ -874,6 +1092,35 @@ describe('chats2GPTMessages', () => {
});
});
it
(
'should fallback to empty object arguments when historical tool params cannot stringify'
,
()
=>
{
const
circularParams
:
Record
<
string
,
unknown
>
=
{};
circularParams
.
self
=
circularParams
;
const
messages
:
ChatItemMiniType
[]
=
[
{
obj
:
ChatRoleEnum
.
AI
,
value
:
[
{
tool
:
{
id
:
'call_circular_params'
,
toolName
:
'Search'
,
toolAvatar
:
''
,
functionName
:
'search_web'
,
params
:
circularParams
,
response
:
'ok'
}
as
any
}
]
}
];
const
result
=
chats2GPTMessages
({
messages
,
reserveId
:
false
,
reserveTool
:
true
});
expect
((
result
[
0
]
as
any
).
tool_calls
?.[
0
].
function
).
toEqual
({
name
:
'search_web'
,
arguments
:
'{}'
});
});
it
(
'should attach assistant text and reasoning to following tool_calls'
,
()
=>
{
const
messages
:
ChatItemMiniType
[]
=
[
{
...
...
@@ -978,7 +1225,34 @@ describe('chats2GPTMessages', () => {
expect
((
result
[
0
]
as
any
).
hideInUI
).
toBe
(
true
);
});
it
(
'should preserve AI value-level hideInUI property'
,
()
=>
{
it
(
'should keep hideReason independent from GPT context visibility'
,
()
=>
{
const
messages
:
ChatItemMiniType
[]
=
[
{
obj
:
ChatRoleEnum
.
AI
,
value
:
[
{
reasoning
:
{
content
:
'Hidden in UI but still part of reasoning history'
},
text
:
{
content
:
'Visible answer'
},
hideReason
:
true
}
]
}
];
expect
(
chats2GPTMessages
({
messages
,
reserveId
:
false
})).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Hidden in UI but still part of reasoning history'
,
content
:
'Visible answer'
}
]);
});
it
(
'should drop value-level reasoning when hideInUI prevents attaching to visible text'
,
()
=>
{
const
messages
:
ChatItemMiniType
[]
=
[
{
obj
:
ChatRoleEnum
.
AI
,
...
...
@@ -1003,11 +1277,6 @@ describe('chats2GPTMessages', () => {
expect
(
result
).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Hidden reasoning'
,
hideInUI
:
true
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Visible answer'
}
]);
...
...
@@ -1103,7 +1372,7 @@ describe('chats2GPTMessages', () => {
expect
(
result
).
toHaveLength
(
0
);
});
it
(
'should
convert
AI message with reasoning only'
,
()
=>
{
it
(
'should
drop
AI message with reasoning only'
,
()
=>
{
const
messages
:
ChatItemMiniType
[]
=
[
{
obj
:
ChatRoleEnum
.
AI
,
...
...
@@ -1113,10 +1382,7 @@ describe('chats2GPTMessages', () => {
const
result
=
chats2GPTMessages
({
messages
,
reserveId
:
false
});
expect
(
result
).
toHaveLength
(
1
);
expect
(
result
[
0
].
role
).
toBe
(
ChatCompletionRequestMessageRoleEnum
.
Assistant
);
expect
((
result
[
0
]
as
any
).
reasoning_content
).
toBe
(
'Let me think...'
);
expect
((
result
[
0
]
as
any
).
content
).
toBeUndefined
();
expect
(
result
).
toHaveLength
(
0
);
});
it
(
'should merge reasoning + text into a single assistant message'
,
()
=>
{
...
...
@@ -1138,24 +1404,121 @@ describe('chats2GPTMessages', () => {
expect
((
result
[
0
]
as
any
).
content
).
toBe
(
'Final answer'
);
});
it
(
'should
attach preceding reasoning to following tool_calls
'
,
()
=>
{
it
(
'should
concatenate continuous reasoning values before attaching to text
'
,
()
=>
{
const
messages
:
ChatItemMiniType
[]
=
[
{
obj
:
ChatRoleEnum
.
AI
,
value
:
[
{
reasoning
:
{
content
:
'Need to call a tool'
}
},
{
tools
:
[
{
id
:
'tool-1'
,
toolName
:
'Search'
,
toolAvatar
:
''
,
functionName
:
'search_web'
,
params
:
'{"q":"x"}'
,
response
:
'{}'
}
]
}
{
reasoning
:
{
content
:
'reason 1'
}
},
{
reasoning
:
{
content
:
'reason 2'
}
},
{
text
:
{
content
:
'Final answer'
}
}
]
}
];
const
result
=
chats2GPTMessages
({
messages
,
reserveId
:
false
});
expect
(
result
).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'reason 1\n\nreason 2'
,
content
:
'Final answer'
}
]);
});
it
(
'should remove reasoning when reserveReason is false'
,
()
=>
{
const
messages
:
ChatItemMiniType
[]
=
[
{
obj
:
ChatRoleEnum
.
AI
,
value
:
[
{
reasoning
:
{
content
:
'hidden thought'
}
},
{
text
:
{
content
:
'Visible answer'
}
}
]
}
];
const
result
=
chats2GPTMessages
({
messages
,
reserveId
:
false
,
reserveReason
:
false
});
expect
(
result
).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Visible answer'
}
]);
});
it
(
'should keep tool calls and remove reasoning when reserveReason is false'
,
()
=>
{
const
messages
:
ChatItemMiniType
[]
=
[
{
obj
:
ChatRoleEnum
.
AI
,
value
:
[
{
reasoning
:
{
content
:
'Do not send this reasoning'
},
tools
:
[
{
id
:
'tool-1'
,
toolName
:
'Search'
,
toolAvatar
:
''
,
functionName
:
'search_web'
,
params
:
'{"q":"x"}'
,
response
:
'{"ok":true}'
}
]
}
]
}
];
expect
(
chats2GPTMessages
({
messages
,
reserveId
:
false
,
reserveTool
:
true
,
reserveReason
:
false
})
).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
tool_calls
:
[
{
id
:
'tool-1'
,
type
:
'function'
,
function
:
{
name
:
'search_web'
,
arguments
:
'{"q":"x"}'
}
}
]
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Tool
,
tool_call_id
:
'tool-1'
,
content
:
'{"ok":true}'
}
]);
});
it
(
'should attach preceding reasoning to following tool_calls'
,
()
=>
{
const
messages
:
ChatItemMiniType
[]
=
[
{
obj
:
ChatRoleEnum
.
AI
,
value
:
[
{
reasoning
:
{
content
:
'Need to call a tool'
}
},
{
tools
:
[
{
id
:
'tool-1'
,
toolName
:
'Search'
,
toolAvatar
:
''
,
functionName
:
'search_web'
,
params
:
'{"q":"x"}'
,
response
:
'{}'
}
]
}
]
}
];
...
...
@@ -1251,6 +1614,34 @@ describe('chats2GPTMessages', () => {
]);
});
it
(
'should keep control assistant text and reasoning when control tool metadata is invalid'
,
()
=>
{
const
messages
:
ChatItemMiniType
[]
=
[
{
obj
:
ChatRoleEnum
.
AI
,
value
:
[
{
agentPlanUpdate
:
{
id
:
''
,
functionName
:
''
,
params
:
'{}'
,
response
:
'Plan updated.'
,
assistantText
:
'draft before invalid control tool'
,
reasoningText
:
'planning'
}
}
]
}
];
expect
(
chats2GPTMessages
({
messages
,
reserveId
:
false
,
reserveTool
:
true
})).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'planning'
,
content
:
'draft before invalid control tool'
}
]);
});
it
(
'should restore agent loop control fields from chat value when reserving tools'
,
()
=>
{
const
expectedMessages
:
ChatCompletionMessageParam
[]
=
[
{
...
...
@@ -1410,6 +1801,117 @@ describe('chats2GPTMessages', () => {
expect
((
result
[
1
]
as
any
).
reasoning_content
).
toBe
(
'Step 2 thinking'
);
expect
((
result
[
1
]
as
any
).
content
).
toBe
(
'Final answer'
);
});
it
(
'should keep multi-step reasoning and tool calls attached without reasoning-only messages'
,
()
=>
{
const
messages
:
ChatItemMiniType
[]
=
[
{
obj
:
ChatRoleEnum
.
AI
,
value
:
[
{
reasoning
:
{
content
:
'reason 1'
}
},
{
tools
:
[
{
id
:
'call_1'
,
toolName
:
'Get time'
,
toolAvatar
:
''
,
functionName
:
'get_time'
,
params
:
'{}'
,
response
:
'{"time":"17:19:15"}'
}
]
},
{
reasoning
:
{
content
:
'reason 2'
}
},
{
tools
:
[
{
id
:
'call_2'
,
toolName
:
'Get time'
,
toolAvatar
:
''
,
functionName
:
'get_time'
,
params
:
'{}'
,
response
:
'{"time":"17:19:16"}'
}
]
},
{
reasoning
:
{
content
:
'reason 3'
}
},
{
text
:
{
content
:
'Final answer'
}
}
]
}
];
const
withTools
=
chats2GPTMessages
({
messages
,
reserveId
:
false
,
reserveTool
:
true
,
reserveReason
:
true
});
const
classifyLike
=
chats2GPTMessages
({
messages
,
reserveId
:
false
,
reserveTool
:
false
,
reserveReason
:
false
});
expect
(
withTools
).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'reason 1'
,
tool_calls
:
[
{
id
:
'call_1'
,
type
:
'function'
,
function
:
{
name
:
'get_time'
,
arguments
:
'{}'
}
}
]
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Tool
,
tool_call_id
:
'call_1'
,
content
:
'{"time":"17:19:15"}'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'reason 2'
,
tool_calls
:
[
{
id
:
'call_2'
,
type
:
'function'
,
function
:
{
name
:
'get_time'
,
arguments
:
'{}'
}
}
]
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Tool
,
tool_call_id
:
'call_2'
,
content
:
'{"time":"17:19:16"}'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'reason 3'
,
content
:
'Final answer'
}
]);
expect
(
withTools
.
every
(
(
item
)
=>
item
.
role
!==
ChatCompletionRequestMessageRoleEnum
.
Assistant
||
Boolean
((
item
as
any
).
content
)
||
Boolean
((
item
as
any
).
tool_calls
)
)
).
toBe
(
true
);
expect
(
classifyLike
).
toEqual
([
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Final answer'
}
]);
});
});
describe
(
'GPTMessages2Chats'
,
()
=>
{
...
...
@@ -1491,9 +1993,241 @@ describe('GPTMessages2Chats', () => {
const
result
=
GPTMessages2Chats
({
messages
});
expect
(
result
).
toHaveLength
(
1
);
const
reasoningValue
=
result
[
0
].
value
[
0
]
as
{
reasoning
?:
{
content
:
string
}
};
expect
(
reasoningValue
.
reasoning
?.
content
).
toBe
(
'Let me think about this...'
);
expect
(
result
[
0
].
value
[
1
].
text
?.
content
).
toBe
(
'Final answer'
);
expect
(
result
[
0
].
value
).
toEqual
([
{
reasoning
:
{
content
:
'Let me think about this...'
},
text
:
{
content
:
'Final answer'
}
}
]);
});
it
(
'should drop separated reasoning assistant messages before following content'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'reason 1'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'reason 2'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Final answer'
}
];
const
result
=
GPTMessages2Chats
({
messages
});
expect
(
result
).
toHaveLength
(
1
);
expect
(
result
[
0
].
value
).
toEqual
([
{
text
:
{
content
:
'Final answer'
}
}
]);
});
it
(
'should keep only reasoning that already exists on the attach target'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'pending reason'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'target reason'
,
content
:
'Final answer'
}
];
const
result
=
GPTMessages2Chats
({
messages
});
expect
(
result
).
toHaveLength
(
1
);
expect
(
result
[
0
].
value
).
toEqual
([
{
reasoning
:
{
content
:
'target reason'
},
text
:
{
content
:
'Final answer'
}
}
]);
});
it
(
'should ignore separated reasoning across non-assistant message boundaries'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Reason before user boundary'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
User
,
content
:
'New user turn'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Answer after user turn'
}
];
const
result
=
GPTMessages2Chats
({
messages
});
expect
(
result
).
toEqual
([
{
dataId
:
undefined
,
obj
:
ChatRoleEnum
.
Human
,
hideInUI
:
undefined
,
value
:
[
{
text
:
{
content
:
'New user turn'
}
}
]
},
{
dataId
:
undefined
,
obj
:
ChatRoleEnum
.
AI
,
value
:
[
{
text
:
{
content
:
'Answer after user turn'
}
}
]
}
]);
});
it
(
'should drop separated reasoning assistant message before following tool calls'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Need current time.'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
tool_calls
:
[
{
id
:
'call_time'
,
type
:
'function'
,
function
:
{
name
:
'get_time'
,
arguments
:
'{}'
}
}
]
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Tool
,
tool_call_id
:
'call_time'
,
content
:
'{"time":"2026-05-21 17:19:15"}'
}
];
const
result
=
GPTMessages2Chats
({
messages
,
reserveTool
:
true
});
expect
(
result
).
toHaveLength
(
1
);
expect
(
result
[
0
].
value
).
toEqual
([
{
tools
:
[
{
id
:
'call_time'
,
toolName
:
''
,
toolAvatar
:
''
,
functionName
:
'get_time'
,
params
:
'{}'
,
response
:
'{"time":"2026-05-21 17:19:15"}'
}
]
}
]);
});
it
(
'should not attach separated reasoning to a later answer after filtered tool calls'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Reasoning for a tool call only.'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
tool_calls
:
[
{
id
:
'call_search'
,
type
:
'function'
,
function
:
{
name
:
'search_web'
,
arguments
:
'{}'
}
}
]
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Tool
,
tool_call_id
:
'call_search'
,
content
:
'{}'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Final visible answer.'
}
];
const
result
=
GPTMessages2Chats
({
messages
,
reserveTool
:
false
,
reserveReason
:
true
});
expect
(
result
).
toHaveLength
(
1
);
expect
(
result
[
0
].
value
).
toEqual
([
{
text
:
{
content
:
'Final visible answer.'
}
}
]);
});
it
(
'should not attach separated reasoning after filtered tool calls without tool response'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Reasoning for a filtered tool call.'
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
tool_calls
:
[
{
id
:
'call_search'
,
type
:
'function'
,
function
:
{
name
:
'search_web'
,
arguments
:
'{}'
}
}
]
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Final visible answer.'
}
];
const
result
=
GPTMessages2Chats
({
messages
,
reserveTool
:
false
,
reserveReason
:
true
});
expect
(
result
).
toHaveLength
(
1
);
expect
(
result
[
0
].
value
).
toEqual
([
{
text
:
{
content
:
'Final visible answer.'
}
}
]);
});
it
(
'should drop reasoning when reserveReason is false'
,
()
=>
{
...
...
@@ -1513,7 +2247,7 @@ describe('GPTMessages2Chats', () => {
expect
((
result
[
0
].
value
[
0
]
as
any
).
reasoning
).
toBeUndefined
();
});
it
(
'should
keep only
reasoning when assistant message has no content'
,
()
=>
{
it
(
'should
drop
reasoning when assistant message has no content'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
...
...
@@ -1524,10 +2258,7 @@ describe('GPTMessages2Chats', () => {
const
result
=
GPTMessages2Chats
({
messages
});
expect
(
result
).
toHaveLength
(
1
);
expect
(
result
[
0
].
value
).
toHaveLength
(
1
);
const
value
=
result
[
0
].
value
[
0
]
as
{
reasoning
?:
{
content
:
string
}
};
expect
(
value
.
reasoning
?.
content
).
toBe
(
'Thinking only'
);
expect
(
result
).
toHaveLength
(
0
);
});
it
(
'should merge messages with same dataId'
,
()
=>
{
...
...
@@ -1645,6 +2376,36 @@ describe('GPTMessages2Chats', () => {
expect
(
toolValue
.
tools
[
0
].
response
).
toBe
(
'{"results": []}'
);
});
it
(
'should stringify non-string tool response content'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
tool_calls
:
[
{
id
:
'call_1'
,
type
:
'function'
,
function
:
{
name
:
'search_web'
,
arguments
:
'{}'
}
}
]
},
{
role
:
ChatCompletionRequestMessageRoleEnum
.
Tool
,
tool_call_id
:
'call_1'
,
content
:
[{
type
:
'text'
,
text
:
'chunked result'
}]
}
];
const
result
=
GPTMessages2Chats
({
messages
,
reserveTool
:
true
});
expect
(
result
).
toHaveLength
(
1
);
expect
((
result
[
0
].
value
[
0
]
as
any
).
tools
[
0
].
response
).
toBe
(
'[{"type":"text","text":"chunked result"}]'
);
});
it
(
'should keep assistant content, reasoning and parallel tools continuous after chat roundtrip'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
...
...
@@ -1694,14 +2455,10 @@ describe('GPTMessages2Chats', () => {
{
reasoning
:
{
content
:
'Need weather and time.'
}
},
{
},
text
:
{
content
:
'Checking both tools.'
}
},
{
},
tools
:
[
{
id
:
'call_weather'
,
...
...
@@ -1856,7 +2613,34 @@ describe('GPTMessages2Chats', () => {
expect
((
result
[
0
]
as
any
).
hideInUI
).
toBe
(
true
);
});
it
(
'should restore assistant hideInUI to value level without hiding the whole AI chat'
,
()
=>
{
it
(
'should drop assistant reasoning when dataId prevents attaching to visible content'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
dataId
:
'reasoning-message'
,
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
reasoning_content
:
'Reasoning from another assistant item'
},
{
dataId
:
'answer-message'
,
role
:
ChatCompletionRequestMessageRoleEnum
.
Assistant
,
content
:
'Visible answer'
}
];
const
result
=
GPTMessages2Chats
({
messages
});
expect
(
result
).
toHaveLength
(
1
);
expect
(
result
[
0
].
dataId
).
toBe
(
'answer-message'
);
expect
(
result
[
0
].
value
).
toEqual
([
{
text
:
{
content
:
'Visible answer'
}
}
]);
});
it
(
'should drop assistant reasoning when hideInUI prevents attaching to visible content'
,
()
=>
{
const
messages
:
ChatCompletionMessageParam
[]
=
[
{
dataId
:
'assistant-data-id'
,
...
...
@@ -1877,12 +2661,6 @@ describe('GPTMessages2Chats', () => {
expect
(
result
[
0
].
hideInUI
).
toBeUndefined
();
expect
(
result
[
0
].
value
).
toEqual
([
{
reasoning
:
{
content
:
'Hidden reasoning'
},
hideInUI
:
true
},
{
text
:
{
content
:
'Visible answer'
}
...
...
packages/global/test/core/chat/helperBot/adaptor.test.ts
View file @
05254ce0
...
...
@@ -115,6 +115,27 @@ describe('helperChats2GPTMessages', () => {
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'
,
()
=>
{
const
messages
=
[
{
...
...
packages/global/test/core/chat/helperBot/type.test.ts
View file @
05254ce0
...
...
@@ -47,6 +47,22 @@ describe('AIChatItemValueItemSchema', () => {
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'
,
()
=>
{
const
result
=
AIChatItemValueItemSchema
.
safeParse
({
planHint
:
{
type
:
'generation'
}
...
...
packages/service/core/ai/utils.ts
View file @
05254ce0
...
...
@@ -40,7 +40,13 @@ export const computedTemperature = ({
};
// 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
]
=>
{
const
regex
=
/<think>
([\s\S]
*
?)
<
\/
think>/
;
const
match
=
text
.
match
(
regex
);
...
...
@@ -52,7 +58,9 @@ export const parseReasoningContent = (text: string): [string, string] => {
const
thinkContent
=
match
[
1
].
trim
();
// 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
];
};
...
...
@@ -75,6 +83,29 @@ export const parseLLMStreamResponse = () => {
let
buffer_reasoningContent
=
''
;
let
buffer_content
=
''
;
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>,如果接口已经解析了,则不再解析。
...
...
@@ -117,7 +148,7 @@ export const parseLLMStreamResponse = () => {
const
isStreamEnd
=
!!
buffer_finishReason
;
// Parse think
const
{
reasoningContent
:
parsedThinkReasoningContent
,
content
:
p
arsedThinkContent
}
=
const
{
reasoningContent
:
parsedThinkReasoningContent
,
content
:
rawP
arsedThinkContent
}
=
(()
=>
{
if
(
reasoningContent
||
!
parseThinkTag
)
{
isInThinkTag
=
false
;
...
...
@@ -234,6 +265,11 @@ export const parseLLMStreamResponse = () => {
};
})();
const
parsedThinkContent
=
normalizeContentBoundary
({
reasoningContent
:
parsedThinkReasoningContent
,
content
:
rawParsedThinkContent
});
// Parse datset cite
if
(
retainDatasetCite
)
{
return
{
...
...
packages/service/core/chat/HelperBot/dispatch/utils.ts
View file @
05254ce0
...
...
@@ -20,15 +20,14 @@ export const formatAIResponse = ({
}):
AIChatItemValueItemType
[]
=>
{
const
result
:
AIChatItemValueItemType
[]
=
[];
if
(
reasoning
)
{
result
.
push
({
reasoning
:
{
content
:
reasoning
}
});
}
result
.
push
({
...(
reasoning
?
{
reasoning
:
{
content
:
reasoning
}
}
:
{}),
text
:
{
content
:
text
}
...
...
packages/service/core/workflow/dispatch/ai/agent/adapter/eventMapper.ts
View file @
05254ce0
...
...
@@ -192,6 +192,10 @@ export const createWorkflowAgentLoopEventMapper = ({
assistantText
?:
string
;
reasoningText
?:
string
;
})
=>
{
// 没有可见 assistantText 时,不在 request end 阶段回填 reasoning 到已有 tool。
// reasoning 的归属需要按流式顺序附着到后续 content/tool,避免误挂到上一轮工具。
if
(
!
assistantText
)
return
;
const
runtimeToolCalls
=
toolCalls
.
filter
((
call
)
=>
{
const
functionName
=
call
.
function
.
name
;
return
(
...
...
@@ -201,21 +205,22 @@ export const createWorkflowAgentLoopEventMapper = ({
!
isInternalTool
(
functionName
,
internalToolNames
)
);
});
if
(
!
runtimeToolCalls
.
length
||
(
!
assistantText
&&
!
reasoningText
)
)
return
;
if
(
!
runtimeToolCalls
.
length
)
return
;
const
runtimeToolCallIds
=
new
Set
(
runtimeToolCalls
.
map
((
call
)
=>
call
.
id
));
const
existingIndexes
=
assistantResponses
.
map
((
item
,
index
)
=>
item
.
tools
?.
some
((
tool
)
=>
runtimeToolCallIds
.
has
(
tool
.
id
))
?
index
:
-
1
)
.
filter
((
index
)
=>
index
>=
0
);
const
insertIndex
=
existingIndexes
.
length
?
Math
.
min
(...
existingIndexes
)
:
assistantResponses
.
length
;
const
existingIndex
=
assistantResponses
.
findIndex
((
item
)
=>
item
.
tools
?.
some
((
tool
)
=>
runtimeToolCallIds
.
has
(
tool
.
id
))
);
const
insertIndex
=
existingIndex
>=
0
?
existingIndex
:
assistantResponses
.
length
;
const
assistantValue
:
AIChatItemValueItemType
=
{
...(
assistantText
?
{
text
:
{
content
:
assistantText
}
}
:
{}),
...(
reasoningText
?
{
reasoning
:
{
content
:
reasoningText
}
}
:
{})
text
:
{
content
:
assistantText
},
...(
reasoningText
?
{
reasoning
:
{
content
:
reasoningText
},
...(
!
showReasoning
?
{
hideReason
:
true
}
:
{})
}
:
{})
};
assistantResponses
.
splice
(
insertIndex
,
0
,
assistantValue
);
};
...
...
packages/service/core/workflow/dispatch/ai/agent/index.ts
View file @
05254ce0
...
...
@@ -332,26 +332,21 @@ export const dispatchRunAgent = async (props: DispatchAgentModuleProps): Promise
:
result
.
status
===
'aborted'
?
i18nT
(
'chat:completion_finish_error'
)
:
undefined
;
if
(
result
.
reasoningText
)
{
assistantResponses
.
push
({
reasoning
:
{
content
:
result
.
reasoningText
},
...(
aiChatReasoning
===
false
?
{
hideInUI
:
true
}
:
{})
});
}
if
(
result
.
answerText
)
{
assistantResponses
.
push
({
text
:
{
content
:
result
.
answerText
const
reasoningValue
=
result
.
reasoningText
?
{
reasoning
:
{
content
:
result
.
reasoningText
},
...(
aiChatReasoning
===
false
?
{
hideReason
:
true
}
:
{})
}
});
}
else
if
(
errorText
)
{
:
{};
const
finalText
=
result
.
answerText
||
errorText
;
if
(
finalText
)
{
assistantResponses
.
push
({
...
reasoningValue
,
text
:
{
content
:
error
Text
content
:
final
Text
}
});
}
...
...
packages/service/core/workflow/dispatch/ai/agent/piAgent/index.ts
View file @
05254ce0
...
...
@@ -80,19 +80,17 @@ export const dispatchPiAgent = async (props: DispatchAgentModuleProps): Promise<
const
appendFinalAssistantResponses
=
()
=>
{
const
reasoningText
=
piRuntime
?.
getReasoningText
()
||
''
;
const
answerText
=
piRuntime
?.
getAnswerText
()
||
''
;
const
showReasoning
=
aiChatReasoning
!==
false
;
if
(
reasoningText
)
{
assistantResponses
.
push
({
reasoning
:
{
content
:
reasoningText
},
...(
!
showReasoning
?
{
hideInUI
:
true
}
:
{})
});
}
if
(
answerText
)
{
assistantResponses
.
push
({
...(
reasoningText
?
{
reasoning
:
{
content
:
reasoningText
},
...(
aiChatReasoning
===
false
?
{
hideReason
:
true
}
:
{})
}
:
{}),
text
:
{
content
:
answerText
}
...
...
packages/service/core/workflow/dispatch/ai/classifyQuestion.ts
View file @
05254ce0
...
...
@@ -146,7 +146,7 @@ const completions = async ({
body
:
{
model
:
cqModel
.
model
,
temperature
:
0.01
,
messages
:
chats2GPTMessages
({
messages
,
reserveId
:
false
}),
messages
:
chats2GPTMessages
({
messages
,
reserveId
:
false
,
reserveReason
:
false
}),
stream
:
true
},
userKey
:
externalProvider
.
openaiAccount
...
...
packages/service/core/workflow/dispatch/ai/extract.ts
View file @
05254ce0
...
...
@@ -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
({
messages
:
adaptMessages
,
maxContext
:
extractModel
.
maxContext
...
...
@@ -317,7 +321,7 @@ const completions = async (props: ActionProps) => {
body
:
{
model
:
extractModel
.
model
,
temperature
:
0.01
,
messages
:
chats2GPTMessages
({
messages
,
reserveId
:
false
}),
messages
:
chats2GPTMessages
({
messages
,
reserveId
:
false
,
reserveReason
:
false
}),
stream
:
true
},
userKey
:
externalProvider
.
openaiAccount
...
...
packages/service/core/workflow/dispatch/index.ts
View file @
05254ce0
...
...
@@ -1212,15 +1212,16 @@ export class WorkflowQueue {
if
(
assistantResponses
)
{
this
.
chatAssistantResponse
=
this
.
chatAssistantResponse
.
concat
(
assistantResponses
);
}
else
{
if
(
reasoningText
)
{
this
.
chatAssistantResponse
.
push
({
reasoning
:
{
content
:
reasoningText
}
});
}
// reasoning 不能独立落历史;只有存在可见文本时才附着保存。
if
(
answerText
)
{
this
.
chatAssistantResponse
.
push
({
...(
reasoningText
?
{
reasoning
:
{
content
:
reasoningText
}
}
:
{}),
text
:
{
content
:
answerText
}
...
...
packages/service/test/core/ai/utils.test.ts
View file @
05254ce0
...
...
@@ -85,6 +85,14 @@ describe('parseReasoningContent', () => {
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'
,
()
=>
{
expect
(
parseReasoningContent
(
'<think>line1\nline2</think>answer'
)).
toEqual
([
'line1\nline2'
,
...
...
@@ -191,6 +199,22 @@ describe('parseLLMStreamResponse', () => {
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
:
[
{
content
:
'<think>这是'
},
{
content
:
'思考'
},
...
...
packages/service/test/core/workflow/dispatch/ai/agent/adapter/eventMapper.test.ts
View file @
05254ce0
...
...
@@ -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'
,
()
=>
{
const
workflowStreamResponse
=
vi
.
fn
();
const
mapper
=
createWorkflowAgentLoopEventMapper
({
...
...
packages/service/test/core/workflow/dispatch/ai/agent/index.test.ts
View file @
05254ce0
...
...
@@ -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'
}
}
]);
});
});
packages/service/test/core/workflow/dispatch/ai/agent/piAgent/index.test.ts
View file @
05254ce0
...
...
@@ -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'
}
}
]);
});
});
projects/app/src/components/core/chat/ChatContainer/ChatBox/index.tsx
View file @
05254ce0
...
...
@@ -456,17 +456,24 @@ const ChatBox = ({
};
}
if
(
event
===
SseResponseEventEnum
.
answer
||
event
===
SseResponseEventEnum
.
fastAnswer
)
{
const
replaceUpdateValue
=
(
nextValue
:
AIChatItemValueItemType
)
=>
({
...
item
,
value
:
[
...
item
.
value
.
slice
(
0
,
updateIndex
),
nextValue
,
...
item
.
value
.
slice
(
updateIndex
+
1
)
]
});
if
(
reasoningText
)
{
if
(
updateValue
?.
reasoning
)
{
updateValue
.
reasoning
.
content
+=
reasoningText
;
return
{
...
item
,
value
:
[
...
item
.
value
.
slice
(
0
,
updateIndex
),
updateValue
,
...
item
.
value
.
slice
(
updateIndex
+
1
)
]
return
replaceUpdateValue
(
updateValue
);
}
else
if
(
updateValue
?.
text
&&
!
updateValue
.
text
.
content
)
{
updateValue
.
reasoning
=
{
content
:
reasoningText
};
return
replaceUpdateValue
(
updateValue
);
}
else
{
const
val
:
AIChatItemValueItemType
=
{
id
:
responseValueId
,
...
...
@@ -483,14 +490,12 @@ const ChatBox = ({
if
(
text
)
{
if
(
updateValue
?.
text
)
{
updateValue
.
text
.
content
+=
text
;
return
{
...
item
,
value
:
[
...
item
.
value
.
slice
(
0
,
updateIndex
),
updateValue
,
...
item
.
value
.
slice
(
updateIndex
+
1
)
]
return
replaceUpdateValue
(
updateValue
);
}
else
if
(
updateValue
?.
reasoning
)
{
updateValue
.
text
=
{
content
:
text
};
return
replaceUpdateValue
(
updateValue
);
}
else
{
const
newValue
:
AIChatItemValueItemType
=
{
id
:
responseValueId
,
...
...
projects/app/src/components/core/chat/HelperBot/components/AIItem.tsx
View file @
05254ce0
...
...
@@ -173,6 +173,14 @@ const AIItem = ({
onSubmitCollectionForm
:
(
formData
:
string
)
=>
void
;
})
=>
{
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
(
<
Box
_hover=
{
{
...
...
@@ -192,8 +200,7 @@ const AIItem = ({
color=
{
'myGray.900'
}
bg=
{
'myGray.100'
}
>
{
chat
.
value
.
length
===
1
&&
(
!
(
'text'
in
chat
.
value
[
0
])
||
!
chat
.
value
[
0
].
text
?.
content
)
?
(
{
isWaitingForResponse
?
(
<
RenderText
showAnimation=
{
true
}
text=
{
t
(
'chat:chat.waiting_for_response'
)
}
/>
)
:
(
<>
...
...
@@ -203,25 +210,6 @@ const AIItem = ({
<
RenderText
key=
{
i
}
showAnimation=
{
false
}
text=
{
t
(
'chat:plan_check_tip'
)
}
/>
);
}
if
(
'text'
in
value
&&
value
.
text
)
{
return
(
<
RenderText
key=
{
i
}
showAnimation=
{
isChatting
&&
isLastChild
}
text=
{
value
.
text
.
content
}
/>
);
}
if
(
'reasoning'
in
value
&&
value
.
reasoning
)
{
return
(
<
RenderResoningContent
key=
{
i
}
isChatting=
{
isChatting
}
isLastResponseValue=
{
isLastChild
}
content=
{
value
.
reasoning
.
content
}
/>
);
}
if
(
'collectionForm'
in
value
&&
value
.
collectionForm
)
{
return
(
<
RenderCollectionForm
...
...
@@ -232,6 +220,24 @@ const AIItem = ({
/>
);
}
return
(
<
React
.
Fragment
key=
{
i
}
>
{
'reasoning'
in
value
&&
value
.
reasoning
&&
!
value
.
hideReason
&&
(
<
RenderResoningContent
isChatting=
{
isChatting
}
isLastResponseValue=
{
isLastChild
}
content=
{
value
.
reasoning
.
content
}
/>
)
}
{
'text'
in
value
&&
value
.
text
&&
(
<
RenderText
showAnimation=
{
isChatting
&&
isLastChild
}
text=
{
value
.
text
.
content
}
/>
)
}
</
React
.
Fragment
>
);
})
}
</>
)
}
...
...
projects/app/src/components/core/chat/HelperBot/index.tsx
View file @
05254ce0
...
...
@@ -180,17 +180,31 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro
}
if
(
event
===
SseResponseEventEnum
.
answer
||
event
===
SseResponseEventEnum
.
fastAnswer
)
{
const
replaceUpdateValue
=
(
nextValue
:
AIChatItemValueItemType
)
=>
({
...
item
,
value
:
[
...
item
.
value
.
slice
(
0
,
updateIndex
),
nextValue
,
...
item
.
value
.
slice
(
updateIndex
+
1
)
]
});
if
(
reasoningText
)
{
if
(
'reasoning'
in
updateValue
&&
updateValue
.
reasoning
)
{
updateValue
.
reasoning
.
content
+=
reasoningText
;
return
{
...
item
,
value
:
[
...
item
.
value
.
slice
(
0
,
updateIndex
),
updateValue
,
...
item
.
value
.
slice
(
updateIndex
+
1
)
]
};
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
{
const
val
:
AIChatItemValueItemType
=
{
reasoning
:
{
...
...
@@ -205,15 +219,20 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro
}
if
(
text
)
{
if
(
'text'
in
updateValue
&&
updateValue
.
text
)
{
updateValue
.
text
.
content
+=
text
;
return
{
...
item
,
value
:
[
...
item
.
value
.
slice
(
0
,
updateIndex
),
updateValue
,
...
item
.
value
.
slice
(
updateIndex
+
1
)
]
};
return
replaceUpdateValue
({
...
updateValue
,
text
:
{
...
updateValue
.
text
,
content
:
updateValue
.
text
.
content
+
text
}
});
}
else
if
(
'reasoning'
in
updateValue
&&
updateValue
.
reasoning
)
{
return
replaceUpdateValue
({
...
updateValue
,
text
:
{
content
:
text
}
});
}
else
{
const
newValue
:
AIChatItemValueItemType
=
{
text
:
{
...
...
@@ -258,7 +277,7 @@ const ChatBox = ({ type, metadata, onApply, ChatBoxRef, ...props }: HelperBotPro
}
const
chatItemDataId
=
getNanoid
(
24
);
le
t
newChatList
:
HelperBotChatItemSiteType
[]
=
[
cons
t
newChatList
:
HelperBotChatItemSiteType
[]
=
[
...
chatRecords
,
// 用户消息
{
...
...
projects/app/src/components/core/chat/components/AIResponseBox/index.tsx
View file @
05254ce0
...
...
@@ -43,7 +43,7 @@ const AIResponseBox = ({
const
responseBlocks
:
React
.
ReactNode
[]
=
[];
if
(
'reasoning'
in
value
&&
value
.
reasoning
)
{
if
(
'reasoning'
in
value
&&
value
.
reasoning
&&
!
value
.
hideReason
)
{
responseBlocks
.
push
(
<
RenderReasoningContent
key=
"reasoning"
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment