feat: 接入一库一工具知识检索
- 编译知识库英文运行名、描述、检索配置和独立 Registration - 统一最终分数阈值并保持模型上下文、检索事件与引用一致 - 完善 AG-UI 知识库检索运行态与完成态展示
This commit is contained in:
@@ -1169,6 +1169,8 @@ public class AgentRunService {
|
|||||||
StringBuilder answer = new StringBuilder();
|
StringBuilder answer = new StringBuilder();
|
||||||
ChatAssistantAccumulator assistantAccumulator = new ChatAssistantAccumulator();
|
ChatAssistantAccumulator assistantAccumulator = new ChatAssistantAccumulator();
|
||||||
LegacyThinkingTagParser legacyThinkingTagParser = new LegacyThinkingTagParser();
|
LegacyThinkingTagParser legacyThinkingTagParser = new LegacyThinkingTagParser();
|
||||||
|
KnowledgeRetrievalStatusTracker knowledgeRetrievalStatusTracker =
|
||||||
|
new KnowledgeRetrievalStatusTracker();
|
||||||
// 注册 emit 服务
|
// 注册 emit 服务
|
||||||
registerEmitterCancellation(requestId, runOutput, chatContext, answer,
|
registerEmitterCancellation(requestId, runOutput, chatContext, answer,
|
||||||
assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
|
assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
|
||||||
@@ -1177,7 +1179,7 @@ public class AgentRunService {
|
|||||||
if (isAguiCancellationRequested(runOutput)) {
|
if (isAguiCancellationRequested(runOutput)) {
|
||||||
handleRuntimeEvent(cancellationEvent("用户已停止生成"), requestId, runOutput,
|
handleRuntimeEvent(cancellationEvent("用户已停止生成"), requestId, runOutput,
|
||||||
answer, assistantAccumulator, legacyThinkingTagParser,
|
answer, assistantAccumulator, legacyThinkingTagParser,
|
||||||
chatContext, finished, persistChatlog);
|
knowledgeRetrievalStatusTracker, chatContext, finished, persistChatlog);
|
||||||
if (lockHandle != null) {
|
if (lockHandle != null) {
|
||||||
releaseRunLockQuietly(lockHandle, requestId);
|
releaseRunLockQuietly(lockHandle, requestId);
|
||||||
}
|
}
|
||||||
@@ -1195,7 +1197,7 @@ public class AgentRunService {
|
|||||||
request.setAgentDefinition(bundle.getDefinition());
|
request.setAgentDefinition(bundle.getDefinition());
|
||||||
request.setRuntimeContext(runtimeContext);
|
request.setRuntimeContext(runtimeContext);
|
||||||
request.setToolInvokers(bundle.getToolInvokers());
|
request.setToolInvokers(bundle.getToolInvokers());
|
||||||
request.setKnowledgeRetrievers(bundle.getKnowledgeRetrievers());
|
request.setKnowledgeRegistrations(bundle.getKnowledgeRegistrations());
|
||||||
request.setSessionStore(runtimeSessionStore);
|
request.setSessionStore(runtimeSessionStore);
|
||||||
request.setMediaResolver(agentMediaService.runtimeResolver(account));
|
request.setMediaResolver(agentMediaService.runtimeResolver(account));
|
||||||
request.getMetadata().put("assistantCode", assistantCode);
|
request.getMetadata().put("assistantCode", assistantCode);
|
||||||
@@ -1224,6 +1226,7 @@ public class AgentRunService {
|
|||||||
runRuntimeCallbackSafely(
|
runRuntimeCallbackSafely(
|
||||||
() -> handleRuntimeEvent(event, requestId, runOutput, answer,
|
() -> handleRuntimeEvent(event, requestId, runOutput, answer,
|
||||||
assistantAccumulator, legacyThinkingTagParser,
|
assistantAccumulator, legacyThinkingTagParser,
|
||||||
|
knowledgeRetrievalStatusTracker,
|
||||||
chatContext, finished, persistChatlog),
|
chatContext, finished, persistChatlog),
|
||||||
requestId, runOutput, chatContext, finished, persistChatlog);
|
requestId, runOutput, chatContext, finished, persistChatlog);
|
||||||
}
|
}
|
||||||
@@ -1513,7 +1516,8 @@ public class AgentRunService {
|
|||||||
AtomicBoolean finished,
|
AtomicBoolean finished,
|
||||||
boolean persistChatlog) {
|
boolean persistChatlog) {
|
||||||
handleRuntimeEvent(event, requestId, runOutput, answer, assistantAccumulator,
|
handleRuntimeEvent(event, requestId, runOutput, answer, assistantAccumulator,
|
||||||
new LegacyThinkingTagParser(), chatContext, finished, persistChatlog);
|
new LegacyThinkingTagParser(), new KnowledgeRetrievalStatusTracker(),
|
||||||
|
chatContext, finished, persistChatlog);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleRuntimeEvent(AgentRuntimeEvent event,
|
private void handleRuntimeEvent(AgentRuntimeEvent event,
|
||||||
@@ -1525,6 +1529,35 @@ public class AgentRunService {
|
|||||||
ChatRuntimeContext chatContext,
|
ChatRuntimeContext chatContext,
|
||||||
AtomicBoolean finished,
|
AtomicBoolean finished,
|
||||||
boolean persistChatlog) {
|
boolean persistChatlog) {
|
||||||
|
handleRuntimeEvent(event, requestId, runOutput, answer, assistantAccumulator,
|
||||||
|
legacyThinkingTagParser, new KnowledgeRetrievalStatusTracker(),
|
||||||
|
chatContext, finished, persistChatlog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将单个 Runtime 事件投影到聊天协议,并复用本轮知识库工具状态追踪器。
|
||||||
|
*
|
||||||
|
* @param event Runtime 事件
|
||||||
|
* @param requestId 请求 ID
|
||||||
|
* @param runOutput 运行输出
|
||||||
|
* @param answer 回答累积器
|
||||||
|
* @param assistantAccumulator Assistant 结构化累积器
|
||||||
|
* @param legacyThinkingTagParser 旧思考标签解析器
|
||||||
|
* @param knowledgeRetrievalStatusTracker 知识库工具状态追踪器
|
||||||
|
* @param chatContext 聊天上下文
|
||||||
|
* @param finished 终态仲裁标记
|
||||||
|
* @param persistChatlog 是否持久化聊天日志
|
||||||
|
*/
|
||||||
|
private void handleRuntimeEvent(AgentRuntimeEvent event,
|
||||||
|
String requestId,
|
||||||
|
AgentRunOutput runOutput,
|
||||||
|
StringBuilder answer,
|
||||||
|
ChatAssistantAccumulator assistantAccumulator,
|
||||||
|
LegacyThinkingTagParser legacyThinkingTagParser,
|
||||||
|
KnowledgeRetrievalStatusTracker knowledgeRetrievalStatusTracker,
|
||||||
|
ChatRuntimeContext chatContext,
|
||||||
|
AtomicBoolean finished,
|
||||||
|
boolean persistChatlog) {
|
||||||
if (event == null || event.getEventType() == null) {
|
if (event == null || event.getEventType() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1642,6 +1675,17 @@ public class AgentRunService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Map<String, Object> toolPayload = toolStatus;
|
Map<String, Object> toolPayload = toolStatus;
|
||||||
|
if (isKnowledgeToolEvent(event)) {
|
||||||
|
Map<String, Object> statusPayload = buildKnowledgeRetrievalStatusPayload(
|
||||||
|
knowledgeRetrievalStatusTracker.update(event));
|
||||||
|
LOG.info("Agent runtime knowledge tool call, requestId={}, toolCallId={}, toolName={}",
|
||||||
|
requestId, event.getToolCallId(), stringValue(toolPayload, "toolName"));
|
||||||
|
if (!sendEnvelope(runOutput, ChatDomain.BUSINESS, ChatType.STATUS, statusPayload)) {
|
||||||
|
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
|
||||||
|
legacyThinkingTagParser, finished, persistChatlog);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!runOutput.emitRuntimeEvent(publicRuntimeEvent(event, toolPayload))) {
|
if (!runOutput.emitRuntimeEvent(publicRuntimeEvent(event, toolPayload))) {
|
||||||
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
|
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
|
||||||
legacyThinkingTagParser, finished, persistChatlog);
|
legacyThinkingTagParser, finished, persistChatlog);
|
||||||
@@ -1664,6 +1708,20 @@ public class AgentRunService {
|
|||||||
}
|
}
|
||||||
if (event.getEventType() == AgentRuntimeEventType.TOOL_RESULT) {
|
if (event.getEventType() == AgentRuntimeEventType.TOOL_RESULT) {
|
||||||
Map<String, Object> toolPayload = toolStatus;
|
Map<String, Object> toolPayload = toolStatus;
|
||||||
|
if (isKnowledgeToolEvent(event)) {
|
||||||
|
Map<String, Object> statusPayload = buildKnowledgeRetrievalStatusPayload(
|
||||||
|
knowledgeRetrievalStatusTracker.update(event));
|
||||||
|
LOG.info("Agent runtime knowledge tool result, requestId={}, toolCallId={}, toolName={}, status={}",
|
||||||
|
requestId, event.getToolCallId(), stringValue(toolPayload, "toolName"),
|
||||||
|
stringValue(statusPayload, "status"));
|
||||||
|
if (!sendEnvelope(runOutput, ChatDomain.BUSINESS, ChatType.STATUS, statusPayload)) {
|
||||||
|
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
|
||||||
|
legacyThinkingTagParser, finished, persistChatlog);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
legacyThinkingTagParser.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
LOG.info("Agent runtime tool result, requestId={}, toolCallId={}, toolName={}, status={}",
|
LOG.info("Agent runtime tool result, requestId={}, toolCallId={}, toolName={}, status={}",
|
||||||
requestId, event.getToolCallId(), stringValue(toolPayload, "toolName"),
|
requestId, event.getToolCallId(), stringValue(toolPayload, "toolName"),
|
||||||
stringValue(toolPayload, "status"));
|
stringValue(toolPayload, "status"));
|
||||||
@@ -1689,10 +1747,7 @@ public class AgentRunService {
|
|||||||
if (event.getEventType() == AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL) {
|
if (event.getEventType() == AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL) {
|
||||||
LOG.info("Agent runtime knowledge retrieval, requestId={}, payload={}, metadata={}",
|
LOG.info("Agent runtime knowledge retrieval, requestId={}, payload={}, metadata={}",
|
||||||
requestId, event.getPayload(), event.getMetadata());
|
requestId, event.getPayload(), event.getMetadata());
|
||||||
if (!sendEnvelope(runOutput, ChatDomain.BUSINESS, ChatType.STATUS, buildKnowledgeRetrievalStatusPayload(event))) {
|
// 文档摘要事件用于引用与监察;UI 完成态统一以 TOOL_RESULT 为准。
|
||||||
cancelDisconnectedRun(requestId, chatContext, answer, assistantAccumulator,
|
|
||||||
legacyThinkingTagParser, finished, persistChatlog);
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.getEventType() == AgentRuntimeEventType.MEMORY_COMPRESSION_STARTED
|
if (event.getEventType() == AgentRuntimeEventType.MEMORY_COMPRESSION_STARTED
|
||||||
@@ -1750,6 +1805,10 @@ public class AgentRunService {
|
|||||||
if (event.getEventType() == AgentRuntimeEventType.FAILED) {
|
if (event.getEventType() == AgentRuntimeEventType.FAILED) {
|
||||||
emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, runOutput, chatContext,
|
emitAssistantSegments(legacyThinkingTagParser.finish(), requestId, runOutput, chatContext,
|
||||||
answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
|
answer, assistantAccumulator, legacyThinkingTagParser, finished, persistChatlog);
|
||||||
|
if (knowledgeRetrievalStatusTracker.failActiveCalls()) {
|
||||||
|
sendEnvelope(runOutput, ChatDomain.BUSINESS, ChatType.STATUS,
|
||||||
|
buildKnowledgeRetrievalStatusPayload("error"));
|
||||||
|
}
|
||||||
runOutput.emitRuntimeEvent(event);
|
runOutput.emitRuntimeEvent(event);
|
||||||
assistantAccumulator.finalizePendingSkillInvocations("FAILED", "技能调用失败");
|
assistantAccumulator.finalizePendingSkillInvocations("FAILED", "技能调用失败");
|
||||||
if (persistChatlog) {
|
if (persistChatlog) {
|
||||||
@@ -2813,7 +2872,8 @@ public class AgentRunService {
|
|||||||
Map<String, Object> rawPayload = event.getPayload() == null ? Map.of() : event.getPayload();
|
Map<String, Object> rawPayload = event.getPayload() == null ? Map.of() : event.getPayload();
|
||||||
Map<String, Object> payload = selectPayload(rawPayload,
|
Map<String, Object> payload = selectPayload(rawPayload,
|
||||||
"name", "status", "success", "toolDisplayName", "toolName",
|
"name", "status", "success", "toolDisplayName", "toolName",
|
||||||
"skillDisplayName", "skillId");
|
"skillDisplayName", "skillId", "toolCategory",
|
||||||
|
"knowledgeId", "knowledgeName", "knowledgeRuntimeName");
|
||||||
String toolCallId = firstText(event.getToolCallId(), stringValue(rawPayload, "toolCallId"));
|
String toolCallId = firstText(event.getToolCallId(), stringValue(rawPayload, "toolCallId"));
|
||||||
if (toolCallId != null && !toolCallId.isBlank()) {
|
if (toolCallId != null && !toolCallId.isBlank()) {
|
||||||
payload.put("toolCallId", toolCallId);
|
payload.put("toolCallId", toolCallId);
|
||||||
@@ -2949,17 +3009,110 @@ public class AgentRunService {
|
|||||||
/**
|
/**
|
||||||
* 构建知识库检索状态载荷,确保前端可按稳定 key 合并同一轮状态行。
|
* 构建知识库检索状态载荷,确保前端可按稳定 key 合并同一轮状态行。
|
||||||
*
|
*
|
||||||
* @param event 知识库检索运行时事件
|
* @param status running、done 或 error
|
||||||
* @return 知识库检索状态载荷
|
* @return 知识库检索状态载荷
|
||||||
*/
|
*/
|
||||||
private Map<String, Object> buildKnowledgeRetrievalStatusPayload(AgentRuntimeEvent event) {
|
private Map<String, Object> buildKnowledgeRetrievalStatusPayload(String status) {
|
||||||
|
String normalizedStatus = "running".equals(status) || "error".equals(status)
|
||||||
|
? status : "done";
|
||||||
Map<String, Object> payload = new LinkedHashMap<>();
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
payload.put("statusKey", "knowledge-retrieval");
|
payload.put("statusKey", "knowledge-retrieval");
|
||||||
payload.put("status", "done");
|
payload.put("status", normalizedStatus);
|
||||||
payload.put("label", "已检索知识库");
|
payload.put("label", switch (normalizedStatus) {
|
||||||
|
case "running" -> "正在检索知识库";
|
||||||
|
case "error" -> "知识库检索失败";
|
||||||
|
default -> "已检索知识库";
|
||||||
|
});
|
||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断标准工具生命周期事件是否属于知识库工具。
|
||||||
|
*
|
||||||
|
* @param event 运行时工具事件
|
||||||
|
* @return 知识库工具事件时为 true
|
||||||
|
*/
|
||||||
|
private boolean isKnowledgeToolEvent(AgentRuntimeEvent event) {
|
||||||
|
String category = stringPayload(event, "toolCategory");
|
||||||
|
if ("KNOWLEDGE".equalsIgnoreCase(category)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String toolName = firstText(stringPayload(event, "toolName"), stringPayload(event, "name"));
|
||||||
|
if (toolName == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String normalizedName = toolName.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return "retrieve_knowledge".equals(normalizedName)
|
||||||
|
|| normalizedName.startsWith("retrieve_knowledge_");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 聚合同一批知识库工具调用,避免并行检索中首个结果提前结束 UI 状态。
|
||||||
|
*/
|
||||||
|
static final class KnowledgeRetrievalStatusTracker {
|
||||||
|
|
||||||
|
private final Set<String> activeToolCallIds = new LinkedHashSet<>();
|
||||||
|
private boolean failed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用一次知识库工具生命周期事件。
|
||||||
|
*
|
||||||
|
* @param event TOOL_CALL 或 TOOL_RESULT 事件
|
||||||
|
* @return 聚合后的 running、done 或 error 状态
|
||||||
|
*/
|
||||||
|
String update(AgentRuntimeEvent event) {
|
||||||
|
String toolCallId = toolCallIdentity(event);
|
||||||
|
if (event.getEventType() == AgentRuntimeEventType.TOOL_CALL) {
|
||||||
|
if (activeToolCallIds.isEmpty()) {
|
||||||
|
failed = false;
|
||||||
|
}
|
||||||
|
activeToolCallIds.add(toolCallId);
|
||||||
|
return "running";
|
||||||
|
}
|
||||||
|
if (event.getEventType() == AgentRuntimeEventType.TOOL_RESULT) {
|
||||||
|
activeToolCallIds.remove(toolCallId);
|
||||||
|
failed = failed || !toolSucceeded(event);
|
||||||
|
if (!activeToolCallIds.isEmpty()) {
|
||||||
|
return "running";
|
||||||
|
}
|
||||||
|
return failed ? "error" : "done";
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Knowledge status only accepts TOOL_CALL or TOOL_RESULT events.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将运行失败时仍未结束的知识库调用收口为失败。
|
||||||
|
*
|
||||||
|
* @return 存在未结束调用时为 true
|
||||||
|
*/
|
||||||
|
boolean failActiveCalls() {
|
||||||
|
if (activeToolCallIds.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
activeToolCallIds.clear();
|
||||||
|
failed = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toolCallIdentity(AgentRuntimeEvent event) {
|
||||||
|
String toolCallId = event.getToolCallId();
|
||||||
|
if (toolCallId == null || toolCallId.isBlank()) {
|
||||||
|
Object payloadId = event.getPayload() == null ? null : event.getPayload().get("toolCallId");
|
||||||
|
toolCallId = payloadId == null ? event.getEventId() : String.valueOf(payloadId);
|
||||||
|
}
|
||||||
|
return toolCallId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean toolSucceeded(AgentRuntimeEvent event) {
|
||||||
|
Map<String, Object> payload = event.getPayload() == null ? Map.of() : event.getPayload();
|
||||||
|
if (Boolean.FALSE.equals(payload.get("success"))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Object status = payload.get("status");
|
||||||
|
return status == null || !"FAILED".equalsIgnoreCase(String.valueOf(status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建不含 Runtime 原始上下文的内存压缩公开状态载荷。
|
* 构建不含 Runtime 原始上下文的内存压缩公开状态载荷。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package tech.easyflow.agent.runtime;
|
package tech.easyflow.agent.runtime;
|
||||||
|
|
||||||
import com.easyagents.agent.runtime.AgentDefinition;
|
import com.easyagents.agent.runtime.AgentDefinition;
|
||||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetriever;
|
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRegistration;
|
||||||
import com.easyagents.agent.runtime.tool.AgentToolInvoker;
|
import com.easyagents.agent.runtime.tool.AgentToolInvoker;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -14,7 +16,7 @@ public class AgentRuntimeBundle {
|
|||||||
|
|
||||||
private AgentDefinition definition;
|
private AgentDefinition definition;
|
||||||
private Map<String, AgentToolInvoker> toolInvokers = new LinkedHashMap<>();
|
private Map<String, AgentToolInvoker> toolInvokers = new LinkedHashMap<>();
|
||||||
private Map<String, AgentKnowledgeRetriever> knowledgeRetrievers = new LinkedHashMap<>();
|
private List<AgentKnowledgeRegistration> knowledgeRegistrations = new ArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 Agent 定义。
|
* 获取 Agent 定义。
|
||||||
@@ -57,16 +59,18 @@ public class AgentRuntimeBundle {
|
|||||||
*
|
*
|
||||||
* @return 知识库检索器
|
* @return 知识库检索器
|
||||||
*/
|
*/
|
||||||
public Map<String, AgentKnowledgeRetriever> getKnowledgeRetrievers() {
|
public List<AgentKnowledgeRegistration> getKnowledgeRegistrations() {
|
||||||
return knowledgeRetrievers;
|
return knowledgeRegistrations;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置知识库检索器。
|
* 设置知识库检索器。
|
||||||
*
|
*
|
||||||
* @param knowledgeRetrievers 知识库检索器
|
* @param knowledgeRegistrations 知识库运行时绑定
|
||||||
*/
|
*/
|
||||||
public void setKnowledgeRetrievers(Map<String, AgentKnowledgeRetriever> knowledgeRetrievers) {
|
public void setKnowledgeRegistrations(List<AgentKnowledgeRegistration> knowledgeRegistrations) {
|
||||||
this.knowledgeRetrievers = knowledgeRetrievers == null ? new LinkedHashMap<>() : knowledgeRetrievers;
|
this.knowledgeRegistrations = knowledgeRegistrations == null
|
||||||
|
? new ArrayList<>()
|
||||||
|
: new ArrayList<>(knowledgeRegistrations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import com.easyagents.agent.runtime.AgentRuntimeContext;
|
|||||||
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeDocument;
|
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeDocument;
|
||||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgePolicy;
|
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRegistration;
|
||||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetrievalResult;
|
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetrievalResult;
|
||||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
|
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
|
||||||
|
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeToolNames;
|
||||||
import com.easyagents.agent.runtime.memory.AgentMemoryCompressionParameter;
|
import com.easyagents.agent.runtime.memory.AgentMemoryCompressionParameter;
|
||||||
import com.easyagents.agent.runtime.memory.AgentMemoryPolicy;
|
import com.easyagents.agent.runtime.memory.AgentMemoryPolicy;
|
||||||
import com.easyagents.agent.runtime.memory.AgentMemoryType;
|
import com.easyagents.agent.runtime.memory.AgentMemoryType;
|
||||||
@@ -118,11 +119,11 @@ public class AgentRuntimeCompiler {
|
|||||||
bundle.setDefinition(definition);
|
bundle.setDefinition(definition);
|
||||||
|
|
||||||
compileTools(agent, definition, bundle);
|
compileTools(agent, definition, bundle);
|
||||||
|
compileKnowledge(agent, definition, bundle);
|
||||||
if (agentBuiltinToolsConfigResolver != null) {
|
if (agentBuiltinToolsConfigResolver != null) {
|
||||||
validateBuiltinTools(definition,
|
validateBuiltinTools(definition,
|
||||||
agentBuiltinToolsConfigResolver.resolvePublishedRuntime(agent.getExecutionConfigJson()));
|
agentBuiltinToolsConfigResolver.resolvePublishedRuntime(agent.getExecutionConfigJson()));
|
||||||
}
|
}
|
||||||
compileKnowledge(agent, definition, bundle);
|
|
||||||
return bundle;
|
return bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +295,7 @@ public class AgentRuntimeCompiler {
|
|||||||
if (config.artifactPublish().enabled()) {
|
if (config.artifactPublish().enabled()) {
|
||||||
specs.add(buildArtifactPublishSpec(config.artifactPublish()));
|
specs.add(buildArtifactPublishSpec(config.artifactPublish()));
|
||||||
}
|
}
|
||||||
assertToolBudget(specs, definition.getMcpSpecs());
|
assertToolBudget(specs, definition.getMcpSpecs(), definition.getKnowledgeSpecs().size());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void attachBuiltinTools(Agent agent,
|
private void attachBuiltinTools(Agent agent,
|
||||||
@@ -510,11 +511,21 @@ public class AgentRuntimeCompiler {
|
|||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验内置工具与普通工具、知识库工具及 MCP 工具不存在运行名冲突。
|
||||||
|
*
|
||||||
|
* @param definition 已编译 Agent 定义
|
||||||
|
* @param builtinNames 待启用内置工具名称
|
||||||
|
* @throws BusinessException 工具名称冲突时抛出
|
||||||
|
*/
|
||||||
private void assertNoBuiltinNameConflict(AgentDefinition definition, Set<String> builtinNames) {
|
private void assertNoBuiltinNameConflict(AgentDefinition definition, Set<String> builtinNames) {
|
||||||
Set<String> existing = new LinkedHashSet<>();
|
Set<String> existing = new LinkedHashSet<>();
|
||||||
for (AgentToolSpec spec : definition.getToolSpecs()) {
|
for (AgentToolSpec spec : definition.getToolSpecs()) {
|
||||||
existing.add(spec.getName());
|
existing.add(spec.getName());
|
||||||
}
|
}
|
||||||
|
for (AgentKnowledgeSpec spec : definition.getKnowledgeSpecs()) {
|
||||||
|
existing.add(AgentKnowledgeToolNames.build(spec.getRuntimeName()));
|
||||||
|
}
|
||||||
for (McpSpec mcp : definition.getMcpSpecs()) {
|
for (McpSpec mcp : definition.getMcpSpecs()) {
|
||||||
if (mcp.getFrozenToolManifest() != null) {
|
if (mcp.getFrozenToolManifest() != null) {
|
||||||
mcp.getFrozenToolManifest().forEach(entry -> existing.add(entry.getName()));
|
mcp.getFrozenToolManifest().forEach(entry -> existing.add(entry.getName()));
|
||||||
@@ -540,6 +551,14 @@ public class AgentRuntimeCompiler {
|
|||||||
assertToolBudget(toolSpecs, mcpSpecs, 0);
|
assertToolBudget(toolSpecs, mcpSpecs, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验最终工具数量和 Schema 大小预算。
|
||||||
|
*
|
||||||
|
* @param toolSpecs 静态 Tool 声明
|
||||||
|
* @param mcpSpecs MCP 声明
|
||||||
|
* @param additionalToolCount 知识库等额外工具数量
|
||||||
|
* @throws BusinessException 超出预算时抛出
|
||||||
|
*/
|
||||||
private void assertToolBudget(List<AgentToolSpec> toolSpecs,
|
private void assertToolBudget(List<AgentToolSpec> toolSpecs,
|
||||||
List<McpSpec> mcpSpecs,
|
List<McpSpec> mcpSpecs,
|
||||||
int additionalToolCount) {
|
int additionalToolCount) {
|
||||||
@@ -570,7 +589,7 @@ public class AgentRuntimeCompiler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (toolCount > MAX_RUNTIME_TOOL_COUNT) {
|
if (toolCount > MAX_RUNTIME_TOOL_COUNT) {
|
||||||
throw new BusinessException("Agent Runtime Tool 数量超过 128 个,请减少直接工具或 Skill 绑定");
|
throw new BusinessException("Agent Runtime Tool 数量超过 128 个,请减少工具、知识库或 Skill 绑定");
|
||||||
}
|
}
|
||||||
if (schemaBytes > MAX_RUNTIME_SCHEMA_BYTES) {
|
if (schemaBytes > MAX_RUNTIME_SCHEMA_BYTES) {
|
||||||
throw new BusinessException("Agent Runtime Tool Schema 超过 2 MiB,请减少工具或精简 Schema");
|
throw new BusinessException("Agent Runtime Tool Schema 超过 2 MiB,请减少工具或精简 Schema");
|
||||||
@@ -591,12 +610,27 @@ public class AgentRuntimeCompiler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 EasyFlow 知识库绑定编译为一库一工具所需的声明和 Retriever 绑定。
|
||||||
|
*
|
||||||
|
* @param agent Agent 发布视图
|
||||||
|
* @param definition 中立 Agent 定义
|
||||||
|
* @param bundle 运行时编译结果
|
||||||
|
* @throws BusinessException 知识库不存在、英文运行名非法或工具名冲突时抛出
|
||||||
|
*/
|
||||||
private void compileKnowledge(Agent agent, AgentDefinition definition, AgentRuntimeBundle bundle) {
|
private void compileKnowledge(Agent agent, AgentDefinition definition, AgentRuntimeBundle bundle) {
|
||||||
if (agent.getKnowledgeBindings() == null) {
|
if (agent.getKnowledgeBindings() == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
List<AgentKnowledgeSpec> specs = new ArrayList<>();
|
List<AgentKnowledgeSpec> specs = new ArrayList<>();
|
||||||
Map<String, com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetriever> retrievers = new LinkedHashMap<>();
|
List<AgentKnowledgeRegistration> registrations = new ArrayList<>();
|
||||||
|
Set<String> knowledgeToolNames = new LinkedHashSet<>();
|
||||||
|
Set<String> existingToolNames = new LinkedHashSet<>();
|
||||||
|
definition.getToolSpecs().stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.map(AgentToolSpec::getName)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.forEach(existingToolNames::add);
|
||||||
for (AgentKnowledgeBinding binding : agent.getKnowledgeBindings()) {
|
for (AgentKnowledgeBinding binding : agent.getKnowledgeBindings()) {
|
||||||
if (!Boolean.TRUE.equals(binding.getEnabled())) {
|
if (!Boolean.TRUE.equals(binding.getEnabled())) {
|
||||||
continue;
|
continue;
|
||||||
@@ -607,9 +641,9 @@ public class AgentRuntimeCompiler {
|
|||||||
}
|
}
|
||||||
AgentKnowledgeSpec spec = new AgentKnowledgeSpec();
|
AgentKnowledgeSpec spec = new AgentKnowledgeSpec();
|
||||||
spec.setKnowledgeId(binding.getKnowledgeId().toString());
|
spec.setKnowledgeId(binding.getKnowledgeId().toString());
|
||||||
|
spec.setRuntimeName(requireKnowledgeRuntimeName(knowledge));
|
||||||
spec.setName(knowledge.getTitle());
|
spec.setName(knowledge.getTitle());
|
||||||
spec.setDescription(knowledge.getDescription());
|
spec.setDescription(knowledge.getDescription());
|
||||||
spec.setRetrievalMode(AgentKnowledgePolicy.AGENTIC);
|
|
||||||
spec.getMetadata().put("knowledgeType", knowledge.getCollectionType());
|
spec.getMetadata().put("knowledgeType", knowledge.getCollectionType());
|
||||||
spec.getMetadata().put("faqCollection", knowledge.isFaqCollection());
|
spec.getMetadata().put("faqCollection", knowledge.isFaqCollection());
|
||||||
Integer limit = intValue(binding.getOptionsJson(), "limit");
|
Integer limit = intValue(binding.getOptionsJson(), "limit");
|
||||||
@@ -618,11 +652,37 @@ public class AgentRuntimeCompiler {
|
|||||||
if (threshold != null) {
|
if (threshold != null) {
|
||||||
spec.setScoreThreshold(threshold);
|
spec.setScoreThreshold(threshold);
|
||||||
}
|
}
|
||||||
|
String toolName = AgentKnowledgeToolNames.build(spec.getRuntimeName());
|
||||||
|
if (!knowledgeToolNames.add(toolName) || existingToolNames.contains(toolName)) {
|
||||||
|
throw new BusinessException("Agent 知识库工具运行名冲突:" + toolName);
|
||||||
|
}
|
||||||
specs.add(spec);
|
specs.add(spec);
|
||||||
retrievers.put(spec.getKnowledgeId(), request -> retrieveKnowledge(binding, request.getQuery(), request.getLimit(), request.getScoreThreshold()));
|
registrations.add(new AgentKnowledgeRegistration(spec,
|
||||||
|
request -> retrieveKnowledge(binding, request.getQuery(), request.getLimit(), request.getScoreThreshold())));
|
||||||
}
|
}
|
||||||
definition.setKnowledgeSpecs(specs);
|
definition.setKnowledgeSpecs(specs);
|
||||||
bundle.setKnowledgeRetrievers(retrievers);
|
bundle.setKnowledgeRegistrations(registrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取并校验知识库英文运行名。
|
||||||
|
*
|
||||||
|
* @param knowledge 知识库发布视图
|
||||||
|
* @return 合法英文运行名
|
||||||
|
* @throws BusinessException 英文运行名缺失或非法时抛出
|
||||||
|
*/
|
||||||
|
private String requireKnowledgeRuntimeName(DocumentCollection knowledge) {
|
||||||
|
String runtimeName = knowledge == null ? null : knowledge.getEnglishName();
|
||||||
|
try {
|
||||||
|
AgentKnowledgeToolNames.build(runtimeName);
|
||||||
|
return runtimeName.trim();
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
String knowledgeName = knowledge == null || knowledge.getTitle() == null
|
||||||
|
? "未知知识库"
|
||||||
|
: knowledge.getTitle();
|
||||||
|
throw new BusinessException(400, 400, "知识库“" + knowledgeName
|
||||||
|
+ "”的英文名称不能为空,且只能包含字母、数字、下划线和连字符", exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AgentKnowledgeRetrievalResult retrieveKnowledge(AgentKnowledgeBinding binding, String query, int limit, double scoreThreshold) {
|
private AgentKnowledgeRetrievalResult retrieveKnowledge(AgentKnowledgeBinding binding, String query, int limit, double scoreThreshold) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import java.time.Instant;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -563,7 +564,9 @@ public final class AguiAgentRunOutput implements AgentRunOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isHiddenToolName(String toolName) {
|
private static boolean isHiddenToolName(String toolName) {
|
||||||
return "retrieve_knowledge".equalsIgnoreCase(toolName)
|
String normalizedName = toolName == null ? "" : toolName.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return "retrieve_knowledge".equals(normalizedName)
|
||||||
|
|| normalizedName.startsWith("retrieve_knowledge_")
|
||||||
|| "context_reload".equalsIgnoreCase(toolName)
|
|| "context_reload".equalsIgnoreCase(toolName)
|
||||||
|| "__fragment__".equalsIgnoreCase(toolName);
|
|| "__fragment__".equalsIgnoreCase(toolName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import org.junit.Test;
|
|||||||
import tech.easyflow.agent.entity.Agent;
|
import tech.easyflow.agent.entity.Agent;
|
||||||
import tech.easyflow.agent.entity.AgentToolBinding;
|
import tech.easyflow.agent.entity.AgentToolBinding;
|
||||||
import tech.easyflow.agent.enums.AgentToolType;
|
import tech.easyflow.agent.enums.AgentToolType;
|
||||||
|
import tech.easyflow.agent.runtime.skill.AgentSkillRuntimeCompiler;
|
||||||
import tech.easyflow.agent.runtime.tool.AgentToolRuntimeCompiler;
|
import tech.easyflow.agent.runtime.tool.AgentToolRuntimeCompiler;
|
||||||
import tech.easyflow.ai.entity.Mcp;
|
import tech.easyflow.ai.entity.Mcp;
|
||||||
import tech.easyflow.ai.entity.Model;
|
import tech.easyflow.ai.entity.Model;
|
||||||
@@ -44,6 +45,8 @@ public class AgentDefinitionCompilerMcpTest {
|
|||||||
setField(toolCompiler, "objectMapper", new com.fasterxml.jackson.databind.ObjectMapper());
|
setField(toolCompiler, "objectMapper", new com.fasterxml.jackson.databind.ObjectMapper());
|
||||||
setField(toolCompiler, "mcpService", mcpService(mcp));
|
setField(toolCompiler, "mcpService", mcpService(mcp));
|
||||||
setField(compiler, "agentToolRuntimeCompiler", toolCompiler);
|
setField(compiler, "agentToolRuntimeCompiler", toolCompiler);
|
||||||
|
setField(compiler, "agentSkillRuntimeCompiler", new AgentSkillRuntimeCompiler(
|
||||||
|
null, toolCompiler, new com.fasterxml.jackson.databind.ObjectMapper()));
|
||||||
|
|
||||||
Agent agent = agent(modelId, mcpId);
|
Agent agent = agent(modelId, mcpId);
|
||||||
|
|
||||||
|
|||||||
@@ -476,18 +476,48 @@ public class AgentRunServiceDraftAndHitlTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证知识检索状态不会携带命中文档和内部 metadata。
|
* 验证知识库工具开始事件会投影为脱敏的检索中状态。
|
||||||
*
|
*
|
||||||
* @throws Exception 反射调用失败时抛出
|
* @throws Exception 反射调用失败时抛出
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void handleRuntimeEventShouldWhitelistKnowledgeStatusPayload() throws Exception {
|
public void handleRuntimeEventShouldProjectKnowledgeToolCallAsRunningStatus() throws Exception {
|
||||||
AgentRunService service = new AgentRunService();
|
AgentRunService service = new AgentRunService();
|
||||||
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
|
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
|
||||||
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL);
|
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.TOOL_CALL);
|
||||||
|
event.setToolCallId("knowledge-call-1");
|
||||||
|
event.getPayload().put("toolCallId", "knowledge-call-1");
|
||||||
|
event.getPayload().put("toolName", "retrieve_knowledge_homeinn_faq");
|
||||||
|
event.getPayload().put("toolCategory", "KNOWLEDGE");
|
||||||
event.getPayload().put("documents", List.of(Map.of("chunkContent", "private chunk")));
|
event.getPayload().put("documents", List.of(Map.of("chunkContent", "private chunk")));
|
||||||
event.getPayload().put("metadata", Map.of("sourceUri", "private://document"));
|
event.getPayload().put("metadata", Map.of("sourceUri", "private://document"));
|
||||||
|
|
||||||
|
invoke(service, "handleRuntimeEvent",
|
||||||
|
runtimeEventParameterTypes(),
|
||||||
|
event, "request-knowledge", legacyOutput(emitter), new StringBuilder(),
|
||||||
|
new ChatAssistantAccumulator(), chatContext(), new AtomicBoolean(false), false);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, emitter.envelopes.size());
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> payload = (Map<String, Object>) emitter.envelopes.get(0).getPayload();
|
||||||
|
Assert.assertEquals(Map.of(
|
||||||
|
"label", "正在检索知识库",
|
||||||
|
"status", "running",
|
||||||
|
"statusKey", "knowledge-retrieval"), payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证知识库工具结果事件会投影为完成状态。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败时抛出
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void handleRuntimeEventShouldProjectKnowledgeToolResultAsDoneStatus() throws Exception {
|
||||||
|
AgentRunService service = new AgentRunService();
|
||||||
|
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
|
||||||
|
AgentRuntimeEvent event = knowledgeToolEvent(
|
||||||
|
AgentRuntimeEventType.TOOL_RESULT, "knowledge-call-1", true);
|
||||||
|
|
||||||
invoke(service, "handleRuntimeEvent",
|
invoke(service, "handleRuntimeEvent",
|
||||||
runtimeEventParameterTypes(),
|
runtimeEventParameterTypes(),
|
||||||
event, "request-knowledge", legacyOutput(emitter), new StringBuilder(),
|
event, "request-knowledge", legacyOutput(emitter), new StringBuilder(),
|
||||||
@@ -502,6 +532,48 @@ public class AgentRunServiceDraftAndHitlTest {
|
|||||||
"statusKey", "knowledge-retrieval"), payload);
|
"statusKey", "knowledge-retrieval"), payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证文档摘要事件不会抢先把知识库工具状态标记为完成。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射调用失败时抛出
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void handleRuntimeEventShouldNotCompleteKnowledgeStatusFromDocumentEvent() throws Exception {
|
||||||
|
AgentRunService service = new AgentRunService();
|
||||||
|
RecordingChatSseEmitter emitter = new RecordingChatSseEmitter();
|
||||||
|
AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL);
|
||||||
|
event.getPayload().put("documents", List.of(Map.of("chunkContent", "private chunk")));
|
||||||
|
|
||||||
|
invoke(service, "handleRuntimeEvent",
|
||||||
|
runtimeEventParameterTypes(),
|
||||||
|
event, "request-knowledge", legacyOutput(emitter), new StringBuilder(),
|
||||||
|
new ChatAssistantAccumulator(), chatContext(), new AtomicBoolean(false), false);
|
||||||
|
|
||||||
|
Assert.assertTrue(emitter.envelopes.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证并行知识库调用全部结束后才进入终态,并保留任一调用失败结果。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void knowledgeStatusTrackerShouldAggregateParallelToolCalls() {
|
||||||
|
AgentRunService.KnowledgeRetrievalStatusTracker tracker =
|
||||||
|
new AgentRunService.KnowledgeRetrievalStatusTracker();
|
||||||
|
AgentRuntimeEvent firstCall = knowledgeToolEvent(
|
||||||
|
AgentRuntimeEventType.TOOL_CALL, "knowledge-call-1", true);
|
||||||
|
AgentRuntimeEvent secondCall = knowledgeToolEvent(
|
||||||
|
AgentRuntimeEventType.TOOL_CALL, "knowledge-call-2", true);
|
||||||
|
AgentRuntimeEvent firstResult = knowledgeToolEvent(
|
||||||
|
AgentRuntimeEventType.TOOL_RESULT, "knowledge-call-1", false);
|
||||||
|
AgentRuntimeEvent secondResult = knowledgeToolEvent(
|
||||||
|
AgentRuntimeEventType.TOOL_RESULT, "knowledge-call-2", true);
|
||||||
|
|
||||||
|
Assert.assertEquals("running", tracker.update(firstCall));
|
||||||
|
Assert.assertEquals("running", tracker.update(secondCall));
|
||||||
|
Assert.assertEquals("running", tracker.update(firstResult));
|
||||||
|
Assert.assertEquals("error", tracker.update(secondResult));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证完成事件不会再次发送正文消息,只用于最终收口。
|
* 验证完成事件不会再次发送正文消息,只用于最终收口。
|
||||||
*
|
*
|
||||||
@@ -1579,6 +1651,29 @@ public class AgentRunServiceDraftAndHitlTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建知识库工具生命周期测试事件。
|
||||||
|
*
|
||||||
|
* @param eventType 工具开始或结果事件类型
|
||||||
|
* @param toolCallId 工具调用 ID
|
||||||
|
* @param success 工具结果是否成功
|
||||||
|
* @return 知识库工具事件
|
||||||
|
*/
|
||||||
|
private AgentRuntimeEvent knowledgeToolEvent(AgentRuntimeEventType eventType,
|
||||||
|
String toolCallId,
|
||||||
|
boolean success) {
|
||||||
|
AgentRuntimeEvent event = AgentRuntimeEvent.of(eventType);
|
||||||
|
event.setToolCallId(toolCallId);
|
||||||
|
event.getPayload().put("toolCallId", toolCallId);
|
||||||
|
event.getPayload().put("toolName", "retrieve_knowledge_homeinn_faq");
|
||||||
|
event.getPayload().put("toolCategory", "KNOWLEDGE");
|
||||||
|
if (eventType == AgentRuntimeEventType.TOOL_RESULT) {
|
||||||
|
event.getPayload().put("success", success);
|
||||||
|
event.getPayload().put("status", success ? "SUCCESS" : "FAILED");
|
||||||
|
}
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
|
||||||
private Class<?>[] runtimeEventParameterTypes() {
|
private Class<?>[] runtimeEventParameterTypes() {
|
||||||
return new Class<?>[]{AgentRuntimeEvent.class, String.class, AgentRunOutput.class, StringBuilder.class,
|
return new Class<?>[]{AgentRuntimeEvent.class, String.class, AgentRunOutput.class, StringBuilder.class,
|
||||||
ChatAssistantAccumulator.class,
|
ChatAssistantAccumulator.class,
|
||||||
|
|||||||
@@ -0,0 +1,268 @@
|
|||||||
|
package tech.easyflow.agent.runtime;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeDocument;
|
||||||
|
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRegistration;
|
||||||
|
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetrievalRequest;
|
||||||
|
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetrievalResult;
|
||||||
|
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
|
||||||
|
import com.easyagents.core.document.Document;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import tech.easyflow.agent.entity.Agent;
|
||||||
|
import tech.easyflow.agent.entity.AgentKnowledgeBinding;
|
||||||
|
import tech.easyflow.agent.runtime.skill.AgentSkillRuntimeCompiler;
|
||||||
|
import tech.easyflow.agent.runtime.tool.AgentToolRuntimeCompiler;
|
||||||
|
import tech.easyflow.ai.entity.Model;
|
||||||
|
import tech.easyflow.ai.entity.ModelProvider;
|
||||||
|
import tech.easyflow.ai.rag.KnowledgeRetrievalRequest;
|
||||||
|
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||||
|
import tech.easyflow.ai.service.ModelService;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent 知识库一库一工具运行时编译测试。
|
||||||
|
*/
|
||||||
|
public class AgentRuntimeCompilerKnowledgeTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证知识库英文名称、描述和检索配置会编译到中立声明及独立 Retriever。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射注入依赖失败时抛出
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void compileShouldBuildOneKnowledgeRegistrationWithEnglishRuntimeName() throws Exception {
|
||||||
|
AtomicReference<KnowledgeRetrievalRequest> capturedRequest = new AtomicReference<>();
|
||||||
|
Document document = new Document("如家酒店通常在入住日 14:00 后办理入住。");
|
||||||
|
document.setId("chunk-1");
|
||||||
|
document.setTitle("如家 FAQ");
|
||||||
|
document.setScore(0.92D);
|
||||||
|
document.addMetadata("documentId", "faq-document-1");
|
||||||
|
document.addMetadata("chunkId", "faq-chunk-1");
|
||||||
|
AgentRuntimeCompiler compiler = compiler(capturedRequest, List.of(document));
|
||||||
|
Agent agent = agent(knowledgeBinding("homeinn_faq", BigInteger.valueOf(20L)));
|
||||||
|
|
||||||
|
AgentRuntimeBundle bundle = compiler.compile(agent);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, bundle.getDefinition().getKnowledgeSpecs().size());
|
||||||
|
AgentKnowledgeSpec spec = bundle.getDefinition().getKnowledgeSpecs().get(0);
|
||||||
|
Assert.assertEquals("homeinn_faq", spec.getRuntimeName());
|
||||||
|
Assert.assertEquals("如家 FAQ", spec.getName());
|
||||||
|
Assert.assertTrue(spec.getDescription().contains("入住"));
|
||||||
|
Assert.assertEquals(7, spec.getLimit());
|
||||||
|
Assert.assertEquals(0.55D, spec.getScoreThreshold(), 0.0001D);
|
||||||
|
Assert.assertEquals(1, bundle.getKnowledgeRegistrations().size());
|
||||||
|
|
||||||
|
AgentKnowledgeRegistration registration = bundle.getKnowledgeRegistrations().get(0);
|
||||||
|
AgentKnowledgeRetrievalRequest retrievalRequest = new AgentKnowledgeRetrievalRequest();
|
||||||
|
retrievalRequest.setQuery("如家几点入住");
|
||||||
|
retrievalRequest.setLimit(spec.getLimit());
|
||||||
|
retrievalRequest.setScoreThreshold(spec.getScoreThreshold());
|
||||||
|
AgentKnowledgeRetrievalResult result = registration.getRetriever().retrieve(retrievalRequest);
|
||||||
|
|
||||||
|
Assert.assertEquals("如家几点入住", capturedRequest.get().getQuery());
|
||||||
|
Assert.assertEquals(Integer.valueOf(7), capturedRequest.get().getLimit());
|
||||||
|
Assert.assertEquals(Double.valueOf(0.55D), capturedRequest.get().getMinSimilarity());
|
||||||
|
Assert.assertEquals("AGENT_KNOWLEDGE", capturedRequest.get().getCallerType());
|
||||||
|
Assert.assertEquals(1, result.getDocuments().size());
|
||||||
|
AgentKnowledgeDocument mapped = result.getDocuments().get(0);
|
||||||
|
Assert.assertEquals("faq-document-1", mapped.getDocumentId());
|
||||||
|
Assert.assertEquals("faq-chunk-1", mapped.getChunkId());
|
||||||
|
Assert.assertEquals(0.92D, mapped.getScore(), 0.0001D);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证缺失知识库英文名称时在发布编译阶段明确失败。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射注入依赖失败时抛出
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void compileShouldRejectMissingKnowledgeEnglishName() throws Exception {
|
||||||
|
AgentRuntimeCompiler compiler = compiler(new AtomicReference<>(), List.of());
|
||||||
|
Agent agent = agent(knowledgeBinding(null, BigInteger.valueOf(20L)));
|
||||||
|
|
||||||
|
try {
|
||||||
|
compiler.compile(agent);
|
||||||
|
Assert.fail("缺失英文名称时应拒绝编译");
|
||||||
|
} catch (BusinessException expected) {
|
||||||
|
Assert.assertTrue(expected.getMessage().contains("英文名称不能为空"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证多个知识库生成相同工具名时在编译阶段拒绝发布。
|
||||||
|
*
|
||||||
|
* @throws Exception 反射注入依赖失败时抛出
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void compileShouldRejectDuplicateKnowledgeToolNames() throws Exception {
|
||||||
|
AgentRuntimeCompiler compiler = compiler(new AtomicReference<>(), List.of());
|
||||||
|
AgentKnowledgeBinding first = knowledgeBinding("homeinn_faq", BigInteger.valueOf(20L));
|
||||||
|
AgentKnowledgeBinding second = knowledgeBinding("homeinn_faq", BigInteger.valueOf(21L));
|
||||||
|
Agent agent = agent(first, second);
|
||||||
|
|
||||||
|
try {
|
||||||
|
compiler.compile(agent);
|
||||||
|
Assert.fail("重复知识库工具名时应拒绝编译");
|
||||||
|
} catch (BusinessException expected) {
|
||||||
|
Assert.assertTrue(expected.getMessage().contains("retrieve_knowledge_homeinn_faq"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建仅含测试模型与知识库服务的运行时编译器。
|
||||||
|
*
|
||||||
|
* @param capturedRequest 检索请求捕获器
|
||||||
|
* @param documents 检索服务返回文档
|
||||||
|
* @return 已注入依赖的编译器
|
||||||
|
* @throws Exception 反射注入失败时抛出
|
||||||
|
*/
|
||||||
|
private AgentRuntimeCompiler compiler(AtomicReference<KnowledgeRetrievalRequest> capturedRequest,
|
||||||
|
List<Document> documents) throws Exception {
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
AgentToolRuntimeCompiler toolCompiler = new AgentToolRuntimeCompiler();
|
||||||
|
AgentRuntimeCompiler compiler = new AgentRuntimeCompiler();
|
||||||
|
setField(compiler, "objectMapper", objectMapper);
|
||||||
|
setField(compiler, "modelService", modelService(model()));
|
||||||
|
setField(compiler, "documentCollectionService", documentCollectionService(capturedRequest, documents));
|
||||||
|
setField(compiler, "agentToolRuntimeCompiler", toolCompiler);
|
||||||
|
setField(compiler, "agentSkillRuntimeCompiler",
|
||||||
|
new AgentSkillRuntimeCompiler(null, toolCompiler, objectMapper));
|
||||||
|
return compiler;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建带知识库绑定的 Agent。
|
||||||
|
*
|
||||||
|
* @param bindings 知识库绑定
|
||||||
|
* @return Agent 测试对象
|
||||||
|
*/
|
||||||
|
private Agent agent(AgentKnowledgeBinding... bindings) {
|
||||||
|
Agent agent = new Agent();
|
||||||
|
agent.setId(BigInteger.ONE);
|
||||||
|
agent.setName("如家助手");
|
||||||
|
agent.setModelId(BigInteger.TEN);
|
||||||
|
agent.setKnowledgeBindings(List.of(bindings));
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建冻结知识库绑定。
|
||||||
|
*
|
||||||
|
* @param englishName 知识库英文名称
|
||||||
|
* @param knowledgeId 知识库 ID
|
||||||
|
* @return 知识库绑定
|
||||||
|
*/
|
||||||
|
private AgentKnowledgeBinding knowledgeBinding(String englishName, BigInteger knowledgeId) {
|
||||||
|
AgentKnowledgeBinding binding = new AgentKnowledgeBinding();
|
||||||
|
binding.setAgentId(BigInteger.ONE);
|
||||||
|
binding.setKnowledgeId(knowledgeId);
|
||||||
|
binding.setRetrievalMode("HYBRID");
|
||||||
|
binding.setEnabled(true);
|
||||||
|
binding.setOptionsJson(Map.of("limit", 7, "scoreThreshold", 0.55D));
|
||||||
|
binding.setResourceSnapshot(Map.of(
|
||||||
|
"id", knowledgeId,
|
||||||
|
"title", "如家 FAQ",
|
||||||
|
"description", "如家酒店入住、退房和会员服务常见问题",
|
||||||
|
"collectionType", "FAQ",
|
||||||
|
"englishName", englishName == null ? "" : englishName));
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建模型服务代理。
|
||||||
|
*
|
||||||
|
* @param model 测试模型
|
||||||
|
* @return 模型服务代理
|
||||||
|
*/
|
||||||
|
private ModelService modelService(Model model) {
|
||||||
|
return (ModelService) Proxy.newProxyInstance(
|
||||||
|
ModelService.class.getClassLoader(),
|
||||||
|
new Class<?>[]{ModelService.class},
|
||||||
|
(proxy, method, args) -> "getModelInstance".equals(method.getName())
|
||||||
|
? model
|
||||||
|
: defaultValue(method.getReturnType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建知识库服务代理。
|
||||||
|
*
|
||||||
|
* @param capturedRequest 检索请求捕获器
|
||||||
|
* @param documents 返回文档
|
||||||
|
* @return 知识库服务代理
|
||||||
|
*/
|
||||||
|
private DocumentCollectionService documentCollectionService(
|
||||||
|
AtomicReference<KnowledgeRetrievalRequest> capturedRequest,
|
||||||
|
List<Document> documents) {
|
||||||
|
return (DocumentCollectionService) Proxy.newProxyInstance(
|
||||||
|
DocumentCollectionService.class.getClassLoader(),
|
||||||
|
new Class<?>[]{DocumentCollectionService.class},
|
||||||
|
(proxy, method, args) -> {
|
||||||
|
if ("search".equals(method.getName()) && args != null && args.length == 1
|
||||||
|
&& args[0] instanceof KnowledgeRetrievalRequest request) {
|
||||||
|
capturedRequest.set(request);
|
||||||
|
return documents;
|
||||||
|
}
|
||||||
|
return defaultValue(method.getReturnType());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建可映射为 AgentScope 模型配置的测试模型。
|
||||||
|
*
|
||||||
|
* @return 测试模型
|
||||||
|
*/
|
||||||
|
private Model model() {
|
||||||
|
ModelProvider provider = new ModelProvider();
|
||||||
|
provider.setProviderType("openai");
|
||||||
|
provider.setProviderName("OpenAI");
|
||||||
|
Model model = new Model();
|
||||||
|
model.setId(BigInteger.TEN);
|
||||||
|
model.setModelProvider(provider);
|
||||||
|
model.setModelName("gpt-test");
|
||||||
|
model.setEndpoint("https://example.com");
|
||||||
|
model.setRequestPath("/v1/chat/completions");
|
||||||
|
model.setApiKey("test-key");
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回代理方法所需的默认值。
|
||||||
|
*
|
||||||
|
* @param type 返回类型
|
||||||
|
* @return 对应默认值
|
||||||
|
*/
|
||||||
|
private Object defaultValue(Class<?> type) {
|
||||||
|
if (type == boolean.class) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (type == int.class || type == long.class || type == short.class || type == byte.class) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (type == double.class || type == float.class) {
|
||||||
|
return 0D;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 反射注入测试依赖。
|
||||||
|
*
|
||||||
|
* @param target 目标对象
|
||||||
|
* @param fieldName 字段名称
|
||||||
|
* @param value 字段值
|
||||||
|
* @throws Exception 字段不存在或不可写时抛出
|
||||||
|
*/
|
||||||
|
private void setField(Object target, String fieldName, Object value) throws Exception {
|
||||||
|
Field field = target.getClass().getDeclaredField(fieldName);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(target, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -187,7 +187,7 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
|||||||
RagScoreNormalizer.normalize(searchDocuments, retrievalMode, reranked);
|
RagScoreNormalizer.normalize(searchDocuments, retrievalMode, reranked);
|
||||||
List<Document> formattedDocuments = formatDocuments(
|
List<Document> formattedDocuments = formatDocuments(
|
||||||
searchDocuments,
|
searchDocuments,
|
||||||
shouldApplyMinSimilarityFilter(retrievalMode, reranked),
|
true,
|
||||||
minSimilarity,
|
minSimilarity,
|
||||||
docRecallMaxNum
|
docRecallMaxNum
|
||||||
);
|
);
|
||||||
@@ -396,10 +396,6 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
|||||||
return modelRerank.toRerankModel();
|
return modelRerank.toRerankModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean shouldApplyMinSimilarityFilter(RetrievalMode retrievalMode, boolean reranked) {
|
|
||||||
return !reranked && retrievalMode == RetrievalMode.VECTOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析本次查询使用的召回上限,优先采用请求参数,其次回退到知识库默认配置。
|
* 解析本次查询使用的召回上限,优先采用请求参数,其次回退到知识库默认配置。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -32,6 +32,28 @@ import static tech.easyflow.ai.entity.DocumentCollection.KEY_SIMILARITY_THRESHOL
|
|||||||
*/
|
*/
|
||||||
public class DocumentCollectionServiceImplTest {
|
public class DocumentCollectionServiceImplTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证最终相关度阈值会过滤所有已统一到零到一范围的检索结果。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void formatDocumentsShouldApplyFinalScoreThreshold() {
|
||||||
|
Document lowScore = buildHit(BigInteger.ONE, 0.49D);
|
||||||
|
Document thresholdScore = buildHit(BigInteger.TWO, 0.5D);
|
||||||
|
Document highScore = buildHit(BigInteger.valueOf(3), 0.9D);
|
||||||
|
DocumentCollectionServiceImpl service = new DocumentCollectionServiceImpl();
|
||||||
|
|
||||||
|
List<Document> result = service.formatDocuments(
|
||||||
|
List.of(lowScore, thresholdScore, highScore),
|
||||||
|
true,
|
||||||
|
0.5F,
|
||||||
|
5
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertEquals(2, result.size());
|
||||||
|
Assert.assertEquals(highScore.getId(), result.get(0).getId());
|
||||||
|
Assert.assertEquals(thresholdScore.getId(), result.get(1).getId());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证检索结果会在重排前过滤掉未完成文档,避免高分进行中文档挤占最终名额。
|
* 验证检索结果会在重排前过滤掉未完成文档,避免高分进行中文档挤占最终名额。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { EventType } from '@ag-ui/client';
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { EasyFlowAguiClient, EasyFlowAguiProjectionError } from './client';
|
import { EasyFlowAguiClient, EasyFlowAguiProjectionError } from './client';
|
||||||
|
import { easyFlowAguiCustomEvent } from './custom-events';
|
||||||
import { isRetryableAguiTransportError } from './reconnect';
|
import { isRetryableAguiTransportError } from './reconnect';
|
||||||
|
|
||||||
vi.mock('#/api/request', () => ({
|
vi.mock('#/api/request', () => ({
|
||||||
@@ -276,6 +277,69 @@ describe('easyFlowAguiClient', () => {
|
|||||||
expect(received.at(-1)).toBe(EventType.RUN_FINISHED);
|
expect(received.at(-1)).toBe(EventType.RUN_FINISHED);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('yields a paint opportunity after knowledge retrieval starts', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
let paintCallback: FrameRequestCallback | undefined;
|
||||||
|
vi.stubGlobal(
|
||||||
|
'requestAnimationFrame',
|
||||||
|
vi.fn((callback: FrameRequestCallback) => {
|
||||||
|
paintCallback = callback;
|
||||||
|
return 1;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.stubGlobal(
|
||||||
|
'fetch',
|
||||||
|
vi.fn(async () =>
|
||||||
|
sse([
|
||||||
|
{ runId: 'run-1', threadId: '101', type: EventType.RUN_STARTED },
|
||||||
|
{
|
||||||
|
name: easyFlowAguiCustomEvent.knowledgeRetrievalStatus,
|
||||||
|
type: EventType.CUSTOM,
|
||||||
|
value: {
|
||||||
|
status: 'running',
|
||||||
|
statusKey: 'knowledge-retrieval',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: easyFlowAguiCustomEvent.knowledgeRetrievalStatus,
|
||||||
|
type: EventType.CUSTOM,
|
||||||
|
value: {
|
||||||
|
status: 'done',
|
||||||
|
statusKey: 'knowledge-retrieval',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
runId: 'run-1',
|
||||||
|
threadId: '101',
|
||||||
|
type: EventType.RUN_FINISHED,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const receivedStatuses: string[] = [];
|
||||||
|
|
||||||
|
const runPromise = new EasyFlowAguiClient().run({
|
||||||
|
onEvent: (event) => {
|
||||||
|
if (event.type === EventType.CUSTOM) {
|
||||||
|
const value = event.value as Record<string, unknown>;
|
||||||
|
receivedStatuses.push(String(value.status));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
threadId: '101',
|
||||||
|
url: '/api/v1/agent/1/agui/run',
|
||||||
|
userMessage: { content: '几点退房', id: 'user-1', role: 'user' },
|
||||||
|
});
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
|
||||||
|
expect(receivedStatuses).toEqual(['running']);
|
||||||
|
|
||||||
|
paintCallback?.(0);
|
||||||
|
await vi.advanceTimersByTimeAsync(0);
|
||||||
|
await runPromise;
|
||||||
|
|
||||||
|
expect(receivedStatuses).toEqual(['running', 'done']);
|
||||||
|
});
|
||||||
|
|
||||||
it('replays a completed run from the server journal after refresh', async () => {
|
it('replays a completed run from the server journal after refresh', async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
'fetch',
|
'fetch',
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { events } from 'fetch-event-stream';
|
|||||||
|
|
||||||
import { createEventStreamHeaders, resolveApiUrl } from '#/api/request';
|
import { createEventStreamHeaders, resolveApiUrl } from '#/api/request';
|
||||||
|
|
||||||
|
import { easyFlowAguiCustomEvent } from './custom-events';
|
||||||
|
|
||||||
export interface EasyFlowAguiRunOptions {
|
export interface EasyFlowAguiRunOptions {
|
||||||
forwardedProps?: Record<string, unknown>;
|
forwardedProps?: Record<string, unknown>;
|
||||||
onCursor?: (cursor: number) => void;
|
onCursor?: (cursor: number) => void;
|
||||||
@@ -101,6 +103,31 @@ function waitForToolStartPaint(): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断事件是否开启了需要即时呈现的工具执行状态。
|
||||||
|
*
|
||||||
|
* @param event AG-UI 事件
|
||||||
|
* @returns 标准工具开始或知识库检索开始时为 true
|
||||||
|
*/
|
||||||
|
function startsVisibleToolExecution(event: AguiEvent) {
|
||||||
|
if (event.type === EventType.TOOL_CALL_START) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.type !== EventType.CUSTOM ||
|
||||||
|
event.name !== easyFlowAguiCustomEvent.knowledgeRetrievalStatus
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const value =
|
||||||
|
event.value &&
|
||||||
|
typeof event.value === 'object' &&
|
||||||
|
!Array.isArray(event.value)
|
||||||
|
? (event.value as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
return String(value.status || '').toLowerCase() === 'running';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* EasyFlow 的无头 AG-UI 运行客户端。
|
* EasyFlow 的无头 AG-UI 运行客户端。
|
||||||
*
|
*
|
||||||
@@ -174,7 +201,7 @@ export class EasyFlowAguiClient {
|
|||||||
) {
|
) {
|
||||||
terminalReceived = true;
|
terminalReceived = true;
|
||||||
}
|
}
|
||||||
if (event.type === EventType.TOOL_CALL_START) {
|
if (startsVisibleToolExecution(event as AguiEvent)) {
|
||||||
await waitForToolStartPaint();
|
await waitForToolStartPaint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,7 +260,7 @@ export class EasyFlowAguiClient {
|
|||||||
if (Number.isSafeInteger(cursor) && cursor > 0) {
|
if (Number.isSafeInteger(cursor) && cursor > 0) {
|
||||||
options.onCursor?.(cursor);
|
options.onCursor?.(cursor);
|
||||||
}
|
}
|
||||||
if (event.type === EventType.TOOL_CALL_START) {
|
if (startsVisibleToolExecution(event as AguiEvent)) {
|
||||||
await waitForToolStartPaint();
|
await waitForToolStartPaint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,6 +336,76 @@ describe('aG-UI wire contract and timeline projection', () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('merges knowledge retrieval tool and status events within one turn', () => {
|
||||||
|
const items: ChatTimelineItem[] = [];
|
||||||
|
const state = createAguiTimelineProjectionState();
|
||||||
|
const events = [
|
||||||
|
{
|
||||||
|
toolCallId: 'tool-faq-1',
|
||||||
|
toolCallName: 'retrieve_knowledge_homeinn_faq',
|
||||||
|
type: EventType.TOOL_CALL_START,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: easyFlowAguiCustomEvent.knowledgeRetrievalStatus,
|
||||||
|
type: EventType.CUSTOM,
|
||||||
|
value: {
|
||||||
|
label: '已检索知识库',
|
||||||
|
status: 'done',
|
||||||
|
statusKey: 'knowledge-retrieval',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: 'Retrieved 1 relevant document(s)',
|
||||||
|
messageId: 'tool-result-faq-1',
|
||||||
|
role: 'tool',
|
||||||
|
toolCallId: 'tool-faq-1',
|
||||||
|
type: EventType.TOOL_CALL_RESULT,
|
||||||
|
},
|
||||||
|
].map((event) => EventSchemas.parse(event));
|
||||||
|
|
||||||
|
for (const event of events) {
|
||||||
|
applyAguiEventToTimeline(items, event, { roundId: 'round-faq' }, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(items).toHaveLength(1);
|
||||||
|
expect(items[0]).toMatchObject({
|
||||||
|
label: '已检索知识库',
|
||||||
|
roundId: 'round-faq',
|
||||||
|
status: 'done',
|
||||||
|
statusKey: 'knowledge-retrieval:round-faq',
|
||||||
|
type: 'status',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('projects failed knowledge retrieval without exposing tool details', () => {
|
||||||
|
const items: ChatTimelineItem[] = [];
|
||||||
|
applyAguiEventToTimeline(
|
||||||
|
items,
|
||||||
|
EventSchemas.parse({
|
||||||
|
name: easyFlowAguiCustomEvent.knowledgeRetrievalStatus,
|
||||||
|
type: EventType.CUSTOM,
|
||||||
|
value: {
|
||||||
|
internalError: 'private stack',
|
||||||
|
status: 'error',
|
||||||
|
statusKey: 'knowledge-retrieval',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ roundId: 'round-failed-knowledge' },
|
||||||
|
createAguiTimelineProjectionState(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(items).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
label: '知识库检索失败',
|
||||||
|
status: 'error',
|
||||||
|
statusKey: 'knowledge-retrieval:round-failed-knowledge',
|
||||||
|
tone: 'danger',
|
||||||
|
type: 'status',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(JSON.stringify(items)).not.toContain('private stack');
|
||||||
|
});
|
||||||
|
|
||||||
it('projects Skill invocation status in place through the strict public fields', () => {
|
it('projects Skill invocation status in place through the strict public fields', () => {
|
||||||
const items: ChatTimelineItem[] = [];
|
const items: ChatTimelineItem[] = [];
|
||||||
const state = createAguiTimelineProjectionState();
|
const state = createAguiTimelineProjectionState();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
ChatTimelineKnowledgeHit,
|
ChatTimelineKnowledgeHit,
|
||||||
ChatTimelineMessageItem,
|
ChatTimelineMessageItem,
|
||||||
ChatTimelineSkillInvocationStatus,
|
ChatTimelineSkillInvocationStatus,
|
||||||
|
ChatTimelineStatusStatus,
|
||||||
ChatTimelineToolStatus,
|
ChatTimelineToolStatus,
|
||||||
} from '@easyflow/common-ui';
|
} from '@easyflow/common-ui';
|
||||||
|
|
||||||
@@ -131,6 +132,13 @@ function asyncToolStatus(
|
|||||||
return 'running';
|
return 'running';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function knowledgeRetrievalStatus(value: unknown): ChatTimelineStatusStatus {
|
||||||
|
const status = asText(value).trim().toLowerCase();
|
||||||
|
if (status === 'running') return 'running';
|
||||||
|
if (status === 'error' || status === 'failed') return 'error';
|
||||||
|
return 'done';
|
||||||
|
}
|
||||||
|
|
||||||
function statusKey(
|
function statusKey(
|
||||||
payload: Record<string, unknown>,
|
payload: Record<string, unknown>,
|
||||||
options: AguiTimelineProjectionOptions,
|
options: AguiTimelineProjectionOptions,
|
||||||
@@ -275,7 +283,7 @@ function applyCustomEvent(
|
|||||||
if (event.name === easyFlowAguiCustomEvent.knowledgeRetrievalStatus) {
|
if (event.name === easyFlowAguiCustomEvent.knowledgeRetrievalStatus) {
|
||||||
ChatTimelineBuilder.upsertKnowledgeRetrievalStatus(
|
ChatTimelineBuilder.upsertKnowledgeRetrievalStatus(
|
||||||
items,
|
items,
|
||||||
asText(payload.status).toLowerCase() === 'running' ? 'running' : 'done',
|
knowledgeRetrievalStatus(payload.status),
|
||||||
statusKey(payload, options, 'knowledge-retrieval'),
|
statusKey(payload, options, 'knowledge-retrieval'),
|
||||||
turnMetadata,
|
turnMetadata,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -54,20 +54,29 @@ function isHiddenToolName(toolName?: string) {
|
|||||||
const normalizedName = normalizeToolName(toolName);
|
const normalizedName = normalizeToolName(toolName);
|
||||||
return (
|
return (
|
||||||
normalizedName === 'retrieve_knowledge' ||
|
normalizedName === 'retrieve_knowledge' ||
|
||||||
|
normalizedName.startsWith('retrieve_knowledge_') ||
|
||||||
normalizedName === 'context_reload' ||
|
normalizedName === 'context_reload' ||
|
||||||
normalizedName === '__fragment__'
|
normalizedName === '__fragment__'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isKnowledgeRetrievalToolName(toolName?: string) {
|
function isKnowledgeRetrievalToolName(toolName?: string) {
|
||||||
return normalizeToolName(toolName) === 'retrieve_knowledge';
|
const normalizedName = normalizeToolName(toolName);
|
||||||
|
return (
|
||||||
|
normalizedName === 'retrieve_knowledge' ||
|
||||||
|
normalizedName.startsWith('retrieve_knowledge_')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isBlankToolName(toolName?: string) {
|
function isBlankToolName(toolName?: string) {
|
||||||
return !normalizeToolName(toolName);
|
return !normalizeToolName(toolName);
|
||||||
}
|
}
|
||||||
|
|
||||||
function knowledgeRetrievalStatusKey(statusKey?: string) {
|
function knowledgeRetrievalStatusKey(statusKey?: string, roundId?: string) {
|
||||||
|
const normalizedRoundId = normalizeText(roundId).trim();
|
||||||
|
if (normalizedRoundId) {
|
||||||
|
return `knowledge-retrieval:${normalizedRoundId}`;
|
||||||
|
}
|
||||||
return normalizeText(statusKey).trim() || 'knowledge-retrieval';
|
return normalizeText(statusKey).trim() || 'knowledge-retrieval';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,12 +668,18 @@ export const ChatTimelineBuilder = {
|
|||||||
metadata?: ChatTimelineTurnMetadata,
|
metadata?: ChatTimelineTurnMetadata,
|
||||||
) {
|
) {
|
||||||
finishAssistantMessage(items, false, metadata?.roundId);
|
finishAssistantMessage(items, false, metadata?.roundId);
|
||||||
|
let label = '已检索知识库';
|
||||||
|
if (status === 'running') {
|
||||||
|
label = '正在检索知识库';
|
||||||
|
} else if (status === 'error') {
|
||||||
|
label = '知识库检索失败';
|
||||||
|
}
|
||||||
upsertStatus(items, {
|
upsertStatus(items, {
|
||||||
...metadata,
|
...metadata,
|
||||||
label: status === 'running' ? '正在检索知识库' : '已检索知识库',
|
label,
|
||||||
status,
|
status,
|
||||||
statusKey: knowledgeRetrievalStatusKey(statusKey),
|
statusKey: knowledgeRetrievalStatusKey(statusKey, metadata?.roundId),
|
||||||
tone: 'muted',
|
tone: status === 'error' ? 'danger' : 'muted',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user