Compare commits
5 Commits
2bc525c16e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| bb37d9d708 | |||
| 3bd346ea77 | |||
| 55434466d4 | |||
| 7cac558b6c | |||
| 43f45956ff |
@@ -23,6 +23,11 @@
|
|||||||
<artifactId>agentscope</artifactId>
|
<artifactId>agentscope</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.fastjson2</groupId>
|
||||||
|
<artifactId>fastjson2</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.anthropic</groupId>
|
<groupId>com.anthropic</groupId>
|
||||||
<artifactId>anthropic-java</artifactId>
|
<artifactId>anthropic-java</artifactId>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.easyagents.agent.runtime;
|
|||||||
|
|
||||||
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
|
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
|
||||||
import com.easyagents.agent.runtime.memory.AgentMemoryPolicy;
|
import com.easyagents.agent.runtime.memory.AgentMemoryPolicy;
|
||||||
|
import com.easyagents.agent.runtime.mcp.McpSpec;
|
||||||
import com.easyagents.agent.runtime.model.AgentGenerationOptions;
|
import com.easyagents.agent.runtime.model.AgentGenerationOptions;
|
||||||
import com.easyagents.agent.runtime.model.AgentModelSpec;
|
import com.easyagents.agent.runtime.model.AgentModelSpec;
|
||||||
import com.easyagents.agent.runtime.persistence.AgentPersistencePolicy;
|
import com.easyagents.agent.runtime.persistence.AgentPersistencePolicy;
|
||||||
@@ -28,6 +29,7 @@ public class AgentDefinition {
|
|||||||
private AgentGenerationOptions generationOptions = new AgentGenerationOptions();
|
private AgentGenerationOptions generationOptions = new AgentGenerationOptions();
|
||||||
private AgentExecutionOptions executionOptions = new AgentExecutionOptions();
|
private AgentExecutionOptions executionOptions = new AgentExecutionOptions();
|
||||||
private List<AgentToolSpec> toolSpecs = new ArrayList<>();
|
private List<AgentToolSpec> toolSpecs = new ArrayList<>();
|
||||||
|
private List<McpSpec> mcpSpecs = new ArrayList<>();
|
||||||
private List<AgentOperateToolSpec> operateToolSpecs = new ArrayList<>();
|
private List<AgentOperateToolSpec> operateToolSpecs = new ArrayList<>();
|
||||||
private List<AgentKnowledgeSpec> knowledgeSpecs = new ArrayList<>();
|
private List<AgentKnowledgeSpec> knowledgeSpecs = new ArrayList<>();
|
||||||
private AgentMemoryPolicy memoryPolicy = AgentMemoryPolicy.autoContext();
|
private AgentMemoryPolicy memoryPolicy = AgentMemoryPolicy.autoContext();
|
||||||
@@ -179,6 +181,24 @@ public class AgentDefinition {
|
|||||||
this.toolSpecs = toolSpecs == null ? new ArrayList<>() : new ArrayList<>(toolSpecs);
|
this.toolSpecs = toolSpecs == null ? new ArrayList<>() : new ArrayList<>(toolSpecs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 MCP 声明。
|
||||||
|
*
|
||||||
|
* @return MCP 声明
|
||||||
|
*/
|
||||||
|
public List<McpSpec> getMcpSpecs() {
|
||||||
|
return mcpSpecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 MCP 声明。
|
||||||
|
*
|
||||||
|
* @param mcpSpecs MCP 声明
|
||||||
|
*/
|
||||||
|
public void setMcpSpecs(List<McpSpec> mcpSpecs) {
|
||||||
|
this.mcpSpecs = mcpSpecs == null ? new ArrayList<>() : new ArrayList<>(mcpSpecs);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取操作类工具定义。
|
* 获取操作类工具定义。
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -31,4 +31,10 @@ public interface AgentRuntime {
|
|||||||
* @return 运行事件流
|
* @return 运行事件流
|
||||||
*/
|
*/
|
||||||
Flux<AgentRuntimeEvent> resume(AgentResumeRequest request);
|
Flux<AgentRuntimeEvent> resume(AgentResumeRequest request);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭运行器并释放底层资源。
|
||||||
|
*/
|
||||||
|
default void close() {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
|
|||||||
import com.easyagents.agent.runtime.knowledge.citation.AgentKnowledgeCitationMatcher;
|
import com.easyagents.agent.runtime.knowledge.citation.AgentKnowledgeCitationMatcher;
|
||||||
import com.easyagents.agent.runtime.knowledge.citation.HeuristicKnowledgeCitationMatcher;
|
import com.easyagents.agent.runtime.knowledge.citation.HeuristicKnowledgeCitationMatcher;
|
||||||
import com.easyagents.agent.runtime.message.*;
|
import com.easyagents.agent.runtime.message.*;
|
||||||
|
import com.easyagents.agent.runtime.mcp.McpRegistration;
|
||||||
|
import com.easyagents.agent.runtime.mcp.McpSpecValidator;
|
||||||
|
import com.easyagents.agent.runtime.mcp.McpToolkitAdapter;
|
||||||
import com.easyagents.agent.runtime.persistence.session.noop.NoopAgentSessionStore;
|
import com.easyagents.agent.runtime.persistence.session.noop.NoopAgentSessionStore;
|
||||||
import com.easyagents.agent.runtime.skill.AgentSkillBinding;
|
import com.easyagents.agent.runtime.skill.AgentSkillBinding;
|
||||||
import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
|
import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
|
||||||
@@ -36,6 +39,7 @@ import io.agentscope.core.skill.SkillBox;
|
|||||||
import io.agentscope.core.state.SessionKey;
|
import io.agentscope.core.state.SessionKey;
|
||||||
import io.agentscope.core.tool.AgentTool;
|
import io.agentscope.core.tool.AgentTool;
|
||||||
import io.agentscope.core.tool.Toolkit;
|
import io.agentscope.core.tool.Toolkit;
|
||||||
|
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Sinks;
|
import reactor.core.publisher.Sinks;
|
||||||
|
|
||||||
@@ -50,6 +54,18 @@ import java.util.function.Supplier;
|
|||||||
*/
|
*/
|
||||||
public class AgentScopeReActRuntime implements AgentRuntime {
|
public class AgentScopeReActRuntime implements AgentRuntime {
|
||||||
|
|
||||||
|
private static final String ASYNC_TOOL_SYSTEM_PROMPT = """
|
||||||
|
|
||||||
|
Async tool protocol:
|
||||||
|
- Async tools may expose submit, observe, result, cancel, and list sub-tools. Treat these sub-tools as one user-facing tool.
|
||||||
|
- Do not ask the user to choose submit, observe, result, cancel, or list. These are internal execution phases.
|
||||||
|
- For a normal user request to use an async tool, call its submit sub-tool first with the user-provided arguments by default.
|
||||||
|
- After submit returns task_id, immediately call observe with that task_id to check progress.
|
||||||
|
- If the task is completed and result is available, use the returned result to answer the user.
|
||||||
|
- If the task is still running after observation, tell the user that the task is running and keep task_id/next_action for later tool calls.
|
||||||
|
- Use result, list, or cancel directly only when the user explicitly asks to get a known task result, list tasks, or cancel a task.
|
||||||
|
""";
|
||||||
|
|
||||||
private final AgentScopeModelFactory modelFactory;
|
private final AgentScopeModelFactory modelFactory;
|
||||||
private final AgentScopeToolAdapter toolAdapter;
|
private final AgentScopeToolAdapter toolAdapter;
|
||||||
private final AgentScopeKnowledgeAdapter knowledgeAdapter;
|
private final AgentScopeKnowledgeAdapter knowledgeAdapter;
|
||||||
@@ -57,6 +73,7 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
|||||||
private final AgentScopeSkillAdapter skillAdapter;
|
private final AgentScopeSkillAdapter skillAdapter;
|
||||||
private final AgentScopeMessageAdapter messageAdapter;
|
private final AgentScopeMessageAdapter messageAdapter;
|
||||||
private final AgentOperateToolAdapter operateToolAdapter = new AgentOperateToolAdapter();
|
private final AgentOperateToolAdapter operateToolAdapter = new AgentOperateToolAdapter();
|
||||||
|
private final McpToolkitAdapter mcpToolkitAdapter = new McpToolkitAdapter();
|
||||||
private final AgentKnowledgeCitationMatcher citationMatcher = new HeuristicKnowledgeCitationMatcher();
|
private final AgentKnowledgeCitationMatcher citationMatcher = new HeuristicKnowledgeCitationMatcher();
|
||||||
private final AtomicBoolean initialized = new AtomicBoolean(false);
|
private final AtomicBoolean initialized = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
@@ -68,6 +85,7 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
|||||||
private Session session;
|
private Session session;
|
||||||
private SessionKey sessionKey;
|
private SessionKey sessionKey;
|
||||||
private ReActAgent agent;
|
private ReActAgent agent;
|
||||||
|
private final List<McpClientWrapper> mcpClients = new CopyOnWriteArrayList<>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用默认适配器创建运行时。
|
* 使用默认适配器创建运行时。
|
||||||
@@ -112,6 +130,7 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
|||||||
if (!initialized.compareAndSet(false, true)) {
|
if (!initialized.compareAndSet(false, true)) {
|
||||||
throw new AgentRuntimeException("Agent runtime has already been initialized.");
|
throw new AgentRuntimeException("Agent runtime has already been initialized.");
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
this.initRequest = request;
|
this.initRequest = request;
|
||||||
this.runtimeContext = createRuntimeContext(request);
|
this.runtimeContext = createRuntimeContext(request);
|
||||||
this.skillContext = AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec());
|
this.skillContext = AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec());
|
||||||
@@ -121,6 +140,20 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
|||||||
this.sessionKey = AgentScopeSessionAdapter.sessionKey(request.getSessionId());
|
this.sessionKey = AgentScopeSessionAdapter.sessionKey(request.getSessionId());
|
||||||
this.agent = buildAgent(runtimeContext);
|
this.agent = buildAgent(runtimeContext);
|
||||||
this.agent.loadIfExists(session, sessionKey);
|
this.agent.loadIfExists(session, sessionKey);
|
||||||
|
} catch (RuntimeException error) {
|
||||||
|
closeMcpClients();
|
||||||
|
initialized.set(false);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭运行时并释放 MCP client。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
closeMcpClients();
|
||||||
|
initialized.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1054,12 +1087,14 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
|||||||
if (memory instanceof AutoContextMemory) {
|
if (memory instanceof AutoContextMemory) {
|
||||||
interceptors.add(new AutoContextInterceptor(eventBridge, memoryResult.getAutoContextConfig()));
|
interceptors.add(new AutoContextInterceptor(eventBridge, memoryResult.getAutoContextConfig()));
|
||||||
}
|
}
|
||||||
|
List<AgentToolSpec> runtimeToolSpecs = mergeToolSpecs(definition.getToolSpecs(), toolkitBuildResult.mcpToolSpecs(),
|
||||||
|
toolkitBuildResult.operateToolSpecs());
|
||||||
interceptors.add(new ToolHitlInterceptor(eventBridge, approvalCoordinator,
|
interceptors.add(new ToolHitlInterceptor(eventBridge, approvalCoordinator,
|
||||||
mergeToolSpecs(definition.getToolSpecs(), toolkitBuildResult.operateToolSpecs())));
|
runtimeToolSpecs));
|
||||||
// 注册旁路事件监听器与主线路干预器。观察器只发旁路事件,不修改 AgentScope HookEvent。
|
// 注册旁路事件监听器与主线路干预器。观察器只发旁路事件,不修改 AgentScope HookEvent。
|
||||||
List<AgentRuntimeObserver> observers = new ArrayList<>();
|
List<AgentRuntimeObserver> observers = new ArrayList<>();
|
||||||
observers.add(new SkillExecutionObserver(eventBridge, skillContext, skillBox));
|
observers.add(new SkillExecutionObserver(eventBridge, skillContext, skillBox));
|
||||||
observers.add(new ToolExecutionObserver(eventBridge, skillContext));
|
observers.add(new ToolExecutionObserver(eventBridge, skillContext, runtimeToolSpecs));
|
||||||
observers.add(new ReasoningLifecycleObserver(eventBridge));
|
observers.add(new ReasoningLifecycleObserver(eventBridge));
|
||||||
observers.add(new AgentRuntimeErrorObserver(eventBridge));
|
observers.add(new AgentRuntimeErrorObserver(eventBridge));
|
||||||
AgentRuntimeObservationManager observationManager =
|
AgentRuntimeObservationManager observationManager =
|
||||||
@@ -1067,7 +1102,7 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
|||||||
ReActAgent.Builder builder = ReActAgent.builder()
|
ReActAgent.Builder builder = ReActAgent.builder()
|
||||||
.name(definition.getAgentName())
|
.name(definition.getAgentName())
|
||||||
.description(definition.getDescription())
|
.description(definition.getDescription())
|
||||||
.sysPrompt(definition.getSystemPrompt())
|
.sysPrompt(systemPrompt(definition))
|
||||||
.model(model)
|
.model(model)
|
||||||
.toolkit(toolkit)
|
.toolkit(toolkit)
|
||||||
.memory(memory)
|
.memory(memory)
|
||||||
@@ -1087,6 +1122,30 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
|||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String systemPrompt(AgentDefinition definition) {
|
||||||
|
String prompt = definition.getSystemPrompt();
|
||||||
|
if (!hasAsyncTool(definition)) {
|
||||||
|
return prompt;
|
||||||
|
}
|
||||||
|
if (prompt == null || prompt.isBlank()) {
|
||||||
|
return ASYNC_TOOL_SYSTEM_PROMPT.strip();
|
||||||
|
}
|
||||||
|
return prompt.stripTrailing() + ASYNC_TOOL_SYSTEM_PROMPT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasAsyncTool(AgentDefinition definition) {
|
||||||
|
if (definition == null || definition.getToolSpecs() == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (AgentToolSpec toolSpec : definition.getToolSpecs()) {
|
||||||
|
// AsyncToolSpecExpander marks all generated sub-tools with this runtime metadata.
|
||||||
|
if (toolSpec != null && Boolean.TRUE.equals(toolSpec.getMetadata().get("asyncTool"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建 AgentScope Toolkit,并返回按 Skill ID 分组的工具。
|
* 构建 AgentScope Toolkit,并返回按 Skill ID 分组的工具。
|
||||||
*
|
*
|
||||||
@@ -1098,7 +1157,7 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
|||||||
Toolkit toolkit) {
|
Toolkit toolkit) {
|
||||||
Map<String, List<AgentTool>> skillTools = new LinkedHashMap<>();
|
Map<String, List<AgentTool>> skillTools = new LinkedHashMap<>();
|
||||||
if (!context.getAgentDefinition().getExecutionOptions().isToolCallingEnabled()) {
|
if (!context.getAgentDefinition().getExecutionOptions().isToolCallingEnabled()) {
|
||||||
return new AgentScopeToolkitBuildResult(skillTools, List.of());
|
return new AgentScopeToolkitBuildResult(skillTools, List.of(), List.of());
|
||||||
}
|
}
|
||||||
for (AgentToolSpec toolSpec : context.getAgentDefinition().getToolSpecs()) {
|
for (AgentToolSpec toolSpec : context.getAgentDefinition().getToolSpecs()) {
|
||||||
AgentToolInvoker invoker = context.getToolInvokers().get(toolSpec.getName());
|
AgentToolInvoker invoker = context.getToolInvokers().get(toolSpec.getName());
|
||||||
@@ -1111,22 +1170,45 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
|||||||
skillTools.computeIfAbsent(skillBinding.getSkillId(), key -> new ArrayList<>()).add(agentTool);
|
skillTools.computeIfAbsent(skillBinding.getSkillId(), key -> new ArrayList<>()).add(agentTool);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
McpRegistration mcpRegistration = mcpToolkitAdapter.register(
|
||||||
|
context.getAgentDefinition().getMcpSpecs(), toolkit);
|
||||||
|
mcpClients.addAll(mcpRegistration.getClients());
|
||||||
List<AgentToolSpec> operateToolSpecs = operateToolAdapter.register(
|
List<AgentToolSpec> operateToolSpecs = operateToolAdapter.register(
|
||||||
context.getAgentDefinition().getOperateToolSpecs(), toolkit);
|
context.getAgentDefinition().getOperateToolSpecs(), toolkit);
|
||||||
return new AgentScopeToolkitBuildResult(skillTools, operateToolSpecs);
|
McpSpecValidator.validateToolConflicts(context.getAgentDefinition().getToolSpecs(),
|
||||||
|
mcpRegistration.getToolSpecs(), context.getAgentDefinition().getOperateToolSpecs());
|
||||||
|
return new AgentScopeToolkitBuildResult(skillTools, mcpRegistration.getToolSpecs(), operateToolSpecs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<AgentToolSpec> mergeToolSpecs(List<AgentToolSpec> toolSpecs, List<AgentToolSpec> operateToolSpecs) {
|
private List<AgentToolSpec> mergeToolSpecs(List<AgentToolSpec> toolSpecs,
|
||||||
|
List<AgentToolSpec> mcpToolSpecs,
|
||||||
|
List<AgentToolSpec> operateToolSpecs) {
|
||||||
List<AgentToolSpec> merged = new ArrayList<>();
|
List<AgentToolSpec> merged = new ArrayList<>();
|
||||||
if (toolSpecs != null) {
|
if (toolSpecs != null) {
|
||||||
merged.addAll(toolSpecs);
|
merged.addAll(toolSpecs);
|
||||||
}
|
}
|
||||||
|
if (mcpToolSpecs != null) {
|
||||||
|
merged.addAll(mcpToolSpecs);
|
||||||
|
}
|
||||||
if (operateToolSpecs != null) {
|
if (operateToolSpecs != null) {
|
||||||
merged.addAll(operateToolSpecs);
|
merged.addAll(operateToolSpecs);
|
||||||
}
|
}
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void closeMcpClients() {
|
||||||
|
for (McpClientWrapper client : mcpClients) {
|
||||||
|
if (client == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
client.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mcpClients.clear();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建聚合知识库的默认检索配置。
|
* 构建聚合知识库的默认检索配置。
|
||||||
*
|
*
|
||||||
@@ -1163,6 +1245,7 @@ public class AgentScopeReActRuntime implements AgentRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private record AgentScopeToolkitBuildResult(Map<String, List<AgentTool>> skillTools,
|
private record AgentScopeToolkitBuildResult(Map<String, List<AgentTool>> skillTools,
|
||||||
|
List<AgentToolSpec> mcpToolSpecs,
|
||||||
List<AgentToolSpec> operateToolSpecs) {
|
List<AgentToolSpec> operateToolSpecs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -364,6 +364,7 @@ public class AgentScopeToolAdapter {
|
|||||||
if (param != null && param.getToolUseBlock() != null) {
|
if (param != null && param.getToolUseBlock() != null) {
|
||||||
context.setToolCallId(param.getToolUseBlock().getId());
|
context.setToolCallId(param.getToolUseBlock().getId());
|
||||||
}
|
}
|
||||||
|
context.setEventEmitter(this::emit);
|
||||||
context.getMetadata().put("toolName", toolSpec.getName());
|
context.getMetadata().put("toolName", toolSpec.getName());
|
||||||
context.getMetadata().put("category", toolSpec.getCategory());
|
context.getMetadata().put("category", toolSpec.getCategory());
|
||||||
appendSkillPayload(context.getMetadata(), activeSkillBinding());
|
appendSkillPayload(context.getMetadata(), activeSkillBinding());
|
||||||
@@ -372,7 +373,7 @@ public class AgentScopeToolAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将运行时结果转换为 AgentScope 结果块。
|
* 将 AgentToolResult 转换为 AgentScope 结果块。
|
||||||
*
|
*
|
||||||
* @param param 工具调用参数
|
* @param param 工具调用参数
|
||||||
* @param result 运行时结果
|
* @param result 运行时结果
|
||||||
|
|||||||
@@ -39,6 +39,36 @@ public enum AgentRuntimeEventType {
|
|||||||
*/
|
*/
|
||||||
TOOL_RESULT,
|
TOOL_RESULT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具已提交任务。
|
||||||
|
*/
|
||||||
|
ASYNC_TOOL_SUBMITTED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具已观察任务状态。
|
||||||
|
*/
|
||||||
|
ASYNC_TOOL_OBSERVED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具已读取任务结果。
|
||||||
|
*/
|
||||||
|
ASYNC_TOOL_RESULT,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具已请求取消任务。
|
||||||
|
*/
|
||||||
|
ASYNC_TOOL_CANCELLED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具已查询任务列表。
|
||||||
|
*/
|
||||||
|
ASYNC_TOOL_LISTED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具执行失败。
|
||||||
|
*/
|
||||||
|
ASYNC_TOOL_FAILED,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库检索完成并返回文档摘要。
|
* 知识库检索完成并返回文档摘要。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -142,6 +142,9 @@ public class ToolHitlInterceptor implements AgentRuntimeInterceptor {
|
|||||||
Map<String, Object> metadata = approvalRequest == null
|
Map<String, Object> metadata = approvalRequest == null
|
||||||
? new LinkedHashMap<>()
|
? new LinkedHashMap<>()
|
||||||
: new LinkedHashMap<>(approvalRequest.getMetadata());
|
: new LinkedHashMap<>(approvalRequest.getMetadata());
|
||||||
|
if (toolSpec.getMetadata() != null && !toolSpec.getMetadata().isEmpty()) {
|
||||||
|
metadata.putAll(toolSpec.getMetadata());
|
||||||
|
}
|
||||||
metadata.put("phase", "POST_REASONING");
|
metadata.put("phase", "POST_REASONING");
|
||||||
metadata.put("source", "TOOL_HITL_INTERCEPTOR");
|
metadata.put("source", "TOOL_HITL_INTERCEPTOR");
|
||||||
metadata.putAll(toolUse.getMetadata() == null ? Map.of() : toolUse.getMetadata());
|
metadata.putAll(toolUse.getMetadata() == null ? Map.of() : toolUse.getMetadata());
|
||||||
@@ -170,6 +173,7 @@ public class ToolHitlInterceptor implements AgentRuntimeInterceptor {
|
|||||||
event.getPayload().put("approvalPrompt", approvalPrompt(toolSpec.getApprovalRequest()));
|
event.getPayload().put("approvalPrompt", approvalPrompt(toolSpec.getApprovalRequest()));
|
||||||
event.getPayload().put("approvalMetadata", pendingState.getMetadata());
|
event.getPayload().put("approvalMetadata", pendingState.getMetadata());
|
||||||
event.getPayload().put("toolDescription", toolSpec.getDescription());
|
event.getPayload().put("toolDescription", toolSpec.getDescription());
|
||||||
|
enrichToolPayload(event.getPayload(), toolSpec);
|
||||||
event.getMetadata().put("source", "TOOL_HITL_INTERCEPTOR");
|
event.getMetadata().put("source", "TOOL_HITL_INTERCEPTOR");
|
||||||
event.getMetadata().put("phase", "POST_REASONING");
|
event.getMetadata().put("phase", "POST_REASONING");
|
||||||
return event;
|
return event;
|
||||||
@@ -187,6 +191,24 @@ public class ToolHitlInterceptor implements AgentRuntimeInterceptor {
|
|||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void enrichToolPayload(Map<String, Object> payload, AgentToolSpec toolSpec) {
|
||||||
|
if (toolSpec == null || toolSpec.getMetadata() == null || toolSpec.getMetadata().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Object> metadata = toolSpec.getMetadata();
|
||||||
|
putIfPresent(payload, metadata, "toolDisplayName");
|
||||||
|
putIfPresent(payload, metadata, "rawMcpToolName");
|
||||||
|
putIfPresent(payload, metadata, "mcpToolName");
|
||||||
|
putIfPresent(payload, metadata, "mcpName");
|
||||||
|
putIfPresent(payload, metadata, "mcpTitle");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putIfPresent(Map<String, Object> payload, Map<String, Object> metadata, String key) {
|
||||||
|
if (metadata.containsKey(key)) {
|
||||||
|
payload.put(key, metadata.get(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String approvalPrompt(AgentToolApprovalRequest approvalRequest) {
|
private String approvalPrompt(AgentToolApprovalRequest approvalRequest) {
|
||||||
if (approvalRequest != null
|
if (approvalRequest != null
|
||||||
&& approvalRequest.getApprovalPrompt() != null
|
&& approvalRequest.getApprovalPrompt() != null
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.easyagents.agent.runtime.event.AgentRuntimeEventBridge;
|
|||||||
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||||
import com.easyagents.agent.runtime.event.AgentRuntimeObserver;
|
import com.easyagents.agent.runtime.event.AgentRuntimeObserver;
|
||||||
import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
|
import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||||
import io.agentscope.core.hook.HookEvent;
|
import io.agentscope.core.hook.HookEvent;
|
||||||
import io.agentscope.core.hook.PostActingEvent;
|
import io.agentscope.core.hook.PostActingEvent;
|
||||||
import io.agentscope.core.hook.PreActingEvent;
|
import io.agentscope.core.hook.PreActingEvent;
|
||||||
@@ -15,7 +16,10 @@ import io.agentscope.core.message.ToolUseBlock;
|
|||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听 AgentScope 原生工具执行生命周期,并发射工具状态旁路事件。
|
* 监听 AgentScope 原生工具执行生命周期,并发射工具状态旁路事件。
|
||||||
@@ -28,6 +32,7 @@ public class ToolExecutionObserver implements AgentRuntimeObserver {
|
|||||||
|
|
||||||
private final AgentRuntimeEventBridge eventBridge;
|
private final AgentRuntimeEventBridge eventBridge;
|
||||||
private final AgentSkillRuntimeContext skillContext;
|
private final AgentSkillRuntimeContext skillContext;
|
||||||
|
private final Map<String, AgentToolSpec> toolSpecs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建工具执行观察器。
|
* 创建工具执行观察器。
|
||||||
@@ -35,7 +40,7 @@ public class ToolExecutionObserver implements AgentRuntimeObserver {
|
|||||||
* @param eventBridge 旁路事件桥
|
* @param eventBridge 旁路事件桥
|
||||||
*/
|
*/
|
||||||
public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge) {
|
public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge) {
|
||||||
this(eventBridge, null);
|
this(eventBridge, null, List.of());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,8 +51,25 @@ public class ToolExecutionObserver implements AgentRuntimeObserver {
|
|||||||
*/
|
*/
|
||||||
public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge,
|
public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge,
|
||||||
AgentSkillRuntimeContext skillContext) {
|
AgentSkillRuntimeContext skillContext) {
|
||||||
|
this(eventBridge, skillContext, List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建工具执行观察器。
|
||||||
|
*
|
||||||
|
* @param eventBridge 旁路事件桥
|
||||||
|
* @param skillContext Skill 上下文,用于跳过由 SkillExecutionObserver 处理的工具
|
||||||
|
* @param toolSpecs 工具声明列表,用于补齐展示名称和治理元数据
|
||||||
|
*/
|
||||||
|
public ToolExecutionObserver(AgentRuntimeEventBridge eventBridge,
|
||||||
|
AgentSkillRuntimeContext skillContext,
|
||||||
|
List<AgentToolSpec> toolSpecs) {
|
||||||
this.eventBridge = eventBridge;
|
this.eventBridge = eventBridge;
|
||||||
this.skillContext = skillContext;
|
this.skillContext = skillContext;
|
||||||
|
this.toolSpecs = (toolSpecs == null ? List.<AgentToolSpec>of() : toolSpecs).stream()
|
||||||
|
.filter(spec -> spec != null && spec.getName() != null && !spec.getName().isBlank())
|
||||||
|
.collect(Collectors.toMap(AgentToolSpec::getName, Function.identity(), (left, right) -> left,
|
||||||
|
LinkedHashMap::new));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,6 +109,7 @@ public class ToolExecutionObserver implements AgentRuntimeObserver {
|
|||||||
runtimeEvent.getPayload().put("source", "HOOK");
|
runtimeEvent.getPayload().put("source", "HOOK");
|
||||||
runtimeEvent.getPayload().put("phase", "PRE_ACTING");
|
runtimeEvent.getPayload().put("phase", "PRE_ACTING");
|
||||||
runtimeEvent.getMetadata().putAll(nullToEmpty(toolUse.getMetadata()));
|
runtimeEvent.getMetadata().putAll(nullToEmpty(toolUse.getMetadata()));
|
||||||
|
enrichToolPayload(runtimeEvent, toolUse.getName());
|
||||||
eventBridge.emit(runtimeEvent);
|
eventBridge.emit(runtimeEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,9 +138,31 @@ public class ToolExecutionObserver implements AgentRuntimeObserver {
|
|||||||
if (result != null) {
|
if (result != null) {
|
||||||
runtimeEvent.getMetadata().putAll(nullToEmpty(result.getMetadata()));
|
runtimeEvent.getMetadata().putAll(nullToEmpty(result.getMetadata()));
|
||||||
}
|
}
|
||||||
|
enrichToolPayload(runtimeEvent, toolName);
|
||||||
eventBridge.emit(runtimeEvent);
|
eventBridge.emit(runtimeEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void enrichToolPayload(AgentRuntimeEvent runtimeEvent, String toolName) {
|
||||||
|
AgentToolSpec toolSpec = toolSpecs.get(toolName);
|
||||||
|
if (toolSpec == null || toolSpec.getMetadata() == null || toolSpec.getMetadata().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Map<String, Object> metadata = toolSpec.getMetadata();
|
||||||
|
putIfPresent(runtimeEvent.getPayload(), metadata, "toolDisplayName");
|
||||||
|
putIfPresent(runtimeEvent.getPayload(), metadata, "rawMcpToolName");
|
||||||
|
putIfPresent(runtimeEvent.getPayload(), metadata, "mcpToolName");
|
||||||
|
putIfPresent(runtimeEvent.getPayload(), metadata, "mcpName");
|
||||||
|
putIfPresent(runtimeEvent.getPayload(), metadata, "mcpTitle");
|
||||||
|
putIfPresent(runtimeEvent.getPayload(), metadata, "source");
|
||||||
|
runtimeEvent.getMetadata().putAll(metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putIfPresent(Map<String, Object> payload, Map<String, Object> metadata, String key) {
|
||||||
|
if (metadata.containsKey(key)) {
|
||||||
|
payload.put(key, metadata.get(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean success(ToolResultBlock result) {
|
private boolean success(ToolResultBlock result) {
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package com.easyagents.agent.runtime.mcp;
|
||||||
|
|
||||||
|
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为 MCP client 增加运行时工具别名。
|
||||||
|
*/
|
||||||
|
class AliasedMcpClientWrapper extends McpClientWrapper {
|
||||||
|
|
||||||
|
static final String RAW_TOOL_NAME_META_KEY = "easyagentsRawMcpToolName";
|
||||||
|
|
||||||
|
private final McpClientWrapper delegate;
|
||||||
|
private final Map<String, String> rawToAlias;
|
||||||
|
private final Map<String, String> aliasToRaw;
|
||||||
|
private final String toolNamePrefix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 MCP client 别名包装器。
|
||||||
|
*
|
||||||
|
* @param delegate 原始 MCP client
|
||||||
|
* @param rawToAlias 原始工具名到运行时工具名的映射
|
||||||
|
*/
|
||||||
|
AliasedMcpClientWrapper(McpClientWrapper delegate, Map<String, String> rawToAlias) {
|
||||||
|
this(delegate, rawToAlias, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 MCP client 别名包装器。
|
||||||
|
*
|
||||||
|
* @param delegate 原始 MCP client
|
||||||
|
* @param rawToAlias 原始工具名到运行时工具名的映射
|
||||||
|
* @param toolNamePrefix 动态工具名前缀
|
||||||
|
*/
|
||||||
|
AliasedMcpClientWrapper(McpClientWrapper delegate, Map<String, String> rawToAlias, String toolNamePrefix) {
|
||||||
|
super(delegate == null ? "mcp" : delegate.getName());
|
||||||
|
this.delegate = delegate;
|
||||||
|
this.rawToAlias = rawToAlias == null ? Map.of() : new LinkedHashMap<>(rawToAlias);
|
||||||
|
this.aliasToRaw = new LinkedHashMap<>();
|
||||||
|
this.toolNamePrefix = toolNamePrefix == null || toolNamePrefix.isBlank() ? null : toolNamePrefix.trim();
|
||||||
|
this.rawToAlias.forEach((rawName, aliasName) -> {
|
||||||
|
if (rawName != null && aliasName != null && !aliasName.isBlank()) {
|
||||||
|
this.aliasToRaw.put(aliasName, rawName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化底层 MCP client。
|
||||||
|
*
|
||||||
|
* @return 初始化完成信号
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Mono<Void> initialize() {
|
||||||
|
return delegate.initialize().doOnSuccess(ignored -> initialized = delegate.isInitialized());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回已替换为运行时别名的工具列表。
|
||||||
|
*
|
||||||
|
* @return 工具列表
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Mono<List<McpSchema.Tool>> listTools() {
|
||||||
|
return delegate.listTools().map(this::aliasTools);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 MCP 工具,运行时别名会映射回原始工具名。
|
||||||
|
*
|
||||||
|
* @param toolName 运行时工具名
|
||||||
|
* @param arguments 工具参数
|
||||||
|
* @return 工具调用结果
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Mono<McpSchema.CallToolResult> callTool(String toolName, Map<String, Object> arguments) {
|
||||||
|
return delegate.callTool(rawToolName(toolName), arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭底层 MCP client。
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
delegate.close();
|
||||||
|
initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<McpSchema.Tool> aliasTools(List<McpSchema.Tool> tools) {
|
||||||
|
if (tools == null || tools.isEmpty()) {
|
||||||
|
cachedTools.clear();
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<McpSchema.Tool> aliased = new ArrayList<>();
|
||||||
|
cachedTools.clear();
|
||||||
|
Map<String, String> usedAliases = new LinkedHashMap<>();
|
||||||
|
for (McpSchema.Tool tool : tools) {
|
||||||
|
if (tool == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
McpSchema.Tool aliasTool = aliasTool(tool, usedAliases);
|
||||||
|
cachedTools.put(aliasTool.name(), aliasTool);
|
||||||
|
aliased.add(aliasTool);
|
||||||
|
}
|
||||||
|
return aliased;
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpSchema.Tool aliasTool(McpSchema.Tool tool, Map<String, String> usedAliases) {
|
||||||
|
String aliasName = uniqueAliasName(aliasName(tool.name()), tool.name(), usedAliases);
|
||||||
|
if (aliasName == null || aliasName.isBlank() || aliasName.equals(tool.name())) {
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
aliasToRaw.put(aliasName, tool.name());
|
||||||
|
Map<String, Object> meta = new LinkedHashMap<>();
|
||||||
|
if (tool.meta() != null) {
|
||||||
|
meta.putAll(tool.meta());
|
||||||
|
}
|
||||||
|
meta.put(RAW_TOOL_NAME_META_KEY, tool.name());
|
||||||
|
return new McpSchema.Tool(aliasName, tool.title(), tool.description(), tool.inputSchema(),
|
||||||
|
tool.outputSchema(), tool.annotations(), meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String uniqueAliasName(String aliasName, String rawName, Map<String, String> usedAliases) {
|
||||||
|
if (aliasName == null || aliasName.isBlank()) {
|
||||||
|
return aliasName;
|
||||||
|
}
|
||||||
|
String existingRawName = usedAliases.get(aliasName);
|
||||||
|
if (existingRawName == null || existingRawName.equals(rawName)) {
|
||||||
|
usedAliases.put(aliasName, rawName);
|
||||||
|
return aliasName;
|
||||||
|
}
|
||||||
|
int suffix = 2;
|
||||||
|
String candidate = aliasName + "_" + suffix;
|
||||||
|
while (usedAliases.containsKey(candidate)) {
|
||||||
|
suffix++;
|
||||||
|
candidate = aliasName + "_" + suffix;
|
||||||
|
}
|
||||||
|
usedAliases.put(candidate, rawName);
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String aliasName(String rawName) {
|
||||||
|
String explicitAlias = rawToAlias.get(rawName);
|
||||||
|
if (explicitAlias != null && !explicitAlias.isBlank()) {
|
||||||
|
return explicitAlias;
|
||||||
|
}
|
||||||
|
if (toolNamePrefix == null) {
|
||||||
|
return rawName;
|
||||||
|
}
|
||||||
|
return toolNamePrefix + safeToolNameSegment(rawName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeToolNameSegment(String value) {
|
||||||
|
String normalized = String.valueOf(value == null ? "" : value).trim()
|
||||||
|
.replaceAll("[^A-Za-z0-9_-]", "_")
|
||||||
|
.replaceAll("_+", "_");
|
||||||
|
if (normalized.isBlank()) {
|
||||||
|
return "tool";
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String rawToolName(String toolName) {
|
||||||
|
return aliasToRaw.getOrDefault(toolName, toolName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.easyagents.agent.runtime.mcp;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||||
|
import io.agentscope.core.tool.mcp.McpClientBuilder;
|
||||||
|
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 AgentScope MCP client。
|
||||||
|
*/
|
||||||
|
public class McpClientFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据 MCP 运行时声明创建 AgentScope MCP client。
|
||||||
|
*
|
||||||
|
* @param spec MCP 运行时声明
|
||||||
|
* @return AgentScope MCP client
|
||||||
|
*/
|
||||||
|
public McpClientWrapper create(McpSpec spec) {
|
||||||
|
McpSpecValidator.validateConnection(spec);
|
||||||
|
McpClientBuilder builder = McpClientBuilder.create(spec.getName())
|
||||||
|
.timeout(timeout(spec.getTimeout(), Duration.ofSeconds(120)))
|
||||||
|
.initializationTimeout(timeout(spec.getInitializationTimeout(), Duration.ofSeconds(30)));
|
||||||
|
switch (spec.getTransportType()) {
|
||||||
|
case STDIO -> builder.stdioTransport(spec.getCommand(), spec.getArgs(), spec.getEnv());
|
||||||
|
case SSE -> builder.sseTransport(spec.getUrl())
|
||||||
|
.headers(spec.getHeaders())
|
||||||
|
.queryParams(spec.getQueryParams());
|
||||||
|
case HTTP -> builder.streamableHttpTransport(spec.getUrl())
|
||||||
|
.headers(spec.getHeaders())
|
||||||
|
.queryParams(spec.getQueryParams());
|
||||||
|
default -> throw new AgentRuntimeException("Unsupported MCP transport type: " + spec.getTransportType());
|
||||||
|
}
|
||||||
|
return builder.buildAsync().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Duration timeout(Duration value, Duration defaultValue) {
|
||||||
|
return value == null || value.isZero() || value.isNegative() ? defaultValue : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.easyagents.agent.runtime.mcp;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||||
|
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 注册结果。
|
||||||
|
*/
|
||||||
|
public class McpRegistration {
|
||||||
|
|
||||||
|
private final List<McpClientWrapper> clients;
|
||||||
|
private final List<AgentToolSpec> toolSpecs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 MCP 注册结果。
|
||||||
|
*
|
||||||
|
* @param clients 已创建 MCP client
|
||||||
|
* @param toolSpecs 已注册工具声明
|
||||||
|
*/
|
||||||
|
public McpRegistration(List<McpClientWrapper> clients, List<AgentToolSpec> toolSpecs) {
|
||||||
|
this.clients = clients == null ? List.of() : new ArrayList<>(clients);
|
||||||
|
this.toolSpecs = toolSpecs == null ? List.of() : new ArrayList<>(toolSpecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空注册结果。
|
||||||
|
*
|
||||||
|
* @return 空注册结果
|
||||||
|
*/
|
||||||
|
public static McpRegistration empty() {
|
||||||
|
return new McpRegistration(List.of(), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已创建 MCP client。
|
||||||
|
*
|
||||||
|
* @return 已创建 MCP client
|
||||||
|
*/
|
||||||
|
public List<McpClientWrapper> getClients() {
|
||||||
|
return clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已注册工具声明。
|
||||||
|
*
|
||||||
|
* @return 已注册工具声明
|
||||||
|
*/
|
||||||
|
public List<AgentToolSpec> getToolSpecs() {
|
||||||
|
return toolSpecs;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
package com.easyagents.agent.runtime.mcp;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 运行时声明。
|
||||||
|
*/
|
||||||
|
public class McpSpec {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private McpTransportType transportType = McpTransportType.STDIO;
|
||||||
|
private String command;
|
||||||
|
private List<String> args = new ArrayList<>();
|
||||||
|
private Map<String, String> env = new LinkedHashMap<>();
|
||||||
|
private String url;
|
||||||
|
private Map<String, String> headers = new LinkedHashMap<>();
|
||||||
|
private Map<String, String> queryParams = new LinkedHashMap<>();
|
||||||
|
private Duration timeout = Duration.ofSeconds(120);
|
||||||
|
private Duration initializationTimeout = Duration.ofSeconds(30);
|
||||||
|
private List<String> enableTools = new ArrayList<>();
|
||||||
|
private List<String> disableTools = new ArrayList<>();
|
||||||
|
private String groupName;
|
||||||
|
private Map<String, Map<String, Object>> presetParameters = new LinkedHashMap<>();
|
||||||
|
private Map<String, String> toolAliases = new LinkedHashMap<>();
|
||||||
|
private String toolNamePrefix;
|
||||||
|
private boolean approvalRequired;
|
||||||
|
private AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
|
||||||
|
private Map<String, AgentToolApprovalRequest> toolApprovalRequests = new LinkedHashMap<>();
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 MCP client 名称。
|
||||||
|
*
|
||||||
|
* @return MCP client 名称
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 MCP client 名称。
|
||||||
|
*
|
||||||
|
* @param name MCP client 名称
|
||||||
|
*/
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 MCP 描述。
|
||||||
|
*
|
||||||
|
* @return MCP 描述
|
||||||
|
*/
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 MCP 描述。
|
||||||
|
*
|
||||||
|
* @param description MCP 描述
|
||||||
|
*/
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取连接方式。
|
||||||
|
*
|
||||||
|
* @return 连接方式
|
||||||
|
*/
|
||||||
|
public McpTransportType getTransportType() {
|
||||||
|
return transportType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置连接方式。
|
||||||
|
*
|
||||||
|
* @param transportType 连接方式
|
||||||
|
*/
|
||||||
|
public void setTransportType(McpTransportType transportType) {
|
||||||
|
this.transportType = transportType == null ? McpTransportType.STDIO : transportType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过字符串设置连接方式。
|
||||||
|
*
|
||||||
|
* @param transportType 连接方式文本
|
||||||
|
*/
|
||||||
|
public void setTransportType(String transportType) {
|
||||||
|
this.transportType = McpTransportType.from(transportType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 stdio 命令。
|
||||||
|
*
|
||||||
|
* @return stdio 命令
|
||||||
|
*/
|
||||||
|
public String getCommand() {
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 stdio 命令。
|
||||||
|
*
|
||||||
|
* @param command stdio 命令
|
||||||
|
*/
|
||||||
|
public void setCommand(String command) {
|
||||||
|
this.command = command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 stdio 参数。
|
||||||
|
*
|
||||||
|
* @return stdio 参数
|
||||||
|
*/
|
||||||
|
public List<String> getArgs() {
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 stdio 参数。
|
||||||
|
*
|
||||||
|
* @param args stdio 参数
|
||||||
|
*/
|
||||||
|
public void setArgs(List<String> args) {
|
||||||
|
this.args = args == null ? new ArrayList<>() : new ArrayList<>(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 stdio 环境变量。
|
||||||
|
*
|
||||||
|
* @return stdio 环境变量
|
||||||
|
*/
|
||||||
|
public Map<String, String> getEnv() {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 stdio 环境变量。
|
||||||
|
*
|
||||||
|
* @param env stdio 环境变量
|
||||||
|
*/
|
||||||
|
public void setEnv(Map<String, String> env) {
|
||||||
|
this.env = env == null ? new LinkedHashMap<>() : new LinkedHashMap<>(env);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 HTTP 地址。
|
||||||
|
*
|
||||||
|
* @return HTTP 地址
|
||||||
|
*/
|
||||||
|
public String getUrl() {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 HTTP 地址。
|
||||||
|
*
|
||||||
|
* @param url HTTP 地址
|
||||||
|
*/
|
||||||
|
public void setUrl(String url) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 HTTP 请求头。
|
||||||
|
*
|
||||||
|
* @return HTTP 请求头
|
||||||
|
*/
|
||||||
|
public Map<String, String> getHeaders() {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 HTTP 请求头。
|
||||||
|
*
|
||||||
|
* @param headers HTTP 请求头
|
||||||
|
*/
|
||||||
|
public void setHeaders(Map<String, String> headers) {
|
||||||
|
this.headers = headers == null ? new LinkedHashMap<>() : new LinkedHashMap<>(headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 HTTP 查询参数。
|
||||||
|
*
|
||||||
|
* @return HTTP 查询参数
|
||||||
|
*/
|
||||||
|
public Map<String, String> getQueryParams() {
|
||||||
|
return queryParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 HTTP 查询参数。
|
||||||
|
*
|
||||||
|
* @param queryParams HTTP 查询参数
|
||||||
|
*/
|
||||||
|
public void setQueryParams(Map<String, String> queryParams) {
|
||||||
|
this.queryParams = queryParams == null ? new LinkedHashMap<>() : new LinkedHashMap<>(queryParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取请求超时时间。
|
||||||
|
*
|
||||||
|
* @return 请求超时时间
|
||||||
|
*/
|
||||||
|
public Duration getTimeout() {
|
||||||
|
return timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置请求超时时间。
|
||||||
|
*
|
||||||
|
* @param timeout 请求超时时间
|
||||||
|
*/
|
||||||
|
public void setTimeout(Duration timeout) {
|
||||||
|
this.timeout = timeout == null ? Duration.ofSeconds(120) : timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取初始化超时时间。
|
||||||
|
*
|
||||||
|
* @return 初始化超时时间
|
||||||
|
*/
|
||||||
|
public Duration getInitializationTimeout() {
|
||||||
|
return initializationTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置初始化超时时间。
|
||||||
|
*
|
||||||
|
* @param initializationTimeout 初始化超时时间
|
||||||
|
*/
|
||||||
|
public void setInitializationTimeout(Duration initializationTimeout) {
|
||||||
|
this.initializationTimeout = initializationTimeout == null ? Duration.ofSeconds(30) : initializationTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取启用工具白名单。
|
||||||
|
*
|
||||||
|
* @return 启用工具白名单
|
||||||
|
*/
|
||||||
|
public List<String> getEnableTools() {
|
||||||
|
return enableTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置启用工具白名单。
|
||||||
|
*
|
||||||
|
* @param enableTools 启用工具白名单
|
||||||
|
*/
|
||||||
|
public void setEnableTools(List<String> enableTools) {
|
||||||
|
this.enableTools = enableTools == null ? new ArrayList<>() : new ArrayList<>(enableTools);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取禁用工具黑名单。
|
||||||
|
*
|
||||||
|
* @return 禁用工具黑名单
|
||||||
|
*/
|
||||||
|
public List<String> getDisableTools() {
|
||||||
|
return disableTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置禁用工具黑名单。
|
||||||
|
*
|
||||||
|
* @param disableTools 禁用工具黑名单
|
||||||
|
*/
|
||||||
|
public void setDisableTools(List<String> disableTools) {
|
||||||
|
this.disableTools = disableTools == null ? new ArrayList<>() : new ArrayList<>(disableTools);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具分组名。
|
||||||
|
*
|
||||||
|
* @return 工具分组名
|
||||||
|
*/
|
||||||
|
public String getGroupName() {
|
||||||
|
return groupName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工具分组名。
|
||||||
|
*
|
||||||
|
* @param groupName 工具分组名
|
||||||
|
*/
|
||||||
|
public void setGroupName(String groupName) {
|
||||||
|
this.groupName = groupName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取预置参数。
|
||||||
|
*
|
||||||
|
* @return 预置参数
|
||||||
|
*/
|
||||||
|
public Map<String, Map<String, Object>> getPresetParameters() {
|
||||||
|
return presetParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置预置参数。
|
||||||
|
*
|
||||||
|
* @param presetParameters 预置参数
|
||||||
|
*/
|
||||||
|
public void setPresetParameters(Map<String, Map<String, Object>> presetParameters) {
|
||||||
|
this.presetParameters = presetParameters == null ? new LinkedHashMap<>() : new LinkedHashMap<>(presetParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 MCP 原始工具名到运行时工具名的别名映射。
|
||||||
|
*
|
||||||
|
* @return 工具别名映射
|
||||||
|
*/
|
||||||
|
public Map<String, String> getToolAliases() {
|
||||||
|
return toolAliases;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 MCP 原始工具名到运行时工具名的别名映射。
|
||||||
|
*
|
||||||
|
* @param toolAliases 工具别名映射
|
||||||
|
*/
|
||||||
|
public void setToolAliases(Map<String, String> toolAliases) {
|
||||||
|
this.toolAliases = toolAliases == null ? new LinkedHashMap<>() : new LinkedHashMap<>(toolAliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取动态工具名前缀。
|
||||||
|
*
|
||||||
|
* @return 动态工具名前缀
|
||||||
|
*/
|
||||||
|
public String getToolNamePrefix() {
|
||||||
|
return toolNamePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置动态工具名前缀。
|
||||||
|
*
|
||||||
|
* @param toolNamePrefix 动态工具名前缀
|
||||||
|
*/
|
||||||
|
public void setToolNamePrefix(String toolNamePrefix) {
|
||||||
|
this.toolNamePrefix = toolNamePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回 MCP 工具是否默认需要人工审批。
|
||||||
|
*
|
||||||
|
* @return 需要审批时为 true
|
||||||
|
*/
|
||||||
|
public boolean isApprovalRequired() {
|
||||||
|
return approvalRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 MCP 工具是否默认需要人工审批。
|
||||||
|
*
|
||||||
|
* @param approvalRequired 审批标记
|
||||||
|
*/
|
||||||
|
public void setApprovalRequired(boolean approvalRequired) {
|
||||||
|
this.approvalRequired = approvalRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取审批请求。
|
||||||
|
*
|
||||||
|
* @return 审批请求
|
||||||
|
*/
|
||||||
|
public AgentToolApprovalRequest getApprovalRequest() {
|
||||||
|
return approvalRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置审批请求。
|
||||||
|
*
|
||||||
|
* @param approvalRequest 审批请求
|
||||||
|
*/
|
||||||
|
public void setApprovalRequest(AgentToolApprovalRequest approvalRequest) {
|
||||||
|
this.approvalRequest = approvalRequest == null ? new AgentToolApprovalRequest() : approvalRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取运行时工具名到审批请求的映射。
|
||||||
|
*
|
||||||
|
* @return 工具审批请求映射
|
||||||
|
*/
|
||||||
|
public Map<String, AgentToolApprovalRequest> getToolApprovalRequests() {
|
||||||
|
return toolApprovalRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置运行时工具名到审批请求的映射。
|
||||||
|
*
|
||||||
|
* @param toolApprovalRequests 工具审批请求映射
|
||||||
|
*/
|
||||||
|
public void setToolApprovalRequests(Map<String, AgentToolApprovalRequest> toolApprovalRequests) {
|
||||||
|
this.toolApprovalRequests = toolApprovalRequests == null
|
||||||
|
? new LinkedHashMap<>()
|
||||||
|
: new LinkedHashMap<>(toolApprovalRequests);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @return 元数据
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置元数据。
|
||||||
|
*
|
||||||
|
* @param metadata 元数据
|
||||||
|
*/
|
||||||
|
public void setMetadata(Map<String, Object> metadata) {
|
||||||
|
this.metadata = metadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
package com.easyagents.agent.runtime.mcp;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||||
|
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolAdapter;
|
||||||
|
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolSpec;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 声明校验器。
|
||||||
|
*/
|
||||||
|
public final class McpSpecValidator {
|
||||||
|
|
||||||
|
private McpSpecValidator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 MCP 连接配置。
|
||||||
|
*
|
||||||
|
* @param spec MCP 运行时声明
|
||||||
|
*/
|
||||||
|
public static void validateConnection(McpSpec spec) {
|
||||||
|
if (spec == null) {
|
||||||
|
throw new AgentRuntimeException("MCP spec is required.");
|
||||||
|
}
|
||||||
|
if (spec.getName() == null || spec.getName().isBlank()) {
|
||||||
|
throw new AgentRuntimeException("MCP name is required.");
|
||||||
|
}
|
||||||
|
if (spec.getTransportType() == null) {
|
||||||
|
throw new AgentRuntimeException("MCP transport type is required: " + spec.getName());
|
||||||
|
}
|
||||||
|
validateToolAliases(spec);
|
||||||
|
switch (spec.getTransportType()) {
|
||||||
|
case STDIO -> {
|
||||||
|
if (spec.getCommand() == null || spec.getCommand().isBlank()) {
|
||||||
|
throw new AgentRuntimeException("MCP stdio command is required: " + spec.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case SSE, HTTP -> {
|
||||||
|
if (spec.getUrl() == null || spec.getUrl().isBlank()) {
|
||||||
|
throw new AgentRuntimeException("MCP url is required: " + spec.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default -> throw new AgentRuntimeException("Unsupported MCP transport type: " + spec.getTransportType());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateToolAliases(McpSpec spec) {
|
||||||
|
Map<String, String> aliases = spec.getToolAliases();
|
||||||
|
if (aliases == null || aliases.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<String> runtimeNames = new HashSet<>();
|
||||||
|
for (Map.Entry<String, String> entry : aliases.entrySet()) {
|
||||||
|
String rawName = entry.getKey();
|
||||||
|
String runtimeName = entry.getValue();
|
||||||
|
if (rawName == null || rawName.isBlank()) {
|
||||||
|
throw new AgentRuntimeException("MCP raw tool name is required: " + spec.getName());
|
||||||
|
}
|
||||||
|
if (runtimeName == null || runtimeName.isBlank()) {
|
||||||
|
throw new AgentRuntimeException("MCP runtime tool name is required: " + spec.getName());
|
||||||
|
}
|
||||||
|
if (!runtimeNames.add(runtimeName)) {
|
||||||
|
throw new AgentRuntimeException("MCP runtime tool alias conflicts: " + runtimeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验 MCP 工具与既有工具名冲突。
|
||||||
|
*
|
||||||
|
* @param businessToolSpecs 普通工具声明
|
||||||
|
* @param mcpToolSpecs MCP 工具声明
|
||||||
|
* @param operateToolSpecs 操作工具声明
|
||||||
|
*/
|
||||||
|
public static void validateToolConflicts(List<AgentToolSpec> businessToolSpecs,
|
||||||
|
List<AgentToolSpec> mcpToolSpecs,
|
||||||
|
List<AgentOperateToolSpec> operateToolSpecs) {
|
||||||
|
Set<String> names = new HashSet<>();
|
||||||
|
addToolNames(names, businessToolSpecs, "Agent tool conflicts with existing tool: ");
|
||||||
|
addToolNames(names, mcpToolSpecs, "MCP tool conflicts with existing tool: ");
|
||||||
|
Set<String> operateToolNames = new AgentOperateToolAdapter().enabledToolNames(operateToolSpecs);
|
||||||
|
for (String operateToolName : operateToolNames) {
|
||||||
|
if (!names.add(operateToolName)) {
|
||||||
|
throw new AgentRuntimeException("Agent operate tool conflicts with existing tool: " + operateToolName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addToolNames(Set<String> names, List<AgentToolSpec> toolSpecs, String messagePrefix) {
|
||||||
|
if (toolSpecs == null || toolSpecs.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (AgentToolSpec toolSpec : toolSpecs) {
|
||||||
|
if (toolSpec == null || toolSpec.getName() == null || toolSpec.getName().isBlank()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!names.add(toolSpec.getName())) {
|
||||||
|
throw new AgentRuntimeException(messagePrefix + toolSpec.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
package com.easyagents.agent.runtime.mcp;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||||
|
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolCategory;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolVisibility;
|
||||||
|
import io.agentscope.core.tool.Toolkit;
|
||||||
|
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||||
|
import io.agentscope.core.tool.mcp.McpTool;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 MCP 运行时声明注册到 AgentScope Toolkit。
|
||||||
|
*/
|
||||||
|
public class McpToolkitAdapter {
|
||||||
|
|
||||||
|
private final McpClientFactory clientFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用默认 MCP client factory 创建适配器。
|
||||||
|
*/
|
||||||
|
public McpToolkitAdapter() {
|
||||||
|
this(new McpClientFactory());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用指定 MCP client factory 创建适配器。
|
||||||
|
*
|
||||||
|
* @param clientFactory MCP client factory
|
||||||
|
*/
|
||||||
|
public McpToolkitAdapter(McpClientFactory clientFactory) {
|
||||||
|
this.clientFactory = clientFactory == null ? new McpClientFactory() : clientFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 MCP 声明注册到 Toolkit。
|
||||||
|
*
|
||||||
|
* @param specs MCP 声明
|
||||||
|
* @param toolkit AgentScope Toolkit
|
||||||
|
* @return MCP 注册结果
|
||||||
|
*/
|
||||||
|
public McpRegistration register(List<McpSpec> specs, Toolkit toolkit) {
|
||||||
|
if (specs == null || specs.isEmpty()) {
|
||||||
|
return McpRegistration.empty();
|
||||||
|
}
|
||||||
|
if (toolkit == null) {
|
||||||
|
throw new AgentRuntimeException("AgentScope toolkit is required for MCP registration.");
|
||||||
|
}
|
||||||
|
List<McpClientWrapper> clients = new ArrayList<>();
|
||||||
|
List<AgentToolSpec> toolSpecs = new ArrayList<>();
|
||||||
|
try {
|
||||||
|
for (McpSpec spec : specs) {
|
||||||
|
if (spec == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
McpSpecValidator.validateConnection(spec);
|
||||||
|
McpClientWrapper client = clientFactory.create(spec);
|
||||||
|
client = applyAliases(spec, client);
|
||||||
|
clients.add(client);
|
||||||
|
registerClient(spec, client, toolkit);
|
||||||
|
toolSpecs.addAll(toToolSpecs(spec, registeredTools(spec, client)));
|
||||||
|
}
|
||||||
|
} catch (RuntimeException error) {
|
||||||
|
closeQuietly(clients);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return new McpRegistration(clients, toolSpecs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpClientWrapper applyAliases(McpSpec spec, McpClientWrapper client) {
|
||||||
|
boolean hasExplicitAliases = spec.getToolAliases() != null && !spec.getToolAliases().isEmpty();
|
||||||
|
boolean hasDynamicPrefix = spec.getToolNamePrefix() != null && !spec.getToolNamePrefix().isBlank();
|
||||||
|
if (!hasExplicitAliases && !hasDynamicPrefix) {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
return new AliasedMcpClientWrapper(client, spec.getToolAliases(), spec.getToolNamePrefix());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void registerClient(McpSpec spec, McpClientWrapper client, Toolkit toolkit) {
|
||||||
|
String groupName = blankToNull(spec.getGroupName());
|
||||||
|
if (groupName != null && toolkit.getToolGroup(groupName) == null) {
|
||||||
|
toolkit.createToolGroup(groupName, spec.getDescription(), true);
|
||||||
|
}
|
||||||
|
toolkit.registration()
|
||||||
|
.mcpClient(client)
|
||||||
|
.enableTools(emptyToNull(spec.getEnableTools()))
|
||||||
|
.disableTools(emptyToNull(spec.getDisableTools()))
|
||||||
|
.group(groupName)
|
||||||
|
.presetParameters(emptyToNull(spec.getPresetParameters()))
|
||||||
|
.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<McpSchema.Tool> registeredTools(McpSpec spec, McpClientWrapper client) {
|
||||||
|
List<McpSchema.Tool> tools = client.listTools().block();
|
||||||
|
if (tools == null || tools.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<McpSchema.Tool> filtered = new ArrayList<>();
|
||||||
|
for (McpSchema.Tool tool : tools) {
|
||||||
|
if (tool != null && shouldRegister(tool.name(), spec.getEnableTools(), spec.getDisableTools())) {
|
||||||
|
filtered.add(tool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldRegister(String toolName, List<String> enableTools, List<String> disableTools) {
|
||||||
|
if (enableTools != null && !enableTools.isEmpty()) {
|
||||||
|
return enableTools.contains(toolName);
|
||||||
|
}
|
||||||
|
return disableTools == null || disableTools.isEmpty() || !disableTools.contains(toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<AgentToolSpec> toToolSpecs(McpSpec spec, List<McpSchema.Tool> tools) {
|
||||||
|
if (tools == null || tools.isEmpty()) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<AgentToolSpec> toolSpecs = new ArrayList<>();
|
||||||
|
for (McpSchema.Tool tool : tools) {
|
||||||
|
AgentToolSpec toolSpec = new AgentToolSpec();
|
||||||
|
Set<String> excludedPresetNames = Set.of();
|
||||||
|
Map<String, Object> toolPresetParameters = spec.getPresetParameters() == null
|
||||||
|
? null
|
||||||
|
: spec.getPresetParameters().get(tool.name());
|
||||||
|
if (toolPresetParameters != null) {
|
||||||
|
excludedPresetNames = toolPresetParameters.keySet();
|
||||||
|
}
|
||||||
|
toolSpec.setName(tool.name());
|
||||||
|
toolSpec.setDescription(tool.description());
|
||||||
|
toolSpec.setCategory(AgentToolCategory.MCP);
|
||||||
|
toolSpec.setVisibility(AgentToolVisibility.VISIBLE);
|
||||||
|
toolSpec.setParametersSchema(McpTool.convertMcpSchemaToParameters(tool.inputSchema(), excludedPresetNames));
|
||||||
|
toolSpec.setOutputSchema(tool.outputSchema());
|
||||||
|
AgentToolApprovalRequest toolApprovalRequest = toolApprovalRequest(spec, tool.name());
|
||||||
|
toolSpec.setApprovalRequired(spec.isApprovalRequired() || toolApprovalRequest != null);
|
||||||
|
toolSpec.setApprovalRequest(toolApprovalRequest == null ? spec.getApprovalRequest() : toolApprovalRequest);
|
||||||
|
toolSpec.setMetadata(metadata(spec, tool));
|
||||||
|
toolSpecs.add(toolSpec);
|
||||||
|
}
|
||||||
|
return toolSpecs;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolApprovalRequest toolApprovalRequest(McpSpec spec, String toolName) {
|
||||||
|
if (spec.getToolApprovalRequests() == null || spec.getToolApprovalRequests().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return spec.getToolApprovalRequests().get(toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> metadata(McpSpec spec, McpSchema.Tool tool) {
|
||||||
|
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
if (spec.getMetadata() != null) {
|
||||||
|
spec.getMetadata().forEach((key, value) -> {
|
||||||
|
if (!isSensitiveMetadataKey(key)) {
|
||||||
|
metadata.put(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
metadata.put("source", "MCP");
|
||||||
|
metadata.put("mcpName", spec.getName());
|
||||||
|
metadata.put("mcpToolName", tool.name());
|
||||||
|
metadata.put("rawMcpToolName", rawToolName(spec, tool));
|
||||||
|
metadata.put("toolDisplayName", toolDisplayName(spec, tool));
|
||||||
|
metadata.put("transportType", spec.getTransportType().configValue());
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String rawToolName(McpSpec spec, McpSchema.Tool tool) {
|
||||||
|
if (tool != null && tool.meta() != null) {
|
||||||
|
Object rawName = tool.meta().get(AliasedMcpClientWrapper.RAW_TOOL_NAME_META_KEY);
|
||||||
|
if (rawName != null && !String.valueOf(rawName).isBlank()) {
|
||||||
|
return String.valueOf(rawName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String toolName = tool == null ? null : tool.name();
|
||||||
|
if (spec.getToolAliases() != null && !spec.getToolAliases().isEmpty()) {
|
||||||
|
for (Map.Entry<String, String> entry : spec.getToolAliases().entrySet()) {
|
||||||
|
if (Objects.equals(entry.getValue(), toolName)) {
|
||||||
|
return entry.getKey();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toolDisplayName(McpSpec spec, McpSchema.Tool tool) {
|
||||||
|
String rawToolName = rawToolName(spec, tool);
|
||||||
|
String mcpName = spec == null ? null : spec.getDescription();
|
||||||
|
if (mcpName == null || mcpName.isBlank()) {
|
||||||
|
mcpName = spec == null ? null : spec.getName();
|
||||||
|
}
|
||||||
|
if (mcpName == null || mcpName.isBlank()) {
|
||||||
|
return rawToolName;
|
||||||
|
}
|
||||||
|
if (rawToolName == null || rawToolName.isBlank()) {
|
||||||
|
return mcpName;
|
||||||
|
}
|
||||||
|
return mcpName + " - " + rawToolName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSensitiveMetadataKey(String key) {
|
||||||
|
if (key == null || key.isBlank()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String normalized = key.toLowerCase(Locale.ROOT).replace("-", "").replace("_", "");
|
||||||
|
return normalized.contains("key")
|
||||||
|
|| normalized.contains("token")
|
||||||
|
|| normalized.contains("secret")
|
||||||
|
|| normalized.contains("password")
|
||||||
|
|| normalized.contains("authorization")
|
||||||
|
|| normalized.contains("credential");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeQuietly(List<McpClientWrapper> clients) {
|
||||||
|
for (McpClientWrapper client : clients) {
|
||||||
|
if (client == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
client.close();
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private <T> List<T> emptyToNull(List<T> values) {
|
||||||
|
return values == null || values.isEmpty() ? null : values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private <K, V> Map<K, V> emptyToNull(Map<K, V> values) {
|
||||||
|
return values == null || values.isEmpty() ? null : values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String blankToNull(String value) {
|
||||||
|
return value == null || value.isBlank() ? null : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.easyagents.agent.runtime.mcp;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 连接方式。
|
||||||
|
*/
|
||||||
|
public enum McpTransportType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标准输入输出进程通信。
|
||||||
|
*/
|
||||||
|
STDIO,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP Server-Sent Events 通信。
|
||||||
|
*/
|
||||||
|
SSE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Streamable HTTP 通信。
|
||||||
|
*/
|
||||||
|
HTTP;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 MCP 连接方式。
|
||||||
|
*
|
||||||
|
* @param value 连接方式文本
|
||||||
|
* @return MCP 连接方式
|
||||||
|
*/
|
||||||
|
public static McpTransportType from(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return STDIO;
|
||||||
|
}
|
||||||
|
String normalized = value.trim().toLowerCase(Locale.ROOT);
|
||||||
|
return switch (normalized) {
|
||||||
|
case "stdio" -> STDIO;
|
||||||
|
case "sse", "http-sse" -> SSE;
|
||||||
|
case "http", "streamable-http", "http-stream" -> HTTP;
|
||||||
|
default -> throw new AgentRuntimeException("Unsupported MCP transport type: " + value);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 转换为 Easy MCP 配置兼容值。
|
||||||
|
*
|
||||||
|
* @return transport 配置值
|
||||||
|
*/
|
||||||
|
public String configValue() {
|
||||||
|
return switch (this) {
|
||||||
|
case STDIO -> "stdio";
|
||||||
|
case SSE -> "http-sse";
|
||||||
|
case HTTP -> "http-stream";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.easyagents.agent.runtime.tool;
|
package com.easyagents.agent.runtime.tool;
|
||||||
|
|
||||||
import com.easyagents.agent.runtime.AgentRuntimeContext;
|
import com.easyagents.agent.runtime.AgentRuntimeContext;
|
||||||
|
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 传递给动态工具调用的上下文。
|
* 传递给动态工具调用的上下文。
|
||||||
@@ -17,6 +19,7 @@ public class AgentToolContext {
|
|||||||
private String toolCallId;
|
private String toolCallId;
|
||||||
private AgentRuntimeContext runtimeContext = new AgentRuntimeContext();
|
private AgentRuntimeContext runtimeContext = new AgentRuntimeContext();
|
||||||
private Map<String, Object> metadata = new LinkedHashMap<>();
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
private Consumer<AgentRuntimeEvent> eventEmitter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取请求ID。
|
* 获取请求ID。
|
||||||
@@ -143,4 +146,36 @@ public class AgentToolContext {
|
|||||||
public void setMetadata(Map<String, Object> metadata) {
|
public void setMetadata(Map<String, Object> metadata) {
|
||||||
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发射运行时旁路事件。
|
||||||
|
*
|
||||||
|
* <p>该方法主要供 runtime 包装层发射业务无关事件,例如异步工具生命周期事件。
|
||||||
|
* 未配置事件发射器时静默忽略,避免非流式测试路径产生额外副作用。</p>
|
||||||
|
*
|
||||||
|
* @param event 运行时事件
|
||||||
|
*/
|
||||||
|
public void emitEvent(AgentRuntimeEvent event) {
|
||||||
|
if (eventEmitter != null && event != null) {
|
||||||
|
eventEmitter.accept(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取运行时事件发射器。
|
||||||
|
*
|
||||||
|
* @return 运行时事件发射器
|
||||||
|
*/
|
||||||
|
public Consumer<AgentRuntimeEvent> getEventEmitter() {
|
||||||
|
return eventEmitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置运行时事件发射器。
|
||||||
|
*
|
||||||
|
* @param eventEmitter 运行时事件发射器
|
||||||
|
*/
|
||||||
|
public void setEventEmitter(Consumer<AgentRuntimeEvent> eventEmitter) {
|
||||||
|
this.eventEmitter = eventEmitter;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolContext;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用方实现的异步业务子工具集合。
|
||||||
|
*/
|
||||||
|
public interface AsyncSubTools {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交异步任务。
|
||||||
|
*
|
||||||
|
* @param arguments 模型传入的业务参数
|
||||||
|
* @param context 工具调用上下文
|
||||||
|
* @return 提交结果
|
||||||
|
*/
|
||||||
|
AsyncToolSubmitResult submit(Map<String, Object> arguments, AgentToolContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 非阻塞观察任务状态和增量事件。
|
||||||
|
*
|
||||||
|
* @param request 观察请求
|
||||||
|
* @param context 工具调用上下文
|
||||||
|
* @return 当前任务视图
|
||||||
|
*/
|
||||||
|
AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务结果,未完成时返回当前观察态。
|
||||||
|
*
|
||||||
|
* @param request 结果请求
|
||||||
|
* @param context 工具调用上下文
|
||||||
|
* @return 当前任务视图
|
||||||
|
*/
|
||||||
|
AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求取消任务。
|
||||||
|
*
|
||||||
|
* @param request 取消请求
|
||||||
|
* @param context 工具调用上下文
|
||||||
|
* @return 取消结果
|
||||||
|
*/
|
||||||
|
AsyncToolCancelResult cancel(AsyncToolCancelRequest request, AgentToolContext context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询当前上下文可见的任务列表。
|
||||||
|
*
|
||||||
|
* @param request 列表请求
|
||||||
|
* @param context 工具调用上下文
|
||||||
|
* @return 任务列表
|
||||||
|
*/
|
||||||
|
AsyncToolTaskListResult list(AsyncToolListRequest request, AgentToolContext context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具取消请求。
|
||||||
|
*/
|
||||||
|
public class AsyncToolCancelRequest {
|
||||||
|
|
||||||
|
private String taskId;
|
||||||
|
private String reason;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空取消请求。
|
||||||
|
*/
|
||||||
|
public AsyncToolCancelRequest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务 ID。
|
||||||
|
*
|
||||||
|
* @return 任务 ID
|
||||||
|
*/
|
||||||
|
public String getTaskId() {
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务 ID。
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID
|
||||||
|
*/
|
||||||
|
public void setTaskId(String taskId) {
|
||||||
|
this.taskId = taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取取消原因。
|
||||||
|
*
|
||||||
|
* @return 取消原因
|
||||||
|
*/
|
||||||
|
public String getReason() {
|
||||||
|
return reason;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置取消原因。
|
||||||
|
*
|
||||||
|
* @param reason 取消原因
|
||||||
|
*/
|
||||||
|
public void setReason(String reason) {
|
||||||
|
this.reason = reason;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具取消结果。
|
||||||
|
*/
|
||||||
|
public class AsyncToolCancelResult {
|
||||||
|
|
||||||
|
private String taskId;
|
||||||
|
private AsyncToolTaskStatus status;
|
||||||
|
private String message;
|
||||||
|
private String errorMessage;
|
||||||
|
private Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空取消结果。
|
||||||
|
*/
|
||||||
|
public AsyncToolCancelResult() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务 ID。
|
||||||
|
*
|
||||||
|
* @return 任务 ID
|
||||||
|
*/
|
||||||
|
public String getTaskId() {
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务 ID。
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID
|
||||||
|
*/
|
||||||
|
public void setTaskId(String taskId) {
|
||||||
|
this.taskId = taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务状态。
|
||||||
|
*
|
||||||
|
* @return 任务状态
|
||||||
|
*/
|
||||||
|
public AsyncToolTaskStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务状态。
|
||||||
|
*
|
||||||
|
* @param status 任务状态
|
||||||
|
*/
|
||||||
|
public void setStatus(AsyncToolTaskStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取取消消息。
|
||||||
|
*
|
||||||
|
* @return 取消消息
|
||||||
|
*/
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置取消消息。
|
||||||
|
*
|
||||||
|
* @param message 取消消息
|
||||||
|
*/
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取错误消息。
|
||||||
|
*
|
||||||
|
* @return 错误消息
|
||||||
|
*/
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置错误消息。
|
||||||
|
*
|
||||||
|
* @param errorMessage 错误消息
|
||||||
|
*/
|
||||||
|
public void setErrorMessage(String errorMessage) {
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取业务扩展载荷。
|
||||||
|
*
|
||||||
|
* @return 业务扩展载荷
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getPayload() {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置业务扩展载荷。
|
||||||
|
*
|
||||||
|
* @param payload 业务扩展载荷
|
||||||
|
*/
|
||||||
|
public void setPayload(Map<String, Object> payload) {
|
||||||
|
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @return 元数据
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置元数据。
|
||||||
|
*
|
||||||
|
* @param metadata 元数据
|
||||||
|
*/
|
||||||
|
public void setMetadata(Map<String, Object> metadata) {
|
||||||
|
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具列表请求。
|
||||||
|
*/
|
||||||
|
public class AsyncToolListRequest {
|
||||||
|
|
||||||
|
private AsyncToolTaskStatus status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空列表请求。
|
||||||
|
*/
|
||||||
|
public AsyncToolListRequest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取状态过滤条件。
|
||||||
|
*
|
||||||
|
* @return 状态过滤条件
|
||||||
|
*/
|
||||||
|
public AsyncToolTaskStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置状态过滤条件。
|
||||||
|
*
|
||||||
|
* @param status 状态过滤条件
|
||||||
|
*/
|
||||||
|
public void setStatus(AsyncToolTaskStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具观察请求。
|
||||||
|
*/
|
||||||
|
public class AsyncToolObserveRequest {
|
||||||
|
|
||||||
|
private String taskId;
|
||||||
|
/**
|
||||||
|
* 调用方已读取到的事件位置,用于增量读取任务事件,避免重复返回全量日志。
|
||||||
|
*/
|
||||||
|
private Long cursor;
|
||||||
|
private Integer limit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空观察请求。
|
||||||
|
*/
|
||||||
|
public AsyncToolObserveRequest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务 ID。
|
||||||
|
*
|
||||||
|
* @return 任务 ID
|
||||||
|
*/
|
||||||
|
public String getTaskId() {
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务 ID。
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID
|
||||||
|
*/
|
||||||
|
public void setTaskId(String taskId) {
|
||||||
|
this.taskId = taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已读事件位置。
|
||||||
|
*
|
||||||
|
* @return 已读事件位置
|
||||||
|
*/
|
||||||
|
public Long getCursor() {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置已读事件位置。
|
||||||
|
*
|
||||||
|
* @param cursor 已读事件位置
|
||||||
|
*/
|
||||||
|
public void setCursor(Long cursor) {
|
||||||
|
this.cursor = cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件读取数量。
|
||||||
|
*
|
||||||
|
* @return 事件读取数量
|
||||||
|
*/
|
||||||
|
public Integer getLimit() {
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件读取数量。
|
||||||
|
*
|
||||||
|
* @param limit 事件读取数量
|
||||||
|
*/
|
||||||
|
public void setLimit(Integer limit) {
|
||||||
|
this.limit = limit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具 runtime 包装层的通用选项。
|
||||||
|
*/
|
||||||
|
public class AsyncToolOptions {
|
||||||
|
|
||||||
|
private Duration submitTimeout = Duration.ofSeconds(5);
|
||||||
|
private Duration observeTimeout = Duration.ofSeconds(3);
|
||||||
|
private Duration resultTimeout = Duration.ofSeconds(3);
|
||||||
|
private Duration cancelTimeout = Duration.ofSeconds(3);
|
||||||
|
private Duration listTimeout = Duration.ofSeconds(3);
|
||||||
|
private int defaultEventLimit = 20;
|
||||||
|
private int maxEventLimit = 100;
|
||||||
|
private int maxModelContentLength = 1200;
|
||||||
|
private int maxEventTextLength = 800;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认异步工具选项实例。
|
||||||
|
*/
|
||||||
|
public AsyncToolOptions() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建默认异步工具选项。
|
||||||
|
*
|
||||||
|
* @return 默认选项
|
||||||
|
*/
|
||||||
|
public static AsyncToolOptions defaults() {
|
||||||
|
return new AsyncToolOptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取提交子工具超时时间。
|
||||||
|
*
|
||||||
|
* @return 超时时间
|
||||||
|
*/
|
||||||
|
public Duration getSubmitTimeout() {
|
||||||
|
return submitTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置提交子工具超时时间。
|
||||||
|
*
|
||||||
|
* @param submitTimeout 超时时间
|
||||||
|
*/
|
||||||
|
public void setSubmitTimeout(Duration submitTimeout) {
|
||||||
|
this.submitTimeout = submitTimeout == null ? Duration.ofSeconds(5) : submitTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取观察子工具超时时间。
|
||||||
|
*
|
||||||
|
* @return 超时时间
|
||||||
|
*/
|
||||||
|
public Duration getObserveTimeout() {
|
||||||
|
return observeTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置观察子工具超时时间。
|
||||||
|
*
|
||||||
|
* @param observeTimeout 超时时间
|
||||||
|
*/
|
||||||
|
public void setObserveTimeout(Duration observeTimeout) {
|
||||||
|
this.observeTimeout = observeTimeout == null ? Duration.ofSeconds(3) : observeTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取结果子工具超时时间。
|
||||||
|
*
|
||||||
|
* @return 超时时间
|
||||||
|
*/
|
||||||
|
public Duration getResultTimeout() {
|
||||||
|
return resultTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置结果子工具超时时间。
|
||||||
|
*
|
||||||
|
* @param resultTimeout 超时时间
|
||||||
|
*/
|
||||||
|
public void setResultTimeout(Duration resultTimeout) {
|
||||||
|
this.resultTimeout = resultTimeout == null ? Duration.ofSeconds(3) : resultTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取取消子工具超时时间。
|
||||||
|
*
|
||||||
|
* @return 超时时间
|
||||||
|
*/
|
||||||
|
public Duration getCancelTimeout() {
|
||||||
|
return cancelTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置取消子工具超时时间。
|
||||||
|
*
|
||||||
|
* @param cancelTimeout 超时时间
|
||||||
|
*/
|
||||||
|
public void setCancelTimeout(Duration cancelTimeout) {
|
||||||
|
this.cancelTimeout = cancelTimeout == null ? Duration.ofSeconds(3) : cancelTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取列表子工具超时时间。
|
||||||
|
*
|
||||||
|
* @return 超时时间
|
||||||
|
*/
|
||||||
|
public Duration getListTimeout() {
|
||||||
|
return listTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置列表子工具超时时间。
|
||||||
|
*
|
||||||
|
* @param listTimeout 超时时间
|
||||||
|
*/
|
||||||
|
public void setListTimeout(Duration listTimeout) {
|
||||||
|
this.listTimeout = listTimeout == null ? Duration.ofSeconds(3) : listTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认事件读取数量。
|
||||||
|
*
|
||||||
|
* @return 默认事件数量
|
||||||
|
*/
|
||||||
|
public int getDefaultEventLimit() {
|
||||||
|
return defaultEventLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置默认事件读取数量。
|
||||||
|
*
|
||||||
|
* @param defaultEventLimit 默认事件数量
|
||||||
|
*/
|
||||||
|
public void setDefaultEventLimit(int defaultEventLimit) {
|
||||||
|
this.defaultEventLimit = defaultEventLimit <= 0 ? 20 : defaultEventLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最大事件读取数量。
|
||||||
|
*
|
||||||
|
* @return 最大事件数量
|
||||||
|
*/
|
||||||
|
public int getMaxEventLimit() {
|
||||||
|
return maxEventLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最大事件读取数量。
|
||||||
|
*
|
||||||
|
* @param maxEventLimit 最大事件数量
|
||||||
|
*/
|
||||||
|
public void setMaxEventLimit(int maxEventLimit) {
|
||||||
|
this.maxEventLimit = maxEventLimit <= 0 ? 100 : maxEventLimit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取模型可见内容最大长度。
|
||||||
|
*
|
||||||
|
* @return 最大长度
|
||||||
|
*/
|
||||||
|
public int getMaxModelContentLength() {
|
||||||
|
return maxModelContentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置模型可见内容最大长度。
|
||||||
|
*
|
||||||
|
* @param maxModelContentLength 最大长度
|
||||||
|
*/
|
||||||
|
public void setMaxModelContentLength(int maxModelContentLength) {
|
||||||
|
this.maxModelContentLength = maxModelContentLength <= 0 ? 1200 : maxModelContentLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件文本最大长度。
|
||||||
|
*
|
||||||
|
* @return 最大长度
|
||||||
|
*/
|
||||||
|
public int getMaxEventTextLength() {
|
||||||
|
return maxEventTextLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件文本最大长度。
|
||||||
|
*
|
||||||
|
* @param maxEventTextLength 最大长度
|
||||||
|
*/
|
||||||
|
public void setMaxEventTextLength(int maxEventTextLength) {
|
||||||
|
this.maxEventTextLength = maxEventTextLength <= 0 ? 800 : maxEventTextLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具结果请求。
|
||||||
|
*/
|
||||||
|
public class AsyncToolResultRequest {
|
||||||
|
|
||||||
|
private String taskId;
|
||||||
|
/**
|
||||||
|
* 调用方已读取到的事件位置,用于增量读取任务事件,避免 result 重复返回全量日志。
|
||||||
|
*/
|
||||||
|
private Long cursor;
|
||||||
|
private Integer limit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空结果请求。
|
||||||
|
*/
|
||||||
|
public AsyncToolResultRequest() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务 ID。
|
||||||
|
*
|
||||||
|
* @return 任务 ID
|
||||||
|
*/
|
||||||
|
public String getTaskId() {
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务 ID。
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID
|
||||||
|
*/
|
||||||
|
public void setTaskId(String taskId) {
|
||||||
|
this.taskId = taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取已读事件位置。
|
||||||
|
*
|
||||||
|
* @return 已读事件位置
|
||||||
|
*/
|
||||||
|
public Long getCursor() {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置已读事件位置。
|
||||||
|
*
|
||||||
|
* @param cursor 已读事件位置
|
||||||
|
*/
|
||||||
|
public void setCursor(Long cursor) {
|
||||||
|
this.cursor = cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件读取数量。
|
||||||
|
*
|
||||||
|
* @return 事件读取数量
|
||||||
|
*/
|
||||||
|
public Integer getLimit() {
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件读取数量。
|
||||||
|
*
|
||||||
|
* @param limit 事件读取数量
|
||||||
|
*/
|
||||||
|
public void setLimit(Integer limit) {
|
||||||
|
this.limit = limit;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具声明。
|
||||||
|
*
|
||||||
|
* <p>一个声明会被 runtime 展开为 submit、observe、result、cancel 和 list 五个普通工具。</p>
|
||||||
|
*/
|
||||||
|
public class AsyncToolSpec {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private String description;
|
||||||
|
private Map<String, Object> submitParametersSchema = new LinkedHashMap<>();
|
||||||
|
private AsyncSubTools subTools;
|
||||||
|
private AsyncToolOptions options = AsyncToolOptions.defaults();
|
||||||
|
private boolean approvalRequired;
|
||||||
|
private AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空异步工具声明。
|
||||||
|
*/
|
||||||
|
public AsyncToolSpec() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取异步工具基础名称。
|
||||||
|
*
|
||||||
|
* @return 工具名称
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置异步工具基础名称。
|
||||||
|
*
|
||||||
|
* @param name 工具名称
|
||||||
|
*/
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具描述。
|
||||||
|
*
|
||||||
|
* @return 工具描述
|
||||||
|
*/
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工具描述。
|
||||||
|
*
|
||||||
|
* @param description 工具描述
|
||||||
|
*/
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取提交子工具参数 Schema。
|
||||||
|
*
|
||||||
|
* @return 参数 Schema
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getSubmitParametersSchema() {
|
||||||
|
return submitParametersSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置提交子工具参数 Schema。
|
||||||
|
*
|
||||||
|
* @param submitParametersSchema 参数 Schema
|
||||||
|
*/
|
||||||
|
public void setSubmitParametersSchema(Map<String, Object> submitParametersSchema) {
|
||||||
|
this.submitParametersSchema = submitParametersSchema == null ? new LinkedHashMap<>() : submitParametersSchema;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取业务子工具实现。
|
||||||
|
*
|
||||||
|
* @return 子工具实现
|
||||||
|
*/
|
||||||
|
public AsyncSubTools getSubTools() {
|
||||||
|
return subTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置业务子工具实现。
|
||||||
|
*
|
||||||
|
* @param subTools 子工具实现
|
||||||
|
*/
|
||||||
|
public void setSubTools(AsyncSubTools subTools) {
|
||||||
|
this.subTools = subTools;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取异步工具选项。
|
||||||
|
*
|
||||||
|
* @return 工具选项
|
||||||
|
*/
|
||||||
|
public AsyncToolOptions getOptions() {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置异步工具选项。
|
||||||
|
*
|
||||||
|
* @param options 工具选项
|
||||||
|
*/
|
||||||
|
public void setOptions(AsyncToolOptions options) {
|
||||||
|
this.options = options == null ? AsyncToolOptions.defaults() : options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回提交子工具是否需要人工审批。
|
||||||
|
*
|
||||||
|
* @return 需要审批时为 true
|
||||||
|
*/
|
||||||
|
public boolean isApprovalRequired() {
|
||||||
|
return approvalRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置提交子工具是否需要人工审批。
|
||||||
|
*
|
||||||
|
* @param approvalRequired 审批标记
|
||||||
|
*/
|
||||||
|
public void setApprovalRequired(boolean approvalRequired) {
|
||||||
|
this.approvalRequired = approvalRequired;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取提交子工具的审批请求配置。
|
||||||
|
*
|
||||||
|
* @return 审批请求配置
|
||||||
|
*/
|
||||||
|
public AgentToolApprovalRequest getApprovalRequest() {
|
||||||
|
return approvalRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置提交子工具的审批请求配置。
|
||||||
|
*
|
||||||
|
* @param approvalRequest 审批请求配置
|
||||||
|
*/
|
||||||
|
public void setApprovalRequest(AgentToolApprovalRequest approvalRequest) {
|
||||||
|
this.approvalRequest = approvalRequest == null ? new AgentToolApprovalRequest() : approvalRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @return 元数据
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置元数据。
|
||||||
|
*
|
||||||
|
* @param metadata 元数据
|
||||||
|
*/
|
||||||
|
public void setMetadata(Map<String, Object> metadata) {
|
||||||
|
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,691 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||||
|
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||||
|
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolCategory;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolContext;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolInvoker;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolResult;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolVisibility;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具声明展开器。
|
||||||
|
*
|
||||||
|
* <p>该类将一个业务无关的 {@link AsyncToolSpec} 展开为五个普通
|
||||||
|
* {@link AgentToolSpec} 与 {@link AgentToolInvoker},业务方只需要实现
|
||||||
|
* {@link AsyncSubTools}。</p>
|
||||||
|
*/
|
||||||
|
public class AsyncToolSpecExpander {
|
||||||
|
|
||||||
|
private static final Pattern SAFE_NAME = Pattern.compile("^[a-z][a-z0-9_]*$");
|
||||||
|
private static final String PHASE_SUBMIT = "submit";
|
||||||
|
private static final String PHASE_OBSERVE = "observe";
|
||||||
|
private static final String PHASE_RESULT = "result";
|
||||||
|
private static final String PHASE_CANCEL = "cancel";
|
||||||
|
private static final String PHASE_LIST = "list";
|
||||||
|
private static final String ERROR_TYPE_TIMEOUT = "TIMEOUT";
|
||||||
|
private static final String ERROR_TYPE_EXCEPTION = "EXCEPTION";
|
||||||
|
|
||||||
|
private final ExecutorService executor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用公共 ForkJoinPool 创建展开器。
|
||||||
|
*/
|
||||||
|
public AsyncToolSpecExpander() {
|
||||||
|
this(ForkJoinPool.commonPool());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用指定执行器创建展开器。
|
||||||
|
*
|
||||||
|
* @param executor 执行器
|
||||||
|
*/
|
||||||
|
public AsyncToolSpecExpander(Executor executor) {
|
||||||
|
if (executor instanceof ExecutorService executorService) {
|
||||||
|
this.executor = executorService;
|
||||||
|
} else {
|
||||||
|
this.executor = new DelegatingExecutorService(executor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展开工具声明。
|
||||||
|
*
|
||||||
|
* @param spec 异步工具声明
|
||||||
|
* @return 五个普通工具声明
|
||||||
|
*/
|
||||||
|
public List<AgentToolSpec> expandSpecs(AsyncToolSpec spec) {
|
||||||
|
AsyncToolSpec safeSpec = validate(spec);
|
||||||
|
List<AgentToolSpec> specs = new ArrayList<>(5);
|
||||||
|
specs.add(toolSpec(safeSpec, PHASE_SUBMIT, safeSpec.getSubmitParametersSchema(), submitOutputSchema()));
|
||||||
|
specs.add(toolSpec(safeSpec, PHASE_OBSERVE, observeSchema(safeSpec), taskViewOutputSchema()));
|
||||||
|
specs.add(toolSpec(safeSpec, PHASE_RESULT, observeSchema(safeSpec), taskViewOutputSchema()));
|
||||||
|
specs.add(toolSpec(safeSpec, PHASE_CANCEL, cancelSchema(), cancelOutputSchema()));
|
||||||
|
specs.add(toolSpec(safeSpec, PHASE_LIST, listSchema(), listOutputSchema()));
|
||||||
|
return specs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展开工具调用器。
|
||||||
|
*
|
||||||
|
* @param spec 异步工具声明
|
||||||
|
* @return 按工具名索引的调用器
|
||||||
|
*/
|
||||||
|
public Map<String, AgentToolInvoker> expandInvokers(AsyncToolSpec spec) {
|
||||||
|
AsyncToolSpec safeSpec = validate(spec);
|
||||||
|
Map<String, AgentToolInvoker> invokers = new LinkedHashMap<>();
|
||||||
|
invokers.put(toolName(safeSpec, PHASE_SUBMIT), (arguments, context) -> submit(safeSpec, arguments, context));
|
||||||
|
invokers.put(toolName(safeSpec, PHASE_OBSERVE), (arguments, context) -> observe(safeSpec, arguments, context));
|
||||||
|
invokers.put(toolName(safeSpec, PHASE_RESULT), (arguments, context) -> result(safeSpec, arguments, context));
|
||||||
|
invokers.put(toolName(safeSpec, PHASE_CANCEL), (arguments, context) -> cancel(safeSpec, arguments, context));
|
||||||
|
invokers.put(toolName(safeSpec, PHASE_LIST), (arguments, context) -> list(safeSpec, arguments, context));
|
||||||
|
return invokers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsyncToolSpec validate(AsyncToolSpec spec) {
|
||||||
|
if (spec == null) {
|
||||||
|
throw new AgentRuntimeException("Async tool spec is required.");
|
||||||
|
}
|
||||||
|
if (spec.getName() == null || spec.getName().isBlank()) {
|
||||||
|
throw new AgentRuntimeException("Async tool name is required.");
|
||||||
|
}
|
||||||
|
if (!SAFE_NAME.matcher(spec.getName()).matches()) {
|
||||||
|
throw new AgentRuntimeException("Async tool name must be safe snake_case: " + spec.getName());
|
||||||
|
}
|
||||||
|
if (spec.getSubTools() == null) {
|
||||||
|
throw new AgentRuntimeException("Async sub tools are required: " + spec.getName());
|
||||||
|
}
|
||||||
|
if (spec.getSubmitParametersSchema() == null || spec.getSubmitParametersSchema().isEmpty()) {
|
||||||
|
spec.setSubmitParametersSchema(emptyObjectSchema());
|
||||||
|
}
|
||||||
|
if (spec.getOptions() == null) {
|
||||||
|
spec.setOptions(AsyncToolOptions.defaults());
|
||||||
|
}
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolSpec toolSpec(AsyncToolSpec spec,
|
||||||
|
String phase,
|
||||||
|
Map<String, Object> parametersSchema,
|
||||||
|
Map<String, Object> outputSchema) {
|
||||||
|
AgentToolSpec toolSpec = new AgentToolSpec();
|
||||||
|
toolSpec.setName(toolName(spec, phase));
|
||||||
|
toolSpec.setDescription(description(spec, phase));
|
||||||
|
toolSpec.setCategory(AgentToolCategory.CUSTOM);
|
||||||
|
toolSpec.setVisibility(AgentToolVisibility.VISIBLE);
|
||||||
|
toolSpec.setParametersSchema(parametersSchema);
|
||||||
|
toolSpec.setOutputSchema(outputSchema);
|
||||||
|
toolSpec.setApprovalRequired(PHASE_SUBMIT.equals(phase) && spec.isApprovalRequired());
|
||||||
|
toolSpec.setApprovalRequest(spec.getApprovalRequest());
|
||||||
|
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
metadata.putAll(spec.getMetadata());
|
||||||
|
metadata.put("asyncTool", true);
|
||||||
|
metadata.put("asyncToolName", spec.getName());
|
||||||
|
metadata.put("asyncToolPhase", phase);
|
||||||
|
toolSpec.setMetadata(metadata);
|
||||||
|
return toolSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String description(AsyncToolSpec spec, String phase) {
|
||||||
|
String prefix = spec.getDescription() == null || spec.getDescription().isBlank()
|
||||||
|
? "Async tool " + spec.getName()
|
||||||
|
: spec.getDescription();
|
||||||
|
return switch (phase) {
|
||||||
|
case PHASE_SUBMIT -> prefix
|
||||||
|
+ " This is the default entry point when the user asks to run this tool. Submit an asynchronous task with the normal tool arguments and return task_id.";
|
||||||
|
case PHASE_OBSERVE -> prefix
|
||||||
|
+ " Use immediately after submit with the returned task_id to check progress and incremental events. Do not ask the user for task_id immediately after submit.";
|
||||||
|
case PHASE_RESULT -> prefix
|
||||||
|
+ " Use only when a known task_id should return the final result, or the current observation if the task is still running.";
|
||||||
|
case PHASE_CANCEL -> prefix
|
||||||
|
+ " Use only when the user explicitly asks to cancel a known asynchronous task by task_id.";
|
||||||
|
case PHASE_LIST -> prefix
|
||||||
|
+ " Use only when the user explicitly asks to list visible asynchronous tasks in the current context.";
|
||||||
|
default -> prefix;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult submit(AsyncToolSpec spec, Map<String, Object> arguments, AgentToolContext context) {
|
||||||
|
return execute(spec, PHASE_SUBMIT, context, spec.getOptions().getSubmitTimeout(),
|
||||||
|
guardedContext -> {
|
||||||
|
AsyncToolSubmitResult result = spec.getSubTools().submit(safeMap(arguments), guardedContext);
|
||||||
|
return wrapSubmit(spec, result, guardedContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult observe(AsyncToolSpec spec, Map<String, Object> arguments, AgentToolContext context) {
|
||||||
|
return execute(spec, PHASE_OBSERVE, context, spec.getOptions().getObserveTimeout(),
|
||||||
|
guardedContext -> {
|
||||||
|
AsyncToolObserveRequest request = observeRequest(arguments, spec.getOptions());
|
||||||
|
return wrapTaskView(spec, PHASE_OBSERVE,
|
||||||
|
spec.getSubTools().observe(request, guardedContext), guardedContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult result(AsyncToolSpec spec, Map<String, Object> arguments, AgentToolContext context) {
|
||||||
|
return execute(spec, PHASE_RESULT, context, spec.getOptions().getResultTimeout(),
|
||||||
|
guardedContext -> {
|
||||||
|
AsyncToolResultRequest request = resultRequest(arguments, spec.getOptions());
|
||||||
|
return wrapTaskView(spec, PHASE_RESULT,
|
||||||
|
spec.getSubTools().result(request, guardedContext), guardedContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult cancel(AsyncToolSpec spec, Map<String, Object> arguments, AgentToolContext context) {
|
||||||
|
return execute(spec, PHASE_CANCEL, context, spec.getOptions().getCancelTimeout(),
|
||||||
|
guardedContext -> {
|
||||||
|
AsyncToolCancelRequest request = cancelRequest(arguments);
|
||||||
|
return wrapCancel(spec, spec.getSubTools().cancel(request, guardedContext), guardedContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult list(AsyncToolSpec spec, Map<String, Object> arguments, AgentToolContext context) {
|
||||||
|
return execute(spec, PHASE_LIST, context, spec.getOptions().getListTimeout(),
|
||||||
|
guardedContext -> {
|
||||||
|
AsyncToolListRequest request = listRequest(arguments);
|
||||||
|
return wrapList(spec, spec.getSubTools().list(request, guardedContext), guardedContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult execute(AsyncToolSpec spec,
|
||||||
|
String phase,
|
||||||
|
AgentToolContext context,
|
||||||
|
Duration timeout,
|
||||||
|
Function<AgentToolContext, AgentToolResult> supplier) {
|
||||||
|
AtomicBoolean active = new AtomicBoolean(true);
|
||||||
|
AgentToolContext guardedContext = guardedContext(context, active);
|
||||||
|
Future<AgentToolResult> future = executor.submit(() -> supplier.apply(guardedContext));
|
||||||
|
try {
|
||||||
|
return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||||
|
} catch (TimeoutException error) {
|
||||||
|
active.set(false);
|
||||||
|
future.cancel(true);
|
||||||
|
AgentToolResult result = failureResult(spec, phase, null, AsyncToolTaskStatus.TIMEOUT,
|
||||||
|
ERROR_TYPE_TIMEOUT, "Async tool " + phase + " timed out.");
|
||||||
|
emitFailure(spec, phase, context, null, AsyncToolTaskStatus.TIMEOUT, result.getErrorMessage());
|
||||||
|
return result;
|
||||||
|
} catch (InterruptedException error) {
|
||||||
|
active.set(false);
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
AgentToolResult result = failureResult(spec, phase, null, AsyncToolTaskStatus.FAILED,
|
||||||
|
ERROR_TYPE_EXCEPTION, "Async tool " + phase + " interrupted.");
|
||||||
|
emitFailure(spec, phase, context, null, AsyncToolTaskStatus.FAILED, result.getErrorMessage());
|
||||||
|
return result;
|
||||||
|
} catch (ExecutionException error) {
|
||||||
|
active.set(false);
|
||||||
|
Throwable cause = error.getCause() == null ? error : error.getCause();
|
||||||
|
String message = cause.getMessage() == null || cause.getMessage().isBlank()
|
||||||
|
? "Async tool " + phase + " failed."
|
||||||
|
: cause.getMessage();
|
||||||
|
AgentToolResult result = failureResult(spec, phase, null, AsyncToolTaskStatus.FAILED,
|
||||||
|
ERROR_TYPE_EXCEPTION, message);
|
||||||
|
emitFailure(spec, phase, context, null, AsyncToolTaskStatus.FAILED, result.getErrorMessage());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolContext guardedContext(AgentToolContext source, AtomicBoolean active) {
|
||||||
|
if (source == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
AgentToolContext context = new AgentToolContext();
|
||||||
|
context.setRequestId(source.getRequestId());
|
||||||
|
context.setTraceId(source.getTraceId());
|
||||||
|
context.setSessionId(source.getSessionId());
|
||||||
|
context.setAgentId(source.getAgentId());
|
||||||
|
context.setToolCallId(source.getToolCallId());
|
||||||
|
context.setRuntimeContext(source.getRuntimeContext());
|
||||||
|
context.setMetadata(new LinkedHashMap<>(source.getMetadata()));
|
||||||
|
context.setEventEmitter(event -> {
|
||||||
|
// 超时后底层业务可能仍在运行,迟到事件不能再覆盖 runtime 已返回的失败语义。
|
||||||
|
if (active.get()) {
|
||||||
|
source.emitEvent(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult wrapSubmit(AsyncToolSpec spec, AsyncToolSubmitResult result, AgentToolContext context) {
|
||||||
|
AsyncToolSubmitResult safe = result == null ? new AsyncToolSubmitResult() : result;
|
||||||
|
AsyncToolTaskStatus status = status(safe.getStatus(), AsyncToolTaskStatus.RUNNING);
|
||||||
|
safe.setStatus(status);
|
||||||
|
if (safe.getTaskId() == null || safe.getTaskId().isBlank()) {
|
||||||
|
String message = "Async tool submit must return taskId.";
|
||||||
|
AgentToolResult toolResult = failureResult(spec, PHASE_SUBMIT, null, AsyncToolTaskStatus.FAILED,
|
||||||
|
ERROR_TYPE_EXCEPTION, message, safe);
|
||||||
|
emitFailure(spec, PHASE_SUBMIT, context, null, AsyncToolTaskStatus.FAILED, message);
|
||||||
|
return toolResult;
|
||||||
|
}
|
||||||
|
if (safe.getNextAction() == null || safe.getNextAction().isBlank()) {
|
||||||
|
safe.setNextAction(toolName(spec, PHASE_OBSERVE) + " 查看任务进度。");
|
||||||
|
}
|
||||||
|
AgentToolResult toolResult = successResult(spec, PHASE_SUBMIT, safe.getTaskId(), status,
|
||||||
|
modelContent(safe.getTaskId(), status, safe.getNextAction(), safe.getSummary()), safe);
|
||||||
|
emit(spec, PHASE_SUBMIT, AgentRuntimeEventType.ASYNC_TOOL_SUBMITTED, context, safe.getTaskId(), status,
|
||||||
|
safe.getCursor(), null, safe.getSummary(), null);
|
||||||
|
return toolResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult wrapTaskView(AsyncToolSpec spec,
|
||||||
|
String phase,
|
||||||
|
AsyncToolTaskView view,
|
||||||
|
AgentToolContext context) {
|
||||||
|
AsyncToolTaskView safe = view == null ? new AsyncToolTaskView() : view;
|
||||||
|
AsyncToolTaskStatus status = status(safe.getStatus(), AsyncToolTaskStatus.RUNNING);
|
||||||
|
safe.setStatus(status);
|
||||||
|
if (safe.getTerminal() == null) {
|
||||||
|
safe.setTerminal(status.isTerminal());
|
||||||
|
}
|
||||||
|
if (safe.getResultAvailable() == null) {
|
||||||
|
safe.setResultAvailable(status.isSuccess() && safe.getResult() != null);
|
||||||
|
}
|
||||||
|
if (safe.getNextAction() == null || safe.getNextAction().isBlank()) {
|
||||||
|
safe.setNextAction(status.isTerminal()
|
||||||
|
? "任务已结束。"
|
||||||
|
: toolName(spec, PHASE_OBSERVE) + " 继续查看任务进度。");
|
||||||
|
}
|
||||||
|
AgentToolResult toolResult = successResult(spec, phase, safe.getTaskId(), status,
|
||||||
|
modelContent(safe.getTaskId(), status, safe.getNextAction(), safe.getSummary(),
|
||||||
|
Boolean.TRUE.equals(safe.getResultAvailable()), safe.getResult()), safe);
|
||||||
|
emit(spec, phase, PHASE_RESULT.equals(phase)
|
||||||
|
? AgentRuntimeEventType.ASYNC_TOOL_RESULT
|
||||||
|
: AgentRuntimeEventType.ASYNC_TOOL_OBSERVED,
|
||||||
|
context, safe.getTaskId(), status, safe.getCursor(), safe.getNextCursor(), safe.getSummary(),
|
||||||
|
safe.getErrorMessage(), safe.getResultAvailable());
|
||||||
|
return toolResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult wrapCancel(AsyncToolSpec spec, AsyncToolCancelResult result, AgentToolContext context) {
|
||||||
|
AsyncToolCancelResult safe = result == null ? new AsyncToolCancelResult() : result;
|
||||||
|
AsyncToolTaskStatus status = status(safe.getStatus(), AsyncToolTaskStatus.CANCELLING);
|
||||||
|
safe.setStatus(status);
|
||||||
|
boolean success = safe.getErrorMessage() == null || safe.getErrorMessage().isBlank();
|
||||||
|
AgentToolResult toolResult = success
|
||||||
|
? successResult(spec, PHASE_CANCEL, safe.getTaskId(), status,
|
||||||
|
modelContent(safe.getTaskId(), status, "继续使用 " + toolName(spec, PHASE_OBSERVE) + " 查看取消状态。",
|
||||||
|
safe.getMessage()), safe)
|
||||||
|
: failureResult(spec, PHASE_CANCEL, safe.getTaskId(), status, ERROR_TYPE_EXCEPTION, safe.getErrorMessage(), safe);
|
||||||
|
emit(spec, PHASE_CANCEL, success ? AgentRuntimeEventType.ASYNC_TOOL_CANCELLED : AgentRuntimeEventType.ASYNC_TOOL_FAILED,
|
||||||
|
context, safe.getTaskId(), status, null, null, safe.getMessage(), safe.getErrorMessage());
|
||||||
|
return toolResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult wrapList(AsyncToolSpec spec, AsyncToolTaskListResult result, AgentToolContext context) {
|
||||||
|
AsyncToolTaskListResult safe = result == null ? new AsyncToolTaskListResult() : result;
|
||||||
|
String summary = "共 " + safe.getTasks().size() + " 个任务。";
|
||||||
|
AgentToolResult toolResult = successResult(spec, PHASE_LIST, null, null,
|
||||||
|
modelContent(null, null, "按 task_id 使用观察或结果工具查看详情。", summary), safe);
|
||||||
|
emit(spec, PHASE_LIST, AgentRuntimeEventType.ASYNC_TOOL_LISTED, context, null, null, null, null, summary, null);
|
||||||
|
return toolResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult successResult(AsyncToolSpec spec,
|
||||||
|
String phase,
|
||||||
|
String taskId,
|
||||||
|
AsyncToolTaskStatus status,
|
||||||
|
String modelContent,
|
||||||
|
Object displayContent) {
|
||||||
|
AgentToolResult result = AgentToolResult.success(truncate(modelContent, spec.getOptions().getMaxModelContentLength()));
|
||||||
|
result.setDisplayContent(displayContent);
|
||||||
|
result.setMetadata(metadata(spec, phase, taskId, status));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult failureResult(AsyncToolSpec spec,
|
||||||
|
String phase,
|
||||||
|
String taskId,
|
||||||
|
AsyncToolTaskStatus status,
|
||||||
|
String errorType,
|
||||||
|
String errorMessage) {
|
||||||
|
return failureResult(spec, phase, taskId, status, errorType, errorMessage, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult failureResult(AsyncToolSpec spec,
|
||||||
|
String phase,
|
||||||
|
String taskId,
|
||||||
|
AsyncToolTaskStatus status,
|
||||||
|
String errorType,
|
||||||
|
String errorMessage,
|
||||||
|
Object displayContent) {
|
||||||
|
String message = errorMessage == null || errorMessage.isBlank() ? "Async tool failed." : errorMessage;
|
||||||
|
AgentToolResult result = AgentToolResult.failure(message);
|
||||||
|
result.setDisplayContent(displayContent == null ? Map.of("errorType", errorType, "message", message) : displayContent);
|
||||||
|
result.setMetadata(metadata(spec, phase, taskId, status));
|
||||||
|
result.getMetadata().put("errorType", errorType);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void emitFailure(AsyncToolSpec spec,
|
||||||
|
String phase,
|
||||||
|
AgentToolContext context,
|
||||||
|
String taskId,
|
||||||
|
AsyncToolTaskStatus status,
|
||||||
|
String errorMessage) {
|
||||||
|
emit(spec, phase, AgentRuntimeEventType.ASYNC_TOOL_FAILED, context, taskId, status, null, null, null, errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void emit(AsyncToolSpec spec,
|
||||||
|
String phase,
|
||||||
|
AgentRuntimeEventType type,
|
||||||
|
AgentToolContext context,
|
||||||
|
String taskId,
|
||||||
|
AsyncToolTaskStatus status,
|
||||||
|
Long cursor,
|
||||||
|
Long nextCursor,
|
||||||
|
String summary,
|
||||||
|
String errorMessage) {
|
||||||
|
emit(spec, phase, type, context, taskId, status, cursor, nextCursor, summary, errorMessage, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void emit(AsyncToolSpec spec,
|
||||||
|
String phase,
|
||||||
|
AgentRuntimeEventType type,
|
||||||
|
AgentToolContext context,
|
||||||
|
String taskId,
|
||||||
|
AsyncToolTaskStatus status,
|
||||||
|
Long cursor,
|
||||||
|
Long nextCursor,
|
||||||
|
String summary,
|
||||||
|
String errorMessage,
|
||||||
|
Boolean resultAvailable) {
|
||||||
|
if (context == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
AgentRuntimeEvent event = AgentRuntimeEvent.of(type);
|
||||||
|
event.setTraceId(context.getTraceId());
|
||||||
|
event.setSessionId(context.getSessionId());
|
||||||
|
event.setAgentId(context.getAgentId());
|
||||||
|
event.setToolCallId(context.getToolCallId());
|
||||||
|
event.getMetadata().putAll(spec.getMetadata());
|
||||||
|
putIfNotNull(event.getMetadata(), "requestId", context.getRequestId());
|
||||||
|
event.getPayload().put("asyncToolName", spec.getName());
|
||||||
|
event.getPayload().put("phase", phase);
|
||||||
|
putIfNotNull(event.getPayload(), "toolDisplayName", spec.getMetadata().get("toolDisplayName"));
|
||||||
|
putIfNotNull(event.getPayload(), "taskId", taskId);
|
||||||
|
putIfNotNull(event.getPayload(), "status", status == null ? null : status.name());
|
||||||
|
putIfNotNull(event.getPayload(), "cursor", cursor);
|
||||||
|
putIfNotNull(event.getPayload(), "nextCursor", nextCursor);
|
||||||
|
putIfNotNull(event.getPayload(), "summary", truncate(summary, spec.getOptions().getMaxEventTextLength()));
|
||||||
|
putIfNotNull(event.getPayload(), "errorMessage", truncate(errorMessage, spec.getOptions().getMaxEventTextLength()));
|
||||||
|
putIfNotNull(event.getPayload(), "resultAvailable", resultAvailable);
|
||||||
|
event.getMetadata().put("asyncTool", true);
|
||||||
|
event.getMetadata().put("asyncToolName", spec.getName());
|
||||||
|
event.getMetadata().put("asyncToolPhase", phase);
|
||||||
|
context.emitEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> metadata(AsyncToolSpec spec, String phase, String taskId, AsyncToolTaskStatus status) {
|
||||||
|
Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
metadata.putAll(spec.getMetadata());
|
||||||
|
metadata.put("asyncTool", true);
|
||||||
|
metadata.put("asyncToolName", spec.getName());
|
||||||
|
metadata.put("asyncToolPhase", phase);
|
||||||
|
putIfNotNull(metadata, "taskId", taskId);
|
||||||
|
putIfNotNull(metadata, "status", status == null ? null : status.name());
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsyncToolObserveRequest observeRequest(Map<String, Object> arguments, AsyncToolOptions options) {
|
||||||
|
AsyncToolObserveRequest request = new AsyncToolObserveRequest();
|
||||||
|
request.setTaskId(stringValue(arguments, "taskId"));
|
||||||
|
request.setCursor(longValue(arguments, "cursor"));
|
||||||
|
request.setLimit(limit(arguments, options));
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsyncToolResultRequest resultRequest(Map<String, Object> arguments, AsyncToolOptions options) {
|
||||||
|
AsyncToolResultRequest request = new AsyncToolResultRequest();
|
||||||
|
request.setTaskId(stringValue(arguments, "taskId"));
|
||||||
|
request.setCursor(longValue(arguments, "cursor"));
|
||||||
|
request.setLimit(limit(arguments, options));
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsyncToolCancelRequest cancelRequest(Map<String, Object> arguments) {
|
||||||
|
AsyncToolCancelRequest request = new AsyncToolCancelRequest();
|
||||||
|
request.setTaskId(stringValue(arguments, "taskId"));
|
||||||
|
request.setReason(stringValue(arguments, "reason"));
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsyncToolListRequest listRequest(Map<String, Object> arguments) {
|
||||||
|
AsyncToolListRequest request = new AsyncToolListRequest();
|
||||||
|
String status = stringValue(arguments, "status");
|
||||||
|
if (status != null && !status.isBlank()) {
|
||||||
|
request.setStatus(AsyncToolTaskStatus.valueOf(status.trim().toUpperCase(Locale.ROOT)));
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer limit(Map<String, Object> arguments, AsyncToolOptions options) {
|
||||||
|
Integer limit = intValue(arguments, "limit");
|
||||||
|
if (limit == null || limit <= 0) {
|
||||||
|
return options.getDefaultEventLimit();
|
||||||
|
}
|
||||||
|
return Math.min(limit, options.getMaxEventLimit());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String modelContent(String taskId, AsyncToolTaskStatus status, String nextAction, String summary) {
|
||||||
|
return modelContent(taskId, status, nextAction, summary, false, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String modelContent(String taskId,
|
||||||
|
AsyncToolTaskStatus status,
|
||||||
|
String nextAction,
|
||||||
|
String summary,
|
||||||
|
boolean resultAvailable,
|
||||||
|
Object result) {
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
if (taskId != null && !taskId.isBlank()) {
|
||||||
|
builder.append("task_id: ").append(taskId).append('\n');
|
||||||
|
}
|
||||||
|
if (status != null) {
|
||||||
|
builder.append("status: ").append(status.name()).append('\n');
|
||||||
|
}
|
||||||
|
if (summary != null && !summary.isBlank()) {
|
||||||
|
builder.append("summary: ").append(summary).append('\n');
|
||||||
|
}
|
||||||
|
if (resultAvailable) {
|
||||||
|
builder.append("result_available: true").append('\n');
|
||||||
|
builder.append("result: ").append(modelResult(result)).append('\n');
|
||||||
|
}
|
||||||
|
if (nextAction != null && !nextAction.isBlank()) {
|
||||||
|
builder.append("next_action: ").append(nextAction);
|
||||||
|
}
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String modelResult(Object result) {
|
||||||
|
if (result == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if (result instanceof CharSequence
|
||||||
|
|| result instanceof Number
|
||||||
|
|| result instanceof Boolean
|
||||||
|
|| result instanceof Character
|
||||||
|
|| result instanceof Enum<?>) {
|
||||||
|
return String.valueOf(result);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.toJSONString(result);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return String.valueOf(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toolName(AsyncToolSpec spec, String phase) {
|
||||||
|
return spec.getName() + "_" + phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsyncToolTaskStatus status(AsyncToolTaskStatus status, AsyncToolTaskStatus defaultStatus) {
|
||||||
|
return status == null ? defaultStatus : status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> emptyObjectSchema() {
|
||||||
|
Map<String, Object> schema = new LinkedHashMap<>();
|
||||||
|
schema.put("type", "object");
|
||||||
|
schema.put("properties", new LinkedHashMap<>());
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> observeSchema(AsyncToolSpec spec) {
|
||||||
|
Map<String, Object> schema = new LinkedHashMap<>();
|
||||||
|
schema.put("type", "object");
|
||||||
|
Map<String, Object> properties = new LinkedHashMap<>();
|
||||||
|
properties.put("taskId", property("string", "Task id returned by submit."));
|
||||||
|
properties.put("cursor", property("integer", "Event cursor returned by previous observe/result."));
|
||||||
|
properties.put("limit", property("integer", "Maximum number of incremental events."));
|
||||||
|
schema.put("properties", properties);
|
||||||
|
schema.put("required", List.of("taskId"));
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> cancelSchema() {
|
||||||
|
Map<String, Object> schema = new LinkedHashMap<>();
|
||||||
|
schema.put("type", "object");
|
||||||
|
Map<String, Object> properties = new LinkedHashMap<>();
|
||||||
|
properties.put("taskId", property("string", "Task id returned by submit."));
|
||||||
|
properties.put("reason", property("string", "Optional cancellation reason."));
|
||||||
|
schema.put("properties", properties);
|
||||||
|
schema.put("required", List.of("taskId"));
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> listSchema() {
|
||||||
|
Map<String, Object> schema = new LinkedHashMap<>();
|
||||||
|
schema.put("type", "object");
|
||||||
|
Map<String, Object> properties = new LinkedHashMap<>();
|
||||||
|
properties.put("status", property("string", "Optional task status filter."));
|
||||||
|
schema.put("properties", properties);
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> submitOutputSchema() {
|
||||||
|
return outputSchema("AsyncToolSubmitResult");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> taskViewOutputSchema() {
|
||||||
|
return outputSchema("AsyncToolTaskView");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> cancelOutputSchema() {
|
||||||
|
return outputSchema("AsyncToolCancelResult");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> listOutputSchema() {
|
||||||
|
return outputSchema("AsyncToolTaskListResult");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> outputSchema(String title) {
|
||||||
|
Map<String, Object> schema = new LinkedHashMap<>();
|
||||||
|
schema.put("type", "object");
|
||||||
|
schema.put("title", title);
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> property(String type, String description) {
|
||||||
|
Map<String, Object> property = new LinkedHashMap<>();
|
||||||
|
property.put("type", type);
|
||||||
|
property.put("description", description);
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> safeMap(Map<String, Object> arguments) {
|
||||||
|
return arguments == null ? new LinkedHashMap<>() : new LinkedHashMap<>(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String stringValue(Map<String, Object> arguments, String key) {
|
||||||
|
Object value = arguments == null ? null : arguments.get(key);
|
||||||
|
return value == null ? null : String.valueOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long longValue(Map<String, Object> arguments, String key) {
|
||||||
|
Object value = arguments == null ? null : arguments.get(key);
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return number.longValue();
|
||||||
|
}
|
||||||
|
if (value == null || String.valueOf(value).isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Long.parseLong(String.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Integer intValue(Map<String, Object> arguments, String key) {
|
||||||
|
Object value = arguments == null ? null : arguments.get(key);
|
||||||
|
if (value instanceof Number number) {
|
||||||
|
return number.intValue();
|
||||||
|
}
|
||||||
|
if (value == null || String.valueOf(value).isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Integer.parseInt(String.valueOf(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putIfNotNull(Map<String, Object> target, String key, Object value) {
|
||||||
|
if (value != null) {
|
||||||
|
target.put(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String truncate(String value, int maxLength) {
|
||||||
|
if (value == null || value.length() <= maxLength) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value.substring(0, Math.max(0, maxLength)) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DelegatingExecutorService extends AbstractExecutorService {
|
||||||
|
|
||||||
|
private final Executor executor;
|
||||||
|
private volatile boolean shutdown;
|
||||||
|
|
||||||
|
private DelegatingExecutorService(Executor executor) {
|
||||||
|
this.executor = executor == null ? ForkJoinPool.commonPool() : executor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
shutdown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Runnable> shutdownNow() {
|
||||||
|
shutdown = true;
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isShutdown() {
|
||||||
|
return shutdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isTerminated() {
|
||||||
|
return shutdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean awaitTermination(long timeout, TimeUnit unit) {
|
||||||
|
return shutdown;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(Runnable command) {
|
||||||
|
executor.execute(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具提交结果。
|
||||||
|
*/
|
||||||
|
public class AsyncToolSubmitResult {
|
||||||
|
|
||||||
|
private String taskId;
|
||||||
|
private AsyncToolTaskStatus status;
|
||||||
|
/**
|
||||||
|
* 提交后调用方已读取到的事件位置,后续 observe 可从该位置继续增量读取。
|
||||||
|
*/
|
||||||
|
private Long cursor;
|
||||||
|
private String summary;
|
||||||
|
private String nextAction;
|
||||||
|
private Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空提交结果。
|
||||||
|
*/
|
||||||
|
public AsyncToolSubmitResult() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务 ID。
|
||||||
|
*
|
||||||
|
* @return 任务 ID
|
||||||
|
*/
|
||||||
|
public String getTaskId() {
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务 ID。
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID
|
||||||
|
*/
|
||||||
|
public void setTaskId(String taskId) {
|
||||||
|
this.taskId = taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务状态。
|
||||||
|
*
|
||||||
|
* @return 任务状态
|
||||||
|
*/
|
||||||
|
public AsyncToolTaskStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务状态。
|
||||||
|
*
|
||||||
|
* @param status 任务状态
|
||||||
|
*/
|
||||||
|
public void setStatus(AsyncToolTaskStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前事件读取位置。
|
||||||
|
*
|
||||||
|
* @return 当前事件读取位置
|
||||||
|
*/
|
||||||
|
public Long getCursor() {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前事件读取位置。
|
||||||
|
*
|
||||||
|
* @param cursor 当前事件读取位置
|
||||||
|
*/
|
||||||
|
public void setCursor(Long cursor) {
|
||||||
|
this.cursor = cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取摘要。
|
||||||
|
*
|
||||||
|
* @return 摘要
|
||||||
|
*/
|
||||||
|
public String getSummary() {
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置摘要。
|
||||||
|
*
|
||||||
|
* @param summary 摘要
|
||||||
|
*/
|
||||||
|
public void setSummary(String summary) {
|
||||||
|
this.summary = summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下一步建议。
|
||||||
|
*
|
||||||
|
* @return 下一步建议
|
||||||
|
*/
|
||||||
|
public String getNextAction() {
|
||||||
|
return nextAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置下一步建议。
|
||||||
|
*
|
||||||
|
* @param nextAction 下一步建议
|
||||||
|
*/
|
||||||
|
public void setNextAction(String nextAction) {
|
||||||
|
this.nextAction = nextAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取业务扩展载荷。
|
||||||
|
*
|
||||||
|
* @return 业务扩展载荷
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getPayload() {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置业务扩展载荷。
|
||||||
|
*
|
||||||
|
* @param payload 业务扩展载荷
|
||||||
|
*/
|
||||||
|
public void setPayload(Map<String, Object> payload) {
|
||||||
|
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @return 元数据
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置元数据。
|
||||||
|
*
|
||||||
|
* @param metadata 元数据
|
||||||
|
*/
|
||||||
|
public void setMetadata(Map<String, Object> metadata) {
|
||||||
|
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具任务事件。
|
||||||
|
*/
|
||||||
|
public class AsyncToolTaskEvent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务内单调递增事件序号,用于 cursor 增量读取。
|
||||||
|
*/
|
||||||
|
private Long sequence;
|
||||||
|
private String type;
|
||||||
|
private String text;
|
||||||
|
private Instant createdAt = Instant.now();
|
||||||
|
private Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空任务事件。
|
||||||
|
*/
|
||||||
|
public AsyncToolTaskEvent() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件序号。
|
||||||
|
*
|
||||||
|
* @return 事件序号
|
||||||
|
*/
|
||||||
|
public Long getSequence() {
|
||||||
|
return sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件序号。
|
||||||
|
*
|
||||||
|
* @param sequence 事件序号
|
||||||
|
*/
|
||||||
|
public void setSequence(Long sequence) {
|
||||||
|
this.sequence = sequence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件类型。
|
||||||
|
*
|
||||||
|
* @return 事件类型
|
||||||
|
*/
|
||||||
|
public String getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件类型。
|
||||||
|
*
|
||||||
|
* @param type 事件类型
|
||||||
|
*/
|
||||||
|
public void setType(String type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件文本。
|
||||||
|
*
|
||||||
|
* @return 事件文本
|
||||||
|
*/
|
||||||
|
public String getText() {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件文本。
|
||||||
|
*
|
||||||
|
* @param text 事件文本
|
||||||
|
*/
|
||||||
|
public void setText(String text) {
|
||||||
|
this.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取事件创建时间。
|
||||||
|
*
|
||||||
|
* @return 事件创建时间
|
||||||
|
*/
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置事件创建时间。
|
||||||
|
*
|
||||||
|
* @param createdAt 事件创建时间
|
||||||
|
*/
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt == null ? Instant.now() : createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取业务扩展载荷。
|
||||||
|
*
|
||||||
|
* @return 业务扩展载荷
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getPayload() {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置业务扩展载荷。
|
||||||
|
*
|
||||||
|
* @param payload 业务扩展载荷
|
||||||
|
*/
|
||||||
|
public void setPayload(Map<String, Object> payload) {
|
||||||
|
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具任务列表结果。
|
||||||
|
*/
|
||||||
|
public class AsyncToolTaskListResult {
|
||||||
|
|
||||||
|
private List<AsyncToolTaskSummary> tasks = new ArrayList<>();
|
||||||
|
private Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空任务列表结果。
|
||||||
|
*/
|
||||||
|
public AsyncToolTaskListResult() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务摘要列表。
|
||||||
|
*
|
||||||
|
* @return 任务摘要列表
|
||||||
|
*/
|
||||||
|
public List<AsyncToolTaskSummary> getTasks() {
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务摘要列表。
|
||||||
|
*
|
||||||
|
* @param tasks 任务摘要列表
|
||||||
|
*/
|
||||||
|
public void setTasks(List<AsyncToolTaskSummary> tasks) {
|
||||||
|
this.tasks = tasks == null ? new ArrayList<>() : tasks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取业务扩展载荷。
|
||||||
|
*
|
||||||
|
* @return 业务扩展载荷
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getPayload() {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置业务扩展载荷。
|
||||||
|
*
|
||||||
|
* @param payload 业务扩展载荷
|
||||||
|
*/
|
||||||
|
public void setPayload(Map<String, Object> payload) {
|
||||||
|
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @return 元数据
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置元数据。
|
||||||
|
*
|
||||||
|
* @param metadata 元数据
|
||||||
|
*/
|
||||||
|
public void setMetadata(Map<String, Object> metadata) {
|
||||||
|
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具任务对 Agent Runtime 暴露的统一状态。
|
||||||
|
*/
|
||||||
|
public enum AsyncToolTaskStatus {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务已创建,等待业务侧执行。
|
||||||
|
*/
|
||||||
|
PENDING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务正在执行。
|
||||||
|
*/
|
||||||
|
RUNNING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务执行成功,结果可用。
|
||||||
|
*/
|
||||||
|
SUCCEEDED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务执行失败。
|
||||||
|
*/
|
||||||
|
FAILED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务正在取消。
|
||||||
|
*/
|
||||||
|
CANCELLING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务已取消。
|
||||||
|
*/
|
||||||
|
CANCELLED,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务执行超时。
|
||||||
|
*/
|
||||||
|
TIMEOUT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断状态是否为终态。
|
||||||
|
*
|
||||||
|
* @return 终态返回 true
|
||||||
|
*/
|
||||||
|
public boolean isTerminal() {
|
||||||
|
return this == SUCCEEDED || this == FAILED || this == CANCELLED || this == TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断状态是否仍在执行或等待执行。
|
||||||
|
*
|
||||||
|
* @return 仍在运行返回 true
|
||||||
|
*/
|
||||||
|
public boolean isRunning() {
|
||||||
|
return this == PENDING || this == RUNNING || this == CANCELLING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断状态是否表示成功。
|
||||||
|
*
|
||||||
|
* @return 成功返回 true
|
||||||
|
*/
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return this == SUCCEEDED;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断状态是否表示失败类终态。
|
||||||
|
*
|
||||||
|
* @return 失败、取消或超时返回 true
|
||||||
|
*/
|
||||||
|
public boolean isFailure() {
|
||||||
|
return this == FAILED || this == CANCELLED || this == TIMEOUT;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具任务摘要。
|
||||||
|
*/
|
||||||
|
public class AsyncToolTaskSummary {
|
||||||
|
|
||||||
|
private String taskId;
|
||||||
|
private AsyncToolTaskStatus status;
|
||||||
|
private String summary;
|
||||||
|
private Instant createdAt;
|
||||||
|
private Instant updatedAt;
|
||||||
|
private Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空任务摘要。
|
||||||
|
*/
|
||||||
|
public AsyncToolTaskSummary() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务 ID。
|
||||||
|
*
|
||||||
|
* @return 任务 ID
|
||||||
|
*/
|
||||||
|
public String getTaskId() {
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务 ID。
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID
|
||||||
|
*/
|
||||||
|
public void setTaskId(String taskId) {
|
||||||
|
this.taskId = taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务状态。
|
||||||
|
*
|
||||||
|
* @return 任务状态
|
||||||
|
*/
|
||||||
|
public AsyncToolTaskStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务状态。
|
||||||
|
*
|
||||||
|
* @param status 任务状态
|
||||||
|
*/
|
||||||
|
public void setStatus(AsyncToolTaskStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务摘要。
|
||||||
|
*
|
||||||
|
* @return 任务摘要
|
||||||
|
*/
|
||||||
|
public String getSummary() {
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务摘要。
|
||||||
|
*
|
||||||
|
* @param summary 任务摘要
|
||||||
|
*/
|
||||||
|
public void setSummary(String summary) {
|
||||||
|
this.summary = summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取创建时间。
|
||||||
|
*
|
||||||
|
* @return 创建时间
|
||||||
|
*/
|
||||||
|
public Instant getCreatedAt() {
|
||||||
|
return createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置创建时间。
|
||||||
|
*
|
||||||
|
* @param createdAt 创建时间
|
||||||
|
*/
|
||||||
|
public void setCreatedAt(Instant createdAt) {
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取更新时间。
|
||||||
|
*
|
||||||
|
* @return 更新时间
|
||||||
|
*/
|
||||||
|
public Instant getUpdatedAt() {
|
||||||
|
return updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置更新时间。
|
||||||
|
*
|
||||||
|
* @param updatedAt 更新时间
|
||||||
|
*/
|
||||||
|
public void setUpdatedAt(Instant updatedAt) {
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取业务扩展载荷。
|
||||||
|
*
|
||||||
|
* @return 业务扩展载荷
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getPayload() {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置业务扩展载荷。
|
||||||
|
*
|
||||||
|
* @param payload 业务扩展载荷
|
||||||
|
*/
|
||||||
|
public void setPayload(Map<String, Object> payload) {
|
||||||
|
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步工具任务视图。
|
||||||
|
*
|
||||||
|
* <p>observe 和 result 子工具共用该实体。未完成时表达观察态,完成时可同时携带最终结果。</p>
|
||||||
|
*/
|
||||||
|
public class AsyncToolTaskView {
|
||||||
|
|
||||||
|
private String taskId;
|
||||||
|
private AsyncToolTaskStatus status;
|
||||||
|
/**
|
||||||
|
* 调用方本次传入的已读事件位置,用于增量读取任务事件。
|
||||||
|
*/
|
||||||
|
private Long cursor;
|
||||||
|
/**
|
||||||
|
* 服务端返回的下一次观察起点,调用方下次应使用该值继续读取增量事件。
|
||||||
|
*/
|
||||||
|
private Long nextCursor;
|
||||||
|
private Integer progress;
|
||||||
|
private String summary;
|
||||||
|
private String nextAction;
|
||||||
|
private List<AsyncToolTaskEvent> events = new ArrayList<>();
|
||||||
|
private Object result;
|
||||||
|
private String errorMessage;
|
||||||
|
private String errorType;
|
||||||
|
private Boolean terminal;
|
||||||
|
private Boolean resultAvailable;
|
||||||
|
private Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
private Map<String, Object> metadata = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建空任务视图。
|
||||||
|
*/
|
||||||
|
public AsyncToolTaskView() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务 ID。
|
||||||
|
*
|
||||||
|
* @return 任务 ID
|
||||||
|
*/
|
||||||
|
public String getTaskId() {
|
||||||
|
return taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务 ID。
|
||||||
|
*
|
||||||
|
* @param taskId 任务 ID
|
||||||
|
*/
|
||||||
|
public void setTaskId(String taskId) {
|
||||||
|
this.taskId = taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取任务状态。
|
||||||
|
*
|
||||||
|
* @return 任务状态
|
||||||
|
*/
|
||||||
|
public AsyncToolTaskStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置任务状态。
|
||||||
|
*
|
||||||
|
* @param status 任务状态
|
||||||
|
*/
|
||||||
|
public void setStatus(AsyncToolTaskStatus status) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取本次请求的已读事件位置。
|
||||||
|
*
|
||||||
|
* @return 已读事件位置
|
||||||
|
*/
|
||||||
|
public Long getCursor() {
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置本次请求的已读事件位置。
|
||||||
|
*
|
||||||
|
* @param cursor 已读事件位置
|
||||||
|
*/
|
||||||
|
public void setCursor(Long cursor) {
|
||||||
|
this.cursor = cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下一次观察起点。
|
||||||
|
*
|
||||||
|
* @return 下一次观察起点
|
||||||
|
*/
|
||||||
|
public Long getNextCursor() {
|
||||||
|
return nextCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置下一次观察起点。
|
||||||
|
*
|
||||||
|
* @param nextCursor 下一次观察起点
|
||||||
|
*/
|
||||||
|
public void setNextCursor(Long nextCursor) {
|
||||||
|
this.nextCursor = nextCursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取进度百分比。
|
||||||
|
*
|
||||||
|
* @return 进度百分比
|
||||||
|
*/
|
||||||
|
public Integer getProgress() {
|
||||||
|
return progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置进度百分比。
|
||||||
|
*
|
||||||
|
* @param progress 进度百分比
|
||||||
|
*/
|
||||||
|
public void setProgress(Integer progress) {
|
||||||
|
this.progress = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取摘要。
|
||||||
|
*
|
||||||
|
* @return 摘要
|
||||||
|
*/
|
||||||
|
public String getSummary() {
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置摘要。
|
||||||
|
*
|
||||||
|
* @param summary 摘要
|
||||||
|
*/
|
||||||
|
public void setSummary(String summary) {
|
||||||
|
this.summary = summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取下一步建议。
|
||||||
|
*
|
||||||
|
* @return 下一步建议
|
||||||
|
*/
|
||||||
|
public String getNextAction() {
|
||||||
|
return nextAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置下一步建议。
|
||||||
|
*
|
||||||
|
* @param nextAction 下一步建议
|
||||||
|
*/
|
||||||
|
public void setNextAction(String nextAction) {
|
||||||
|
this.nextAction = nextAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取本次增量事件。
|
||||||
|
*
|
||||||
|
* @return 本次增量事件
|
||||||
|
*/
|
||||||
|
public List<AsyncToolTaskEvent> getEvents() {
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置本次增量事件。
|
||||||
|
*
|
||||||
|
* @param events 本次增量事件
|
||||||
|
*/
|
||||||
|
public void setEvents(List<AsyncToolTaskEvent> events) {
|
||||||
|
this.events = events == null ? new ArrayList<>() : events;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最终结果。
|
||||||
|
*
|
||||||
|
* @return 最终结果
|
||||||
|
*/
|
||||||
|
public Object getResult() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最终结果。
|
||||||
|
*
|
||||||
|
* @param result 最终结果
|
||||||
|
*/
|
||||||
|
public void setResult(Object result) {
|
||||||
|
this.result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取错误消息。
|
||||||
|
*
|
||||||
|
* @return 错误消息
|
||||||
|
*/
|
||||||
|
public String getErrorMessage() {
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置错误消息。
|
||||||
|
*
|
||||||
|
* @param errorMessage 错误消息
|
||||||
|
*/
|
||||||
|
public void setErrorMessage(String errorMessage) {
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取错误类型。
|
||||||
|
*
|
||||||
|
* @return 错误类型
|
||||||
|
*/
|
||||||
|
public String getErrorType() {
|
||||||
|
return errorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置错误类型。
|
||||||
|
*
|
||||||
|
* @param errorType 错误类型
|
||||||
|
*/
|
||||||
|
public void setErrorType(String errorType) {
|
||||||
|
this.errorType = errorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取是否终态。
|
||||||
|
*
|
||||||
|
* @return 是否终态
|
||||||
|
*/
|
||||||
|
public Boolean getTerminal() {
|
||||||
|
return terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置是否终态。
|
||||||
|
*
|
||||||
|
* @param terminal 是否终态
|
||||||
|
*/
|
||||||
|
public void setTerminal(Boolean terminal) {
|
||||||
|
this.terminal = terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最终结果是否可用。
|
||||||
|
*
|
||||||
|
* @return 最终结果是否可用
|
||||||
|
*/
|
||||||
|
public Boolean getResultAvailable() {
|
||||||
|
return resultAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置最终结果是否可用。
|
||||||
|
*
|
||||||
|
* @param resultAvailable 最终结果是否可用
|
||||||
|
*/
|
||||||
|
public void setResultAvailable(Boolean resultAvailable) {
|
||||||
|
this.resultAvailable = resultAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取业务扩展载荷。
|
||||||
|
*
|
||||||
|
* @return 业务扩展载荷
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getPayload() {
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置业务扩展载荷。
|
||||||
|
*
|
||||||
|
* @param payload 业务扩展载荷
|
||||||
|
*/
|
||||||
|
public void setPayload(Map<String, Object> payload) {
|
||||||
|
this.payload = payload == null ? new LinkedHashMap<>() : payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取元数据。
|
||||||
|
*
|
||||||
|
* @return 元数据
|
||||||
|
*/
|
||||||
|
public Map<String, Object> getMetadata() {
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置元数据。
|
||||||
|
*
|
||||||
|
* @param metadata 元数据
|
||||||
|
*/
|
||||||
|
public void setMetadata(Map<String, Object> metadata) {
|
||||||
|
this.metadata = metadata == null ? new LinkedHashMap<>() : metadata;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,9 +39,12 @@ import io.agentscope.core.model.ToolSchema;
|
|||||||
import io.agentscope.core.skill.SkillBox;
|
import io.agentscope.core.skill.SkillBox;
|
||||||
import io.agentscope.core.tool.AgentTool;
|
import io.agentscope.core.tool.AgentTool;
|
||||||
import io.agentscope.core.tool.Toolkit;
|
import io.agentscope.core.tool.Toolkit;
|
||||||
|
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.core.publisher.Sinks;
|
import reactor.core.publisher.Sinks;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
@@ -104,6 +107,36 @@ public class AgentScopeStatefulRuntimeTest {
|
|||||||
Assert.assertFalse(runtime.getAgent().getHooks().stream().anyMatch(AutoContextHook.class::isInstance));
|
Assert.assertFalse(runtime.getAgent().getHooks().stream().anyMatch(AutoContextHook.class::isInstance));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldKeepSystemPromptUnchangedWithoutAsyncTool() {
|
||||||
|
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||||
|
|
||||||
|
runtime.init(initRequest());
|
||||||
|
|
||||||
|
Assert.assertEquals("system", runtime.getAgent().getSysPrompt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldAppendAsyncToolProtocolPromptWhenAsyncToolsExist() {
|
||||||
|
AgentInitRequest request = initRequest();
|
||||||
|
AgentToolSpec submit = new AgentToolSpec();
|
||||||
|
submit.setName("demo_submit");
|
||||||
|
submit.setDescription("submit demo");
|
||||||
|
submit.getMetadata().put("asyncTool", true);
|
||||||
|
submit.getMetadata().put("asyncToolPhase", "submit");
|
||||||
|
request.getAgentDefinition().setToolSpecs(List.of(submit));
|
||||||
|
request.setToolInvokers(Map.of("demo_submit", (arguments, context) -> AgentToolResult.success("submitted")));
|
||||||
|
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||||
|
|
||||||
|
runtime.init(request);
|
||||||
|
|
||||||
|
Assert.assertTrue(runtime.getAgent().getSysPrompt().startsWith("system"));
|
||||||
|
Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("Async tool protocol:"));
|
||||||
|
Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("These are internal execution phases."));
|
||||||
|
Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("call its submit sub-tool first with the user-provided arguments by default"));
|
||||||
|
Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("immediately call observe"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldEmitSideEventWithRuntimeIdentityFromBridge() throws Exception {
|
public void shouldEmitSideEventWithRuntimeIdentityFromBridge() throws Exception {
|
||||||
AgentRuntimeExecutionContext context = new AgentRuntimeExecutionContext();
|
AgentRuntimeExecutionContext context = new AgentRuntimeExecutionContext();
|
||||||
@@ -255,6 +288,42 @@ public class AgentScopeStatefulRuntimeTest {
|
|||||||
.anyMatch(PendingToolRecoveryHook.class::isInstance));
|
.anyMatch(PendingToolRecoveryHook.class::isInstance));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCloseMcpClientsWhenRuntimeCloses() throws Exception {
|
||||||
|
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||||
|
FakeMcpClientWrapper client = new FakeMcpClientWrapper("mcp-close");
|
||||||
|
Field mcpClientsField = AgentScopeReActRuntime.class.getDeclaredField("mcpClients");
|
||||||
|
mcpClientsField.setAccessible(true);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<McpClientWrapper> clients = (List<McpClientWrapper>) mcpClientsField.get(runtime);
|
||||||
|
clients.add(client);
|
||||||
|
|
||||||
|
runtime.close();
|
||||||
|
|
||||||
|
Assert.assertTrue(client.closed.get());
|
||||||
|
Assert.assertTrue(clients.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCloseMcpClientsEvenWhenRuntimeIsStreaming() throws Exception {
|
||||||
|
AgentScopeReActRuntime runtime = fakeRuntime();
|
||||||
|
FakeMcpClientWrapper client = new FakeMcpClientWrapper("mcp-close-streaming");
|
||||||
|
Field mcpClientsField = AgentScopeReActRuntime.class.getDeclaredField("mcpClients");
|
||||||
|
mcpClientsField.setAccessible(true);
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
List<McpClientWrapper> clients = (List<McpClientWrapper>) mcpClientsField.get(runtime);
|
||||||
|
clients.add(client);
|
||||||
|
Field runningField = AgentScopeReActRuntime.class.getDeclaredField("running");
|
||||||
|
runningField.setAccessible(true);
|
||||||
|
AtomicBoolean running = (AtomicBoolean) runningField.get(runtime);
|
||||||
|
running.set(true);
|
||||||
|
|
||||||
|
runtime.close();
|
||||||
|
|
||||||
|
Assert.assertTrue(client.closed.get());
|
||||||
|
Assert.assertTrue(clients.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void shouldEmitAutoContextCompressionEventsFromInterceptor() throws Exception {
|
public void shouldEmitAutoContextCompressionEventsFromInterceptor() throws Exception {
|
||||||
AgentInitRequest request = initRequest();
|
AgentInitRequest request = initRequest();
|
||||||
@@ -403,7 +472,11 @@ public class AgentScopeStatefulRuntimeTest {
|
|||||||
AgentRuntimeExecutionContext context = executionContext();
|
AgentRuntimeExecutionContext context = executionContext();
|
||||||
Sinks.Many<AgentRuntimeEvent> sink = Sinks.many().replay().all();
|
Sinks.Many<AgentRuntimeEvent> sink = Sinks.many().replay().all();
|
||||||
AgentRuntimeEventBridge bridge = AgentRuntimeEventBridge.fixed(context, sink);
|
AgentRuntimeEventBridge bridge = AgentRuntimeEventBridge.fixed(context, sink);
|
||||||
ToolExecutionObserver observer = new ToolExecutionObserver(bridge);
|
AgentToolSpec toolSpec = new AgentToolSpec();
|
||||||
|
toolSpec.setName("search");
|
||||||
|
toolSpec.getMetadata().put("toolDisplayName", "Search Tool");
|
||||||
|
toolSpec.getMetadata().put("rawMcpToolName", "search");
|
||||||
|
ToolExecutionObserver observer = new ToolExecutionObserver(bridge, null, List.of(toolSpec));
|
||||||
ReActAgent agent = initializedAgent();
|
ReActAgent agent = initializedAgent();
|
||||||
Toolkit toolkit = agent.getToolkit();
|
Toolkit toolkit = agent.getToolkit();
|
||||||
ToolUseBlock toolUse = ToolUseBlock.builder()
|
ToolUseBlock toolUse = ToolUseBlock.builder()
|
||||||
@@ -421,9 +494,12 @@ public class AgentScopeStatefulRuntimeTest {
|
|||||||
Assert.assertEquals(AgentRuntimeEventType.TOOL_CALL, events.get(0).getEventType());
|
Assert.assertEquals(AgentRuntimeEventType.TOOL_CALL, events.get(0).getEventType());
|
||||||
Assert.assertEquals("RUNNING", events.get(0).getPayload().get("status"));
|
Assert.assertEquals("RUNNING", events.get(0).getPayload().get("status"));
|
||||||
Assert.assertEquals("PRE_ACTING", events.get(0).getPayload().get("phase"));
|
Assert.assertEquals("PRE_ACTING", events.get(0).getPayload().get("phase"));
|
||||||
|
Assert.assertEquals("Search Tool", events.get(0).getPayload().get("toolDisplayName"));
|
||||||
|
Assert.assertEquals("search", events.get(0).getPayload().get("rawMcpToolName"));
|
||||||
Assert.assertEquals(AgentRuntimeEventType.TOOL_RESULT, events.get(1).getEventType());
|
Assert.assertEquals(AgentRuntimeEventType.TOOL_RESULT, events.get(1).getEventType());
|
||||||
Assert.assertEquals("SUCCESS", events.get(1).getPayload().get("status"));
|
Assert.assertEquals("SUCCESS", events.get(1).getPayload().get("status"));
|
||||||
Assert.assertEquals("POST_ACTING", events.get(1).getPayload().get("phase"));
|
Assert.assertEquals("POST_ACTING", events.get(1).getPayload().get("phase"));
|
||||||
|
Assert.assertEquals("Search Tool", events.get(1).getPayload().get("toolDisplayName"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -712,6 +788,8 @@ public class AgentScopeStatefulRuntimeTest {
|
|||||||
toolSpec.setDescription("search");
|
toolSpec.setDescription("search");
|
||||||
toolSpec.setApprovalRequired(true);
|
toolSpec.setApprovalRequired(true);
|
||||||
toolSpec.getApprovalRequest().setApprovalPrompt("Approve search?");
|
toolSpec.getApprovalRequest().setApprovalPrompt("Approve search?");
|
||||||
|
toolSpec.getMetadata().put("toolDisplayName", "MCP Search - search");
|
||||||
|
toolSpec.getMetadata().put("rawMcpToolName", "search");
|
||||||
request.getAgentDefinition().setToolSpecs(List.of(toolSpec));
|
request.getAgentDefinition().setToolSpecs(List.of(toolSpec));
|
||||||
AtomicBoolean invoked = new AtomicBoolean(false);
|
AtomicBoolean invoked = new AtomicBoolean(false);
|
||||||
request.setToolInvokers(Map.of("search", (arguments, context) -> {
|
request.setToolInvokers(Map.of("search", (arguments, context) -> {
|
||||||
@@ -736,6 +814,12 @@ public class AgentScopeStatefulRuntimeTest {
|
|||||||
Assert.assertFalse(invoked.get());
|
Assert.assertFalse(invoked.get());
|
||||||
Assert.assertTrue(events.stream()
|
Assert.assertTrue(events.stream()
|
||||||
.anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED));
|
.anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED));
|
||||||
|
AgentRuntimeEvent approval = events.stream()
|
||||||
|
.filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
Assert.assertEquals("MCP Search - search", approval.getPayload().get("toolDisplayName"));
|
||||||
|
Assert.assertEquals("search", approval.getPayload().get("rawMcpToolName"));
|
||||||
AgentRuntimeEvent suspended = events.stream()
|
AgentRuntimeEvent suspended = events.stream()
|
||||||
.filter(event -> event.getEventType() == AgentRuntimeEventType.SUSPENDED)
|
.filter(event -> event.getEventType() == AgentRuntimeEventType.SUSPENDED)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
@@ -1117,6 +1201,37 @@ public class AgentScopeStatefulRuntimeTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class FakeMcpClientWrapper extends McpClientWrapper {
|
||||||
|
|
||||||
|
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||||
|
|
||||||
|
private FakeMcpClientWrapper(String name) {
|
||||||
|
super(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> initialize() {
|
||||||
|
initialized = true;
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<List<McpSchema.Tool>> listTools() {
|
||||||
|
return Mono.just(List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<McpSchema.CallToolResult> callTool(String toolName, Map<String, Object> arguments) {
|
||||||
|
return Mono.just(new McpSchema.CallToolResult(List.of(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
closed.set(true);
|
||||||
|
initialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private AgentRuntimeExecutionContext executionContext() {
|
private AgentRuntimeExecutionContext executionContext() {
|
||||||
AgentRuntimeExecutionContext context = new AgentRuntimeExecutionContext();
|
AgentRuntimeExecutionContext context = new AgentRuntimeExecutionContext();
|
||||||
AgentDefinition definition = new AgentDefinition();
|
AgentDefinition definition = new AgentDefinition();
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package com.easyagents.agent.runtime.mcp;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||||
|
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolAdapter;
|
||||||
|
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolSpec;
|
||||||
|
import com.easyagents.agent.runtime.tool.operate.AgentOperateToolType;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试 MCP 声明校验器。
|
||||||
|
*/
|
||||||
|
public class McpSpecValidatorTest {
|
||||||
|
|
||||||
|
@Test(expected = AgentRuntimeException.class)
|
||||||
|
public void shouldRejectMissingStdioCommand() {
|
||||||
|
McpSpec spec = spec(McpTransportType.STDIO);
|
||||||
|
|
||||||
|
McpSpecValidator.validateConnection(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = AgentRuntimeException.class)
|
||||||
|
public void shouldRejectMissingSseUrl() {
|
||||||
|
McpSpec spec = spec(McpTransportType.SSE);
|
||||||
|
|
||||||
|
McpSpecValidator.validateConnection(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = AgentRuntimeException.class)
|
||||||
|
public void shouldRejectMissingHttpUrl() {
|
||||||
|
McpSpec spec = spec(McpTransportType.HTTP);
|
||||||
|
|
||||||
|
McpSpecValidator.validateConnection(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = AgentRuntimeException.class)
|
||||||
|
public void shouldRejectMcpToolNameConflictWithBusinessTool() {
|
||||||
|
AgentToolSpec businessTool = toolSpec("search");
|
||||||
|
AgentToolSpec mcpTool = toolSpec("search");
|
||||||
|
|
||||||
|
McpSpecValidator.validateToolConflicts(List.of(businessTool), List.of(mcpTool), List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = AgentRuntimeException.class)
|
||||||
|
public void shouldRejectMcpToolNameConflictWithOperateTool() {
|
||||||
|
AgentToolSpec mcpTool = toolSpec(AgentOperateToolAdapter.EXECUTE_SHELL_COMMAND_TOOL);
|
||||||
|
AgentOperateToolSpec operateTool = new AgentOperateToolSpec();
|
||||||
|
operateTool.setType(AgentOperateToolType.SHELL);
|
||||||
|
|
||||||
|
McpSpecValidator.validateToolConflicts(List.of(), List.of(mcpTool), List.of(operateTool));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = AgentRuntimeException.class)
|
||||||
|
public void shouldRejectDuplicatedRuntimeToolAliases() {
|
||||||
|
McpSpec spec = spec(McpTransportType.STDIO);
|
||||||
|
spec.setCommand("node");
|
||||||
|
spec.setToolAliases(Map.of("search", "mcp_1_tool", "search.v2", "mcp_1_tool"));
|
||||||
|
|
||||||
|
McpSpecValidator.validateConnection(spec);
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpSpec spec(McpTransportType type) {
|
||||||
|
McpSpec spec = new McpSpec();
|
||||||
|
spec.setName("mcp");
|
||||||
|
spec.setTransportType(type);
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolSpec toolSpec(String name) {
|
||||||
|
AgentToolSpec spec = new AgentToolSpec();
|
||||||
|
spec.setName(name);
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
package com.easyagents.agent.runtime.mcp;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||||
|
import io.agentscope.core.message.ToolResultBlock;
|
||||||
|
import io.agentscope.core.tool.Toolkit;
|
||||||
|
import io.agentscope.core.tool.mcp.McpClientWrapper;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试 MCP Toolkit 适配器。
|
||||||
|
*/
|
||||||
|
public class McpToolkitAdapterTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRegisterEnabledMcpToolsAndBuildRuntimeToolSpecs() {
|
||||||
|
FakeMcpClientWrapper client = new FakeMcpClientWrapper("demo",
|
||||||
|
List.of(tool("search"), tool("write_file")));
|
||||||
|
McpToolkitAdapter adapter = new McpToolkitAdapter(new FakeMcpClientFactory(client));
|
||||||
|
McpSpec spec = stdioSpec();
|
||||||
|
spec.setEnableTools(List.of("search"));
|
||||||
|
spec.setApprovalRequired(true);
|
||||||
|
spec.setMetadata(Map.of("owner", "runtime"));
|
||||||
|
Toolkit toolkit = new Toolkit();
|
||||||
|
|
||||||
|
McpRegistration registration = adapter.register(List.of(spec), toolkit);
|
||||||
|
|
||||||
|
Assert.assertEquals(1, registration.getClients().size());
|
||||||
|
Assert.assertEquals(1, registration.getToolSpecs().size());
|
||||||
|
Assert.assertNotNull(toolkit.getTool("search"));
|
||||||
|
Assert.assertNull(toolkit.getTool("write_file"));
|
||||||
|
AgentToolSpec toolSpec = registration.getToolSpecs().get(0);
|
||||||
|
Assert.assertEquals("search", toolSpec.getName());
|
||||||
|
Assert.assertTrue(toolSpec.isApprovalRequired());
|
||||||
|
Assert.assertEquals("MCP", toolSpec.getMetadata().get("source"));
|
||||||
|
Assert.assertEquals("demo", toolSpec.getMetadata().get("mcpName"));
|
||||||
|
Assert.assertEquals("stdio", toolSpec.getMetadata().get("transportType"));
|
||||||
|
Assert.assertEquals("runtime", toolSpec.getMetadata().get("owner"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFilterSensitiveMetadataKeysFromRuntimeToolSpec() {
|
||||||
|
FakeMcpClientWrapper client = new FakeMcpClientWrapper("demo", List.of(tool("search")));
|
||||||
|
McpToolkitAdapter adapter = new McpToolkitAdapter(new FakeMcpClientFactory(client));
|
||||||
|
McpSpec spec = stdioSpec();
|
||||||
|
spec.setMetadata(Map.of(
|
||||||
|
"owner", "runtime",
|
||||||
|
"apiKey", "secret-key",
|
||||||
|
"Authorization", "Bearer secret-token",
|
||||||
|
"password", "secret-password"));
|
||||||
|
|
||||||
|
McpRegistration registration = adapter.register(List.of(spec), new Toolkit());
|
||||||
|
|
||||||
|
Map<String, Object> metadata = registration.getToolSpecs().get(0).getMetadata();
|
||||||
|
Assert.assertEquals("runtime", metadata.get("owner"));
|
||||||
|
Assert.assertFalse(metadata.containsKey("apiKey"));
|
||||||
|
Assert.assertFalse(metadata.containsKey("Authorization"));
|
||||||
|
Assert.assertFalse(metadata.containsKey("password"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldRegisterAliasedMcpToolAndCallRawToolName() {
|
||||||
|
FakeMcpClientWrapper client = new FakeMcpClientWrapper("demo", List.of(tool("search")));
|
||||||
|
McpToolkitAdapter adapter = new McpToolkitAdapter(new FakeMcpClientFactory(client));
|
||||||
|
McpSpec spec = stdioSpec();
|
||||||
|
spec.setEnableTools(List.of("mcp_1_search"));
|
||||||
|
spec.setToolAliases(Map.of("search", "mcp_1_search"));
|
||||||
|
Toolkit toolkit = new Toolkit();
|
||||||
|
|
||||||
|
McpRegistration registration = adapter.register(List.of(spec), toolkit);
|
||||||
|
|
||||||
|
Assert.assertNotNull(toolkit.getTool("mcp_1_search"));
|
||||||
|
Assert.assertNull(toolkit.getTool("search"));
|
||||||
|
AgentToolSpec toolSpec = registration.getToolSpecs().get(0);
|
||||||
|
Assert.assertEquals("mcp_1_search", toolSpec.getName());
|
||||||
|
Assert.assertEquals("search", toolSpec.getMetadata().get("rawMcpToolName"));
|
||||||
|
registration.getClients().get(0).callTool("mcp_1_search", Map.of("q", "hello")).block();
|
||||||
|
Assert.assertEquals("search", client.lastCalledToolName.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证未配置工具白名单时会为 MCP 下全部工具动态生成运行时别名。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldRegisterAllToolsWithDynamicPrefixAndCallRawToolName() {
|
||||||
|
FakeMcpClientWrapper client = new FakeMcpClientWrapper("demo",
|
||||||
|
List.of(tool("search.tool"), tool("write-file")));
|
||||||
|
McpToolkitAdapter adapter = new McpToolkitAdapter(new FakeMcpClientFactory(client));
|
||||||
|
McpSpec spec = stdioSpec();
|
||||||
|
spec.setDescription("Demo MCP");
|
||||||
|
spec.setToolNamePrefix("mcp_20_");
|
||||||
|
Toolkit toolkit = new Toolkit();
|
||||||
|
|
||||||
|
McpRegistration registration = adapter.register(List.of(spec), toolkit);
|
||||||
|
|
||||||
|
Assert.assertNotNull(toolkit.getTool("mcp_20_search_tool"));
|
||||||
|
Assert.assertNotNull(toolkit.getTool("mcp_20_write-file"));
|
||||||
|
Assert.assertNull(toolkit.getTool("search.tool"));
|
||||||
|
Assert.assertEquals(2, registration.getToolSpecs().size());
|
||||||
|
AgentToolSpec toolSpec = registration.getToolSpecs().get(0);
|
||||||
|
Assert.assertEquals("mcp_20_search_tool", toolSpec.getName());
|
||||||
|
Assert.assertEquals("search.tool", toolSpec.getMetadata().get("rawMcpToolName"));
|
||||||
|
Assert.assertEquals("Demo MCP - search.tool", toolSpec.getMetadata().get("toolDisplayName"));
|
||||||
|
registration.getClients().get(0).callTool("mcp_20_search_tool", Map.of("q", "hello")).block();
|
||||||
|
Assert.assertEquals("search.tool", client.lastCalledToolName.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证动态别名安全化后发生重名时会自动追加序号。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldDeduplicateDynamicAliasesAfterSanitizingToolNames() {
|
||||||
|
FakeMcpClientWrapper client = new FakeMcpClientWrapper("demo",
|
||||||
|
List.of(tool("search.tool"), tool("search_tool")));
|
||||||
|
McpToolkitAdapter adapter = new McpToolkitAdapter(new FakeMcpClientFactory(client));
|
||||||
|
McpSpec spec = stdioSpec();
|
||||||
|
spec.setToolNamePrefix("mcp_20_");
|
||||||
|
Toolkit toolkit = new Toolkit();
|
||||||
|
|
||||||
|
McpRegistration registration = adapter.register(List.of(spec), toolkit);
|
||||||
|
|
||||||
|
Assert.assertNotNull(toolkit.getTool("mcp_20_search_tool"));
|
||||||
|
Assert.assertNotNull(toolkit.getTool("mcp_20_search_tool_2"));
|
||||||
|
Assert.assertEquals("search.tool", registration.getToolSpecs().get(0).getMetadata().get("rawMcpToolName"));
|
||||||
|
Assert.assertEquals("search_tool", registration.getToolSpecs().get(1).getMetadata().get("rawMcpToolName"));
|
||||||
|
registration.getClients().get(0).callTool("mcp_20_search_tool_2", Map.of("q", "hello")).block();
|
||||||
|
Assert.assertEquals("search_tool", client.lastCalledToolName.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnEmptyToolSpecsWhenMcpServerHasNoTools() {
|
||||||
|
FakeMcpClientWrapper client = new FakeMcpClientWrapper("demo", List.of());
|
||||||
|
McpToolkitAdapter adapter = new McpToolkitAdapter(new FakeMcpClientFactory(client));
|
||||||
|
|
||||||
|
McpRegistration registration = adapter.register(List.of(stdioSpec()), new Toolkit());
|
||||||
|
|
||||||
|
Assert.assertEquals(1, registration.getClients().size());
|
||||||
|
Assert.assertTrue(registration.getToolSpecs().isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCloseCreatedClientWhenRegistrationFails() {
|
||||||
|
FakeMcpClientWrapper client = new FakeMcpClientWrapper("demo", List.of(tool("search")));
|
||||||
|
client.failOnListTools = true;
|
||||||
|
McpToolkitAdapter adapter = new McpToolkitAdapter(new FakeMcpClientFactory(client));
|
||||||
|
|
||||||
|
try {
|
||||||
|
adapter.register(List.of(stdioSpec()), new Toolkit());
|
||||||
|
Assert.fail("Expected MCP registration failure.");
|
||||||
|
} catch (AgentRuntimeException | IllegalStateException ignored) {
|
||||||
|
Assert.assertTrue(client.closed.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpSpec stdioSpec() {
|
||||||
|
McpSpec spec = new McpSpec();
|
||||||
|
spec.setName("demo");
|
||||||
|
spec.setTransportType(McpTransportType.STDIO);
|
||||||
|
spec.setCommand("node");
|
||||||
|
spec.setTimeout(Duration.ofSeconds(10));
|
||||||
|
spec.setInitializationTimeout(Duration.ofSeconds(3));
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpSchema.Tool tool(String name) {
|
||||||
|
McpSchema.JsonSchema schema = new McpSchema.JsonSchema("object",
|
||||||
|
Map.of("q", Map.of("type", "string", "description", "query")),
|
||||||
|
List.of("q"), null, null, null);
|
||||||
|
return new McpSchema.Tool(name, name, name + " description", schema, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FakeMcpClientFactory extends McpClientFactory {
|
||||||
|
|
||||||
|
private final McpClientWrapper client;
|
||||||
|
|
||||||
|
private FakeMcpClientFactory(McpClientWrapper client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public McpClientWrapper create(McpSpec spec) {
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FakeMcpClientWrapper extends McpClientWrapper {
|
||||||
|
|
||||||
|
private final List<McpSchema.Tool> tools;
|
||||||
|
private final AtomicBoolean closed = new AtomicBoolean(false);
|
||||||
|
private final AtomicReference<String> lastCalledToolName = new AtomicReference<>();
|
||||||
|
private boolean failOnListTools;
|
||||||
|
|
||||||
|
private FakeMcpClientWrapper(String name, List<McpSchema.Tool> tools) {
|
||||||
|
super(name);
|
||||||
|
this.tools = tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> initialize() {
|
||||||
|
initialized = true;
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<List<McpSchema.Tool>> listTools() {
|
||||||
|
if (failOnListTools) {
|
||||||
|
return Mono.error(new IllegalStateException("list tools failed"));
|
||||||
|
}
|
||||||
|
return Mono.just(tools);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<McpSchema.CallToolResult> callTool(String toolName, Map<String, Object> arguments) {
|
||||||
|
lastCalledToolName.set(toolName);
|
||||||
|
return Mono.just(new McpSchema.CallToolResult(List.of(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
closed.set(true);
|
||||||
|
initialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.easyagents.agent.runtime.mcp;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.AgentRuntimeException;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试 MCP 连接方式解析。
|
||||||
|
*/
|
||||||
|
public class McpTransportTypeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldParseCompatibleTransportValues() {
|
||||||
|
Assert.assertEquals(McpTransportType.STDIO, McpTransportType.from(null));
|
||||||
|
Assert.assertEquals(McpTransportType.STDIO, McpTransportType.from("stdio"));
|
||||||
|
Assert.assertEquals(McpTransportType.SSE, McpTransportType.from("http-sse"));
|
||||||
|
Assert.assertEquals(McpTransportType.SSE, McpTransportType.from("SSE"));
|
||||||
|
Assert.assertEquals(McpTransportType.HTTP, McpTransportType.from("http-stream"));
|
||||||
|
Assert.assertEquals(McpTransportType.HTTP, McpTransportType.from("HTTP"));
|
||||||
|
Assert.assertEquals(McpTransportType.HTTP, McpTransportType.from("streamable-http"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = AgentRuntimeException.class)
|
||||||
|
public void shouldRejectUnsupportedTransportValue() {
|
||||||
|
McpTransportType.from("websocket");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,408 @@
|
|||||||
|
package com.easyagents.agent.runtime.tool.asynctool;
|
||||||
|
|
||||||
|
import com.easyagents.agent.runtime.event.AgentRuntimeEvent;
|
||||||
|
import com.easyagents.agent.runtime.event.AgentRuntimeEventType;
|
||||||
|
import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolContext;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolInvoker;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolResult;
|
||||||
|
import com.easyagents.agent.runtime.tool.AgentToolSpec;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试异步工具声明展开器。
|
||||||
|
*/
|
||||||
|
public class AsyncToolSpecExpanderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldExpandFiveToolSpecsAndInvokers() {
|
||||||
|
AsyncToolSpec spec = spec(new StubSubTools());
|
||||||
|
AsyncToolSpecExpander expander = new AsyncToolSpecExpander();
|
||||||
|
|
||||||
|
List<AgentToolSpec> specs = expander.expandSpecs(spec);
|
||||||
|
Map<String, AgentToolInvoker> invokers = expander.expandInvokers(spec);
|
||||||
|
|
||||||
|
Assert.assertEquals(List.of("demo_task_submit", "demo_task_observe", "demo_task_result",
|
||||||
|
"demo_task_cancel", "demo_task_list"), specs.stream().map(AgentToolSpec::getName).toList());
|
||||||
|
Assert.assertEquals(5, invokers.size());
|
||||||
|
Assert.assertEquals("object", specs.get(1).getParametersSchema().get("type"));
|
||||||
|
Assert.assertEquals("AsyncToolTaskView", specs.get(1).getOutputSchema().get("title"));
|
||||||
|
Assert.assertTrue(specs.get(0).getDescription().contains("default entry point when the user asks to run this tool"));
|
||||||
|
Assert.assertTrue(specs.get(1).getDescription().contains("Use immediately after submit"));
|
||||||
|
Assert.assertTrue(specs.get(1).getDescription().contains("Do not ask the user for task_id immediately after submit"));
|
||||||
|
Assert.assertTrue(specs.get(2).getDescription().contains("final result"));
|
||||||
|
Assert.assertTrue(specs.get(3).getDescription().contains("only when the user explicitly asks to cancel"));
|
||||||
|
Assert.assertTrue(specs.get(4).getDescription().contains("only when the user explicitly asks to list"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldKeepRuntimeMetadataWhenUserMetadataUsesReservedKeys() {
|
||||||
|
AsyncToolSpec spec = spec(new StubSubTools());
|
||||||
|
spec.setMetadata(Map.of("asyncTool", false, "asyncToolName", "user_name", "custom", "value"));
|
||||||
|
AsyncToolSpecExpander expander = new AsyncToolSpecExpander();
|
||||||
|
|
||||||
|
AgentToolSpec toolSpec = expander.expandSpecs(spec).get(0);
|
||||||
|
AgentToolResult result = expander.expandInvokers(spec).get("demo_task_submit")
|
||||||
|
.invoke(Map.of(), context(new ArrayList<>()));
|
||||||
|
|
||||||
|
Assert.assertEquals(true, toolSpec.getMetadata().get("asyncTool"));
|
||||||
|
Assert.assertEquals("demo_task", toolSpec.getMetadata().get("asyncToolName"));
|
||||||
|
Assert.assertEquals("value", toolSpec.getMetadata().get("custom"));
|
||||||
|
Assert.assertEquals(true, result.getMetadata().get("asyncTool"));
|
||||||
|
Assert.assertEquals("demo_task", result.getMetadata().get("asyncToolName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldApplyApprovalOnlyToSubmitTool() {
|
||||||
|
AsyncToolSpec spec = spec(new StubSubTools());
|
||||||
|
AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest();
|
||||||
|
approvalRequest.setApprovalPrompt("确认提交任务?");
|
||||||
|
spec.setApprovalRequired(true);
|
||||||
|
spec.setApprovalRequest(approvalRequest);
|
||||||
|
|
||||||
|
List<AgentToolSpec> specs = new AsyncToolSpecExpander().expandSpecs(spec);
|
||||||
|
|
||||||
|
Assert.assertTrue(specs.get(0).isApprovalRequired());
|
||||||
|
Assert.assertEquals("确认提交任务?", specs.get(0).getApprovalRequest().getApprovalPrompt());
|
||||||
|
Assert.assertFalse(specs.get(1).isApprovalRequired());
|
||||||
|
Assert.assertFalse(specs.get(2).isApprovalRequired());
|
||||||
|
Assert.assertFalse(specs.get(3).isApprovalRequired());
|
||||||
|
Assert.assertFalse(specs.get(4).isApprovalRequired());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldSubmitAndEmitSubmittedEvent() {
|
||||||
|
List<AgentRuntimeEvent> events = new ArrayList<>();
|
||||||
|
AgentToolResult result = invoke("demo_task_submit", Map.of("input", "hello"), new StubSubTools(), events);
|
||||||
|
|
||||||
|
Assert.assertTrue(result.isSuccess());
|
||||||
|
Assert.assertTrue(result.getModelContent().contains("task_id: task-1"));
|
||||||
|
Assert.assertEquals("task-1", result.getMetadata().get("taskId"));
|
||||||
|
Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_SUBMITTED, events.get(0).getEventType());
|
||||||
|
Assert.assertEquals("task-1", events.get(0).getPayload().get("taskId"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldFailWhenSubmitDoesNotReturnTaskId() {
|
||||||
|
List<AgentRuntimeEvent> events = new ArrayList<>();
|
||||||
|
AgentToolResult result = invoke("demo_task_submit", Map.of(), new StubSubTools() {
|
||||||
|
@Override
|
||||||
|
public AsyncToolSubmitResult submit(Map<String, Object> arguments, AgentToolContext context) {
|
||||||
|
AsyncToolSubmitResult submitResult = super.submit(arguments, context);
|
||||||
|
submitResult.setTaskId(null);
|
||||||
|
return submitResult;
|
||||||
|
}
|
||||||
|
}, events);
|
||||||
|
|
||||||
|
Assert.assertFalse(result.isSuccess());
|
||||||
|
Assert.assertTrue(result.getErrorMessage().contains("taskId"));
|
||||||
|
Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_FAILED, events.get(0).getEventType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldObserveWithCursorLimitAndContext() {
|
||||||
|
AtomicReference<AsyncToolObserveRequest> requestRef = new AtomicReference<>();
|
||||||
|
AtomicReference<AgentToolContext> contextRef = new AtomicReference<>();
|
||||||
|
StubSubTools subTools = new StubSubTools() {
|
||||||
|
@Override
|
||||||
|
public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) {
|
||||||
|
requestRef.set(request);
|
||||||
|
contextRef.set(context);
|
||||||
|
return super.observe(request, context);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
List<AgentRuntimeEvent> events = new ArrayList<>();
|
||||||
|
|
||||||
|
AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1", "cursor", 7, "limit", 999),
|
||||||
|
subTools, events);
|
||||||
|
|
||||||
|
AsyncToolTaskView view = (AsyncToolTaskView) result.getDisplayContent();
|
||||||
|
Assert.assertEquals(Long.valueOf(7), requestRef.get().getCursor());
|
||||||
|
Assert.assertEquals(Integer.valueOf(100), requestRef.get().getLimit());
|
||||||
|
Assert.assertEquals(Long.valueOf(8), view.getNextCursor());
|
||||||
|
Assert.assertEquals("request-1", contextRef.get().getRequestId());
|
||||||
|
Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_OBSERVED, events.get(0).getEventType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldWrapInvalidArgumentsAndEmitFailedEvent() {
|
||||||
|
List<AgentRuntimeEvent> events = new ArrayList<>();
|
||||||
|
|
||||||
|
AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1", "cursor", "bad"),
|
||||||
|
new StubSubTools(), events);
|
||||||
|
|
||||||
|
Assert.assertFalse(result.isSuccess());
|
||||||
|
Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_FAILED, events.get(0).getEventType());
|
||||||
|
Assert.assertEquals("FAILED", result.getMetadata().get("status"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldReturnObservationFromResultWhenRunningAndFinalResultWhenSucceeded() {
|
||||||
|
StubSubTools running = new StubSubTools();
|
||||||
|
AgentToolResult runningResult = invoke("demo_task_result", Map.of("taskId", "task-1"), running, new ArrayList<>());
|
||||||
|
AsyncToolTaskView runningView = (AsyncToolTaskView) runningResult.getDisplayContent();
|
||||||
|
|
||||||
|
StubSubTools succeeded = new StubSubTools() {
|
||||||
|
@Override
|
||||||
|
public AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context) {
|
||||||
|
AsyncToolTaskView view = super.result(request, context);
|
||||||
|
view.setStatus(AsyncToolTaskStatus.SUCCEEDED);
|
||||||
|
view.setResult("done");
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AgentToolResult successResult = invoke("demo_task_result", Map.of("taskId", "task-1"), succeeded, new ArrayList<>());
|
||||||
|
AsyncToolTaskView successView = (AsyncToolTaskView) successResult.getDisplayContent();
|
||||||
|
|
||||||
|
Assert.assertEquals(AsyncToolTaskStatus.RUNNING, runningView.getStatus());
|
||||||
|
Assert.assertEquals(AsyncToolTaskStatus.SUCCEEDED, successView.getStatus());
|
||||||
|
Assert.assertEquals("done", successView.getResult());
|
||||||
|
Assert.assertTrue(successView.getTerminal());
|
||||||
|
Assert.assertTrue(successView.getResultAvailable());
|
||||||
|
Assert.assertFalse(runningResult.getModelContent().contains("result:"));
|
||||||
|
Assert.assertTrue(successResult.getModelContent().contains("result_available: true"));
|
||||||
|
Assert.assertTrue(successResult.getModelContent().contains("result: done"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldExposeCompletedObservationResultToModelContentAsJson() {
|
||||||
|
List<AgentRuntimeEvent> events = new ArrayList<>();
|
||||||
|
LinkedHashMap<String, Object> businessResult = new LinkedHashMap<>();
|
||||||
|
businessResult.put("answer", "42");
|
||||||
|
businessResult.put("items", List.of(1, 2));
|
||||||
|
StubSubTools succeeded = new StubSubTools() {
|
||||||
|
@Override
|
||||||
|
public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) {
|
||||||
|
AsyncToolTaskView view = super.observe(request, context);
|
||||||
|
view.setStatus(AsyncToolTaskStatus.SUCCEEDED);
|
||||||
|
view.setResult(businessResult);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1"), succeeded, events);
|
||||||
|
AsyncToolTaskView view = (AsyncToolTaskView) result.getDisplayContent();
|
||||||
|
|
||||||
|
Assert.assertEquals(businessResult, view.getResult());
|
||||||
|
Assert.assertTrue(result.getModelContent().contains("result_available: true"));
|
||||||
|
Assert.assertTrue(result.getModelContent().contains("result: {\"answer\":\"42\",\"items\":[1,2]}"));
|
||||||
|
Assert.assertEquals(Boolean.TRUE, events.get(0).getPayload().get("resultAvailable"));
|
||||||
|
Assert.assertFalse(events.get(0).getPayload().containsKey("result"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldCancelAndRepresentUnsupportedCancelAsFailure() {
|
||||||
|
AgentToolResult success = invoke("demo_task_cancel", Map.of("taskId", "task-1"), new StubSubTools(), new ArrayList<>());
|
||||||
|
AgentToolResult failure = invoke("demo_task_cancel", Map.of("taskId", "task-1"), new StubSubTools() {
|
||||||
|
@Override
|
||||||
|
public AsyncToolCancelResult cancel(AsyncToolCancelRequest request, AgentToolContext context) {
|
||||||
|
AsyncToolCancelResult result = super.cancel(request, context);
|
||||||
|
result.setErrorMessage("不支持取消");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}, new ArrayList<>());
|
||||||
|
|
||||||
|
Assert.assertTrue(success.isSuccess());
|
||||||
|
Assert.assertFalse(failure.isSuccess());
|
||||||
|
Assert.assertEquals("不支持取消", failure.getErrorMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldListTasksWithoutPagination() {
|
||||||
|
AgentToolResult result = invoke("demo_task_list", Map.of("status", "running"), new StubSubTools(), new ArrayList<>());
|
||||||
|
AsyncToolTaskListResult list = (AsyncToolTaskListResult) result.getDisplayContent();
|
||||||
|
|
||||||
|
Assert.assertTrue(result.isSuccess());
|
||||||
|
Assert.assertEquals(1, list.getTasks().size());
|
||||||
|
Assert.assertFalse(Arrays.stream(AsyncToolTaskListResult.class.getDeclaredFields())
|
||||||
|
.anyMatch(field -> field.getName().contains("PageToken")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldWrapExceptionAndEmitFailedEvent() {
|
||||||
|
List<AgentRuntimeEvent> events = new ArrayList<>();
|
||||||
|
AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1"), new StubSubTools() {
|
||||||
|
@Override
|
||||||
|
public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) {
|
||||||
|
throw new IllegalStateException("boom");
|
||||||
|
}
|
||||||
|
}, events);
|
||||||
|
|
||||||
|
Assert.assertFalse(result.isSuccess());
|
||||||
|
Assert.assertEquals("boom", result.getErrorMessage());
|
||||||
|
Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_FAILED, events.get(0).getEventType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldTimeoutAndEmitFailedEvent() {
|
||||||
|
List<AgentRuntimeEvent> events = new ArrayList<>();
|
||||||
|
AsyncToolOptions options = AsyncToolOptions.defaults();
|
||||||
|
options.setObserveTimeout(Duration.ofMillis(20));
|
||||||
|
StubSubTools subTools = new StubSubTools() {
|
||||||
|
@Override
|
||||||
|
public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(200);
|
||||||
|
} catch (InterruptedException ignored) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
return super.observe(request, context);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AgentToolResult result = invoke("demo_task_observe", Map.of("taskId", "task-1"), subTools, events, options);
|
||||||
|
|
||||||
|
Assert.assertFalse(result.isSuccess());
|
||||||
|
Assert.assertTrue(result.getErrorMessage().contains("timed out"));
|
||||||
|
Assert.assertEquals(AgentRuntimeEventType.ASYNC_TOOL_FAILED, events.get(0).getEventType());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldNotPutLargePayloadIntoModelContentOrEventPayload() {
|
||||||
|
List<AgentRuntimeEvent> events = new ArrayList<>();
|
||||||
|
AgentToolResult result = invoke("demo_task_submit", Map.of(), new StubSubTools() {
|
||||||
|
@Override
|
||||||
|
public AsyncToolSubmitResult submit(Map<String, Object> arguments, AgentToolContext context) {
|
||||||
|
AsyncToolSubmitResult result = super.submit(arguments, context);
|
||||||
|
result.getPayload().put("large", "x".repeat(5000));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}, events);
|
||||||
|
|
||||||
|
Assert.assertFalse(result.getModelContent().contains("xxxxx"));
|
||||||
|
Assert.assertFalse(events.get(0).getPayload().containsKey("large"));
|
||||||
|
Assert.assertTrue(((AsyncToolSubmitResult) result.getDisplayContent()).getPayload().containsKey("large"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void shouldTrimLargeResultForModelContentAndKeepFullDisplayContent() {
|
||||||
|
List<AgentRuntimeEvent> events = new ArrayList<>();
|
||||||
|
AsyncToolOptions options = AsyncToolOptions.defaults();
|
||||||
|
options.setMaxModelContentLength(160);
|
||||||
|
String largeResult = "x".repeat(5000);
|
||||||
|
StubSubTools succeeded = new StubSubTools() {
|
||||||
|
@Override
|
||||||
|
public AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context) {
|
||||||
|
AsyncToolTaskView view = super.result(request, context);
|
||||||
|
view.setStatus(AsyncToolTaskStatus.SUCCEEDED);
|
||||||
|
view.setResult(Map.of("large", largeResult));
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AgentToolResult result = invoke("demo_task_result", Map.of("taskId", "task-1"), succeeded, events, options);
|
||||||
|
AsyncToolTaskView view = (AsyncToolTaskView) result.getDisplayContent();
|
||||||
|
|
||||||
|
Assert.assertTrue(result.getModelContent().length() <= options.getMaxModelContentLength() + 3);
|
||||||
|
Assert.assertTrue(result.getModelContent().contains("result_available: true"));
|
||||||
|
Assert.assertFalse(result.getModelContent().contains(largeResult));
|
||||||
|
Assert.assertEquals(Map.of("large", largeResult), view.getResult());
|
||||||
|
Assert.assertEquals(Boolean.TRUE, events.get(0).getPayload().get("resultAvailable"));
|
||||||
|
Assert.assertFalse(events.get(0).getPayload().containsKey("result"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult invoke(String toolName,
|
||||||
|
Map<String, Object> arguments,
|
||||||
|
AsyncSubTools subTools,
|
||||||
|
List<AgentRuntimeEvent> events) {
|
||||||
|
return invoke(toolName, arguments, subTools, events, AsyncToolOptions.defaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolResult invoke(String toolName,
|
||||||
|
Map<String, Object> arguments,
|
||||||
|
AsyncSubTools subTools,
|
||||||
|
List<AgentRuntimeEvent> events,
|
||||||
|
AsyncToolOptions options) {
|
||||||
|
AsyncToolSpec spec = spec(subTools);
|
||||||
|
spec.setOptions(options);
|
||||||
|
AsyncToolSpecExpander expander = new AsyncToolSpecExpander(Executors.newCachedThreadPool());
|
||||||
|
AgentToolContext context = context(events);
|
||||||
|
return expander.expandInvokers(spec).get(toolName).invoke(arguments, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AsyncToolSpec spec(AsyncSubTools subTools) {
|
||||||
|
AsyncToolSpec spec = new AsyncToolSpec();
|
||||||
|
spec.setName("demo_task");
|
||||||
|
spec.setDescription("Demo async task.");
|
||||||
|
spec.setSubTools(subTools);
|
||||||
|
spec.setSubmitParametersSchema(Map.of("type", "object", "properties", Map.of("input", Map.of("type", "string"))));
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AgentToolContext context(List<AgentRuntimeEvent> events) {
|
||||||
|
AgentToolContext context = new AgentToolContext();
|
||||||
|
context.setRequestId("request-1");
|
||||||
|
context.setTraceId("trace-1");
|
||||||
|
context.setSessionId("session-1");
|
||||||
|
context.setAgentId("agent-1");
|
||||||
|
context.setToolCallId("tool-call-1");
|
||||||
|
context.setEventEmitter(events::add);
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class StubSubTools implements AsyncSubTools {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AsyncToolSubmitResult submit(Map<String, Object> arguments, AgentToolContext context) {
|
||||||
|
AsyncToolSubmitResult result = new AsyncToolSubmitResult();
|
||||||
|
result.setTaskId("task-1");
|
||||||
|
result.setStatus(AsyncToolTaskStatus.RUNNING);
|
||||||
|
result.setCursor(0L);
|
||||||
|
result.setSummary("submitted");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AsyncToolTaskView observe(AsyncToolObserveRequest request, AgentToolContext context) {
|
||||||
|
AsyncToolTaskView view = new AsyncToolTaskView();
|
||||||
|
view.setTaskId(request.getTaskId());
|
||||||
|
view.setStatus(AsyncToolTaskStatus.RUNNING);
|
||||||
|
view.setCursor(request.getCursor());
|
||||||
|
view.setNextCursor(request.getCursor() == null ? 1L : request.getCursor() + 1);
|
||||||
|
view.setSummary("running");
|
||||||
|
AsyncToolTaskEvent event = new AsyncToolTaskEvent();
|
||||||
|
event.setSequence(view.getNextCursor());
|
||||||
|
event.setType("TASK_LOG");
|
||||||
|
event.setText("progress");
|
||||||
|
event.setCreatedAt(Instant.now());
|
||||||
|
view.setEvents(List.of(event));
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AsyncToolTaskView result(AsyncToolResultRequest request, AgentToolContext context) {
|
||||||
|
AsyncToolObserveRequest observeRequest = new AsyncToolObserveRequest();
|
||||||
|
observeRequest.setTaskId(request.getTaskId());
|
||||||
|
observeRequest.setCursor(request.getCursor());
|
||||||
|
observeRequest.setLimit(request.getLimit());
|
||||||
|
return observe(observeRequest, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AsyncToolCancelResult cancel(AsyncToolCancelRequest request, AgentToolContext context) {
|
||||||
|
AsyncToolCancelResult result = new AsyncToolCancelResult();
|
||||||
|
result.setTaskId(request.getTaskId());
|
||||||
|
result.setStatus(AsyncToolTaskStatus.CANCELLING);
|
||||||
|
result.setMessage("cancelling");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AsyncToolTaskListResult list(AsyncToolListRequest request, AgentToolContext context) {
|
||||||
|
AsyncToolTaskSummary summary = new AsyncToolTaskSummary();
|
||||||
|
summary.setTaskId("task-1");
|
||||||
|
summary.setStatus(AsyncToolTaskStatus.RUNNING);
|
||||||
|
summary.setSummary("running");
|
||||||
|
AsyncToolTaskListResult result = new AsyncToolTaskListResult();
|
||||||
|
result.setTasks(List.of(summary));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,11 @@
|
|||||||
<groupId>com.easyagents</groupId>
|
<groupId>com.easyagents</groupId>
|
||||||
<artifactId>easy-agents-core</artifactId>
|
<artifactId>easy-agents-core</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,20 +1,7 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
|
||||||
* <p>
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
* <p>
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
* <p>
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.easyagents.embedding.openai;
|
package com.easyagents.embedding.openai;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.easyagents.core.document.Document;
|
import com.easyagents.core.document.Document;
|
||||||
import com.easyagents.core.model.client.HttpClient;
|
import com.easyagents.core.model.client.HttpClient;
|
||||||
import com.easyagents.core.model.embedding.BaseEmbeddingModel;
|
import com.easyagents.core.model.embedding.BaseEmbeddingModel;
|
||||||
@@ -24,10 +11,9 @@ import com.easyagents.core.store.VectorData;
|
|||||||
import com.easyagents.core.util.JSONUtil;
|
import com.easyagents.core.util.JSONUtil;
|
||||||
import com.easyagents.core.util.Maps;
|
import com.easyagents.core.util.Maps;
|
||||||
import com.easyagents.core.util.StringUtil;
|
import com.easyagents.core.util.StringUtil;
|
||||||
import com.alibaba.fastjson2.JSON;
|
|
||||||
import com.alibaba.fastjson2.JSONObject;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class OpenAIEmbeddingModel extends BaseEmbeddingModel<OpenAIEmbeddingConfig> {
|
public class OpenAIEmbeddingModel extends BaseEmbeddingModel<OpenAIEmbeddingConfig> {
|
||||||
@@ -69,19 +55,63 @@ public class OpenAIEmbeddingModel extends BaseEmbeddingModel<OpenAIEmbeddingConf
|
|||||||
|
|
||||||
VectorData vectorData = new VectorData();
|
VectorData vectorData = new VectorData();
|
||||||
double[] embedding = JSONUtil.readDoubleArray(jsonObject, "$.data[0].embedding");
|
double[] embedding = JSONUtil.readDoubleArray(jsonObject, "$.data[0].embedding");
|
||||||
|
if (embedding == null || embedding.length == 0) {
|
||||||
|
throw new ModelException(buildMissingEmbeddingMessage());
|
||||||
|
}
|
||||||
vectorData.setVector(embedding);
|
vectorData.setVector(embedding);
|
||||||
|
|
||||||
return vectorData;
|
return vectorData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the embeddings request payload for OpenAI-compatible providers.
|
||||||
|
*
|
||||||
|
* @param text document to embed
|
||||||
|
* @param options embedding request options
|
||||||
|
* @param config model configuration
|
||||||
|
* @return JSON payload for the embeddings endpoint
|
||||||
|
*/
|
||||||
public static String promptToEmbeddingsPayload(Document text, EmbeddingOptions options, OpenAIEmbeddingConfig config) {
|
public static String promptToEmbeddingsPayload(Document text, EmbeddingOptions options, OpenAIEmbeddingConfig config) {
|
||||||
// https://platform.openai.com/docs/api-reference/making-requests
|
String model = options.getModelOrDefault(config.getModel());
|
||||||
return Maps.of("model", options.getModelOrDefault(config.getModel()))
|
return Maps.of("model", model)
|
||||||
.set("encoding_format", options.getEncodingFormatOrDefault("float"))
|
.set("encoding_format", options.getEncodingFormatOrDefault("float"))
|
||||||
.set("input", text.getContent())
|
.set("input", text.getContent())
|
||||||
.setIfNotEmpty("user", options.getUser())
|
.setIfNotEmpty("user", options.getUser())
|
||||||
.setIfNotEmpty("dimensions", options.getDimensions())
|
.setIf(
|
||||||
|
supportsDimensionsParameter(model) && options.getDimensions() != null,
|
||||||
|
"dimensions",
|
||||||
|
options.getDimensions()
|
||||||
|
)
|
||||||
.toJSON();
|
.toJSON();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether the upstream embeddings endpoint supports dynamic dimensions.
|
||||||
|
*
|
||||||
|
* @param model model name sent to the provider
|
||||||
|
* @return true when dimensions should be sent
|
||||||
|
*/
|
||||||
|
static boolean supportsDimensionsParameter(String model) {
|
||||||
|
if (StringUtil.noText(model)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String normalizedModel = model.toLowerCase(Locale.ROOT);
|
||||||
|
return normalizedModel.contains("qwen3-embedding")
|
||||||
|
|| normalizedModel.contains("qwen");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a safe diagnostic message for malformed embeddings responses.
|
||||||
|
*
|
||||||
|
* @return diagnostic message without secrets or raw response content
|
||||||
|
*/
|
||||||
|
private String buildMissingEmbeddingMessage() {
|
||||||
|
return "Embedding response does not contain data[0].embedding."
|
||||||
|
+ " Please check provider, model, request path, dimensions, or response format."
|
||||||
|
+ " provider=" + config.getProvider()
|
||||||
|
+ ", model=" + config.getModel()
|
||||||
|
+ ", endpoint=" + config.getEndpoint()
|
||||||
|
+ ", requestPath=" + config.getRequestPath();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.easyagents.embedding.openai;
|
||||||
|
|
||||||
|
import com.easyagents.core.document.Document;
|
||||||
|
import com.easyagents.core.model.client.HttpClient;
|
||||||
|
import com.easyagents.core.model.embedding.EmbeddingOptions;
|
||||||
|
import com.easyagents.core.model.exception.ModelException;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for OpenAI-compatible embeddings request payload generation.
|
||||||
|
*/
|
||||||
|
public class OpenAIEmbeddingModelTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that fixed-dimension embeddings models do not receive dimensions.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldNotSendDimensionsForBgeModel() {
|
||||||
|
OpenAIEmbeddingConfig config = new OpenAIEmbeddingConfig();
|
||||||
|
config.setModel("BAAI/bge-m3");
|
||||||
|
EmbeddingOptions options = new EmbeddingOptions();
|
||||||
|
options.setDimensions(1024);
|
||||||
|
|
||||||
|
String payload = OpenAIEmbeddingModel.promptToEmbeddingsPayload(Document.of("hello"), options, config);
|
||||||
|
|
||||||
|
Assert.assertFalse(payload.contains("\"dimensions\""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that Qwen3 embedding models keep the dynamic dimensions parameter.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldSendDimensionsForQwen3EmbeddingModel() {
|
||||||
|
OpenAIEmbeddingConfig config = new OpenAIEmbeddingConfig();
|
||||||
|
config.setModel("Qwen/Qwen3-Embedding-8B");
|
||||||
|
EmbeddingOptions options = new EmbeddingOptions();
|
||||||
|
options.setDimensions(1024);
|
||||||
|
|
||||||
|
String payload = OpenAIEmbeddingModel.promptToEmbeddingsPayload(Document.of("hello"), options, config);
|
||||||
|
|
||||||
|
Assert.assertTrue(payload.contains("\"dimensions\":1024"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that malformed embeddings responses fail with a clear model exception.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldThrowModelExceptionWhenEmbeddingMissing() {
|
||||||
|
OpenAIEmbeddingConfig config = new OpenAIEmbeddingConfig();
|
||||||
|
config.setProvider("test-provider");
|
||||||
|
config.setModel("BAAI/bge-m3");
|
||||||
|
config.setApiKey("test-key");
|
||||||
|
OpenAIEmbeddingModel model = new OpenAIEmbeddingModel(config);
|
||||||
|
model.setHttpClient(new HttpClient() {
|
||||||
|
@Override
|
||||||
|
public String post(String url, Map<String, String> headers, String payload) {
|
||||||
|
return "{\"data\":[{}]}";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ModelException exception = Assert.assertThrows(
|
||||||
|
ModelException.class,
|
||||||
|
() -> model.embed(Document.of("hello"))
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertTrue(exception.getMessage().contains("data[0].embedding"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||||
|
* <p>
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
*/
|
||||||
|
package com.easyagents.mcp.client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 单项检测结果。
|
||||||
|
*/
|
||||||
|
public class McpCheckItem {
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
private McpCheckStatus status = McpCheckStatus.SUCCESS;
|
||||||
|
private String message;
|
||||||
|
private String detail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建检测项。
|
||||||
|
*
|
||||||
|
* @param name 检测项名称
|
||||||
|
* @param status 检测状态
|
||||||
|
* @param message 检测消息
|
||||||
|
* @param detail 检测详情
|
||||||
|
* @return 检测项
|
||||||
|
*/
|
||||||
|
public static McpCheckItem of(String name, McpCheckStatus status, String message, String detail) {
|
||||||
|
McpCheckItem item = new McpCheckItem();
|
||||||
|
item.setName(name);
|
||||||
|
item.setStatus(status);
|
||||||
|
item.setMessage(message);
|
||||||
|
item.setDetail(detail);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检测项名称。
|
||||||
|
*
|
||||||
|
* @return 检测项名称
|
||||||
|
*/
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置检测项名称。
|
||||||
|
*
|
||||||
|
* @param name 检测项名称
|
||||||
|
*/
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检测状态。
|
||||||
|
*
|
||||||
|
* @return 检测状态
|
||||||
|
*/
|
||||||
|
public McpCheckStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置检测状态。
|
||||||
|
*
|
||||||
|
* @param status 检测状态
|
||||||
|
*/
|
||||||
|
public void setStatus(McpCheckStatus status) {
|
||||||
|
this.status = status == null ? McpCheckStatus.SUCCESS : status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检测消息。
|
||||||
|
*
|
||||||
|
* @return 检测消息
|
||||||
|
*/
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置检测消息。
|
||||||
|
*
|
||||||
|
* @param message 检测消息
|
||||||
|
*/
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检测详情。
|
||||||
|
*
|
||||||
|
* @return 检测详情
|
||||||
|
*/
|
||||||
|
public String getDetail() {
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置检测详情。
|
||||||
|
*
|
||||||
|
* @param detail 检测详情
|
||||||
|
*/
|
||||||
|
public void setDetail(String detail) {
|
||||||
|
this.detail = detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||||
|
* <p>
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
*/
|
||||||
|
package com.easyagents.mcp.client;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 检测状态。
|
||||||
|
*/
|
||||||
|
public enum McpCheckStatus {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测通过。
|
||||||
|
*/
|
||||||
|
SUCCESS,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测存在警告但不一定阻断使用。
|
||||||
|
*/
|
||||||
|
WARNING,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测失败。
|
||||||
|
*/
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||||
|
* <p>
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
*/
|
||||||
|
package com.easyagents.mcp.client;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 环境检测结果。
|
||||||
|
*/
|
||||||
|
public class McpEnvironmentCheckResult {
|
||||||
|
|
||||||
|
private McpCheckStatus overallStatus = McpCheckStatus.SUCCESS;
|
||||||
|
private List<McpServerCheckResult> servers = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并两个状态。
|
||||||
|
*
|
||||||
|
* @param current 当前状态
|
||||||
|
* @param incoming 新状态
|
||||||
|
* @return 合并后的状态
|
||||||
|
*/
|
||||||
|
public static McpCheckStatus mergeStatus(McpCheckStatus current, McpCheckStatus incoming) {
|
||||||
|
if (current == McpCheckStatus.FAILED || incoming == McpCheckStatus.FAILED) {
|
||||||
|
return McpCheckStatus.FAILED;
|
||||||
|
}
|
||||||
|
if (current == McpCheckStatus.WARNING || incoming == McpCheckStatus.WARNING) {
|
||||||
|
return McpCheckStatus.WARNING;
|
||||||
|
}
|
||||||
|
return McpCheckStatus.SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加 Server 检测结果。
|
||||||
|
*
|
||||||
|
* @param server Server 检测结果
|
||||||
|
*/
|
||||||
|
public void addServer(McpServerCheckResult server) {
|
||||||
|
if (server == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.servers.add(server);
|
||||||
|
this.overallStatus = mergeStatus(this.overallStatus, server.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取整体状态。
|
||||||
|
*
|
||||||
|
* @return 整体状态
|
||||||
|
*/
|
||||||
|
public McpCheckStatus getOverallStatus() {
|
||||||
|
return overallStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置整体状态。
|
||||||
|
*
|
||||||
|
* @param overallStatus 整体状态
|
||||||
|
*/
|
||||||
|
public void setOverallStatus(McpCheckStatus overallStatus) {
|
||||||
|
this.overallStatus = overallStatus == null ? McpCheckStatus.SUCCESS : overallStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Server 检测结果。
|
||||||
|
*
|
||||||
|
* @return Server 检测结果
|
||||||
|
*/
|
||||||
|
public List<McpServerCheckResult> getServers() {
|
||||||
|
return servers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Server 检测结果。
|
||||||
|
*
|
||||||
|
* @param servers Server 检测结果
|
||||||
|
*/
|
||||||
|
public void setServers(List<McpServerCheckResult> servers) {
|
||||||
|
this.servers = servers == null ? new ArrayList<>() : new ArrayList<>(servers);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||||
|
* <p>
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
*/
|
||||||
|
package com.easyagents.mcp.client;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import io.modelcontextprotocol.client.McpClient;
|
||||||
|
import io.modelcontextprotocol.client.McpSyncClient;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 配置与运行环境检测器。
|
||||||
|
*/
|
||||||
|
public class McpEnvironmentChecker {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(McpEnvironmentChecker.class);
|
||||||
|
private static final Duration COMMAND_TIMEOUT = Duration.ofSeconds(3);
|
||||||
|
private static final Duration MCP_REQUEST_TIMEOUT = Duration.ofSeconds(10);
|
||||||
|
private static final Set<String> SUPPORTED_TRANSPORTS = Set.of("stdio", "http-sse", "http-stream");
|
||||||
|
private static final Set<String> KNOWN_VERSION_COMMANDS = Set.of(
|
||||||
|
"node", "npm", "npx", "pnpm", "python", "python3", "pip", "pip3");
|
||||||
|
private final boolean probeEnabled;
|
||||||
|
private final Function<String, McpTransportFactory> transportFactoryProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建启用连接探测的检测器。
|
||||||
|
*/
|
||||||
|
public McpEnvironmentChecker() {
|
||||||
|
this(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建可控制连接探测行为的检测器。
|
||||||
|
*
|
||||||
|
* @param probeEnabled 是否启用 MCP 连接探测
|
||||||
|
*/
|
||||||
|
McpEnvironmentChecker(boolean probeEnabled) {
|
||||||
|
this(probeEnabled, McpEnvironmentChecker::defaultTransportFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建可注入 transport 工厂的检测器。
|
||||||
|
*
|
||||||
|
* @param probeEnabled 是否启用 MCP 连接探测
|
||||||
|
* @param transportFactoryProvider transport 工厂提供器
|
||||||
|
*/
|
||||||
|
McpEnvironmentChecker(boolean probeEnabled,
|
||||||
|
Function<String, McpTransportFactory> transportFactoryProvider) {
|
||||||
|
this.probeEnabled = probeEnabled;
|
||||||
|
this.transportFactoryProvider = transportFactoryProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测 MCP 配置。
|
||||||
|
*
|
||||||
|
* @param configJson MCP 配置 JSON
|
||||||
|
* @return 检测结果
|
||||||
|
*/
|
||||||
|
public McpEnvironmentCheckResult check(String configJson) {
|
||||||
|
McpEnvironmentCheckResult result = new McpEnvironmentCheckResult();
|
||||||
|
if (configJson == null || configJson.isBlank()) {
|
||||||
|
result.setOverallStatus(McpCheckStatus.FAILED);
|
||||||
|
result.addServer(failedServer("config", null, "configJson", "MCP 配置 JSON 不能为空", null));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
McpConfig config;
|
||||||
|
try {
|
||||||
|
config = JSON.parseObject(configJson, McpConfig.class);
|
||||||
|
} catch (Exception error) {
|
||||||
|
result.setOverallStatus(McpCheckStatus.FAILED);
|
||||||
|
result.addServer(failedServer("config", null, "json", "MCP 配置 JSON 格式错误", sanitize(error)));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config == null || config.getMcpServers() == null || config.getMcpServers().isEmpty()) {
|
||||||
|
result.setOverallStatus(McpCheckStatus.FAILED);
|
||||||
|
result.addServer(failedServer("config", null, "mcpServers", "mcpServers 不能为空", null));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Map.Entry<String, McpConfig.ServerSpec> entry : config.getMcpServers().entrySet()) {
|
||||||
|
result.addServer(checkServer(entry.getKey(), entry.getValue()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpServerCheckResult checkServer(String serverName, McpConfig.ServerSpec spec) {
|
||||||
|
McpServerCheckResult result = new McpServerCheckResult();
|
||||||
|
result.setServerName(serverName);
|
||||||
|
result.setTransport(transport(spec));
|
||||||
|
|
||||||
|
if (serverName == null || serverName.isBlank()) {
|
||||||
|
result.addCheck(McpCheckItem.of("serverName", McpCheckStatus.FAILED,
|
||||||
|
"MCP 服务名称不能为空", null));
|
||||||
|
}
|
||||||
|
if (spec == null) {
|
||||||
|
result.addCheck(McpCheckItem.of("server", McpCheckStatus.FAILED,
|
||||||
|
"MCP 服务配置不能为空", null));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
String transport = transport(spec);
|
||||||
|
result.setTransport(transport);
|
||||||
|
if (!SUPPORTED_TRANSPORTS.contains(transport)) {
|
||||||
|
result.addCheck(McpCheckItem.of("transport", McpCheckStatus.FAILED,
|
||||||
|
"不支持的 MCP 传输类型", transport));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.addCheck(McpCheckItem.of("transport", McpCheckStatus.SUCCESS,
|
||||||
|
"MCP 传输类型可用", transport));
|
||||||
|
|
||||||
|
Map<String, String> resolvedEnv = resolveEnv(spec.getEnv(), result);
|
||||||
|
if ("stdio".equals(transport)) {
|
||||||
|
validateStdio(spec, result);
|
||||||
|
} else {
|
||||||
|
validateHttp(spec, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (probeEnabled && result.getStatus() != McpCheckStatus.FAILED) {
|
||||||
|
probe(serverName, spec, resolvedEnv, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateStdio(McpConfig.ServerSpec spec, McpServerCheckResult result) {
|
||||||
|
if (spec.getCommand() == null || spec.getCommand().isBlank()) {
|
||||||
|
result.addCheck(McpCheckItem.of("command", McpCheckStatus.FAILED,
|
||||||
|
"stdio MCP 必须配置 command", null));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.addCheck(checkCommand(spec.getCommand()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateHttp(McpConfig.ServerSpec spec, McpServerCheckResult result) {
|
||||||
|
if (spec.getUrl() == null || spec.getUrl().isBlank()) {
|
||||||
|
result.addCheck(McpCheckItem.of("url", McpCheckStatus.FAILED,
|
||||||
|
"HTTP MCP 必须配置 url", null));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
result.addCheck(McpCheckItem.of("url", McpCheckStatus.SUCCESS,
|
||||||
|
"MCP 连接地址已配置", spec.getUrl()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpCheckItem checkCommand(String command) {
|
||||||
|
String executable = executableName(command);
|
||||||
|
boolean known = KNOWN_VERSION_COMMANDS.contains(executable);
|
||||||
|
ProcessBuilder builder = known
|
||||||
|
? new ProcessBuilder(command, "--version")
|
||||||
|
: new ProcessBuilder(command);
|
||||||
|
try {
|
||||||
|
Process process = builder.redirectErrorStream(true).start();
|
||||||
|
boolean finished = process.waitFor(COMMAND_TIMEOUT.toMillis(), TimeUnit.MILLISECONDS);
|
||||||
|
if (!finished) {
|
||||||
|
process.destroyForcibly();
|
||||||
|
return McpCheckItem.of("command", McpCheckStatus.SUCCESS,
|
||||||
|
command + " 可启动", "版本检测超时,已终止检测进程");
|
||||||
|
}
|
||||||
|
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim();
|
||||||
|
if (process.exitValue() == 0 || !known) {
|
||||||
|
return McpCheckItem.of("command", McpCheckStatus.SUCCESS,
|
||||||
|
command + " 可用", firstLine(output));
|
||||||
|
}
|
||||||
|
return McpCheckItem.of("command", McpCheckStatus.WARNING,
|
||||||
|
command + " 可启动但返回非零状态", firstLine(output));
|
||||||
|
} catch (Exception error) {
|
||||||
|
return McpCheckItem.of("command", McpCheckStatus.FAILED,
|
||||||
|
"容器内未找到命令:" + command, sanitize(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void probe(String serverName,
|
||||||
|
McpConfig.ServerSpec spec,
|
||||||
|
Map<String, String> resolvedEnv,
|
||||||
|
McpServerCheckResult result) {
|
||||||
|
CloseableTransport transport = null;
|
||||||
|
McpSyncClient client = null;
|
||||||
|
try {
|
||||||
|
transport = transportFactoryProvider.apply(spec.getTransport()).create(spec, resolvedEnv);
|
||||||
|
client = McpClient.sync(transport.getTransport())
|
||||||
|
.requestTimeout(MCP_REQUEST_TIMEOUT)
|
||||||
|
.build();
|
||||||
|
client.initialize();
|
||||||
|
McpSchema.ListToolsResult toolsResult = client.listTools();
|
||||||
|
int toolCount = toolsResult == null || toolsResult.tools() == null ? 0 : toolsResult.tools().size();
|
||||||
|
result.setToolCount(toolCount);
|
||||||
|
if (toolCount == 0) {
|
||||||
|
result.addCheck(McpCheckItem.of("tools", McpCheckStatus.WARNING,
|
||||||
|
"MCP 已连接,但没有发现工具", null));
|
||||||
|
} else {
|
||||||
|
result.addCheck(McpCheckItem.of("tools", McpCheckStatus.SUCCESS,
|
||||||
|
"MCP 工具列表获取成功", String.valueOf(toolCount)));
|
||||||
|
}
|
||||||
|
} catch (Exception error) {
|
||||||
|
log.debug("MCP check failed for server: {}", serverName, error);
|
||||||
|
result.addCheck(McpCheckItem.of("connection", McpCheckStatus.FAILED,
|
||||||
|
"MCP 初始化或工具发现失败", sanitize(error)));
|
||||||
|
} finally {
|
||||||
|
closeQuietly(client);
|
||||||
|
closeQuietly(transport);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> resolveEnv(Map<String, String> env, McpServerCheckResult result) {
|
||||||
|
Map<String, String> resolved = new HashMap<>();
|
||||||
|
if (env == null || env.isEmpty()) {
|
||||||
|
result.addCheck(McpCheckItem.of("env", McpCheckStatus.SUCCESS,
|
||||||
|
"未配置额外环境变量", null));
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
for (Map.Entry<String, String> entry : env.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
String value = entry.getValue();
|
||||||
|
if (value != null && value.startsWith("${input:") && value.endsWith("}")) {
|
||||||
|
String inputId = value.substring("${input:".length(), value.length() - 1);
|
||||||
|
String resolvedValue = System.getProperty("mcp.input." + inputId);
|
||||||
|
if (resolvedValue == null || resolvedValue.isBlank()) {
|
||||||
|
result.addCheck(McpCheckItem.of("env", McpCheckStatus.FAILED,
|
||||||
|
"环境变量未解析:" + key, "input:" + inputId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
resolved.put(key, resolvedValue);
|
||||||
|
result.addCheck(McpCheckItem.of("env", McpCheckStatus.SUCCESS,
|
||||||
|
"环境变量已解析:" + key, "input:" + inputId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
resolved.put(key, value);
|
||||||
|
result.addCheck(McpCheckItem.of("env", McpCheckStatus.SUCCESS,
|
||||||
|
"环境变量已配置:" + key, null));
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpServerCheckResult failedServer(String serverName,
|
||||||
|
String transport,
|
||||||
|
String name,
|
||||||
|
String message,
|
||||||
|
String detail) {
|
||||||
|
McpServerCheckResult server = new McpServerCheckResult();
|
||||||
|
server.setServerName(serverName);
|
||||||
|
server.setTransport(transport);
|
||||||
|
server.addCheck(McpCheckItem.of(name, McpCheckStatus.FAILED, message, detail));
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static McpTransportFactory defaultTransportFactory(String transportType) {
|
||||||
|
return switch (transport(transportType)) {
|
||||||
|
case "stdio" -> new StdioTransportFactory();
|
||||||
|
case "http-sse" -> new HttpSseTransportFactory();
|
||||||
|
case "http-stream" -> new HttpStreamTransportFactory();
|
||||||
|
default -> throw new IllegalArgumentException("Unsupported transport: " + transportType);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String transport(McpConfig.ServerSpec spec) {
|
||||||
|
return spec == null ? "stdio" : transport(spec.getTransport());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String transport(String value) {
|
||||||
|
return value == null || value.isBlank() ? "stdio" : value.toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String executableName(String command) {
|
||||||
|
int slash = Math.max(command.lastIndexOf('/'), command.lastIndexOf('\\'));
|
||||||
|
String name = slash >= 0 ? command.substring(slash + 1) : command;
|
||||||
|
return name.endsWith(".cmd") ? name.substring(0, name.length() - 4) : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstLine(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
int lineEnd = value.indexOf('\n');
|
||||||
|
String line = lineEnd >= 0 ? value.substring(0, lineEnd) : value;
|
||||||
|
return sanitize(line.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sanitize(Throwable error) {
|
||||||
|
if (error == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String message = error.getMessage();
|
||||||
|
return message == null || message.isBlank() ? error.getClass().getSimpleName() : sanitize(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String sanitize(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String sanitized = value.replaceAll("(?i)(api[_-]?key|token|secret|password)=([^\\s,;]+)", "$1=******");
|
||||||
|
return sanitized.length() > 500 ? sanitized.substring(0, 500) : sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeQuietly(AutoCloseable closeable) {
|
||||||
|
if (closeable == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
closeable.close();
|
||||||
|
} catch (Exception error) {
|
||||||
|
log.debug("Failed to close MCP check resource.", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2023-2026, Easy-Agents (fuhai999@gmail.com).
|
||||||
|
* <p>
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
*/
|
||||||
|
package com.easyagents.mcp.client;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单个 MCP Server 的检测结果。
|
||||||
|
*/
|
||||||
|
public class McpServerCheckResult {
|
||||||
|
|
||||||
|
private String serverName;
|
||||||
|
private String transport;
|
||||||
|
private McpCheckStatus status = McpCheckStatus.SUCCESS;
|
||||||
|
private int toolCount;
|
||||||
|
private List<McpCheckItem> checks = new ArrayList<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加检测项并刷新整体状态。
|
||||||
|
*
|
||||||
|
* @param item 检测项
|
||||||
|
*/
|
||||||
|
public void addCheck(McpCheckItem item) {
|
||||||
|
if (item == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.checks.add(item);
|
||||||
|
this.status = McpEnvironmentCheckResult.mergeStatus(this.status, item.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 Server 名称。
|
||||||
|
*
|
||||||
|
* @return Server 名称
|
||||||
|
*/
|
||||||
|
public String getServerName() {
|
||||||
|
return serverName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Server 名称。
|
||||||
|
*
|
||||||
|
* @param serverName Server 名称
|
||||||
|
*/
|
||||||
|
public void setServerName(String serverName) {
|
||||||
|
this.serverName = serverName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取传输类型。
|
||||||
|
*
|
||||||
|
* @return 传输类型
|
||||||
|
*/
|
||||||
|
public String getTransport() {
|
||||||
|
return transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置传输类型。
|
||||||
|
*
|
||||||
|
* @param transport 传输类型
|
||||||
|
*/
|
||||||
|
public void setTransport(String transport) {
|
||||||
|
this.transport = transport;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检测状态。
|
||||||
|
*
|
||||||
|
* @return 检测状态
|
||||||
|
*/
|
||||||
|
public McpCheckStatus getStatus() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置检测状态。
|
||||||
|
*
|
||||||
|
* @param status 检测状态
|
||||||
|
*/
|
||||||
|
public void setStatus(McpCheckStatus status) {
|
||||||
|
this.status = status == null ? McpCheckStatus.SUCCESS : status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工具数量。
|
||||||
|
*
|
||||||
|
* @return 工具数量
|
||||||
|
*/
|
||||||
|
public int getToolCount() {
|
||||||
|
return toolCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置工具数量。
|
||||||
|
*
|
||||||
|
* @param toolCount 工具数量
|
||||||
|
*/
|
||||||
|
public void setToolCount(int toolCount) {
|
||||||
|
this.toolCount = Math.max(toolCount, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取检测项。
|
||||||
|
*
|
||||||
|
* @return 检测项
|
||||||
|
*/
|
||||||
|
public List<McpCheckItem> getChecks() {
|
||||||
|
return checks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置检测项。
|
||||||
|
*
|
||||||
|
* @param checks 检测项
|
||||||
|
*/
|
||||||
|
public void setChecks(List<McpCheckItem> checks) {
|
||||||
|
this.checks = checks == null ? new ArrayList<>() : new ArrayList<>(checks);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,46 +20,24 @@ import io.modelcontextprotocol.client.transport.StdioClientTransport;
|
|||||||
import io.modelcontextprotocol.json.McpJsonMapper;
|
import io.modelcontextprotocol.json.McpJsonMapper;
|
||||||
import io.modelcontextprotocol.spec.McpClientTransport;
|
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class StdioTransportFactory implements McpTransportFactory {
|
public class StdioTransportFactory implements McpTransportFactory {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CloseableTransport create(McpConfig.ServerSpec spec, Map<String, String> resolvedEnv) {
|
public CloseableTransport create(McpConfig.ServerSpec spec, Map<String, String> resolvedEnv) {
|
||||||
// ProcessBuilder pb = new ProcessBuilder();
|
|
||||||
// List<String> args = spec.getArgs();
|
|
||||||
// if (args != null && !args.isEmpty()) {
|
|
||||||
// pb.command(spec.getCommand(), args.toArray(new String[0]));
|
|
||||||
// } else {
|
|
||||||
// pb.command(spec.getCommand());
|
|
||||||
// }
|
|
||||||
// if (!resolvedEnv.isEmpty()) {
|
|
||||||
// pb.environment().putAll(resolvedEnv);
|
|
||||||
// }
|
|
||||||
// pb.redirectErrorStream(true);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Process process = pb.start();
|
List<String> args = spec.getArgs() == null ? Collections.emptyList() : spec.getArgs();
|
||||||
// OutputStream stdin = process.getOutputStream();
|
Map<String, String> env = resolvedEnv == null ? Collections.emptyMap() : resolvedEnv;
|
||||||
// InputStream stdout = process.getInputStream();
|
|
||||||
|
|
||||||
// StdioClientTransport transport = new StdioClientTransport(
|
|
||||||
// stdin, stdout, McpJsonMapper.getDefault(), () -> {}
|
|
||||||
// );
|
|
||||||
|
|
||||||
|
|
||||||
// ServerParameters params = ServerParameters.builder("npx")
|
|
||||||
// .args("-y", "@modelcontextprotocol/server-everything")
|
|
||||||
// .build();
|
|
||||||
|
|
||||||
|
|
||||||
ServerParameters parameters = ServerParameters.builder(spec.getCommand())
|
ServerParameters parameters = ServerParameters.builder(spec.getCommand())
|
||||||
.args(spec.getArgs())
|
.args(args)
|
||||||
|
.env(env)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
StdioClientTransport transport = new StdioClientTransport(parameters, McpJsonMapper.getDefault());
|
StdioClientTransport transport = new StdioClientTransport(parameters, McpJsonMapper.getDefault());
|
||||||
|
|
||||||
|
|
||||||
return new CloseableTransport() {
|
return new CloseableTransport() {
|
||||||
@Override
|
@Override
|
||||||
public McpClientTransport getTransport() {
|
public McpClientTransport getTransport() {
|
||||||
@@ -73,17 +51,6 @@ public class StdioTransportFactory implements McpTransportFactory {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
// if (process.isAlive()) {
|
|
||||||
// process.destroy();
|
|
||||||
// try {
|
|
||||||
// if (!process.waitFor(3, TimeUnit.SECONDS)) {
|
|
||||||
// process.destroyForcibly();
|
|
||||||
// }
|
|
||||||
// } catch (InterruptedException ex) {
|
|
||||||
// Thread.currentThread().interrupt();
|
|
||||||
// process.destroyForcibly();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
package com.easyagents.mcp.client;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.json.TypeRef;
|
||||||
|
import io.modelcontextprotocol.spec.McpClientTransport;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import org.junit.Test;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP 环境检测测试。
|
||||||
|
*/
|
||||||
|
public class McpEnvironmentCheckerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkValidStdioConfigWithoutProbe() {
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"test": {
|
||||||
|
"transport": "stdio",
|
||||||
|
"command": "java",
|
||||||
|
"args": ["-version"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
McpEnvironmentCheckResult result = new McpEnvironmentChecker(false).check(json);
|
||||||
|
|
||||||
|
assertEquals(McpCheckStatus.SUCCESS, result.getOverallStatus());
|
||||||
|
assertEquals("test", result.getServers().get(0).getServerName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkMissingCommand() {
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"test": {
|
||||||
|
"transport": "stdio"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
McpEnvironmentCheckResult result = new McpEnvironmentChecker(false).check(json);
|
||||||
|
|
||||||
|
assertEquals(McpCheckStatus.FAILED, result.getOverallStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkMissingHttpUrl() {
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"test": {
|
||||||
|
"transport": "http-sse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
McpEnvironmentCheckResult result = new McpEnvironmentChecker(false).check(json);
|
||||||
|
|
||||||
|
assertEquals(McpCheckStatus.FAILED, result.getOverallStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkUnresolvedInputEnv() {
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"test": {
|
||||||
|
"transport": "stdio",
|
||||||
|
"command": "java",
|
||||||
|
"env": {
|
||||||
|
"API_KEY": "${input:api_key}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
McpEnvironmentCheckResult result = new McpEnvironmentChecker(false).check(json);
|
||||||
|
|
||||||
|
assertEquals(McpCheckStatus.FAILED, result.getOverallStatus());
|
||||||
|
assertFalse(result.getServers().get(0).getChecks().toString().contains("secret"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkInvalidJson() {
|
||||||
|
McpEnvironmentCheckResult result = new McpEnvironmentChecker(false).check("{ invalid json }");
|
||||||
|
|
||||||
|
assertEquals(McpCheckStatus.FAILED, result.getOverallStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closeTransportWhenProbeFailed() {
|
||||||
|
TrackingCloseableTransport closeableTransport = new TrackingCloseableTransport();
|
||||||
|
McpEnvironmentChecker checker = new McpEnvironmentChecker(true, transport -> (spec, resolvedEnv) -> closeableTransport);
|
||||||
|
String json = """
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"test": {
|
||||||
|
"transport": "stdio",
|
||||||
|
"command": "java"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
McpEnvironmentCheckResult result = checker.check(json);
|
||||||
|
|
||||||
|
assertEquals(McpCheckStatus.FAILED, result.getOverallStatus());
|
||||||
|
assertTrue(closeableTransport.closed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class TrackingCloseableTransport implements CloseableTransport {
|
||||||
|
private boolean closed;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public McpClientTransport getTransport() {
|
||||||
|
return new FailingClientTransport();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FailingClientTransport implements McpClientTransport {
|
||||||
|
@Override
|
||||||
|
public Mono<Void> connect(java.util.function.Function<Mono<McpSchema.JSONRPCMessage>,
|
||||||
|
Mono<McpSchema.JSONRPCMessage>> handler) {
|
||||||
|
return Mono.error(new IllegalStateException("probe failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> closeGracefully() {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T unmarshalFrom(Object data, TypeRef<T> typeRef) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.easyagents.mcp.client;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.client.transport.ServerParameters;
|
||||||
|
import io.modelcontextprotocol.client.transport.StdioClientTransport;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stdio MCP transport factory tests.
|
||||||
|
*/
|
||||||
|
public class StdioTransportFactoryTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createWithResolvedEnv() throws Exception {
|
||||||
|
McpConfig.ServerSpec spec = new McpConfig.ServerSpec();
|
||||||
|
spec.setCommand("npx");
|
||||||
|
spec.setArgs(List.of("-y", "test-mcp-server"));
|
||||||
|
|
||||||
|
CloseableTransport closeableTransport = new StdioTransportFactory()
|
||||||
|
.create(spec, Map.of("API_KEY", "resolved-secret"));
|
||||||
|
|
||||||
|
assertTrue(closeableTransport.getTransport() instanceof StdioClientTransport);
|
||||||
|
StdioClientTransport transport = (StdioClientTransport) closeableTransport.getTransport();
|
||||||
|
ServerParameters parameters = extractParameters(transport);
|
||||||
|
|
||||||
|
assertEquals("npx", parameters.getCommand());
|
||||||
|
assertEquals(List.of("-y", "test-mcp-server"), parameters.getArgs());
|
||||||
|
assertEquals("resolved-secret", parameters.getEnv().get("API_KEY"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void createWithNullArgsAndEnv() throws Exception {
|
||||||
|
McpConfig.ServerSpec spec = new McpConfig.ServerSpec();
|
||||||
|
spec.setCommand("python");
|
||||||
|
spec.setArgs(null);
|
||||||
|
|
||||||
|
CloseableTransport closeableTransport = new StdioTransportFactory().create(spec, null);
|
||||||
|
|
||||||
|
assertTrue(closeableTransport.getTransport() instanceof StdioClientTransport);
|
||||||
|
StdioClientTransport transport = (StdioClientTransport) closeableTransport.getTransport();
|
||||||
|
ServerParameters parameters = extractParameters(transport);
|
||||||
|
|
||||||
|
assertEquals("python", parameters.getCommand());
|
||||||
|
assertEquals(List.of(), parameters.getArgs());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServerParameters extractParameters(StdioClientTransport transport) throws Exception {
|
||||||
|
Field paramsField = StdioClientTransport.class.getDeclaredField("params");
|
||||||
|
paramsField.setAccessible(true);
|
||||||
|
return (ServerParameters) paramsField.get(transport);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>com.easyagents</groupId>
|
<groupId>com.easyagents</groupId>
|
||||||
<artifactId>easy-agents-helloworld</artifactId>
|
<artifactId>easy-agents-helloworld</artifactId>
|
||||||
<version>1.0-SNAPSHOT</version>
|
<version>1.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<maven.compiler.release>17</maven.compiler.release>
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.easyagents</groupId>
|
<groupId>com.easyagents</groupId>
|
||||||
<artifactId>easy-agents-bom</artifactId>
|
<artifactId>easy-agents-bom</artifactId>
|
||||||
<version>0.0.1</version>
|
<version>1.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
2
pom.xml
2
pom.xml
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- Easy-Agents Version -->
|
<!-- Easy-Agents Version -->
|
||||||
<revision>0.0.1</revision>
|
<revision>1.0.0</revision>
|
||||||
<maven.compiler.release>17</maven.compiler.release>
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
<maven-flatten.version>1.3.0</maven-flatten.version>
|
<maven-flatten.version>1.3.0</maven-flatten.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
|||||||
Reference in New Issue
Block a user