fix: 统一适配 DeepSeek 工具与思考历史回传
- 结构化持久化 assistant/tool 历史,恢复真实消息链回放 - 为 DeepSeek 显式开启 thinking 协议配置,并补齐 public-api 与工具英文名兜底 - 增加聊天历史回放、tool 名称与请求参数解析相关测试
This commit is contained in:
@@ -5,55 +5,136 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 负责聚合单轮聊天中的 assistant thinking、tool 调用和最终回答,
|
||||
* 并产出可用于历史回放的结构化 payload。
|
||||
*/
|
||||
public class ChatAssistantAccumulator {
|
||||
|
||||
private final StringBuilder content = new StringBuilder();
|
||||
private final StringBuilder reasoning = new StringBuilder();
|
||||
private final StringBuilder displayReasoning = new StringBuilder();
|
||||
private final List<Map<String, Object>> chains = new ArrayList<>();
|
||||
private final List<Map<String, Object>> messageChain = new ArrayList<>();
|
||||
private final List<Map<String, Object>> toolMessages = new ArrayList<>();
|
||||
private Map<String, Object> latestToolCallAssistant;
|
||||
private boolean toolCallBatchOpen;
|
||||
|
||||
/**
|
||||
* 追加当前 assistant 片段的文本内容。
|
||||
*
|
||||
* @param delta 内容增量
|
||||
*/
|
||||
public void appendContent(String delta) {
|
||||
if (delta != null && !delta.isEmpty()) {
|
||||
content.append(delta);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加当前 assistant 片段的 reasoning 内容。
|
||||
*
|
||||
* @param delta reasoning 增量
|
||||
*/
|
||||
public void appendReasoning(String delta) {
|
||||
if (delta != null && !delta.isEmpty()) {
|
||||
reasoning.append(delta);
|
||||
displayReasoning.append(delta);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 tool call,同时把当前 assistant 片段固化为一条结构化 assistant 消息。
|
||||
*
|
||||
* @param id tool call id
|
||||
* @param name tool 名称
|
||||
* @param arguments tool 参数
|
||||
*/
|
||||
public void appendToolCall(String id, String name, Object arguments) {
|
||||
Map<String, Object> chain = findToolChain(id, name);
|
||||
chain.put("status", "TOOL_CALL");
|
||||
chain.put("result", arguments);
|
||||
chain.put("arguments", arguments);
|
||||
|
||||
Map<String, Object> assistantMessage = ensureToolCallAssistantMessage();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Map<String, Object>> toolCalls = (List<Map<String, Object>>) assistantMessage.computeIfAbsent("toolCalls",
|
||||
key -> new ArrayList<Map<String, Object>>());
|
||||
Map<String, Object> toolCall = new LinkedHashMap<>();
|
||||
toolCall.put("id", id);
|
||||
toolCall.put("name", name);
|
||||
toolCall.put("arguments", arguments == null ? null : String.valueOf(arguments));
|
||||
toolCalls.add(toolCall);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 tool result,并附加到结构化消息链中。
|
||||
*
|
||||
* @param id tool call id
|
||||
* @param name tool 名称
|
||||
* @param result tool 结果
|
||||
*/
|
||||
public void appendToolResult(String id, String name, Object result) {
|
||||
Map<String, Object> chain = findToolChain(id, name);
|
||||
chain.put("status", "TOOL_RESULT");
|
||||
chain.put("result", result);
|
||||
Map<String, Object> toolMessage = ChatRuntimeHistoryPayloadHelper.toolMessage(
|
||||
id,
|
||||
result == null ? null : String.valueOf(result)
|
||||
);
|
||||
toolMessages.add(toolMessage);
|
||||
messageChain.add(ChatRuntimeHistoryPayloadHelper.deepCopyMap(toolMessage));
|
||||
toolCallBatchOpen = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前 assistant 片段的文本内容。
|
||||
*
|
||||
* @return 文本内容
|
||||
*/
|
||||
public String getContent() {
|
||||
return content.toString();
|
||||
}
|
||||
|
||||
public Map<String, Object> buildPayload() {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
/**
|
||||
* 获取最近一次 tool-call assistant 的 reasoning 内容,供实时内存消息回写复用。
|
||||
*
|
||||
* @return reasoning 内容
|
||||
*/
|
||||
public String getLatestToolCallReasoning() {
|
||||
return latestToolCallAssistant == null ? null : stringValue(latestToolCallAssistant.get("reasoningContent"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近一次 tool-call assistant 的内容,供实时内存消息回写复用。
|
||||
*
|
||||
* @return 内容
|
||||
*/
|
||||
public String getLatestToolCallContent() {
|
||||
return latestToolCallAssistant == null ? null : stringValue(latestToolCallAssistant.get("content"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 产出结构化 payload。
|
||||
*
|
||||
* @param finalContent 最终 assistant 文本
|
||||
* @return payload
|
||||
*/
|
||||
public Map<String, Object> buildPayload(String finalContent) {
|
||||
List<Map<String, Object>> payloadChains = new ArrayList<>();
|
||||
if (reasoning.length() > 0) {
|
||||
if (displayReasoning.length() > 0) {
|
||||
Map<String, Object> think = new LinkedHashMap<>();
|
||||
think.put("reasoning_content", reasoning.toString());
|
||||
think.put("reasoning_content", displayReasoning.toString());
|
||||
think.put("thinkingStatus", "end");
|
||||
think.put("thinlCollapse", Boolean.TRUE);
|
||||
payloadChains.add(think);
|
||||
}
|
||||
payloadChains.addAll(chains);
|
||||
if (!payloadChains.isEmpty()) {
|
||||
payload.put("chains", payloadChains);
|
||||
List<Map<String, Object>> payloadMessageChain = ChatRuntimeHistoryPayloadHelper.deepCopyList(messageChain);
|
||||
Map<String, Object> finalAssistantMessage = buildFinalAssistantMessage(finalContent);
|
||||
if (!finalAssistantMessage.isEmpty()) {
|
||||
payloadMessageChain.add(finalAssistantMessage);
|
||||
}
|
||||
return payload;
|
||||
return ChatRuntimeHistoryPayloadHelper.buildPayload(payloadMessageChain, toolMessages, payloadChains);
|
||||
}
|
||||
|
||||
private Map<String, Object> findToolChain(String id, String name) {
|
||||
@@ -71,4 +152,43 @@ public class ChatAssistantAccumulator {
|
||||
chains.add(chain);
|
||||
return chain;
|
||||
}
|
||||
|
||||
private Map<String, Object> ensureToolCallAssistantMessage() {
|
||||
if (toolCallBatchOpen && latestToolCallAssistant != null && !hasPendingAssistantContent()) {
|
||||
return latestToolCallAssistant;
|
||||
}
|
||||
latestToolCallAssistant = ChatRuntimeHistoryPayloadHelper.assistantMessage(
|
||||
content.length() == 0 ? null : content.toString(),
|
||||
reasoning.length() == 0 ? null : reasoning.toString(),
|
||||
null
|
||||
);
|
||||
messageChain.add(latestToolCallAssistant);
|
||||
content.setLength(0);
|
||||
reasoning.setLength(0);
|
||||
toolCallBatchOpen = true;
|
||||
return latestToolCallAssistant;
|
||||
}
|
||||
|
||||
private Map<String, Object> buildFinalAssistantMessage(String finalContent) {
|
||||
String assistantContent = finalContent;
|
||||
if ((assistantContent == null || assistantContent.isEmpty()) && content.length() > 0) {
|
||||
assistantContent = content.toString();
|
||||
}
|
||||
if ((assistantContent == null || assistantContent.isEmpty()) && reasoning.length() == 0) {
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
return ChatRuntimeHistoryPayloadHelper.assistantMessage(
|
||||
assistantContent,
|
||||
reasoning.length() == 0 ? null : reasoning.toString(),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private boolean hasPendingAssistantContent() {
|
||||
return content.length() > 0 || reasoning.length() > 0;
|
||||
}
|
||||
|
||||
private String stringValue(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
package tech.easyflow.core.runtime;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 聊天运行时历史 payload 的统一读写工具。
|
||||
* <p>
|
||||
* 该工具只处理 {@link Map} / {@link List} 结构,避免把 easy-agents 类型泄漏到通用协议模块。
|
||||
*/
|
||||
public final class ChatRuntimeHistoryPayloadHelper {
|
||||
|
||||
public static final String KEY_MESSAGE_CHAIN = "messageChain";
|
||||
public static final String KEY_ASSISTANT_MESSAGE = "assistantMessage";
|
||||
public static final String KEY_FINAL_ASSISTANT_MESSAGE = "finalAssistantMessage";
|
||||
public static final String KEY_TOOL_MESSAGES = "toolMessages";
|
||||
public static final String KEY_DISPLAY_CHAINS = "displayChains";
|
||||
public static final String KEY_CHAINS = "chains";
|
||||
|
||||
private ChatRuntimeHistoryPayloadHelper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 assistant 历史消息结构。
|
||||
*
|
||||
* @param content assistant 内容
|
||||
* @param reasoningContent assistant reasoning 内容
|
||||
* @param toolCalls assistant tool calls
|
||||
* @return assistant 历史消息结构
|
||||
*/
|
||||
public static Map<String, Object> assistantMessage(String content,
|
||||
String reasoningContent,
|
||||
List<Map<String, Object>> toolCalls) {
|
||||
Map<String, Object> message = new LinkedHashMap<>();
|
||||
message.put("role", "assistant");
|
||||
if (content != null) {
|
||||
message.put("content", content);
|
||||
}
|
||||
if (reasoningContent != null && !reasoningContent.isEmpty()) {
|
||||
message.put("reasoningContent", reasoningContent);
|
||||
}
|
||||
if (toolCalls != null && !toolCalls.isEmpty()) {
|
||||
message.put("toolCalls", deepCopyList(toolCalls));
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 tool 历史消息结构。
|
||||
*
|
||||
* @param toolCallId tool call id
|
||||
* @param content tool 结果内容
|
||||
* @return tool 历史消息结构
|
||||
*/
|
||||
public static Map<String, Object> toolMessage(String toolCallId, String content) {
|
||||
Map<String, Object> message = new LinkedHashMap<>();
|
||||
message.put("role", "tool");
|
||||
message.put("toolCallId", toolCallId);
|
||||
if (content != null) {
|
||||
message.put("content", content);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造统一的历史 payload。
|
||||
*
|
||||
* @param messageChain 完整 assistant/tool 历史链
|
||||
* @param toolMessages tool 历史列表
|
||||
* @param displayChains 前端展示链
|
||||
* @return payload
|
||||
*/
|
||||
public static Map<String, Object> buildPayload(List<Map<String, Object>> messageChain,
|
||||
List<Map<String, Object>> toolMessages,
|
||||
List<Map<String, Object>> displayChains) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
List<Map<String, Object>> safeMessageChain = deepCopyList(messageChain);
|
||||
List<Map<String, Object>> safeToolMessages = deepCopyList(toolMessages);
|
||||
List<Map<String, Object>> safeDisplayChains = deepCopyList(displayChains);
|
||||
if (!safeMessageChain.isEmpty()) {
|
||||
payload.put(KEY_MESSAGE_CHAIN, safeMessageChain);
|
||||
Map<String, Object> firstAssistant = findAssistant(safeMessageChain, false);
|
||||
if (!firstAssistant.isEmpty()) {
|
||||
payload.put(KEY_ASSISTANT_MESSAGE, firstAssistant);
|
||||
}
|
||||
Map<String, Object> lastAssistant = findAssistant(safeMessageChain, true);
|
||||
if (!lastAssistant.isEmpty() && !lastAssistant.equals(firstAssistant)) {
|
||||
payload.put(KEY_FINAL_ASSISTANT_MESSAGE, lastAssistant);
|
||||
}
|
||||
}
|
||||
if (!safeToolMessages.isEmpty()) {
|
||||
payload.put(KEY_TOOL_MESSAGES, safeToolMessages);
|
||||
}
|
||||
if (!safeDisplayChains.isEmpty()) {
|
||||
payload.put(KEY_DISPLAY_CHAINS, safeDisplayChains);
|
||||
payload.put(KEY_CHAINS, deepCopyList(safeDisplayChains));
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取结构化消息链。
|
||||
*
|
||||
* @param payload contentPayload
|
||||
* @return 结构化消息链
|
||||
*/
|
||||
public static List<Map<String, Object>> getMessageChain(Map<String, Object> payload) {
|
||||
return getMapList(payload == null ? null : payload.get(KEY_MESSAGE_CHAIN));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 assistant 历史消息。
|
||||
*
|
||||
* @param payload contentPayload
|
||||
* @return assistant 历史消息
|
||||
*/
|
||||
public static Map<String, Object> getAssistantMessage(Map<String, Object> payload) {
|
||||
return getMap(payload == null ? null : payload.get(KEY_ASSISTANT_MESSAGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取最终 assistant 历史消息。
|
||||
*
|
||||
* @param payload contentPayload
|
||||
* @return 最终 assistant 历史消息
|
||||
*/
|
||||
public static Map<String, Object> getFinalAssistantMessage(Map<String, Object> payload) {
|
||||
return getMap(payload == null ? null : payload.get(KEY_FINAL_ASSISTANT_MESSAGE));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 tool 历史消息列表。
|
||||
*
|
||||
* @param payload contentPayload
|
||||
* @return tool 历史消息列表
|
||||
*/
|
||||
public static List<Map<String, Object>> getToolMessages(Map<String, Object> payload) {
|
||||
return getMapList(payload == null ? null : payload.get(KEY_TOOL_MESSAGES));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 payload 是否已经包含新结构化历史。
|
||||
*
|
||||
* @param payload contentPayload
|
||||
* @return true 表示包含新结构
|
||||
*/
|
||||
public static boolean hasStructuredHistory(Map<String, Object> payload) {
|
||||
return !getMessageChain(payload).isEmpty()
|
||||
|| !getAssistantMessage(payload).isEmpty()
|
||||
|| !getToolMessages(payload).isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取对象为 Map。
|
||||
*
|
||||
* @param value 值
|
||||
* @return Map 视图
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Object> getMap(Object value) {
|
||||
if (!(value instanceof Map<?, ?> source)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
for (Map.Entry<?, ?> entry : source.entrySet()) {
|
||||
if (entry.getKey() != null) {
|
||||
result.put(String.valueOf(entry.getKey()), entry.getValue());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取对象为 Map 列表。
|
||||
*
|
||||
* @param value 值
|
||||
* @return Map 列表
|
||||
*/
|
||||
public static List<Map<String, Object>> getMapList(Object value) {
|
||||
if (!(value instanceof List<?> source)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Map<String, Object>> result = new ArrayList<>(source.size());
|
||||
for (Object item : source) {
|
||||
Map<String, Object> map = getMap(item);
|
||||
if (!map.isEmpty()) {
|
||||
result.add(map);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝 Map 列表。
|
||||
*
|
||||
* @param source 原始列表
|
||||
* @return 深拷贝后的列表
|
||||
*/
|
||||
public static List<Map<String, Object>> deepCopyList(List<Map<String, Object>> source) {
|
||||
if (source == null || source.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<Map<String, Object>> copy = new ArrayList<>(source.size());
|
||||
for (Map<String, Object> item : source) {
|
||||
copy.add(deepCopyMap(item));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深拷贝 Map。
|
||||
*
|
||||
* @param source 原始 Map
|
||||
* @return 深拷贝后的 Map
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static Map<String, Object> deepCopyMap(Map<String, Object> source) {
|
||||
if (source == null || source.isEmpty()) {
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
Map<String, Object> copy = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, Object> entry : source.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof Map<?, ?> mapValue) {
|
||||
copy.put(entry.getKey(), deepCopyMap((Map<String, Object>) mapValue));
|
||||
} else if (value instanceof List<?> listValue) {
|
||||
copy.put(entry.getKey(), deepCopyValueList((List<Object>) listValue));
|
||||
} else {
|
||||
copy.put(entry.getKey(), value);
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static List<Object> deepCopyValueList(List<Object> source) {
|
||||
List<Object> copy = new ArrayList<>(source.size());
|
||||
for (Object item : source) {
|
||||
if (item instanceof Map<?, ?> mapItem) {
|
||||
copy.add(deepCopyMap((Map<String, Object>) mapItem));
|
||||
} else if (item instanceof List<?> listItem) {
|
||||
copy.add(deepCopyValueList((List<Object>) listItem));
|
||||
} else {
|
||||
copy.add(item);
|
||||
}
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
|
||||
private static Map<String, Object> findAssistant(List<Map<String, Object>> messageChain, boolean reverse) {
|
||||
if (messageChain == null || messageChain.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
if (reverse) {
|
||||
for (int i = messageChain.size() - 1; i >= 0; i--) {
|
||||
Map<String, Object> item = messageChain.get(i);
|
||||
if ("assistant".equalsIgnoreCase(String.valueOf(item.get("role")))) {
|
||||
return deepCopyMap(item);
|
||||
}
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
for (Map<String, Object> item : messageChain) {
|
||||
if ("assistant".equalsIgnoreCase(String.valueOf(item.get("role")))) {
|
||||
return deepCopyMap(item);
|
||||
}
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
}
|
||||
@@ -87,6 +87,7 @@ public class ChatStreamListener implements StreamResponseListener {
|
||||
sendToolCallEnvelope(toolCall);
|
||||
}
|
||||
}
|
||||
applyToolCallHistorySnapshot(aiMessage);
|
||||
aiMessage.setContent(null);
|
||||
memoryPrompt.addMessage(aiMessage);
|
||||
List<ToolMessage> toolMessages = aiMessageResponse.executeToolCallsAndGetToolMessages();
|
||||
@@ -275,11 +276,29 @@ public class ChatStreamListener implements StreamResponseListener {
|
||||
message.setContentType("TEXT");
|
||||
String fullContent = context != null && context.getFullMessage() != null ? context.getFullMessage().getContent() : null;
|
||||
message.setContentText(StringUtil.hasText(fullContent) ? fullContent : assistantAccumulator.getContent());
|
||||
message.setContentPayload(assistantAccumulator.buildPayload());
|
||||
message.setContentPayload(assistantAccumulator.buildPayload(message.getContentText()));
|
||||
message.setCreatedAt(new Date());
|
||||
message.setSenderId(runtimeContext.getAssistantId());
|
||||
message.setSenderName(runtimeContext.getAssistantName());
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 tool call assistant 写入临时 memory 前,把 reasoning/content 快照回填到消息对象中,
|
||||
* 以便前端 history 透传和 DeepSeek 下一轮请求都能拿到完整链路。
|
||||
*
|
||||
* @param aiMessage tool call assistant 消息
|
||||
*/
|
||||
private void applyToolCallHistorySnapshot(AiMessage aiMessage) {
|
||||
if (aiMessage == null) {
|
||||
return;
|
||||
}
|
||||
if (!StringUtil.hasText(aiMessage.getReasoningContent())) {
|
||||
aiMessage.setReasoningContent(assistantAccumulator.getLatestToolCallReasoning());
|
||||
}
|
||||
if (!StringUtil.hasText(aiMessage.getTextContent())) {
|
||||
aiMessage.setFullContent(assistantAccumulator.getLatestToolCallContent());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
package tech.easyflow.ai.easyagents.memory;
|
||||
|
||||
import com.easyagents.core.message.AiMessage;
|
||||
import com.easyagents.core.message.Message;
|
||||
import com.easyagents.core.message.ToolCall;
|
||||
import com.easyagents.core.message.ToolMessage;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeHistoryPayloadHelper;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeMessage;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 负责把聊天运行时持久化 payload 恢复为 easy-agents 消息链。
|
||||
*/
|
||||
public final class ChatRuntimeHistoryMessageMapper {
|
||||
|
||||
private ChatRuntimeHistoryMessageMapper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 从运行时消息中恢复 assistant / tool 历史消息链。
|
||||
*
|
||||
* @param runtimeMessage 运行时消息
|
||||
* @return 恢复后的消息列表
|
||||
*/
|
||||
public static List<Message> toStructuredMessages(ChatRuntimeMessage runtimeMessage) {
|
||||
if (runtimeMessage == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Map<String, Object> payload = runtimeMessage.getContentPayload();
|
||||
List<Map<String, Object>> messageChain = ChatRuntimeHistoryPayloadHelper.getMessageChain(payload);
|
||||
if (!messageChain.isEmpty()) {
|
||||
return toMessages(messageChain);
|
||||
}
|
||||
if (!ChatRuntimeHistoryPayloadHelper.hasStructuredHistory(payload)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Message> messages = new ArrayList<>();
|
||||
Map<String, Object> assistantMessage = ChatRuntimeHistoryPayloadHelper.getAssistantMessage(payload);
|
||||
if (!assistantMessage.isEmpty()) {
|
||||
AiMessage aiMessage = toAiMessage(assistantMessage);
|
||||
if (aiMessage != null) {
|
||||
messages.add(aiMessage);
|
||||
}
|
||||
}
|
||||
List<Map<String, Object>> toolMessages = ChatRuntimeHistoryPayloadHelper.getToolMessages(payload);
|
||||
messages.addAll(toMessages(toolMessages));
|
||||
Map<String, Object> finalAssistantMessage = ChatRuntimeHistoryPayloadHelper.getFinalAssistantMessage(payload);
|
||||
if (!finalAssistantMessage.isEmpty()) {
|
||||
AiMessage finalAiMessage = toAiMessage(finalAssistantMessage);
|
||||
if (finalAiMessage != null) {
|
||||
messages.add(finalAiMessage);
|
||||
}
|
||||
} else if (!messages.isEmpty()
|
||||
&& (runtimeMessage.getContentText() != null && !runtimeMessage.getContentText().isBlank())
|
||||
&& assistantMessage.containsKey("toolCalls")) {
|
||||
messages.add(new AiMessage(runtimeMessage.getContentText()));
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
private static List<Message> toMessages(List<Map<String, Object>> messageChain) {
|
||||
List<Message> messages = new ArrayList<>();
|
||||
for (Map<String, Object> item : messageChain) {
|
||||
String role = String.valueOf(item.get("role"));
|
||||
if ("assistant".equalsIgnoreCase(role)) {
|
||||
AiMessage aiMessage = toAiMessage(item);
|
||||
if (aiMessage != null) {
|
||||
messages.add(aiMessage);
|
||||
}
|
||||
} else if ("tool".equalsIgnoreCase(role)) {
|
||||
ToolMessage toolMessage = toToolMessage(item);
|
||||
if (toolMessage != null) {
|
||||
messages.add(toolMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
private static AiMessage toAiMessage(Map<String, Object> item) {
|
||||
if (item == null || item.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String content = stringValue(item.get("content"));
|
||||
String reasoningContent = stringValue(firstNonNull(item.get("reasoningContent"), item.get("reasoning_content")));
|
||||
List<ToolCall> toolCalls = toToolCalls(ChatRuntimeHistoryPayloadHelper.getMapList(firstNonNull(item.get("toolCalls"), item.get("tool_calls"))));
|
||||
boolean hasContent = content != null && !content.isBlank();
|
||||
boolean hasReasoning = reasoningContent != null && !reasoningContent.isBlank();
|
||||
boolean hasToolCalls = toolCalls != null && !toolCalls.isEmpty();
|
||||
if (!hasContent && !hasReasoning && !hasToolCalls) {
|
||||
return null;
|
||||
}
|
||||
AiMessage aiMessage = new AiMessage(hasContent ? content : null);
|
||||
if (hasReasoning) {
|
||||
aiMessage.setReasoningContent(reasoningContent);
|
||||
aiMessage.setFullReasoningContent(reasoningContent);
|
||||
}
|
||||
if (hasToolCalls) {
|
||||
aiMessage.setToolCalls(toolCalls);
|
||||
}
|
||||
return aiMessage;
|
||||
}
|
||||
|
||||
private static ToolMessage toToolMessage(Map<String, Object> item) {
|
||||
if (item == null || item.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String content = stringValue(item.get("content"));
|
||||
String toolCallId = stringValue(firstNonNull(item.get("toolCallId"), item.get("tool_call_id")));
|
||||
if ((content == null || content.isBlank()) && (toolCallId == null || toolCallId.isBlank())) {
|
||||
return null;
|
||||
}
|
||||
ToolMessage toolMessage = new ToolMessage();
|
||||
toolMessage.setContent(content);
|
||||
toolMessage.setToolCallId(toolCallId);
|
||||
return toolMessage;
|
||||
}
|
||||
|
||||
private static List<ToolCall> toToolCalls(List<Map<String, Object>> rawToolCalls) {
|
||||
if (rawToolCalls == null || rawToolCalls.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<ToolCall> toolCalls = new ArrayList<>(rawToolCalls.size());
|
||||
for (Map<String, Object> rawToolCall : rawToolCalls) {
|
||||
ToolCall toolCall = new ToolCall();
|
||||
toolCall.setId(stringValue(rawToolCall.get("id")));
|
||||
toolCall.setName(stringValue(firstNonNull(rawToolCall.get("name"), rawToolCall.get("toolName"))));
|
||||
toolCall.setArguments(stringValue(rawToolCall.get("arguments")));
|
||||
toolCalls.add(toolCall);
|
||||
}
|
||||
return toolCalls;
|
||||
}
|
||||
|
||||
private static Object firstNonNull(Object first, Object second) {
|
||||
return first != null ? first : second;
|
||||
}
|
||||
|
||||
private static String stringValue(Object value) {
|
||||
return value == null ? null : String.valueOf(value);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ import tech.easyflow.core.runtime.ChatRuntimeMessage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 从聊天运行时消息恢复 easy-agents 历史消息。
|
||||
*/
|
||||
public class RuntimeChatMemory implements ChatMemory {
|
||||
|
||||
private final Object id;
|
||||
@@ -19,10 +22,7 @@ public class RuntimeChatMemory implements ChatMemory {
|
||||
this.id = id;
|
||||
if (runtimeMessages != null) {
|
||||
for (ChatRuntimeMessage runtimeMessage : runtimeMessages) {
|
||||
Message message = toMessage(runtimeMessage);
|
||||
if (message != null) {
|
||||
this.messages.add(message);
|
||||
}
|
||||
this.messages.addAll(toMessages(runtimeMessage));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,20 +55,40 @@ public class RuntimeChatMemory implements ChatMemory {
|
||||
return id;
|
||||
}
|
||||
|
||||
private Message toMessage(ChatRuntimeMessage runtimeMessage) {
|
||||
if (runtimeMessage == null || runtimeMessage.getContentText() == null || runtimeMessage.getContentText().isBlank()) {
|
||||
return null;
|
||||
private List<Message> toMessages(ChatRuntimeMessage runtimeMessage) {
|
||||
if (runtimeMessage == null) {
|
||||
return List.of();
|
||||
}
|
||||
String role = runtimeMessage.getRole();
|
||||
if ("assistant".equalsIgnoreCase(role)) {
|
||||
return new AiMessage(runtimeMessage.getContentText());
|
||||
List<Message> structuredMessages = ChatRuntimeHistoryMessageMapper.toStructuredMessages(runtimeMessage);
|
||||
if (!structuredMessages.isEmpty()) {
|
||||
return structuredMessages;
|
||||
}
|
||||
if (runtimeMessage.getContentText() == null || runtimeMessage.getContentText().isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return List.of(new AiMessage(runtimeMessage.getContentText()));
|
||||
}
|
||||
if ("system".equalsIgnoreCase(role)) {
|
||||
return new SystemMessage(runtimeMessage.getContentText());
|
||||
if (runtimeMessage.getContentText() == null || runtimeMessage.getContentText().isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return List.of(new SystemMessage(runtimeMessage.getContentText()));
|
||||
}
|
||||
if ("tool".equalsIgnoreCase(role)) {
|
||||
return new SystemMessage(runtimeMessage.getContentText());
|
||||
List<Message> structuredMessages = ChatRuntimeHistoryMessageMapper.toStructuredMessages(runtimeMessage);
|
||||
if (!structuredMessages.isEmpty()) {
|
||||
return structuredMessages;
|
||||
}
|
||||
return new UserMessage(runtimeMessage.getContentText());
|
||||
if (runtimeMessage.getContentText() == null || runtimeMessage.getContentText().isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return List.of(new SystemMessage(runtimeMessage.getContentText()));
|
||||
}
|
||||
if (runtimeMessage.getContentText() == null || runtimeMessage.getContentText().isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
return List.of(new UserMessage(runtimeMessage.getContentText()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package tech.easyflow.ai.easyagents.tool;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 聊天工具名称辅助类。
|
||||
*
|
||||
* <p>聊天工具名称最终会作为 OpenAI-compatible 协议里的 function.name 发给上游模型,
|
||||
* 因此在启用英文名称时需要确保名称稳定且满足 ASCII 约束。</p>
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-05-11
|
||||
*/
|
||||
public final class ChatToolNameHelper {
|
||||
|
||||
private static final Pattern SAFE_TOOL_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$");
|
||||
|
||||
private ChatToolNameHelper() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析聊天工具名称。
|
||||
*
|
||||
* @param needEnglishName 是否优先使用英文名称
|
||||
* @param englishName 英文名称
|
||||
* @param displayName 展示名称
|
||||
* @param fallbackPrefix 安全兜底名前缀
|
||||
* @param resourceId 资源 ID
|
||||
* @return 最终工具名称
|
||||
*/
|
||||
public static String resolveToolName(boolean needEnglishName,
|
||||
String englishName,
|
||||
String displayName,
|
||||
String fallbackPrefix,
|
||||
BigInteger resourceId) {
|
||||
if (!needEnglishName) {
|
||||
return StringUtils.hasText(displayName) ? displayName : buildFallbackName(fallbackPrefix, resourceId);
|
||||
}
|
||||
if (StringUtils.hasText(englishName) && SAFE_TOOL_NAME_PATTERN.matcher(englishName).matches()) {
|
||||
return englishName;
|
||||
}
|
||||
return buildFallbackName(fallbackPrefix, resourceId);
|
||||
}
|
||||
|
||||
private static String buildFallbackName(String fallbackPrefix, BigInteger resourceId) {
|
||||
String prefix = StringUtils.hasText(fallbackPrefix) ? fallbackPrefix : "tool";
|
||||
String suffix = resourceId == null ? "unknown" : resourceId.toString();
|
||||
return prefix + "_" + suffix;
|
||||
}
|
||||
}
|
||||
@@ -67,11 +67,13 @@ public class DocumentCollectionTool extends BaseTool {
|
||||
this.knowledgeId = documentCollection.getId();
|
||||
this.retrievalMode = retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode;
|
||||
this.chatTimeContext = chatTimeContext;
|
||||
if (needEnglishName) {
|
||||
this.name = documentCollection.getEnglishName();
|
||||
} else {
|
||||
this.name = documentCollection.getTitle();
|
||||
}
|
||||
this.name = ChatToolNameHelper.resolveToolName(
|
||||
needEnglishName,
|
||||
documentCollection.getEnglishName(),
|
||||
documentCollection.getTitle(),
|
||||
"knowledge",
|
||||
documentCollection.getId()
|
||||
);
|
||||
this.description = documentCollection.getDescription();
|
||||
this.parameters = getDefaultParameters();
|
||||
}
|
||||
|
||||
@@ -26,11 +26,13 @@ public class WorkflowTool extends BaseTool {
|
||||
public WorkflowTool(Workflow workflow, boolean needEnglishName, String definitionId) {
|
||||
this.workflowId = workflow.getId();
|
||||
this.definitionId = definitionId;
|
||||
if (needEnglishName) {
|
||||
this.name = workflow.getEnglishName();
|
||||
} else {
|
||||
this.name = workflow.getTitle();
|
||||
}
|
||||
this.name = ChatToolNameHelper.resolveToolName(
|
||||
needEnglishName,
|
||||
workflow.getEnglishName(),
|
||||
workflow.getTitle(),
|
||||
"workflow",
|
||||
workflow.getId()
|
||||
);
|
||||
this.description = workflow.getDescription();
|
||||
this.parameters = toParameters(workflow, definitionId);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.easyagents.core.message.*;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.easyagents.core.message.AiMessage;
|
||||
import com.easyagents.core.message.Message;
|
||||
import com.easyagents.core.message.SystemMessage;
|
||||
import com.easyagents.core.message.ToolCall;
|
||||
import com.easyagents.core.message.ToolMessage;
|
||||
import com.easyagents.core.message.UserMessage;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
@@ -11,6 +14,9 @@ import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Public API 聊天请求参数。
|
||||
*/
|
||||
public class ChatRequestParams implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@@ -24,7 +30,7 @@ public class ChatRequestParams implements Serializable {
|
||||
@JsonProperty("messages")
|
||||
public void setMessagesFromJson(List<Object> rawMessages) {
|
||||
if (rawMessages == null) {
|
||||
this.messages = null;
|
||||
this.messages = new ArrayList<>();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,8 +63,8 @@ public class ChatRequestParams implements Serializable {
|
||||
return switch (role) {
|
||||
case "user" -> jsonObj.toJavaObject(UserMessage.class);
|
||||
case "system" -> jsonObj.toJavaObject(SystemMessage.class);
|
||||
case "assistant" -> jsonObj.toJavaObject(AiMessage.class);
|
||||
case "tool" -> jsonObj.toJavaObject(ToolMessage.class);
|
||||
case "assistant" -> toAiMessage(jsonObj);
|
||||
case "tool" -> toToolMessage(jsonObj);
|
||||
default -> {
|
||||
UserMessage defaultMsg = new UserMessage();
|
||||
defaultMsg.setContent(content);
|
||||
@@ -67,6 +73,85 @@ public class ChatRequestParams implements Serializable {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 JSON 结构恢复为 assistant 消息,兼容 `reasoningContent` / `reasoning_content`
|
||||
* 以及 `toolCalls` / `tool_calls` 两种写法。
|
||||
*
|
||||
* @param jsonObj 原始 JSON
|
||||
* @return assistant 消息
|
||||
*/
|
||||
private AiMessage toAiMessage(JSONObject jsonObj) {
|
||||
AiMessage aiMessage = jsonObj.toJavaObject(AiMessage.class);
|
||||
String reasoningContent = jsonObj.getString("reasoningContent");
|
||||
if (reasoningContent == null || reasoningContent.isBlank()) {
|
||||
reasoningContent = jsonObj.getString("reasoning_content");
|
||||
}
|
||||
if (reasoningContent != null && !reasoningContent.isBlank()) {
|
||||
aiMessage.setReasoningContent(reasoningContent);
|
||||
aiMessage.setFullReasoningContent(reasoningContent);
|
||||
}
|
||||
List<ToolCall> toolCalls = parseToolCalls(jsonObj);
|
||||
if (!toolCalls.isEmpty()) {
|
||||
aiMessage.setToolCalls(toolCalls);
|
||||
}
|
||||
return aiMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 JSON 结构恢复为 tool 消息,兼容 `toolCallId` / `tool_call_id` 两种写法。
|
||||
*
|
||||
* @param jsonObj 原始 JSON
|
||||
* @return tool 消息
|
||||
*/
|
||||
private ToolMessage toToolMessage(JSONObject jsonObj) {
|
||||
ToolMessage toolMessage = jsonObj.toJavaObject(ToolMessage.class);
|
||||
if (toolMessage.getToolCallId() == null || toolMessage.getToolCallId().isBlank()) {
|
||||
toolMessage.setToolCallId(jsonObj.getString("tool_call_id"));
|
||||
}
|
||||
return toolMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 assistant 上的 tool calls。
|
||||
*
|
||||
* @param jsonObj 原始 JSON
|
||||
* @return tool call 列表
|
||||
*/
|
||||
private List<ToolCall> parseToolCalls(JSONObject jsonObj) {
|
||||
Object rawToolCalls = jsonObj.get("toolCalls");
|
||||
if (rawToolCalls == null) {
|
||||
rawToolCalls = jsonObj.get("tool_calls");
|
||||
}
|
||||
List<ToolCall> toolCalls = new ArrayList<>();
|
||||
if (!(rawToolCalls instanceof List<?> rawList)) {
|
||||
return toolCalls;
|
||||
}
|
||||
for (Object rawToolCall : rawList) {
|
||||
JSONObject toolCallJson = JSONObject.from(rawToolCall);
|
||||
if (toolCallJson == null) {
|
||||
continue;
|
||||
}
|
||||
ToolCall toolCall = new ToolCall();
|
||||
toolCall.setId(toolCallJson.getString("id"));
|
||||
String toolName = toolCallJson.getString("name");
|
||||
if (toolName == null || toolName.isBlank()) {
|
||||
JSONObject functionJson = toolCallJson.getJSONObject("function");
|
||||
if (functionJson != null) {
|
||||
toolName = functionJson.getString("name");
|
||||
if (toolCallJson.getString("arguments") == null || toolCallJson.getString("arguments").isBlank()) {
|
||||
toolCall.setArguments(functionJson.getString("arguments"));
|
||||
}
|
||||
}
|
||||
}
|
||||
toolCall.setName(toolName);
|
||||
if (toolCall.getArguments() == null || toolCall.getArguments().isBlank()) {
|
||||
toolCall.setArguments(toolCallJson.getString("arguments"));
|
||||
}
|
||||
toolCalls.add(toolCall);
|
||||
}
|
||||
return toolCalls;
|
||||
}
|
||||
|
||||
public List<Message> getMessages() {
|
||||
return messages;
|
||||
}
|
||||
@@ -83,7 +168,11 @@ public class ChatRequestParams implements Serializable {
|
||||
this.botId = botId;
|
||||
}
|
||||
|
||||
public String getConversationId() {return conversationId;}
|
||||
|
||||
public void setConversationId(String conversationId) {this.conversationId = conversationId;}
|
||||
public String getConversationId() {
|
||||
return conversationId;
|
||||
}
|
||||
|
||||
public void setConversationId(String conversationId) {
|
||||
this.conversationId = conversationId;
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,12 @@ public class Model extends ModelBase {
|
||||
deepseekConfig.setApiKey(checkAndGetApiKey());
|
||||
deepseekConfig.setModel(checkAndGetModelName());
|
||||
deepseekConfig.setRequestPath(checkAndGetRequestPath());
|
||||
deepseekConfig.setSupportThinking(Boolean.TRUE);
|
||||
deepseekConfig.setThinkingProtocol("deepseek");
|
||||
deepseekConfig.setNeedReasoningContentForToolMessage(Boolean.TRUE);
|
||||
if (getSupportToolMessage() != null) {
|
||||
deepseekConfig.setSupportToolMessage(getSupportToolMessage());
|
||||
}
|
||||
return new DeepseekChatModel(deepseekConfig);
|
||||
default:
|
||||
OpenAIChatConfig openAIChatConfig = new OpenAIChatConfig();
|
||||
|
||||
@@ -298,7 +298,7 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
}
|
||||
UserMessage userMessage = new UserMessage(prompt);
|
||||
userMessage.addTools(buildFunctionList(Maps.of("botId", botId)
|
||||
.set("needEnglishName", false)
|
||||
.set("needEnglishName", true)
|
||||
.set("bot", chatCheckResult.getAiBot())
|
||||
.set("chatTimeContext", chatTimeContext)
|
||||
.set("publishedOnly", chatCheckResult.isPublishedAccess())));
|
||||
@@ -350,13 +350,15 @@ public class BotServiceImpl extends ServiceImpl<BotMapper, Bot> implements BotSe
|
||||
BotServiceImpl.ChatCheckResult chatCheckResult, ChatRuntimeContext runtimeContext) {
|
||||
Map<String, Object> modelOptions = chatCheckResult.getModelOptions();
|
||||
ChatOptions chatOptions = getChatOptions(modelOptions);
|
||||
Boolean enableDeepThinking = MapUtil.getBoolean(modelOptions, Bot.KEY_ENABLE_DEEP_THINKING, false);
|
||||
chatOptions.setThinkingEnabled(enableDeepThinking);
|
||||
ChatModel chatModel = chatCheckResult.getChatModel();
|
||||
String systemPrompt = buildSystemPromptWithFaqImageRule(
|
||||
MapUtil.getString(modelOptions, Bot.KEY_SYSTEM_PROMPT)
|
||||
);
|
||||
UserMessage userMessage = new UserMessage(prompt);
|
||||
userMessage.addTools(buildFunctionList(Maps.of("botId", botId)
|
||||
.set("needEnglishName", false)
|
||||
.set("needEnglishName", true)
|
||||
.set("needAccountId", false)
|
||||
.set("bot", chatCheckResult.getAiBot())
|
||||
.set("publishedOnly", chatCheckResult.isPublishedAccess())
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
package tech.easyflow.ai.easyagents.memory;
|
||||
|
||||
import com.easyagents.core.message.AiMessage;
|
||||
import com.easyagents.core.message.Message;
|
||||
import com.easyagents.core.message.ToolCall;
|
||||
import com.easyagents.core.message.ToolMessage;
|
||||
import com.easyagents.core.model.client.OpenAIChatMessageSerializer;
|
||||
import com.easyagents.llm.deepseek.DeepseekConfig;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.core.runtime.ChatAssistantAccumulator;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeMessage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link RuntimeChatMemory} 测试。
|
||||
*/
|
||||
public class RuntimeChatMemoryTest {
|
||||
|
||||
/**
|
||||
* 应当从结构化 payload 中恢复完整的 assistant/tool 消息链。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRestoreStructuredAssistantAndToolHistory() {
|
||||
ChatAssistantAccumulator accumulator = new ChatAssistantAccumulator();
|
||||
accumulator.appendReasoning("思考-1");
|
||||
accumulator.appendToolCall("call-1", "kb_search", "{\"query\":\"java\"}");
|
||||
accumulator.appendToolResult("call-1", "kb_search", "{\"hits\":1}");
|
||||
accumulator.appendReasoning("思考-2");
|
||||
accumulator.appendContent("最终回答");
|
||||
|
||||
ChatRuntimeMessage runtimeMessage = new ChatRuntimeMessage();
|
||||
runtimeMessage.setRole("assistant");
|
||||
runtimeMessage.setContentText("最终回答");
|
||||
runtimeMessage.setContentPayload(accumulator.buildPayload("最终回答"));
|
||||
|
||||
RuntimeChatMemory memory = new RuntimeChatMemory("c1", List.of(runtimeMessage));
|
||||
List<Message> messages = memory.getMessages(10);
|
||||
|
||||
Assert.assertEquals(3, messages.size());
|
||||
Assert.assertTrue(messages.get(0) instanceof AiMessage);
|
||||
Assert.assertTrue(messages.get(1) instanceof ToolMessage);
|
||||
Assert.assertTrue(messages.get(2) instanceof AiMessage);
|
||||
|
||||
AiMessage toolAssistant = (AiMessage) messages.get(0);
|
||||
Assert.assertEquals("思考-1", toolAssistant.getReasoningContent());
|
||||
Assert.assertEquals(1, toolAssistant.getToolCalls().size());
|
||||
Assert.assertEquals("call-1", toolAssistant.getToolCalls().get(0).getId());
|
||||
Assert.assertEquals("{\"query\":\"java\"}", toolAssistant.getToolCalls().get(0).getArguments());
|
||||
|
||||
ToolMessage toolMessage = (ToolMessage) messages.get(1);
|
||||
Assert.assertEquals("call-1", toolMessage.getToolCallId());
|
||||
Assert.assertEquals("{\"hits\":1}", toolMessage.getContent());
|
||||
|
||||
AiMessage finalAssistant = (AiMessage) messages.get(2);
|
||||
Assert.assertEquals("最终回答", finalAssistant.getTextContent());
|
||||
Assert.assertEquals("思考-2", finalAssistant.getReasoningContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧结构历史应保持纯文本兼容。
|
||||
*/
|
||||
@Test
|
||||
public void shouldFallbackToPlainAssistantMessageForLegacyPayload() {
|
||||
ChatRuntimeMessage runtimeMessage = new ChatRuntimeMessage();
|
||||
runtimeMessage.setRole("assistant");
|
||||
runtimeMessage.setContentText("旧版回答");
|
||||
|
||||
RuntimeChatMemory memory = new RuntimeChatMemory("c2", List.of(runtimeMessage));
|
||||
List<Message> messages = memory.getMessages(10);
|
||||
|
||||
Assert.assertEquals(1, messages.size());
|
||||
Assert.assertTrue(messages.get(0) instanceof AiMessage);
|
||||
Assert.assertEquals("旧版回答", ((AiMessage) messages.get(0)).getTextContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部聊天结构化历史回放后,DeepSeek 序列化应继续带上 reasoning_content 与 tool 链。
|
||||
*/
|
||||
@Test
|
||||
public void shouldSerializeRestoredRuntimeHistoryForDeepseekFollowUp() {
|
||||
ChatAssistantAccumulator accumulator = new ChatAssistantAccumulator();
|
||||
accumulator.appendReasoning("先思考");
|
||||
accumulator.appendToolCall("call-1", "kb_search", "{\"query\":\"java\"}");
|
||||
accumulator.appendToolResult("call-1", "kb_search", "{\"hits\":1}");
|
||||
|
||||
ChatRuntimeMessage runtimeMessage = new ChatRuntimeMessage();
|
||||
runtimeMessage.setRole("assistant");
|
||||
runtimeMessage.setContentPayload(accumulator.buildPayload(null));
|
||||
|
||||
RuntimeChatMemory memory = new RuntimeChatMemory("c3", List.of(runtimeMessage));
|
||||
List<Message> restoredMessages = memory.getMessages(10);
|
||||
|
||||
DeepseekConfig config = new DeepseekConfig();
|
||||
config.setSupportThinking(Boolean.TRUE);
|
||||
config.setThinkingProtocol("deepseek");
|
||||
config.setNeedReasoningContentForToolMessage(Boolean.TRUE);
|
||||
|
||||
List<java.util.Map<String, Object>> serialized = new OpenAIChatMessageSerializer()
|
||||
.serializeMessages(restoredMessages, config);
|
||||
|
||||
Assert.assertEquals(2, serialized.size());
|
||||
Assert.assertEquals("assistant", serialized.get(0).get("role"));
|
||||
Assert.assertEquals("先思考", serialized.get(0).get("reasoning_content"));
|
||||
Assert.assertTrue(serialized.get(0).containsKey("tool_calls"));
|
||||
Assert.assertEquals("tool", serialized.get(1).get("role"));
|
||||
Assert.assertEquals("call-1", serialized.get(1).get("tool_call_id"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 连续多轮 tool-only assistant 回放后仍应保持独立顺序。
|
||||
*/
|
||||
@Test
|
||||
public void shouldRestoreSeparatedToolOnlyAssistantSegments() {
|
||||
ChatAssistantAccumulator accumulator = new ChatAssistantAccumulator();
|
||||
accumulator.appendToolCall("call-1", "tool_one", "{\"step\":1}");
|
||||
accumulator.appendToolResult("call-1", "tool_one", "{\"ok\":1}");
|
||||
accumulator.appendToolCall("call-2", "tool_two", "{\"step\":2}");
|
||||
accumulator.appendToolResult("call-2", "tool_two", "{\"ok\":2}");
|
||||
|
||||
ChatRuntimeMessage runtimeMessage = new ChatRuntimeMessage();
|
||||
runtimeMessage.setRole("assistant");
|
||||
runtimeMessage.setContentPayload(accumulator.buildPayload(null));
|
||||
|
||||
RuntimeChatMemory memory = new RuntimeChatMemory("c4", List.of(runtimeMessage));
|
||||
List<Message> messages = memory.getMessages(10);
|
||||
|
||||
Assert.assertEquals(4, messages.size());
|
||||
Assert.assertTrue(messages.get(0) instanceof AiMessage);
|
||||
Assert.assertTrue(messages.get(1) instanceof ToolMessage);
|
||||
Assert.assertTrue(messages.get(2) instanceof AiMessage);
|
||||
Assert.assertTrue(messages.get(3) instanceof ToolMessage);
|
||||
|
||||
AiMessage firstAssistant = (AiMessage) messages.get(0);
|
||||
AiMessage secondAssistant = (AiMessage) messages.get(2);
|
||||
Assert.assertEquals(1, firstAssistant.getToolCalls().size());
|
||||
Assert.assertEquals("call-1", firstAssistant.getToolCalls().get(0).getId());
|
||||
Assert.assertEquals(1, secondAssistant.getToolCalls().size());
|
||||
Assert.assertEquals("call-2", secondAssistant.getToolCalls().get(0).getId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package tech.easyflow.ai.easyagents.tool;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* {@link ChatToolNameHelper} 单元测试。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-05-11
|
||||
*/
|
||||
public class ChatToolNameHelperTest {
|
||||
|
||||
/**
|
||||
* 启用英文名时应优先返回合法英文名称。
|
||||
*/
|
||||
@Test
|
||||
public void resolveToolNameShouldPreferValidEnglishName() {
|
||||
String name = ChatToolNameHelper.resolveToolName(
|
||||
true,
|
||||
"knowledge_search",
|
||||
"知识库检索",
|
||||
"knowledge",
|
||||
BigInteger.valueOf(101)
|
||||
);
|
||||
|
||||
Assert.assertEquals("knowledge_search", name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 英文名缺失时应回退为稳定安全名称。
|
||||
*/
|
||||
@Test
|
||||
public void resolveToolNameShouldFallbackWhenEnglishNameMissing() {
|
||||
String name = ChatToolNameHelper.resolveToolName(
|
||||
true,
|
||||
null,
|
||||
"知识库检索",
|
||||
"knowledge",
|
||||
BigInteger.valueOf(101)
|
||||
);
|
||||
|
||||
Assert.assertEquals("knowledge_101", name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 英文名不满足协议约束时应回退为稳定安全名称。
|
||||
*/
|
||||
@Test
|
||||
public void resolveToolNameShouldFallbackWhenEnglishNameInvalid() {
|
||||
String name = ChatToolNameHelper.resolveToolName(
|
||||
true,
|
||||
"工作流-A",
|
||||
"工作流检索",
|
||||
"workflow",
|
||||
BigInteger.valueOf(202)
|
||||
);
|
||||
|
||||
Assert.assertEquals("workflow_202", name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 未启用英文名时应保留展示名称。
|
||||
*/
|
||||
@Test
|
||||
public void resolveToolNameShouldKeepDisplayNameWhenEnglishNameDisabled() {
|
||||
String name = ChatToolNameHelper.resolveToolName(
|
||||
false,
|
||||
"workflow_search",
|
||||
"工作流检索",
|
||||
"workflow",
|
||||
BigInteger.valueOf(202)
|
||||
);
|
||||
|
||||
Assert.assertEquals("工作流检索", name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package tech.easyflow.ai.easyagents.tool;
|
||||
|
||||
import com.easyagents.flow.core.chain.ChainDefinition;
|
||||
import com.easyagents.flow.core.chain.repository.ChainDefinitionRepository;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryChainStateRepository;
|
||||
import com.easyagents.flow.core.chain.repository.InMemoryNodeStateRepository;
|
||||
import com.easyagents.flow.core.chain.runtime.ChainExecutor;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import tech.easyflow.ai.entity.Workflow;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* {@link WorkflowTool} 单元测试。
|
||||
*
|
||||
* @author Codex
|
||||
* @since 2026-05-11
|
||||
*/
|
||||
public class WorkflowToolTest {
|
||||
|
||||
/**
|
||||
* 启用英文名且字段合法时,应直接使用英文名。
|
||||
*
|
||||
* @throws Exception 反射注入异常
|
||||
*/
|
||||
@Test
|
||||
public void shouldUseValidEnglishNameWhenEnabled() throws Exception {
|
||||
ApplicationContext previousContext = getStaticField("applicationContext");
|
||||
Object previousBeanFactory = getStaticField("beanFactory");
|
||||
try {
|
||||
setStaticField("beanFactory", null);
|
||||
setStaticField("applicationContext", mockApplicationContext(buildChainExecutor("workflow-1")));
|
||||
|
||||
WorkflowTool tool = new WorkflowTool(buildWorkflow(101, "workflow_search", "工作流检索"), true, "workflow-1");
|
||||
|
||||
Assert.assertEquals("workflow_search", tool.getName());
|
||||
} finally {
|
||||
setStaticField("applicationContext", previousContext);
|
||||
setStaticField("beanFactory", previousBeanFactory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用英文名但字段缺失时,应回退为稳定安全名称。
|
||||
*
|
||||
* @throws Exception 反射注入异常
|
||||
*/
|
||||
@Test
|
||||
public void shouldFallbackToSafeNameWhenEnglishNameMissing() throws Exception {
|
||||
ApplicationContext previousContext = getStaticField("applicationContext");
|
||||
Object previousBeanFactory = getStaticField("beanFactory");
|
||||
try {
|
||||
setStaticField("beanFactory", null);
|
||||
setStaticField("applicationContext", mockApplicationContext(buildChainExecutor("workflow-2")));
|
||||
|
||||
WorkflowTool tool = new WorkflowTool(buildWorkflow(202, null, "工作流检索"), true, "workflow-2");
|
||||
|
||||
Assert.assertEquals("workflow_202", tool.getName());
|
||||
} finally {
|
||||
setStaticField("applicationContext", previousContext);
|
||||
setStaticField("beanFactory", previousBeanFactory);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用英文名但字段非法时,应回退为稳定安全名称。
|
||||
*
|
||||
* @throws Exception 反射注入异常
|
||||
*/
|
||||
@Test
|
||||
public void shouldFallbackToSafeNameWhenEnglishNameInvalid() throws Exception {
|
||||
ApplicationContext previousContext = getStaticField("applicationContext");
|
||||
Object previousBeanFactory = getStaticField("beanFactory");
|
||||
try {
|
||||
setStaticField("beanFactory", null);
|
||||
setStaticField("applicationContext", mockApplicationContext(buildChainExecutor("workflow-3")));
|
||||
|
||||
WorkflowTool tool = new WorkflowTool(buildWorkflow(303, "工作流-A", "工作流检索"), true, "workflow-3");
|
||||
|
||||
Assert.assertEquals("workflow_303", tool.getName());
|
||||
} finally {
|
||||
setStaticField("applicationContext", previousContext);
|
||||
setStaticField("beanFactory", previousBeanFactory);
|
||||
}
|
||||
}
|
||||
|
||||
private Workflow buildWorkflow(long id, String englishName, String title) {
|
||||
Workflow workflow = new Workflow();
|
||||
workflow.setId(BigInteger.valueOf(id));
|
||||
workflow.setEnglishName(englishName);
|
||||
workflow.setTitle(title);
|
||||
workflow.setDescription("desc-" + id);
|
||||
return workflow;
|
||||
}
|
||||
|
||||
private ChainExecutor buildChainExecutor(String definitionId) {
|
||||
ChainDefinitionRepository definitionRepository = id -> {
|
||||
ChainDefinition definition = new ChainDefinition();
|
||||
definition.setId(definitionId);
|
||||
return definition;
|
||||
};
|
||||
return new ChainExecutor(
|
||||
definitionRepository,
|
||||
new InMemoryChainStateRepository(),
|
||||
new InMemoryNodeStateRepository()
|
||||
);
|
||||
}
|
||||
|
||||
private ApplicationContext mockApplicationContext(ChainExecutor chainExecutor) {
|
||||
return (ApplicationContext) Proxy.newProxyInstance(
|
||||
ApplicationContext.class.getClassLoader(),
|
||||
new Class[]{ApplicationContext.class},
|
||||
(proxy, method, args) -> {
|
||||
if ("getBean".equals(method.getName()) && args != null && args.length == 1 && args[0] == ChainExecutor.class) {
|
||||
return chainExecutor;
|
||||
}
|
||||
if ("equals".equals(method.getName())) {
|
||||
return proxy == args[0];
|
||||
}
|
||||
if ("hashCode".equals(method.getName())) {
|
||||
return System.identityHashCode(proxy);
|
||||
}
|
||||
return defaultValue(method.getReturnType());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private static <T> T getStaticField(String name) throws Exception {
|
||||
Field field = Class.forName("tech.easyflow.common.util.SpringContextUtil").getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
@SuppressWarnings("unchecked")
|
||||
T value = (T) field.get(null);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void setStaticField(String name, Object value) throws Exception {
|
||||
Field field = Class.forName("tech.easyflow.common.util.SpringContextUtil").getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
field.set(null, value);
|
||||
}
|
||||
|
||||
private Object defaultValue(Class<?> returnType) {
|
||||
if (returnType == null || returnType == Void.TYPE) {
|
||||
return null;
|
||||
}
|
||||
if (!returnType.isPrimitive()) {
|
||||
return null;
|
||||
}
|
||||
if (returnType == Boolean.TYPE) {
|
||||
return false;
|
||||
}
|
||||
if (returnType == Character.TYPE) {
|
||||
return '\0';
|
||||
}
|
||||
if (returnType == Byte.TYPE) {
|
||||
return (byte) 0;
|
||||
}
|
||||
if (returnType == Short.TYPE) {
|
||||
return (short) 0;
|
||||
}
|
||||
if (returnType == Integer.TYPE) {
|
||||
return 0;
|
||||
}
|
||||
if (returnType == Long.TYPE) {
|
||||
return 0L;
|
||||
}
|
||||
if (returnType == Float.TYPE) {
|
||||
return 0F;
|
||||
}
|
||||
if (returnType == Double.TYPE) {
|
||||
return 0D;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.easyagents.core.message.AiMessage;
|
||||
import com.easyagents.core.message.Message;
|
||||
import com.easyagents.core.message.ToolMessage;
|
||||
import com.easyagents.core.model.client.OpenAIChatMessageSerializer;
|
||||
import com.easyagents.llm.deepseek.DeepseekConfig;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import tech.easyflow.ai.easyagents.memory.PublicBotMessageMemory;
|
||||
import tech.easyflow.core.chat.protocol.sse.ChatSseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link ChatRequestParams} 测试。
|
||||
*/
|
||||
public class ChatRequestParamsTest {
|
||||
|
||||
/**
|
||||
* 应兼容解析 assistant 的 reasoning 与 OpenAI 风格 tool_calls。
|
||||
*/
|
||||
@Test
|
||||
public void shouldParseAssistantReasoningAndToolCalls() {
|
||||
ChatRequestParams params = new ChatRequestParams();
|
||||
params.setMessagesFromJson(List.of(
|
||||
Map.of(
|
||||
"role", "assistant",
|
||||
"content", "",
|
||||
"reasoning_content", "先思考",
|
||||
"tool_calls", List.of(
|
||||
Map.of(
|
||||
"id", "call-1",
|
||||
"type", "function",
|
||||
"function", Map.of(
|
||||
"name", "kb_search",
|
||||
"arguments", "{\"query\":\"test\"}"
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
));
|
||||
|
||||
Message message = params.getMessages().get(0);
|
||||
Assert.assertTrue(message instanceof AiMessage);
|
||||
AiMessage aiMessage = (AiMessage) message;
|
||||
Assert.assertEquals("先思考", aiMessage.getReasoningContent());
|
||||
Assert.assertEquals(1, aiMessage.getToolCalls().size());
|
||||
Assert.assertEquals("kb_search", aiMessage.getToolCalls().get(0).getName());
|
||||
Assert.assertEquals("{\"query\":\"test\"}", aiMessage.getToolCalls().get(0).getArguments());
|
||||
}
|
||||
|
||||
/**
|
||||
* 应兼容解析 tool 消息的下划线字段。
|
||||
*/
|
||||
@Test
|
||||
public void shouldParseToolMessageWithSnakeCaseToolCallId() {
|
||||
ChatRequestParams params = new ChatRequestParams();
|
||||
params.setMessagesFromJson(List.of(
|
||||
Map.of(
|
||||
"role", "tool",
|
||||
"content", "{\"hits\":1}",
|
||||
"tool_call_id", "call-2"
|
||||
)
|
||||
));
|
||||
|
||||
Message message = params.getMessages().get(0);
|
||||
Assert.assertTrue(message instanceof ToolMessage);
|
||||
ToolMessage toolMessage = (ToolMessage) message;
|
||||
Assert.assertEquals("call-2", toolMessage.getToolCallId());
|
||||
Assert.assertEquals("{\"hits\":1}", toolMessage.getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* public-api 多轮 assistant/tool 历史透传后,DeepSeek 后续轮次序列化不应丢字段。
|
||||
*/
|
||||
@Test
|
||||
public void shouldKeepPublicApiAssistantAndToolHistoryForDeepseekFollowUp() {
|
||||
ChatRequestParams params = new ChatRequestParams();
|
||||
params.setMessagesFromJson(List.of(
|
||||
Map.of(
|
||||
"role", "assistant",
|
||||
"content", "",
|
||||
"reasoning_content", "先思考",
|
||||
"tool_calls", List.of(
|
||||
Map.of(
|
||||
"id", "call-1",
|
||||
"type", "function",
|
||||
"function", Map.of(
|
||||
"name", "kb_search",
|
||||
"arguments", "{\"query\":\"test\"}"
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
Map.of(
|
||||
"role", "tool",
|
||||
"content", "{\"hits\":1}",
|
||||
"tool_call_id", "call-1"
|
||||
)
|
||||
));
|
||||
|
||||
PublicBotMessageMemory memory = new PublicBotMessageMemory(new ChatSseEmitter(), params.getMessages());
|
||||
DeepseekConfig config = new DeepseekConfig();
|
||||
config.setSupportThinking(Boolean.TRUE);
|
||||
config.setThinkingProtocol("deepseek");
|
||||
config.setNeedReasoningContentForToolMessage(Boolean.TRUE);
|
||||
|
||||
List<Map<String, Object>> serialized = new OpenAIChatMessageSerializer()
|
||||
.serializeMessages(memory.getMessages(10), config);
|
||||
|
||||
Assert.assertEquals(2, serialized.size());
|
||||
Assert.assertEquals("assistant", serialized.get(0).get("role"));
|
||||
Assert.assertEquals("先思考", serialized.get(0).get("reasoning_content"));
|
||||
Assert.assertTrue(serialized.get(0).containsKey("tool_calls"));
|
||||
Assert.assertEquals("tool", serialized.get(1).get("role"));
|
||||
Assert.assertEquals("call-1", serialized.get(1).get("tool_call_id"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.easyagents.core.message.AiMessage;
|
||||
import com.easyagents.core.message.ToolCall;
|
||||
import com.easyagents.core.message.ToolMessage;
|
||||
import com.easyagents.core.model.client.OpenAIChatMessageSerializer;
|
||||
import com.easyagents.llm.deepseek.DeepseekChatModel;
|
||||
import com.easyagents.llm.deepseek.DeepseekConfig;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* DeepSeek thinking/tool 配置测试。
|
||||
*/
|
||||
public class ModelDeepseekConfigTest {
|
||||
|
||||
/**
|
||||
* DeepSeek 聊天模型应显式开启 thinking/tool 相关协议开关。
|
||||
*/
|
||||
@Test
|
||||
public void shouldEnableDeepseekThinkingProtocolFlags() {
|
||||
ModelProvider provider = new ModelProvider();
|
||||
provider.setProviderType("deepseek");
|
||||
provider.setProviderName("deepseek");
|
||||
|
||||
Model model = new Model();
|
||||
model.setModelProvider(provider);
|
||||
model.setEndpoint("https://api.deepseek.com");
|
||||
model.setApiKey("sk-test");
|
||||
model.setModelName("deepseek-chat");
|
||||
model.setRequestPath("/chat/completions");
|
||||
|
||||
DeepseekChatModel chatModel = (DeepseekChatModel) model.toChatModel();
|
||||
DeepseekConfig config = chatModel.getConfig();
|
||||
|
||||
Assert.assertTrue(config.isSupportThinking());
|
||||
Assert.assertEquals("deepseek", config.getThinkingProtocol());
|
||||
Assert.assertTrue(config.isNeedReasoningContentForToolMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* DeepSeek 在 tool call assistant 历史上应序列化 reasoning_content。
|
||||
*/
|
||||
@Test
|
||||
public void shouldSerializeReasoningContentForToolAssistantMessage() {
|
||||
DeepseekConfig config = new DeepseekConfig();
|
||||
config.setNeedReasoningContentForToolMessage(Boolean.TRUE);
|
||||
config.setThinkingProtocol("deepseek");
|
||||
config.setSupportThinking(Boolean.TRUE);
|
||||
|
||||
AiMessage assistant = new AiMessage(null);
|
||||
assistant.setReasoningContent("先推理");
|
||||
assistant.setToolCalls(List.of(new ToolCall("call-1", "kb_search", "{\"query\":\"java\"}")));
|
||||
|
||||
ToolMessage toolMessage = new ToolMessage();
|
||||
toolMessage.setToolCallId("call-1");
|
||||
toolMessage.setContent("{\"hits\":1}");
|
||||
|
||||
List<Map<String, Object>> serialized = new OpenAIChatMessageSerializer().serializeMessages(
|
||||
List.of(assistant, toolMessage),
|
||||
config
|
||||
);
|
||||
|
||||
Assert.assertEquals(2, serialized.size());
|
||||
Assert.assertEquals("assistant", serialized.get(0).get("role"));
|
||||
Assert.assertEquals("先推理", serialized.get(0).get("reasoning_content"));
|
||||
Assert.assertTrue(serialized.get(0).containsKey("tool_calls"));
|
||||
Assert.assertEquals("tool", serialized.get(1).get("role"));
|
||||
Assert.assertEquals("call-1", serialized.get(1).get("tool_call_id"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package tech.easyflow.core.runtime;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link ChatAssistantAccumulator} 测试。
|
||||
*/
|
||||
public class ChatAssistantAccumulatorTest {
|
||||
|
||||
/**
|
||||
* 应同时保留展示链和可回放的结构化消息链。
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldBuildStructuredPayloadWithDisplayChains() {
|
||||
ChatAssistantAccumulator accumulator = new ChatAssistantAccumulator();
|
||||
accumulator.appendReasoning("思考-1");
|
||||
accumulator.appendToolCall("call-1", "kb_search", "{\"query\":\"java\"}");
|
||||
accumulator.appendToolResult("call-1", "kb_search", "{\"hits\":1}");
|
||||
accumulator.appendReasoning("思考-2");
|
||||
accumulator.appendContent("最终回答");
|
||||
|
||||
Map<String, Object> payload = accumulator.buildPayload("最终回答");
|
||||
List<Map<String, Object>> messageChain = (List<Map<String, Object>>) payload.get("messageChain");
|
||||
List<Map<String, Object>> chains = (List<Map<String, Object>>) payload.get("chains");
|
||||
|
||||
Assert.assertEquals(3, messageChain.size());
|
||||
Assert.assertEquals("assistant", messageChain.get(0).get("role"));
|
||||
Assert.assertEquals("思考-1", messageChain.get(0).get("reasoningContent"));
|
||||
Assert.assertEquals("tool", messageChain.get(1).get("role"));
|
||||
Assert.assertEquals("assistant", messageChain.get(2).get("role"));
|
||||
Assert.assertEquals("最终回答", messageChain.get(2).get("content"));
|
||||
Assert.assertEquals("思考-2", messageChain.get(2).get("reasoningContent"));
|
||||
|
||||
Assert.assertFalse(chains.isEmpty());
|
||||
Assert.assertEquals("思考-1思考-2", chains.get(0).get("reasoning_content"));
|
||||
Assert.assertEquals("{\"query\":\"java\"}", chains.get(1).get("arguments"));
|
||||
Assert.assertEquals("{\"hits\":1}", chains.get(1).get("result"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 连续多轮 tool-only assistant 不应被错误合并到同一条 assistant 历史。
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldKeepToolOnlyAssistantSegmentsSeparatedAcrossRecursiveRounds() {
|
||||
ChatAssistantAccumulator accumulator = new ChatAssistantAccumulator();
|
||||
accumulator.appendToolCall("call-1", "tool_one", "{\"step\":1}");
|
||||
accumulator.appendToolResult("call-1", "tool_one", "{\"ok\":1}");
|
||||
accumulator.appendToolCall("call-2", "tool_two", "{\"step\":2}");
|
||||
accumulator.appendToolResult("call-2", "tool_two", "{\"ok\":2}");
|
||||
|
||||
Map<String, Object> payload = accumulator.buildPayload(null);
|
||||
List<Map<String, Object>> messageChain = (List<Map<String, Object>>) payload.get("messageChain");
|
||||
|
||||
Assert.assertEquals(4, messageChain.size());
|
||||
Assert.assertEquals("assistant", messageChain.get(0).get("role"));
|
||||
Assert.assertEquals("tool", messageChain.get(1).get("role"));
|
||||
Assert.assertEquals("assistant", messageChain.get(2).get("role"));
|
||||
Assert.assertEquals("tool", messageChain.get(3).get("role"));
|
||||
|
||||
List<Map<String, Object>> firstToolCalls = (List<Map<String, Object>>) messageChain.get(0).get("toolCalls");
|
||||
List<Map<String, Object>> secondToolCalls = (List<Map<String, Object>>) messageChain.get(2).get("toolCalls");
|
||||
Assert.assertEquals(1, firstToolCalls.size());
|
||||
Assert.assertEquals("call-1", firstToolCalls.get(0).get("id"));
|
||||
Assert.assertEquals(1, secondToolCalls.size());
|
||||
Assert.assertEquals("call-2", secondToolCalls.get(0).get("id"));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.service.ChatPersistDispatcher;
|
||||
import tech.easyflow.chatlog.service.ChatSessionQueryService;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeHistoryPayloadHelper;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeContext;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeListener;
|
||||
import tech.easyflow.core.runtime.ChatRuntimeMessage;
|
||||
@@ -81,7 +82,8 @@ public class ChatlogRuntimeListener implements ChatRuntimeListener {
|
||||
Collections.reverse(records);
|
||||
List<ChatRuntimeMessage> messages = new ArrayList<>(records.size());
|
||||
for (ChatMessageRecord record : records) {
|
||||
if (record.getContentText() == null || record.getContentText().isBlank()) {
|
||||
if ((record.getContentText() == null || record.getContentText().isBlank())
|
||||
&& !ChatRuntimeHistoryPayloadHelper.hasStructuredHistory(record.getContentPayload())) {
|
||||
continue;
|
||||
}
|
||||
ChatRuntimeMessage message = new ChatRuntimeMessage();
|
||||
|
||||
Reference in New Issue
Block a user