diff --git a/easy-agents-agent-runtime/pom.xml b/easy-agents-agent-runtime/pom.xml index 37ade97..0857fd0 100644 --- a/easy-agents-agent-runtime/pom.xml +++ b/easy-agents-agent-runtime/pom.xml @@ -23,6 +23,18 @@ agentscope + + com.anthropic + anthropic-java + 2.14.0 + + + + com.google.genai + google-genai + 1.38.0 + + junit junit diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentDefinition.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentDefinition.java new file mode 100644 index 0000000..07e1b32 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentDefinition.java @@ -0,0 +1,269 @@ +package com.easyagents.agent.runtime; + +import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec; +import com.easyagents.agent.runtime.memory.AgentMemoryPolicy; +import com.easyagents.agent.runtime.model.AgentGenerationOptions; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import com.easyagents.agent.runtime.persistence.AgentPersistencePolicy; +import com.easyagents.agent.runtime.skill.AgentSkillBoxSpec; +import com.easyagents.agent.runtime.tool.AgentToolSpec; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 主入口 + * ReAct 智能体的声明式定义 + */ +public class AgentDefinition { + + private String agentId; + private String agentName; + private String description; + private String systemPrompt; + private AgentModelSpec modelSpec; + private AgentGenerationOptions generationOptions = new AgentGenerationOptions(); + private AgentExecutionOptions executionOptions = new AgentExecutionOptions(); + private List toolSpecs = new ArrayList<>(); + private List knowledgeSpecs = new ArrayList<>(); + private AgentMemoryPolicy memoryPolicy = AgentMemoryPolicy.autoContext(); + private AgentPersistencePolicy persistencePolicy = AgentPersistencePolicy.disabled(); + private AgentSkillBoxSpec skillBoxSpec; + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取agent id。 + * + * @return 智能体ID + */ + public String getAgentId() { + return agentId; + } + + /** + * 设置agent id。 + * + * @param agentId 智能体ID + */ + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + /** + * 获取agent name。 + * + * @return 智能体名称 + */ + public String getAgentName() { + return agentName; + } + + /** + * 设置agent name。 + * + * @param agentName 智能体名称 + */ + public void setAgentName(String agentName) { + this.agentName = agentName; + } + + /** + * 获取agent description。 + * + * @return 智能体描述 + */ + public String getDescription() { + return description; + } + + /** + * 设置agent description。 + * + * @param description 智能体描述 + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * 获取system prompt。 + * + * @return 系统提示词 + */ + public String getSystemPrompt() { + return systemPrompt; + } + + /** + * 设置system prompt。 + * + * @param systemPrompt 系统提示词 + */ + public void setSystemPrompt(String systemPrompt) { + this.systemPrompt = systemPrompt; + } + + /** + * 获取模型设置。 + * + * @return 模型设置 + */ + public AgentModelSpec getModelSpec() { + return modelSpec; + } + + /** + * 设置模型设置。 + * + * @param modelSpec 模型设置 + */ + public void setModelSpec(AgentModelSpec modelSpec) { + this.modelSpec = modelSpec; + } + + /** + * 获取生成参数。 + * + * @return 生成参数 + */ + public AgentGenerationOptions getGenerationOptions() { + return generationOptions; + } + + /** + * 设置生成参数。 + * + * @param generationOptions 生成参数 + */ + public void setGenerationOptions(AgentGenerationOptions generationOptions) { + this.generationOptions = generationOptions == null ? new AgentGenerationOptions() : generationOptions; + } + + /** + * 获取执行参数。 + * + * @return 执行参数 + */ + public AgentExecutionOptions getExecutionOptions() { + return executionOptions; + } + + /** + * 设置执行参数。 + * + * @param executionOptions 执行参数 + */ + public void setExecutionOptions(AgentExecutionOptions executionOptions) { + this.executionOptions = executionOptions == null ? new AgentExecutionOptions() : executionOptions; + } + + /** + * 获取工具定义。 + * + * @return 工具定义 + */ + public List getToolSpecs() { + return toolSpecs; + } + + /** + * 设置工具定义。 + * + * @param toolSpecs 工具定义 + */ + public void setToolSpecs(List toolSpecs) { + this.toolSpecs = toolSpecs == null ? new ArrayList<>() : new ArrayList<>(toolSpecs); + } + + /** + * 获取知识库定义。 + * + * @return 知识库定义 + */ + public List getKnowledgeSpecs() { + return knowledgeSpecs; + } + + /** + * 设置知识库定义。 + * + * @param knowledgeSpecs 知识库定义 + */ + public void setKnowledgeSpecs(List knowledgeSpecs) { + this.knowledgeSpecs = knowledgeSpecs == null ? new ArrayList<>() : new ArrayList<>(knowledgeSpecs); + } + + /** + * 获取记忆策略。 + * + * @return 记忆策略 + */ + public AgentMemoryPolicy getMemoryPolicy() { + return memoryPolicy; + } + + /** + * 设置记忆策略。 + * + * @param memoryPolicy 记忆策略 + */ + public void setMemoryPolicy(AgentMemoryPolicy memoryPolicy) { + this.memoryPolicy = memoryPolicy == null ? AgentMemoryPolicy.autoContext() : memoryPolicy; + } + + /** + * 获取持久化策略。 + * + * @return 持久化策略 + */ + public AgentPersistencePolicy getPersistencePolicy() { + return persistencePolicy; + } + + /** + * 设置持久化策略。 + * + * @param persistencePolicy 持久化策略 + */ + public void setPersistencePolicy(AgentPersistencePolicy persistencePolicy) { + this.persistencePolicy = persistencePolicy == null ? AgentPersistencePolicy.disabled() : persistencePolicy; + } + + /** + * 获取SkillBox 设置。 + * + * @return SkillBox 设置 + */ + public AgentSkillBoxSpec getSkillBoxSpec() { + return skillBoxSpec; + } + + /** + * 设置SkillBox 设置。 + * + * @param skillBoxSpec SkillBox 设置 + */ + public void setSkillBoxSpec(AgentSkillBoxSpec skillBoxSpec) { + this.skillBoxSpec = skillBoxSpec; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentExecutionOptions.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentExecutionOptions.java new file mode 100644 index 0000000..72e4b2b --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentExecutionOptions.java @@ -0,0 +1,86 @@ +package com.easyagents.agent.runtime; + +import java.time.Duration; + +/** + * 运行时执行参数。 + */ +public class AgentExecutionOptions { + + private int maxIters = 10; + private Duration timeout = Duration.ofMinutes(5); + private boolean reasoningEnabled = true; + private boolean toolCallingEnabled = true; + + /** + * 获取最大 ReAct 迭代次数。 + * + * @return 最大迭代次数 + */ + public int getMaxIters() { + return maxIters; + } + + /** + * 设置最大 ReAct 迭代次数。 + * + * @param maxIters 最大迭代次数 + */ + public void setMaxIters(int maxIters) { + this.maxIters = maxIters <= 0 ? 10 : maxIters; + } + + /** + * 获取超时时间。 + * + * @return 超时时间 + */ + public Duration getTimeout() { + return timeout; + } + + /** + * 设置超时时间。 + * + * @param timeout 超时时间 + */ + public void setTimeout(Duration timeout) { + this.timeout = timeout == null ? Duration.ofMinutes(5) : timeout; + } + + /** + * 返回是否reasoning events are enabled。 + * + * @return 启用时为 true + */ + public boolean isReasoningEnabled() { + return reasoningEnabled; + } + + /** + * 设置whether reasoning events are enabled。 + * + * @param reasoningEnabled 启用时为 true + */ + public void setReasoningEnabled(boolean reasoningEnabled) { + this.reasoningEnabled = reasoningEnabled; + } + + /** + * 返回是否tool calls are enabled。 + * + * @return 启用时为 true + */ + public boolean isToolCallingEnabled() { + return toolCallingEnabled; + } + + /** + * 设置whether tool calls are enabled。 + * + * @param toolCallingEnabled 启用时为 true + */ + public void setToolCallingEnabled(boolean toolCallingEnabled) { + this.toolCallingEnabled = toolCallingEnabled; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunHandle.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunHandle.java new file mode 100644 index 0000000..b10903e --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunHandle.java @@ -0,0 +1,30 @@ +package com.easyagents.agent.runtime; + +import com.easyagents.agent.runtime.event.AgentRuntimeEvent; +import com.easyagents.agent.runtime.hitl.AgentToolApprovalResponse; +import reactor.core.publisher.Flux; + +/** + * 可取消的单次智能体运行句柄。 + */ +public interface AgentRunHandle { + + /** + * 获取本次运行的事件流。 + * + * @return 事件流 + */ + Flux stream(); + + /** + * 取消本次运行。 + */ + void cancel(); + + /** + * 提交工具审批结果。 + * + * @param response 审批响应 + */ + void submitToolApproval(AgentToolApprovalResponse response); +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunRequest.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunRequest.java new file mode 100644 index 0000000..f0ed6cb --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunRequest.java @@ -0,0 +1,267 @@ +package com.easyagents.agent.runtime; + +import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetriever; +import com.easyagents.agent.runtime.memory.AgentMemorySnapshot; +import com.easyagents.agent.runtime.message.AgentMessage; +import com.easyagents.agent.runtime.persistence.AgentConversationRecorder; +import com.easyagents.agent.runtime.persistence.AgentSessionStore; +import com.easyagents.agent.runtime.persistence.noop.NoopAgentConversationRecorder; +import com.easyagents.agent.runtime.persistence.noop.NoopAgentSessionStore; +import com.easyagents.agent.runtime.tool.AgentToolInvoker; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 一次智能体运行请求。 + */ +public class AgentRunRequest { + + private String requestId; + private String traceId; + private String sessionId; + private AgentDefinition agentDefinition; + private AgentRuntimeContext runtimeContext = new AgentRuntimeContext(); + private AgentMessage userMessage; + private AgentMemorySnapshot memorySnapshot = new AgentMemorySnapshot(); + private Map toolInvokers = new LinkedHashMap<>(); + private Map knowledgeRetrievers = new LinkedHashMap<>(); + private AgentSessionStore sessionStore = NoopAgentSessionStore.INSTANCE; + private AgentConversationRecorder conversationRecorder = NoopAgentConversationRecorder.INSTANCE; + private Map metadata = new LinkedHashMap<>(); + private String cancelReason; + + /** + * 获取请求ID。 + * + * @return 请求ID + */ + public String getRequestId() { + return requestId; + } + + /** + * 设置请求ID。 + * + * @param requestId 请求ID + */ + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + /** + * 获取链路ID。 + * + * @return 链路ID + */ + public String getTraceId() { + return traceId; + } + + /** + * 设置链路ID。 + * + * @param traceId 链路ID + */ + public void setTraceId(String traceId) { + this.traceId = traceId; + } + + /** + * 获取会话ID。 + * + * @return 会话ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * 设置会话ID。 + * + * @param sessionId 会话ID + */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** + * 获取智能体定义。 + * + * @return 智能体定义 + */ + public AgentDefinition getAgentDefinition() { + return agentDefinition; + } + + /** + * 设置智能体定义。 + * + * @param agentDefinition 智能体定义 + */ + public void setAgentDefinition(AgentDefinition agentDefinition) { + this.agentDefinition = agentDefinition; + } + + /** + * 获取运行时上下文。 + * + * @return 运行时上下文 + */ + public AgentRuntimeContext getRuntimeContext() { + return runtimeContext; + } + + /** + * 设置运行时上下文。 + * + * @param runtimeContext 运行时上下文 + */ + public void setRuntimeContext(AgentRuntimeContext runtimeContext) { + this.runtimeContext = runtimeContext == null ? new AgentRuntimeContext() : runtimeContext; + } + + /** + * 获取用户消息。 + * + * @return 用户消息 + */ + public AgentMessage getUserMessage() { + return userMessage; + } + + /** + * 设置用户消息。 + * + * @param userMessage 用户消息 + */ + public void setUserMessage(AgentMessage userMessage) { + this.userMessage = userMessage; + } + + /** + * 获取记忆快照。 + * + * @return 记忆快照 + */ + public AgentMemorySnapshot getMemorySnapshot() { + return memorySnapshot; + } + + /** + * 设置记忆快照。 + * + * @param memorySnapshot 记忆快照 + */ + public void setMemorySnapshot(AgentMemorySnapshot memorySnapshot) { + this.memorySnapshot = memorySnapshot == null ? new AgentMemorySnapshot() : memorySnapshot; + } + + /** + * 获取tool invokers by tool name。 + * + * @return 工具调用器 + */ + public Map getToolInvokers() { + return toolInvokers; + } + + /** + * 设置工具调用器。 + * + * @param toolInvokers 工具调用器 + */ + public void setToolInvokers(Map toolInvokers) { + this.toolInvokers = toolInvokers == null ? new LinkedHashMap<>() : toolInvokers; + } + + /** + * 获取knowledge retrievers by knowledge id。 + * + * @return 知识库检索器 + */ + public Map getKnowledgeRetrievers() { + return knowledgeRetrievers; + } + + /** + * 设置知识库检索器。 + * + * @param knowledgeRetrievers 知识库检索器 + */ + public void setKnowledgeRetrievers(Map knowledgeRetrievers) { + this.knowledgeRetrievers = knowledgeRetrievers == null ? new LinkedHashMap<>() : knowledgeRetrievers; + } + + /** + * 获取会话存储。 + * + * @return 会话存储 + */ + public AgentSessionStore getSessionStore() { + return sessionStore; + } + + /** + * 设置会话存储。 + * + * @param sessionStore 会话存储 + */ + public void setSessionStore(AgentSessionStore sessionStore) { + this.sessionStore = sessionStore == null ? NoopAgentSessionStore.INSTANCE : sessionStore; + } + + /** + * 获取会话记录器。 + * + * @return 会话记录器 + */ + public AgentConversationRecorder getConversationRecorder() { + return conversationRecorder; + } + + /** + * 设置会话记录器。 + * + * @param conversationRecorder 会话记录器 + */ + public void setConversationRecorder(AgentConversationRecorder conversationRecorder) { + this.conversationRecorder = conversationRecorder == null ? NoopAgentConversationRecorder.INSTANCE : conversationRecorder; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } + + /** + * 获取取消原因。 + * + * @return 取消原因 + */ + public String getCancelReason() { + return cancelReason; + } + + /** + * 设置取消原因。 + * + * @param cancelReason 取消原因 + */ + public void setCancelReason(String cancelReason) { + this.cancelReason = cancelReason; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunResult.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunResult.java new file mode 100644 index 0000000..f26a553 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRunResult.java @@ -0,0 +1,49 @@ +package com.easyagents.agent.runtime; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 智能体运行的最终结果。 + */ +public class AgentRunResult { + + private String content; + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取最终内容。 + * + * @return 最终内容 + */ + public String getContent() { + return content; + } + + /** + * 设置最终内容。 + * + * @param content 最终内容 + */ + public void setContent(String content) { + this.content = content; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntime.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntime.java new file mode 100644 index 0000000..d0497ab --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntime.java @@ -0,0 +1,26 @@ +package com.easyagents.agent.runtime; + +import com.easyagents.agent.runtime.event.AgentRuntimeEvent; +import reactor.core.publisher.Flux; + +/** + * 执行声明式 ReAct 智能体并流式输出运行事件。 + */ +public interface AgentRuntime { + + /** + * 启动单次智能体运行并返回可取消句柄。 + * + * @param request 包含定义、输入、记忆和适配器的运行请求 + * @return 本次运行句柄 + */ + AgentRunHandle start(AgentRunRequest request); + + /** + * 启动单次智能体运行。 + * + * @param request 包含定义、输入、记忆和适配器的运行请求 + * @return 本次运行的事件流 + */ + Flux stream(AgentRunRequest request); +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntimeContext.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntimeContext.java new file mode 100644 index 0000000..ea46080 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntimeContext.java @@ -0,0 +1,125 @@ +package com.easyagents.agent.runtime; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 传递给工具和检索器的中立运行时上下文。 + */ +public class AgentRuntimeContext { + + private String tenantId; + private String userId; + private String userName; + private String sessionId; + private String traceId; + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取租户ID。 + * + * @return 租户ID + */ + public String getTenantId() { + return tenantId; + } + + /** + * 设置租户ID。 + * + * @param tenantId 租户ID + */ + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + /** + * 获取用户ID。 + * + * @return 用户ID + */ + public String getUserId() { + return userId; + } + + /** + * 设置用户ID。 + * + * @param userId 用户ID + */ + public void setUserId(String userId) { + this.userId = userId; + } + + /** + * 获取用户名。 + * + * @return 用户名 + */ + public String getUserName() { + return userName; + } + + /** + * 设置用户名。 + * + * @param userName 用户名 + */ + public void setUserName(String userName) { + this.userName = userName; + } + + /** + * 获取会话ID。 + * + * @return 会话ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * 设置会话ID。 + * + * @param sessionId 会话ID + */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** + * 获取链路ID。 + * + * @return 链路ID + */ + public String getTraceId() { + return traceId; + } + + /** + * 设置链路ID。 + * + * @param traceId 链路ID + */ + public void setTraceId(String traceId) { + this.traceId = traceId; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntimeException.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntimeException.java new file mode 100644 index 0000000..9bd90f2 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/AgentRuntimeException.java @@ -0,0 +1,26 @@ +package com.easyagents.agent.runtime; + +/** + * 智能体运行时基础设施抛出的异常。 + */ +public class AgentRuntimeException extends RuntimeException { + + /** + * 创建包含错误消息的异常。 + * + * @param message 错误消息 + */ + public AgentRuntimeException(String message) { + super(message); + } + + /** + * 创建包含错误消息和根因的异常。 + * + * @param message 错误消息 + * @param cause 根因 + */ + public AgentRuntimeException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeEventHook.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeEventHook.java new file mode 100644 index 0000000..4b77656 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeEventHook.java @@ -0,0 +1,28 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.AgentRunRequest; +import io.agentscope.core.hook.Hook; +import io.agentscope.core.hook.HookEvent; +import reactor.core.publisher.Mono; + +/** + * 从 AgentScope Hook 回调发射不在主事件流中的运行时事件。 + */ +public class AgentScopeEventHook implements Hook { + + private final AgentRunRequest request; + + /** + * 创建 Hook。 + * + * @param request 运行请求 + */ + public AgentScopeEventHook(AgentRunRequest request) { + this.request = request; + } + + @Override + public Mono onEvent(T event) { + return Mono.just(event); + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeKnowledgeAdapter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeKnowledgeAdapter.java new file mode 100644 index 0000000..97b2720 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeKnowledgeAdapter.java @@ -0,0 +1,240 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.AgentRunRequest; +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.knowledge.*; +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.rag.Knowledge; +import io.agentscope.core.rag.model.Document; +import io.agentscope.core.rag.model.DocumentMetadata; +import io.agentscope.core.rag.model.RetrieveConfig; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; + +import java.util.*; + +/** + * 将运行时知识库检索器适配为一个聚合 AgentScope Knowledge。 + */ +public class AgentScopeKnowledgeAdapter { + + /** + * 创建聚合 Knowledge。 + * + * @param request 运行请求 + * @return 聚合 Knowledge;未配置知识库时返回 null + */ + public Knowledge createAggregateKnowledge(AgentRunRequest request) { + return createAggregateKnowledge(request, null); + } + + /** + * 创建带事件 sink 的聚合 Knowledge。 + * + * @param request 运行请求 + * @param eventSink 事件 sink + * @return 聚合 Knowledge;未配置知识库时返回 null + */ + public Knowledge createAggregateKnowledge(AgentRunRequest request, Sinks.Many eventSink) { + if (request.getAgentDefinition().getKnowledgeSpecs().isEmpty()) { + return null; + } + return new AggregateKnowledge(request, eventSink); + } + + /** + * 将运行时文档转换为 AgentScope 文档。 + * + * @param documents 运行时文档 + * @return AgentScope 文档 + */ + public List toDocuments(List documents) { + List converted = new ArrayList<>(); + if (documents == null) { + return converted; + } + for (AgentKnowledgeDocument document : documents) { + converted.add(toDocument(document)); + } + return converted; + } + + private Document toDocument(AgentKnowledgeDocument document) { + Map payload = new LinkedHashMap<>(); + payload.put("documentId", document.getDocumentId()); + payload.put("documentName", document.getDocumentName()); + payload.put("chunkId", document.getChunkId()); + payload.put("sourceUri", document.getSourceUri()); + payload.put("knowledgeMetadata", document.getKnowledgeMetadata()); + payload.put("documentMetadata", document.getMetadata()); + payload.putAll(document.getMetadata()); + DocumentMetadata metadata = DocumentMetadata.builder() + .content(TextBlock.builder().text(document.getContent()).build()) + .docId(document.getDocumentId()) + .chunkId(document.getChunkId()) + .payload(payload) + .build(); + Document converted = new Document(metadata); + converted.setScore(document.getScore()); + return converted; + } + + /** + * 将检索调用分发到多个知识源的聚合 Knowledge 实现。 + */ + private class AggregateKnowledge implements Knowledge { + + private final AgentRunRequest request; + private final Sinks.Many eventSink; + + private AggregateKnowledge(AgentRunRequest request, Sinks.Many eventSink) { + this.request = request; + this.eventSink = eventSink; + } + + /** + * 忽略文档新增,因为知识库索引由 EasyFlow 负责。 + * + * @param documents 文档列表 + * @return 完成信号 + */ + @Override + public Mono addDocuments(List documents) { + return Mono.error(new UnsupportedOperationException( + "Easy-Agents agent runtime knowledge does not support addDocuments. Use external knowledge service instead.")); + } + + /** + * 从已配置的知识源检索文档。 + * + * @param query 查询 + * @param config 检索配置 + * @return 文档列表 + */ + @Override + public Mono> retrieve(String query, RetrieveConfig config) { + return Mono.fromCallable(() -> retrieveAll(query, config)); + } + + /** + * 检索并合并所有已配置的知识源。 + * + * @param query 查询 + * @param config 检索配置 + * @return 合并后的文档 + */ + private List retrieveAll(String query, RetrieveConfig config) { + List allDocuments = new ArrayList<>(); + int globalLimit = config == null || config.getLimit() <= 0 ? 5 : config.getLimit(); + double globalThreshold = config == null ? 0D : config.getScoreThreshold(); + for (AgentKnowledgeSpec spec : request.getAgentDefinition().getKnowledgeSpecs()) { + AgentKnowledgeRetriever retriever = request.getKnowledgeRetrievers().get(spec.getKnowledgeId()); + if (retriever == null) { + throw new AgentRuntimeException("Knowledge retriever is required: " + spec.getKnowledgeId()); + } + AgentKnowledgeRetrievalRequest retrievalRequest = new AgentKnowledgeRetrievalRequest(); + retrievalRequest.setQuery(query); + retrievalRequest.setLimit(spec.getLimit()); + retrievalRequest.setScoreThreshold(Math.max(spec.getScoreThreshold(), globalThreshold)); + retrievalRequest.setKnowledgeSpec(spec); + retrievalRequest.setRuntimeContext(request.getRuntimeContext()); + retrievalRequest.getMetadata().put("traceId", request.getTraceId()); + retrievalRequest.getMetadata().put("sessionId", request.getSessionId()); + AgentKnowledgeRetrievalResult result = retriever.retrieve(retrievalRequest); + if (result == null || result.getDocuments() == null) { + emitKnowledgeRetrievalEvent(query, spec, retrievalRequest, new ArrayList<>()); + continue; + } + emitKnowledgeRetrievalEvent(query, spec, retrievalRequest, result.getDocuments()); + for (AgentKnowledgeDocument document : result.getDocuments()) { + preserveKnowledgeMetadata(spec, document); + allDocuments.add(document); + } + } + allDocuments.sort(Comparator.comparing( + AgentKnowledgeDocument::getScore, + Comparator.nullsLast(Comparator.reverseOrder()) + )); + if (allDocuments.size() > globalLimit) { + allDocuments = new ArrayList<>(allDocuments.subList(0, globalLimit)); + } + return toDocuments(allDocuments); + } + + /** + * 在单条文档上保留知识库级元数据。 + * + * @param spec 知识库声明 + * @param document 文档 + */ + private void preserveKnowledgeMetadata(AgentKnowledgeSpec spec, AgentKnowledgeDocument document) { + Map knowledgeMetadata = new LinkedHashMap<>(spec.getMetadata()); + knowledgeMetadata.put("knowledgeId", spec.getKnowledgeId()); + knowledgeMetadata.put("knowledgeName", spec.getName()); + knowledgeMetadata.put("retrievalMode", spec.getRetrievalMode().name()); + knowledgeMetadata.putAll(document.getKnowledgeMetadata()); + document.setKnowledgeMetadata(knowledgeMetadata); + document.getMetadata().putIfAbsent("knowledgeId", spec.getKnowledgeId()); + document.getMetadata().putIfAbsent("knowledgeName", spec.getName()); + } + + /** + * 发射知识库检索事件,供聊天界面展示检索过程。 + * + * @param query 查询 + * @param spec 知识库声明 + * @param retrievalRequest 检索请求 + * @param documents 检索文档 + */ + private void emitKnowledgeRetrievalEvent(String query, + AgentKnowledgeSpec spec, + AgentKnowledgeRetrievalRequest retrievalRequest, + List documents) { + if (eventSink == null) { + return; + } + AgentRuntimeEvent event = AgentRuntimeEvent.of(AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL); + event.setTraceId(request.getTraceId()); + event.setSessionId(request.getSessionId()); + event.setAgentId(request.getAgentDefinition().getAgentId()); + event.getMetadata().put("requestId", request.getRequestId()); + event.getPayload().put("query", query); + event.getPayload().put("knowledgeId", spec.getKnowledgeId()); + event.getPayload().put("knowledgeName", spec.getName()); + event.getPayload().put("limit", retrievalRequest.getLimit()); + event.getPayload().put("scoreThreshold", retrievalRequest.getScoreThreshold()); + event.getPayload().put("documentCount", documents == null ? 0 : documents.size()); + event.getPayload().put("documents", documentSummaries(documents)); + Sinks.EmitResult result = eventSink.tryEmitNext(event); + if (result.isFailure()) { + throw new AgentRuntimeException("Failed to emit knowledge retrieval event: " + result); + } + } + + /** + * 构建用于事件展示的文档摘要,避免把全文内容放入事件。 + * + * @param documents 检索文档 + * @return 文档摘要列表 + */ + private List> documentSummaries(List documents) { + List> summaries = new ArrayList<>(); + if (documents == null) { + return summaries; + } + for (AgentKnowledgeDocument document : documents) { + Map summary = new LinkedHashMap<>(); + summary.put("documentId", document.getDocumentId()); + summary.put("documentName", document.getDocumentName()); + summary.put("chunkId", document.getChunkId()); + summary.put("score", document.getScore()); + summary.put("sourceUri", document.getSourceUri()); + summary.put("metadata", document.getMetadata()); + summaries.add(summary); + } + return summaries; + } + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeMemoryAdapter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeMemoryAdapter.java new file mode 100644 index 0000000..6563d9d --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeMemoryAdapter.java @@ -0,0 +1,73 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.memory.AgentMemoryCompressionParameter; +import com.easyagents.agent.runtime.memory.AgentMemoryPolicy; +import com.easyagents.agent.runtime.memory.AgentMemorySnapshot; +import com.easyagents.agent.runtime.message.AgentMessage; +import io.agentscope.core.memory.InMemoryMemory; +import io.agentscope.core.memory.Memory; +import io.agentscope.core.memory.autocontext.AutoContextConfig; +import io.agentscope.core.memory.autocontext.AutoContextMemory; +import io.agentscope.core.model.Model; + +import java.util.List; + +/** + * 将运行时记忆设置适配为 AgentScope 记忆。 + */ +public class AgentScopeMemoryAdapter { + + private final AgentScopeMessageAdapter messageAdapter = new AgentScopeMessageAdapter(); + + /** + * 创建记忆并附加恢复后的历史消息。 + * + * @param snapshot 记忆快照 + * @param policy 记忆策略 + * @param model 用于自动压缩的模型 + * @return AgentScope 记忆 + */ + public Memory createMemory(AgentMemorySnapshot snapshot, AgentMemoryPolicy policy, Model model) { + AgentMemoryPolicy safePolicy = policy == null ? AgentMemoryPolicy.autoContext() : policy; + Memory memory; + if (safePolicy.getType() == com.easyagents.agent.runtime.memory.AgentMemoryType.AUTO_CONTEXT + && safePolicy.getCompressionParameter().isEnabled()) { + memory = new AutoContextMemory(toAutoContextConfig(safePolicy.getCompressionParameter()), model); + } else { + memory = new InMemoryMemory(); + } + attachMessages(memory, snapshot, safePolicy.getMaxAttachedMessageCount()); + return memory; + } + + /** + * 将自动压缩参数转换为 AgentScope 配置。 + * + * @param parameter 自动压缩参数 + * @return AgentScope 配置 + */ + public AutoContextConfig toAutoContextConfig(AgentMemoryCompressionParameter parameter) { + AgentMemoryCompressionParameter safeParameter = parameter == null ? new AgentMemoryCompressionParameter() : parameter; + return AutoContextConfig.builder() + .msgThreshold(safeParameter.getMsgThreshold()) + .lastKeep(safeParameter.getLastKeep()) + .tokenRatio(safeParameter.getTokenRatio()) + .maxToken(safeParameter.getMaxToken()) + .largePayloadThreshold(safeParameter.getLargePayloadThreshold()) + .minCompressionTokenThreshold(safeParameter.getMinCompressionTokenThreshold()) + .currentRoundCompressionRatio(safeParameter.getCurrentRoundCompressionRatio()) + .minConsecutiveToolMessages(safeParameter.getMinConsecutiveToolMessages()) + .build(); + } + + private void attachMessages(Memory memory, AgentMemorySnapshot snapshot, int maxAttachedMessageCount) { + if (snapshot == null || snapshot.getMessages().isEmpty()) { + return; + } + List messages = snapshot.getMessages(); + int fromIndex = Math.max(0, messages.size() - maxAttachedMessageCount); + for (AgentMessage message : messages.subList(fromIndex, messages.size())) { + memory.addMessage(messageAdapter.toMsg(message)); + } + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeMessageAdapter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeMessageAdapter.java new file mode 100644 index 0000000..71672eb --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeMessageAdapter.java @@ -0,0 +1,345 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.message.*; +import io.agentscope.core.message.*; + +import java.time.Instant; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 运行时消息与 AgentScope 消息的双向适配器。 + */ +public class AgentScopeMessageAdapter { + + /** + * 将运行时消息转换为 AgentScope 消息。 + * + * @param message 运行时消息 + * @return AgentScope 消息 + */ + public Msg toMsg(AgentMessage message) { + if (message == null) { + return null; + } + return Msg.builder() + .id(message.getMessageId()) + .name(message.getName()) + .role(toRole(message.getRole())) + .content(toContentBlocks(message.getContentBlocks())) + .metadata(copyMap(message.getMetadata())) + .timestamp(message.getCreatedAt() == null ? null : message.getCreatedAt().toString()) + .build(); + } + + /** + * 将 AgentScope 消息转换为运行时消息。 + * + * @param msg AgentScope 消息 + * @return 运行时消息 + */ + public AgentMessage toAgentMessage(Msg msg) { + if (msg == null) { + return null; + } + AgentMessage message = new AgentMessage(); + message.setMessageId(msg.getId()); + message.setName(msg.getName()); + message.setRole(toRole(msg.getRole())); + message.setMetadata(msg.getMetadata()); + if (msg.getTimestamp() != null && !msg.getTimestamp().isBlank()) { + Instant parsed = parseTimestamp(msg.getTimestamp()); + if (parsed != null) { + message.setCreatedAt(parsed); + } else { + message.getMetadata().put("rawTimestamp", msg.getTimestamp()); + } + } + message.setContentBlocks(toBlocks(msg.getContent())); + return message; + } + + /** + * 将 AgentScope 块转换为运行时块。 + * + * @param block AgentScope 块 + * @return 运行时块 + */ + public AgentContentBlock toAgentBlock(ContentBlock block) { + if (block == null) { + return null; + } + if (block instanceof TextBlock textBlock) { + return new AgentTextBlock(textBlock.getText()); + } + if (block instanceof ThinkingBlock thinkingBlock) { + AgentThinkingBlock converted = new AgentThinkingBlock(thinkingBlock.getThinking()); + converted.getMetadata().putAll(copyMap(thinkingBlock.getMetadata())); + return converted; + } + if (block instanceof ToolUseBlock toolUseBlock) { + AgentToolUseBlock converted = new AgentToolUseBlock(toolUseBlock.getId(), toolUseBlock.getName(), copyMap(toolUseBlock.getInput())); + converted.setContent(toolUseBlock.getContent()); + converted.getMetadata().putAll(copyMap(toolUseBlock.getMetadata())); + return converted; + } + if (block instanceof ToolResultBlock toolResultBlock) { + AgentToolResultBlock converted = new AgentToolResultBlock(toolResultBlock.getId(), toolResultBlock.getName()); + converted.setOutput(toBlocks(toolResultBlock.getOutput())); + converted.setSuspended(toolResultBlock.isSuspended()); + converted.getMetadata().putAll(copyMap(toolResultBlock.getMetadata())); + return converted; + } + if (block instanceof ImageBlock imageBlock) { + return toMediaBlock(imageBlock, "image"); + } + if (block instanceof AudioBlock audioBlock) { + return toMediaBlock(audioBlock, "audio"); + } + if (block instanceof VideoBlock videoBlock) { + return toMediaBlock(videoBlock, "video"); + } + AgentUnknownBlock converted = new AgentUnknownBlock(); + converted.setSourceClassName(block.getClass().getName()); + converted.setSourceTypeName(resolveTypeName(block)); + converted.setRawText(block.toString()); + converted.getRawMetadata().put("className", block.getClass().getName()); + return converted; + } + + /** + * 将运行时块转换为 AgentScope 块。 + * + * @param block 运行时块 + * @return AgentScope 块 + */ + public ContentBlock toContentBlock(AgentContentBlock block) { + if (block == null) { + return null; + } + if (block instanceof AgentTextBlock textBlock) { + return TextBlock.builder().text(textBlock.getText()).build(); + } + if (block instanceof AgentThinkingBlock thinkingBlock) { + return ThinkingBlock.builder() + .thinking(thinkingBlock.getThinking()) + .metadata(copyMap(thinkingBlock.getMetadata())) + .build(); + } + if (block instanceof AgentToolUseBlock toolUseBlock) { + return ToolUseBlock.builder() + .id(toolUseBlock.getId()) + .name(toolUseBlock.getName()) + .input(copyMap(toolUseBlock.getInput())) + .content(toolUseBlock.getContent()) + .metadata(copyMap(toolUseBlock.getMetadata())) + .build(); + } + if (block instanceof AgentToolResultBlock toolResultBlock) { + Map metadata = copyMap(toolResultBlock.getMetadata()); + metadata.put("suspended", toolResultBlock.isSuspended()); + return ToolResultBlock.of( + toolResultBlock.getId(), + toolResultBlock.getName(), + toContentBlocks(toolResultBlock.getOutput()), + metadata); + } + if (block instanceof AgentMediaBlock mediaBlock) { + return toMediaBlock(mediaBlock); + } + if (block instanceof AgentUnknownBlock unknownBlock) { + return toUnknownContentBlock(unknownBlock); + } + return null; + } + + /** + * 将 AgentScope 块列表转换为运行时块列表。 + * + * @param blocks AgentScope 块列表 + * @return 运行时块列表 + */ + public List toBlocks(List blocks) { + List converted = new ArrayList<>(); + if (blocks == null) { + return converted; + } + for (ContentBlock block : blocks) { + AgentContentBlock agentBlock = toAgentBlock(block); + if (agentBlock != null) { + converted.add(agentBlock); + } + } + return converted; + } + + /** + * 将运行时块列表转换为 AgentScope 块列表。 + * + * @param blocks 运行时块列表 + * @return AgentScope 块列表 + */ + public List toContentBlocks(List blocks) { + List converted = new ArrayList<>(); + if (blocks == null) { + return converted; + } + for (AgentContentBlock block : blocks) { + ContentBlock contentBlock = toContentBlock(block); + if (contentBlock != null) { + converted.add(contentBlock); + } + } + return converted; + } + + private MsgRole toRole(AgentMessageRole role) { + if (role == AgentMessageRole.ASSISTANT) { + return MsgRole.ASSISTANT; + } + if (role == AgentMessageRole.SYSTEM) { + return MsgRole.SYSTEM; + } + if (role == AgentMessageRole.TOOL) { + return MsgRole.TOOL; + } + return MsgRole.USER; + } + + private AgentMessageRole toRole(MsgRole role) { + if (role == MsgRole.ASSISTANT) { + return AgentMessageRole.ASSISTANT; + } + if (role == MsgRole.SYSTEM) { + return AgentMessageRole.SYSTEM; + } + if (role == MsgRole.TOOL) { + return AgentMessageRole.TOOL; + } + return AgentMessageRole.USER; + } + + private Map copyMap(Map metadata) { + return metadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(metadata); + } + + private ContentBlock toMediaBlock(AgentMediaBlock block) { + if (block == null) { + return null; + } + if ("audio".equalsIgnoreCase(block.getMediaKind())) { + return AudioBlock.builder().source(mediaSource(block)).build(); + } + if ("video".equalsIgnoreCase(block.getMediaKind())) { + return VideoBlock.builder() + .source(mediaSource(block)) + .fps(block.getFps()) + .maxFrames(block.getMaxFrames()) + .minPixels(block.getMinPixels()) + .maxPixels(block.getMaxPixels()) + .totalPixels(block.getTotalPixels()) + .build(); + } + return ImageBlock.builder() + .source(mediaSource(block)) + .minPixels(block.getMinPixels()) + .maxPixels(block.getMaxPixels()) + .build(); + } + + private AgentContentBlock toMediaBlock(ContentBlock block, String mediaKind) { + AgentMediaBlock converted = new AgentMediaBlock(mediaKind); + if (block instanceof ImageBlock imageBlock) { + readSource(imageBlock.getSource(), converted); + converted.setMinPixels(imageBlock.getMinPixels()); + converted.setMaxPixels(imageBlock.getMaxPixels()); + } else if (block instanceof AudioBlock audioBlock) { + readSource(audioBlock.getSource(), converted); + } else if (block instanceof VideoBlock videoBlock) { + readSource(videoBlock.getSource(), converted); + converted.setFps(videoBlock.getFps()); + converted.setMaxFrames(videoBlock.getMaxFrames()); + converted.setMinPixels(videoBlock.getMinPixels()); + converted.setMaxPixels(videoBlock.getMaxPixels()); + converted.setTotalPixels(videoBlock.getTotalPixels()); + } + return converted; + } + + private io.agentscope.core.message.Source mediaSource(AgentMediaBlock block) { + if (block.getData() != null && !block.getData().isBlank()) { + return Base64Source.builder() + .mediaType(block.getMimeType()) + .data(block.getData()) + .build(); + } + return URLSource.builder().url(block.getUrl()).build(); + } + + private void readSource(io.agentscope.core.message.Source source, AgentMediaBlock block) { + if (source instanceof Base64Source base64Source) { + block.setMimeType(base64Source.getMediaType()); + block.setData(base64Source.getData()); + return; + } + if (source instanceof URLSource urlSource) { + block.setUrl(urlSource.getUrl()); + } + } + + private ContentBlock toUnknownContentBlock(AgentUnknownBlock block) { + String text = block.getRawText(); + if (text == null || text.isBlank()) { + text = "[unsupported content block: " + block.getSourceTypeName() + "]"; + } + return TextBlock.builder() + .text(text) + .build(); + } + + private Instant parseTimestamp(String timestamp) { + try { + return Instant.parse(timestamp); + } catch (Exception ignored) { + // 继续尝试其他格式。 + } + try { + return OffsetDateTime.parse(timestamp).toInstant(); + } catch (Exception ignored) { + // 继续尝试其他格式。 + } + try { + return Instant.ofEpochMilli(Long.parseLong(timestamp)); + } catch (Exception ignored) { + return null; + } + } + + private String resolveTypeName(ContentBlock block) { + if (block instanceof TextBlock) { + return "text"; + } + if (block instanceof ThinkingBlock) { + return "thinking"; + } + if (block instanceof ToolUseBlock) { + return "tool_use"; + } + if (block instanceof ToolResultBlock) { + return "tool_result"; + } + if (block instanceof ImageBlock) { + return "image"; + } + if (block instanceof AudioBlock) { + return "audio"; + } + if (block instanceof VideoBlock) { + return "video"; + } + return block.getClass().getSimpleName(); + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeModelFactory.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeModelFactory.java new file mode 100644 index 0000000..d408b31 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeModelFactory.java @@ -0,0 +1,257 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.AgentRuntimeException; +import com.easyagents.agent.runtime.model.AgentGenerationOptions; +import com.easyagents.agent.runtime.model.AgentModelFactory; +import com.easyagents.agent.runtime.model.AgentModelProviderType; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import io.agentscope.core.formatter.openai.DeepSeekFormatter; +import io.agentscope.core.formatter.openai.GLMFormatter; +import io.agentscope.core.model.*; +import io.agentscope.core.model.ollama.OllamaOptions; + +import java.util.Map; + +/** + * 根据运行时模型声明创建 AgentScope 模型实例。 + */ +public class AgentScopeModelFactory implements AgentModelFactory { + + private static final String DEEPSEEK_BASE_URL = "https://api.deepseek.com"; + private static final String GLM_BASE_URL = "https://open.bigmodel.cn/api/paas/v4"; + private static final String MINIMAX_BASE_URL = "https://api.minimax.io/v1"; + private static final String MOONSHOT_BASE_URL = "https://api.moonshot.cn/v1"; + private static final String ARK_BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"; + private static final String SILICONFLOW_BASE_URL = "https://api.siliconflow.cn/v1"; + + @Override + public Model create(AgentModelSpec modelSpec, AgentGenerationOptions generationOptions) { + if (modelSpec == null) { + throw new AgentRuntimeException("Agent model spec is required."); + } + GenerateOptions options = toGenerateOptions(modelSpec, generationOptions); + AgentModelProviderType providerType = modelSpec.getProviderType(); + if (providerType == AgentModelProviderType.OLLAMA) { + return buildOllama(modelSpec, options); + } + if (providerType == AgentModelProviderType.OPENAI) { + return buildOpenAi(modelSpec, options); + } + if (providerType == AgentModelProviderType.ANTHROPIC) { + return buildAnthropic(modelSpec, options); + } + if (providerType == AgentModelProviderType.GEMINI) { + return buildGemini(modelSpec, options); + } + if (providerType == AgentModelProviderType.DEEPSEEK) { + return buildDeepSeek(modelSpec, options); + } + if (providerType == AgentModelProviderType.GLM) { + return buildGlm(modelSpec, options); + } + if (providerType == AgentModelProviderType.MINIMAX) { + return buildOpenAiCompatible(modelSpec, options, MINIMAX_BASE_URL); + } + if (providerType == AgentModelProviderType.MOONSHOT) { + return buildOpenAiCompatible(modelSpec, options, MOONSHOT_BASE_URL); + } + if (providerType == AgentModelProviderType.ARK) { + return buildOpenAiCompatible(modelSpec, options, ARK_BASE_URL); + } + if (providerType == AgentModelProviderType.SILICONFLOW) { + return buildOpenAiCompatible(modelSpec, options, SILICONFLOW_BASE_URL); + } + if (providerType == AgentModelProviderType.DASHSCOPE) { + return buildDashScope(modelSpec, generationOptions, options); + } + if (providerType == AgentModelProviderType.OPENAI_COMPATIBLE || providerType == AgentModelProviderType.CUSTOM) { + return buildOpenAiCompatible(modelSpec, options, null); + } + throw new AgentRuntimeException("Unsupported agent model provider: " + providerType); + } + + /** + * 将中立参数转换为 AgentScope 参数。 + * + * @param modelSpec 模型声明 + * @param options 中立参数 + * @return AgentScope 参数 + */ + public GenerateOptions toGenerateOptions(AgentModelSpec modelSpec, AgentGenerationOptions options) { + AgentGenerationOptions safeOptions = options == null ? new AgentGenerationOptions() : options; + GenerateOptions.Builder builder = GenerateOptions.builder() + .apiKey(modelSpec.getApiKey()) + .baseUrl(modelSpec.getBaseUrl()) + .endpointPath(modelSpec.getEndpointPath()) + .modelName(modelSpec.getModelName()) + .stream(safeOptions.getStream()) + .temperature(safeOptions.getTemperature()) + .topP(safeOptions.getTopP()) + .topK(safeOptions.getTopK()) + .maxTokens(safeOptions.getMaxTokens()) + .maxCompletionTokens(safeOptions.getMaxCompletionTokens()) + .thinkingBudget(safeOptions.getThinkingBudget()) + .reasoningEffort(safeOptions.getReasoningEffort()); + for (Map.Entry entry : safeOptions.getAdditionalBodyParams().entrySet()) { + builder.additionalBodyParam(entry.getKey(), entry.getValue()); + } + for (Map.Entry entry : safeOptions.getAdditionalHeaders().entrySet()) { + builder.additionalHeader(entry.getKey(), entry.getValue()); + } + for (Map.Entry entry : safeOptions.getAdditionalQueryParams().entrySet()) { + builder.additionalQueryParam(entry.getKey(), entry.getValue()); + } + return builder.build(); + } + + /** + * 构建 OpenAI 原生模型。 + * + * @param modelSpec 模型声明 + * @param options 生成参数 + * @return 模型 + */ + private Model buildOpenAi(AgentModelSpec modelSpec, GenerateOptions options) { + return buildOpenAiCompatible(modelSpec, options, null); + } + + /** + * 通过 OpenAI-compatible 协议构建模型。 + * + * @param modelSpec 模型声明 + * @param options 生成参数 + * @param defaultBaseUrl 默认基础 URL + * @return 模型 + */ + private Model buildOpenAiCompatible(AgentModelSpec modelSpec, GenerateOptions options, String defaultBaseUrl) { + OpenAIChatModel.Builder builder = OpenAIChatModel.builder() + .apiKey(modelSpec.getApiKey()) + .modelName(modelSpec.getModelName()) + .baseUrl(resolveBaseUrl(modelSpec, defaultBaseUrl)) + .endpointPath(modelSpec.getEndpointPath()) + .stream(Boolean.TRUE.equals(options.getStream())) + .generateOptions(options); + return builder.build(); + } + + /** + * 构建 Anthropic 模型。 + * + * @param modelSpec 模型声明 + * @param options 生成参数 + * @return 模型 + */ + private Model buildAnthropic(AgentModelSpec modelSpec, GenerateOptions options) { + return AnthropicChatModel.builder() + .apiKey(modelSpec.getApiKey()) + .modelName(modelSpec.getModelName()) + .baseUrl(modelSpec.getBaseUrl()) + .stream(Boolean.TRUE.equals(options.getStream())) + .defaultOptions(options) + .build(); + } + + /** + * 构建 Gemini 模型。 + * + * @param modelSpec 模型声明 + * @param options 生成参数 + * @return 模型 + */ + private Model buildGemini(AgentModelSpec modelSpec, GenerateOptions options) { + return GeminiChatModel.builder() + .apiKey(modelSpec.getApiKey()) + .modelName(modelSpec.getModelName()) + .baseUrl(modelSpec.getBaseUrl()) + .streamEnabled(Boolean.TRUE.equals(options.getStream())) + .defaultOptions(options) + .build(); + } + + /** + * 通过 OpenAI-compatible 协议构建 DeepSeek 模型。 + * + * @param modelSpec 模型声明 + * @param options 生成参数 + * @return 模型 + */ + private Model buildDeepSeek(AgentModelSpec modelSpec, GenerateOptions options) { + OpenAIChatModel.Builder builder = OpenAIChatModel.builder() + .apiKey(modelSpec.getApiKey()) + .modelName(modelSpec.getModelName()) + .baseUrl(resolveBaseUrl(modelSpec, DEEPSEEK_BASE_URL)) + .endpointPath(modelSpec.getEndpointPath()) + .stream(Boolean.TRUE.equals(options.getStream())) + .formatter(new DeepSeekFormatter()) + .generateOptions(options); + return builder.build(); + } + + /** + * 通过 OpenAI-compatible 协议构建智谱 GLM 模型。 + * + * @param modelSpec 模型声明 + * @param options 生成参数 + * @return 模型 + */ + private Model buildGlm(AgentModelSpec modelSpec, GenerateOptions options) { + OpenAIChatModel.Builder builder = OpenAIChatModel.builder() + .apiKey(modelSpec.getApiKey()) + .modelName(modelSpec.getModelName()) + .baseUrl(resolveBaseUrl(modelSpec, GLM_BASE_URL)) + .endpointPath(modelSpec.getEndpointPath()) + .stream(Boolean.TRUE.equals(options.getStream())) + .formatter(new GLMFormatter()) + .generateOptions(options); + return builder.build(); + } + + /** + * 构建 Ollama 模型。 + * + * @param modelSpec 模型声明 + * @param options 生成参数 + * @return 模型 + */ + private Model buildOllama(AgentModelSpec modelSpec, GenerateOptions options) { + return OllamaChatModel.builder() + .modelName(modelSpec.getModelName()) + .baseUrl(modelSpec.getBaseUrl()) + .defaultOptions(OllamaOptions.fromGenerateOptions(options)) + .build(); + } + + /** + * 构建 DashScope 模型。 + * + * @param modelSpec 模型声明 + * @param generationOptions 中立生成参数 + * @param options AgentScope 生成参数 + * @return 模型 + */ + private Model buildDashScope(AgentModelSpec modelSpec, AgentGenerationOptions generationOptions, GenerateOptions options) { + Boolean thinkingEnabled = generationOptions == null ? null : generationOptions.getThinkingEnabled(); + return DashScopeChatModel.builder() + .apiKey(modelSpec.getApiKey()) + .modelName(modelSpec.getModelName()) + .baseUrl(modelSpec.getBaseUrl()) + .stream(Boolean.TRUE.equals(options.getStream())) + .enableThinking(thinkingEnabled) + .defaultOptions(options) + .build(); + } + + /** + * 优先使用调用方传入的基础 URL,未传入时使用供应商默认地址。 + * + * @param modelSpec 模型声明 + * @param defaultBaseUrl 默认基础 URL + * @return 最终基础 URL + */ + private String resolveBaseUrl(AgentModelSpec modelSpec, String defaultBaseUrl) { + if (modelSpec.getBaseUrl() == null || modelSpec.getBaseUrl().isBlank()) { + return defaultBaseUrl; + } + return modelSpec.getBaseUrl(); + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeReActRuntime.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeReActRuntime.java new file mode 100644 index 0000000..d07d425 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeReActRuntime.java @@ -0,0 +1,732 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.*; +import com.easyagents.agent.runtime.event.AgentRuntimeEvent; +import com.easyagents.agent.runtime.event.AgentRuntimeEventType; +import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator; +import com.easyagents.agent.runtime.hitl.AgentToolApprovalRejectedException; +import com.easyagents.agent.runtime.hitl.AgentToolApprovalResponse; +import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec; +import com.easyagents.agent.runtime.message.AgentKnowledgeReference; +import com.easyagents.agent.runtime.message.AgentMessage; +import com.easyagents.agent.runtime.skill.AgentSkillBinding; +import com.easyagents.agent.runtime.skill.AgentSkillLoadCall; +import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext; +import com.easyagents.agent.runtime.tool.AgentToolInvoker; +import com.easyagents.agent.runtime.tool.AgentToolSpec; +import io.agentscope.core.ReActAgent; +import io.agentscope.core.agent.Event; +import io.agentscope.core.agent.EventType; +import io.agentscope.core.agent.StreamOptions; +import io.agentscope.core.memory.Memory; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.ToolResultBlock; +import io.agentscope.core.message.ToolUseBlock; +import io.agentscope.core.model.Model; +import io.agentscope.core.rag.Knowledge; +import io.agentscope.core.rag.RAGMode; +import io.agentscope.core.rag.model.RetrieveConfig; +import io.agentscope.core.session.Session; +import io.agentscope.core.skill.SkillBox; +import io.agentscope.core.tool.AgentTool; +import io.agentscope.core.tool.Toolkit; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Sinks; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +/** + * 基于 AgentScope 的 ReAct 运行时。 + */ +public class AgentScopeReActRuntime implements AgentRuntime { + + private final AgentScopeModelFactory modelFactory; + private final AgentScopeToolAdapter toolAdapter; + private final AgentScopeKnowledgeAdapter knowledgeAdapter; + private final AgentScopeMemoryAdapter memoryAdapter; + private final AgentScopeSkillAdapter skillAdapter; + private final AgentScopeMessageAdapter messageAdapter; + + /** + * 使用默认适配器创建运行时。 + */ + public AgentScopeReActRuntime() { + this(new AgentScopeModelFactory(), new AgentScopeToolAdapter(), new AgentScopeKnowledgeAdapter(), + new AgentScopeMemoryAdapter(), new AgentScopeSkillAdapter(), new AgentScopeMessageAdapter()); + } + + /** + * 使用自定义适配器创建运行时。 + * + * @param modelFactory 模型工厂 + * @param toolAdapter 工具适配器 + * @param knowledgeAdapter 知识库适配器 + * @param memoryAdapter 记忆适配器 + * @param skillAdapter Skill 适配器 + */ + public AgentScopeReActRuntime(AgentScopeModelFactory modelFactory, + AgentScopeToolAdapter toolAdapter, + AgentScopeKnowledgeAdapter knowledgeAdapter, + AgentScopeMemoryAdapter memoryAdapter, + AgentScopeSkillAdapter skillAdapter, + AgentScopeMessageAdapter messageAdapter) { + this.modelFactory = modelFactory; + this.toolAdapter = toolAdapter; + this.knowledgeAdapter = knowledgeAdapter; + this.memoryAdapter = memoryAdapter; + this.skillAdapter = skillAdapter; + this.messageAdapter = messageAdapter; + } + + @Override + public Flux stream(AgentRunRequest request) { + return Flux.defer(() -> { + try { + return start(request).stream(); + } catch (Throwable error) { + return Flux.just(failed(request, error)); + } + }); + } + + @Override + public AgentRunHandle start(AgentRunRequest request) { + validate(request); + AgentToolApprovalCoordinator approvalCoordinator = AgentToolApprovalCoordinator.enabled(); + Sinks.Many sideEvents = Sinks.many().unicast().onBackpressureBuffer(); + AgentSkillRuntimeContext skillContext = AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec()); + StringBuilder finalText = new StringBuilder(); + AtomicReference finalMessage = new AtomicReference<>(); + Map knowledgeReferences = new LinkedHashMap<>(); + AtomicBoolean cancelled = new AtomicBoolean(false); + ReActAgent agent = buildAgent(request, sideEvents, approvalCoordinator, skillContext); + loadSessionIfNeeded(request, agent); + List input = buildInput(request); + StreamOptions streamOptions = StreamOptions.builder() + .eventTypes(EventType.ALL) + .incremental(true) + .includeReasoningChunk(request.getAgentDefinition().getExecutionOptions().isReasoningEnabled()) + .includeReasoningResult(true) + .includeActingChunk(true) + .includeSummaryChunk(true) + .includeSummaryResult(true) + .build(); + Flux mappedAgentEvents = agent.stream(input, streamOptions) + .timeout(request.getAgentDefinition().getExecutionOptions().getTimeout()) + .flatMapIterable(event -> mapEvent(request, event, skillContext)) + .doOnNext(event -> updateFinalText(finalText, event)) + .doOnNext(event -> updateFinalMessage(finalMessage, event)) + .doOnNext(event -> updateKnowledgeReferences(knowledgeReferences, event)) + .doOnComplete(() -> saveSessionIfNeeded(request, agent)) + .doFinally(signalType -> sideEvents.tryEmitComplete()) + .concatWith(Flux.defer(() -> Flux.just(completed(request, finalText.toString(), finalMessage.get(), knowledgeReferences)))); + Flux stream = Flux.concat(Flux.just(started(request)), Flux.merge(sideEvents.asFlux(), mappedAgentEvents)) + .doOnNext(event -> request.getConversationRecorder().record(request, event)) + .onErrorResume(error -> { + if (error instanceof AgentToolApprovalRejectedException approvalRejectedException) { + // 工具执行被拒绝 + request.setCancelReason(approvalRejectedException.getRejectReason()); + saveSessionIfNeeded(request, agent); + if (cancelled.compareAndSet(false, true)) { + return Flux.just(cancelled(request)) + .doOnNext(event -> request.getConversationRecorder().record(request, event)); + } + return Flux.empty(); + } + return Flux.just(failed(request, error)) + .doOnNext(event -> request.getConversationRecorder().record(request, event)); + }); + return new AgentRunHandle() { + private final AtomicBoolean cancelled = new AtomicBoolean(false); + + @Override + public Flux stream() { + return stream.doOnCancel(() -> cancelInternal(agent, approvalCoordinator, sideEvents, request, cancelled)); + } + + @Override + public void cancel() { + cancelInternal(agent, approvalCoordinator, sideEvents, request, cancelled); + } + + @Override + public void submitToolApproval(AgentToolApprovalResponse response) { + approvalCoordinator.submit(response); + } + }; + } + + /** + * 为单次请求构建 ReActAgent。 + * + * @param request 运行请求 + * @param sideEvents 旁路事件 sink + * @return ReActAgent 实例 + */ + public ReActAgent buildAgent(AgentRunRequest request, Sinks.Many sideEvents) { + return buildAgent(request, sideEvents, AgentToolApprovalCoordinator.disabled()); + } + + /** + * 为单次请求构建 ReActAgent。 + * + * @param request 运行请求 + * @param sideEvents 旁路事件 sink + * @param approvalCoordinator 审批协调器 + * @return ReActAgent 实例 + */ + public ReActAgent buildAgent(AgentRunRequest request, + Sinks.Many sideEvents, + AgentToolApprovalCoordinator approvalCoordinator) { + return buildAgent(request, sideEvents, approvalCoordinator, + AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec())); + } + + /** + * 为单次请求构建 ReActAgent。 + * + * @param request 运行请求 + * @param sideEvents 旁路事件 sink + * @param approvalCoordinator 审批协调器 + * @param skillContext Skill 运行时上下文 + * @return ReActAgent 实例 + */ + public ReActAgent buildAgent(AgentRunRequest request, + Sinks.Many sideEvents, + AgentToolApprovalCoordinator approvalCoordinator, + AgentSkillRuntimeContext skillContext) { + AgentDefinition definition = request.getAgentDefinition(); + Model model = modelFactory.create(definition.getModelSpec(), definition.getGenerationOptions()); + Toolkit toolkit = new Toolkit(); + Map> skillTools = buildToolkit(request, toolkit, sideEvents, approvalCoordinator, skillContext); + Memory memory = memoryAdapter.createMemory(request.getMemorySnapshot(), definition.getMemoryPolicy(), model); + Knowledge knowledge = knowledgeAdapter.createAggregateKnowledge(request, sideEvents); + SkillBox skillBox = skillAdapter.createSkillBox(definition.getSkillBoxSpec(), toolkit, skillTools); + // 构建ReActAgent + ReActAgent.Builder builder = ReActAgent.builder() + .name(definition.getAgentName()) + .description(definition.getDescription()) + .sysPrompt(definition.getSystemPrompt()) + .model(model) + .toolkit(toolkit) + .memory(memory) + .maxIters(definition.getExecutionOptions().getMaxIters()) + .generateOptions(modelFactory.toGenerateOptions(definition.getModelSpec(), definition.getGenerationOptions())) + .hook(new AgentScopeEventHook(request)) + .statePersistence(AgentScopeSessionAdapter.toStatePersistence(definition.getPersistencePolicy())); + if (knowledge != null) { + builder.knowledge(knowledge) + .ragMode(RAGMode.AGENTIC) + .retrieveConfig(defaultRetrieveConfig(definition)); + } + if (skillBox != null) { + builder.skillBox(skillBox); + } + return builder.build(); + } + + private Toolkit buildToolkit(AgentRunRequest request, Sinks.Many sideEvents) { + return buildToolkit(request, sideEvents, AgentToolApprovalCoordinator.disabled()); + } + + /** + * 构建工具箱。 + * + * @param request 运行请求 + * @param sideEvents 旁路事件 sink + * @param approvalCoordinator 审批协调器 + * @return 工具箱 + */ + private Toolkit buildToolkit(AgentRunRequest request, + Sinks.Many sideEvents, + AgentToolApprovalCoordinator approvalCoordinator) { + Toolkit toolkit = new Toolkit(); + buildToolkit(request, toolkit, sideEvents, approvalCoordinator, + AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec())); + return toolkit; + } + + /** + * 构建工具箱并返回 Skill 绑定工具。 + * + * @param request 运行请求 + * @param toolkit 工具箱 + * @param sideEvents 旁路事件 sink + * @param approvalCoordinator 审批协调器 + * @param skillContext Skill 运行时上下文 + * @return 按 Skill ID 分组的工具 + */ + private Map> buildToolkit(AgentRunRequest request, + Toolkit toolkit, + Sinks.Many sideEvents, + AgentToolApprovalCoordinator approvalCoordinator, + AgentSkillRuntimeContext skillContext) { + Map> skillTools = new LinkedHashMap<>(); + if (!request.getAgentDefinition().getExecutionOptions().isToolCallingEnabled()) { + return skillTools; + } + for (AgentToolSpec toolSpec : request.getAgentDefinition().getToolSpecs()) { + AgentToolInvoker invoker = request.getToolInvokers().get(toolSpec.getName()); + AgentSkillBinding skillBinding = skillContext.getToolBinding(toolSpec.getName()); + AgentTool agentTool = toolAdapter.adapt(toolSpec, invoker, request, approvalCoordinator, sideEvents, + skillContext, skillBinding); + if (skillBinding == null) { + toolkit.registerAgentTool(agentTool); + } else { + skillTools.computeIfAbsent(skillBinding.getSkillId(), key -> new ArrayList<>()).add(agentTool); + } + } + return skillTools; + } + + /** + * 构建当前轮的 AgentScope 输入消息。 + * + * @param request 运行请求 + * @return 输入消息 + */ + private List buildInput(AgentRunRequest request) { + List input = new ArrayList<>(); + AgentMessage userMessage = request.getUserMessage(); + if (userMessage != null) { + input.add(messageAdapter.toMsg(userMessage)); + } + return input; + } + + /** + * 将单个 AgentScope 事件映射为运行时事件。 + * + * @param request 运行请求 + * @param event AgentScope 事件 + * @return 运行时事件 + */ + private List mapEvent(AgentRunRequest request, Event event) { + return mapEvent(request, event, AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec())); + } + + /** + * 将单个 AgentScope 事件映射为运行时事件。 + * + * @param request 运行请求 + * @param event AgentScope 事件 + * @param skillContext Skill 运行时上下文 + * @return 运行时事件 + */ + private List mapEvent(AgentRunRequest request, Event event, AgentSkillRuntimeContext skillContext) { + List events = new ArrayList<>(); + if (event == null) { + return events; + } + if (event.getType() == EventType.REASONING) { + events.addAll(mapSkillCalls(request, event, skillContext)); + AgentRuntimeEvent runtimeEvent = base(request, AgentRuntimeEventType.REASONING_DELTA); + runtimeEvent.setMessageId(event.getMessageId()); + runtimeEvent.getPayload().put("text", event.getMessage() == null ? null : event.getMessage().getTextContent()); + runtimeEvent.getPayload().put("last", event.isLast()); + events.add(runtimeEvent); + return events; + } + if (event.getType() == EventType.TOOL_RESULT) { + events.addAll(mapSkillResults(request, event, skillContext)); + return events; + } + if (event.getType() == EventType.AGENT_RESULT) { + AgentRuntimeEvent runtimeEvent = base(request, AgentRuntimeEventType.MESSAGE_DELTA); + runtimeEvent.setMessageId(event.getMessageId()); + runtimeEvent.getPayload().put("text", event.getMessage() == null ? null : event.getMessage().getTextContent()); + runtimeEvent.getPayload().put("last", event.isLast()); + if (event.isLast() && event.getMessage() != null) { + runtimeEvent.setMessage(messageAdapter.toAgentMessage(event.getMessage())); + } + events.add(runtimeEvent); + return events; + } + return events; + } + + /** + * 从推理消息中识别 Skill 加载调用。 + * + * @param request 运行请求 + * @param event AgentScope 事件 + * @param skillContext Skill 运行时上下文 + * @return Skill 调用事件 + */ + private List mapSkillCalls(AgentRunRequest request, + Event event, + AgentSkillRuntimeContext skillContext) { + List events = new ArrayList<>(); + if (event.getMessage() == null) { + return events; + } + for (ToolUseBlock block : event.getMessage().getContentBlocks(ToolUseBlock.class)) { + if (!skillContext.isSkillLoadTool(block.getName())) { + continue; + } + AgentSkillLoadCall call = skillContext.rememberLoadCall(block.getId(), block.getInput()); + if (!skillContext.markLoadCallEmitted(block.getId())) { + continue; + } + AgentRuntimeEvent runtimeEvent = base(request, AgentRuntimeEventType.SKILL_CALL); + runtimeEvent.setMessageId(event.getMessageId()); + runtimeEvent.setToolCallId(block.getId()); + appendSkillLoadPayload(runtimeEvent, call); + runtimeEvent.getPayload().put("toolName", block.getName()); + runtimeEvent.getPayload().put("input", block.getInput()); + runtimeEvent.getPayload().put("status", "RUNNING"); + events.add(runtimeEvent); + } + return events; + } + + /** + * 从工具结果中识别 Skill 加载结果。 + * + * @param request 运行请求 + * @param event AgentScope 事件 + * @param skillContext Skill 运行时上下文 + * @return Skill 结果事件 + */ + private List mapSkillResults(AgentRunRequest request, + Event event, + AgentSkillRuntimeContext skillContext) { + List events = new ArrayList<>(); + if (event.getMessage() == null) { + return events; + } + for (ToolResultBlock block : event.getMessage().getContentBlocks(ToolResultBlock.class)) { + if (!skillContext.isSkillLoadTool(block.getName())) { + continue; + } + AgentSkillLoadCall call = skillContext.removeLoadCall(block.getId()); + AgentRuntimeEvent runtimeEvent = base(request, skillResultType(block)); + if (runtimeEvent.getEventType() == AgentRuntimeEventType.SKILL_RESULT && call != null) { + skillContext.activateSkill(call.getSkillId()); + } + runtimeEvent.setMessageId(event.getMessageId()); + runtimeEvent.setToolCallId(block.getId()); + appendSkillLoadPayload(runtimeEvent, call); + runtimeEvent.getPayload().put("toolName", block.getName()); + runtimeEvent.getPayload().put("text", block.toString()); + runtimeEvent.getPayload().put("status", runtimeEvent.getEventType() == AgentRuntimeEventType.SKILL_RESULT ? "SUCCESS" : "FAILED"); + runtimeEvent.getPayload().put("success", runtimeEvent.getEventType() == AgentRuntimeEventType.SKILL_RESULT); + runtimeEvent.getPayload().put("suspended", block.isSuspended()); + runtimeEvent.getMetadata().putAll(block.getMetadata() == null ? new LinkedHashMap<>() : block.getMetadata()); + events.add(runtimeEvent); + } + return events; + } + + /** + * 判断 Skill 结果事件类型。 + * + * @param block 工具结果块 + * @return Skill 事件类型 + */ + private AgentRuntimeEventType skillResultType(ToolResultBlock block) { + Object success = block.getMetadata() == null ? null : block.getMetadata().get("success"); + if (Boolean.FALSE.equals(success)) { + return AgentRuntimeEventType.SKILL_FAILED; + } + String text = block.toString(); + if (text != null && text.toLowerCase().contains("error")) { + return AgentRuntimeEventType.SKILL_FAILED; + } + return AgentRuntimeEventType.SKILL_RESULT; + } + + /** + * 追加 Skill 加载事件载荷。 + * + * @param event 运行时事件 + * @param call Skill 加载调用 + */ + private void appendSkillLoadPayload(AgentRuntimeEvent event, AgentSkillLoadCall call) { + if (call == null) { + return; + } + event.getPayload().put("skillId", call.getSkillId()); + event.getPayload().put("skillName", call.getSkillName()); + event.getPayload().put("skillBoxId", call.getSkillBoxId()); + event.getPayload().put("path", call.getPath()); + event.getMetadata().put("skillId", call.getSkillId()); + event.getMetadata().put("skillName", call.getSkillName()); + event.getMetadata().put("skillBoxId", call.getSkillBoxId()); + } + + /** + * 构建聚合知识库的默认检索配置。 + * + * @param definition 智能体定义 + * @return 检索配置 + */ + private RetrieveConfig defaultRetrieveConfig(AgentDefinition definition) { + int limit = definition.getKnowledgeSpecs().stream() + .mapToInt(AgentKnowledgeSpec::getLimit) + .filter(value -> value > 0) + .sum(); + double scoreThreshold = definition.getKnowledgeSpecs().stream() + .mapToDouble(AgentKnowledgeSpec::getScoreThreshold) + .filter(value -> value > 0D) + .min() + .orElse(0D); + return RetrieveConfig.builder() + .limit(limit <= 0 ? 5 : limit) + .scoreThreshold(scoreThreshold) + .build(); + } + + /** + * 从消息增量事件中收集最终助手文本。 + * + * @param finalText 最终文本引用 + * @param event 运行时事件 + */ + private void updateFinalText(StringBuilder finalText, AgentRuntimeEvent event) { + if (event.getEventType() == AgentRuntimeEventType.MESSAGE_DELTA) { + Object text = event.getPayload().get("text"); + if (text != null) { + String chunk = String.valueOf(text); + if (Boolean.TRUE.equals(event.getPayload().get("last")) && finalText.length() > chunk.length()) { + finalText.setLength(0); + } + finalText.append(chunk); + } + } + } + + /** + * 从最终消息事件中收集结构化助手消息。 + * + * @param finalMessage 最终消息引用 + * @param event 运行时事件 + */ + private void updateFinalMessage(AtomicReference finalMessage, AgentRuntimeEvent event) { + if (event.getEventType() == AgentRuntimeEventType.MESSAGE_DELTA + && Boolean.TRUE.equals(event.getPayload().get("last")) + && event.getMessage() != null) { + finalMessage.set(event.getMessage()); + } + } + + /** + * 校验请求的最小必要字段。 + * + * @param request 运行请求 + */ + private void validate(AgentRunRequest request) { + if (request == null) { + throw new AgentRuntimeException("Agent run request is required."); + } + if (request.getAgentDefinition() == null) { + throw new AgentRuntimeException("Agent definition is required."); + } + if (request.getAgentDefinition().getModelSpec() == null) { + throw new AgentRuntimeException("Agent model spec is required."); + } + } + + /** + * 启用时加载 AgentScope 会话状态。 + * + * @param request 运行请求 + * @param agent AgentScope 智能体 + */ + private void loadSessionIfNeeded(AgentRunRequest request, ReActAgent agent) { + if (request.getAgentDefinition().getPersistencePolicy().isEnabled() + && request.getAgentDefinition().getPersistencePolicy().isSessionManaged() + && request.getSessionId() != null) { + Session session = new AgentScopeSessionAdapter(request.getSessionStore()); + agent.loadIfExists(session, AgentScopeSessionAdapter.sessionKey(request.getSessionId())); + } + } + + /** + * 启用时保存 AgentScope 会话状态。 + * + * @param request 运行请求 + * @param agent AgentScope 智能体 + */ + private void saveSessionIfNeeded(AgentRunRequest request, ReActAgent agent) { + if (request.getAgentDefinition().getPersistencePolicy().isEnabled() + && request.getAgentDefinition().getPersistencePolicy().isSessionManaged() + && request.getSessionId() != null) { + Session session = new AgentScopeSessionAdapter(request.getSessionStore()); + agent.saveTo(session, AgentScopeSessionAdapter.sessionKey(request.getSessionId())); + } + } + + /** + * 构建开始事件。 + * + * @param request 运行请求 + * @return 开始事件 + */ + private AgentRuntimeEvent started(AgentRunRequest request) { + AgentRuntimeEvent event = base(request, AgentRuntimeEventType.STARTED); + event.getPayload().put("requestId", request.getRequestId()); + return event; + } + + /** + * 构建完成事件。 + * + * @param request 运行请求 + * @param text 最终文本 + * @return 完成事件 + */ + private AgentRuntimeEvent completed(AgentRunRequest request, String text, AgentMessage message) { + AgentRuntimeEvent event = base(request, AgentRuntimeEventType.COMPLETED); + event.getPayload().put("text", text); + event.setMessage(message); + return event; + } + + /** + * 构建完成事件,并附带知识库引用。 + * + * @param request 运行请求 + * @param text 最终文本 + * @param message 最终消息 + * @param knowledgeReferences 知识库引用 + * @return 完成事件 + */ + private AgentRuntimeEvent completed(AgentRunRequest request, + String text, + AgentMessage message, + Map knowledgeReferences) { + if (message != null && knowledgeReferences != null && !knowledgeReferences.isEmpty()) { + message.setKnowledgeReferences(new ArrayList<>(knowledgeReferences.values())); + } + return completed(request, text, message); + } + + /** + * 构建失败事件。 + * + * @param request 运行请求 + * @param error 错误 + * @return 失败事件 + */ + private AgentRuntimeEvent failed(AgentRunRequest request, Throwable error) { + AgentRuntimeEvent event = request == null || request.getAgentDefinition() == null + ? AgentRuntimeEvent.of(AgentRuntimeEventType.FAILED) + : base(request, AgentRuntimeEventType.FAILED); + event.getPayload().put("message", error.getMessage()); + event.getPayload().put("errorType", error.getClass().getName()); + return event; + } + + /** + * 构建取消事件。 + * + * @param request 运行请求 + * @return 取消事件 + */ + private AgentRuntimeEvent cancelled(AgentRunRequest request) { + AgentRuntimeEvent event = base(request, AgentRuntimeEventType.CANCELLED); + event.getPayload().put("reason", request.getCancelReason()); + return event; + } + + /** + * 取消本次运行并避免重复发出取消事件。 + * + * @param agent AgentScope 智能体 + * @param sideEvents 旁路事件 sink + * @param request 运行请求 + * @param cancelled 取消标记 + */ + private void cancelInternal(ReActAgent agent, + AgentToolApprovalCoordinator approvalCoordinator, + Sinks.Many sideEvents, + AgentRunRequest request, + java.util.concurrent.atomic.AtomicBoolean cancelled) { + if (!cancelled.compareAndSet(false, true)) { + return; + } + approvalCoordinator.cancelAll(request.getCancelReason() == null ? "运行已取消" : request.getCancelReason()); + saveSessionIfNeeded(request, agent); + agent.interrupt(); + sideEvents.tryEmitNext(cancelled(request)); + } + + /** + * 收集知识库引用,供最终消息 metadata 使用。 + * + * @param knowledgeReferences 知识库引用 + * @param event 运行时事件 + */ + @SuppressWarnings("unchecked") + private void updateKnowledgeReferences(Map knowledgeReferences, AgentRuntimeEvent event) { + if (event == null || event.getEventType() != AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL) { + return; + } + Object documentsObject = event.getPayload().get("documents"); + if (!(documentsObject instanceof List documents) || documents.isEmpty()) { + return; + } + Object knowledgeId = event.getPayload().get("knowledgeId"); + Object knowledgeName = event.getPayload().get("knowledgeName"); + for (Object documentObject : documents) { + if (!(documentObject instanceof Map documentMap)) { + continue; + } + AgentKnowledgeReference reference = new AgentKnowledgeReference(); + reference.setKnowledgeId(stringValue(knowledgeId)); + reference.setKnowledgeName(stringValue(knowledgeName)); + reference.setDocumentId(stringValue(documentMap.get("documentId"))); + reference.setDocumentName(stringValue(documentMap.get("documentName"))); + reference.setChunkId(stringValue(documentMap.get("chunkId"))); + reference.setSourceUri(stringValue(documentMap.get("sourceUri"))); + reference.setScore(documentMap.get("score") instanceof Number score ? score.doubleValue() : null); + Object metadata = documentMap.get("metadata"); + reference.setMetadata(metadata instanceof Map map ? new LinkedHashMap<>((Map) map) : new LinkedHashMap<>()); + String key = String.valueOf(knowledgeId) + "|" + String.valueOf(documentMap.get("documentId")) + "|" + String.valueOf(documentMap.get("chunkId")); + knowledgeReferences.putIfAbsent(key, reference); + } + } + + /** + * 将对象转换为字符串。 + * + * @param value 值 + * @return 字符串值 + */ + private String stringValue(Object value) { + return value == null ? null : String.valueOf(value); + } + + /** + * 构建携带通用标识的运行时事件。 + * + * @param request 运行请求 + * @param type 事件类型 + * @return 事件 + */ + private AgentRuntimeEvent base(AgentRunRequest request, AgentRuntimeEventType type) { + AgentRuntimeEvent event = AgentRuntimeEvent.of(type); + event.setTraceId(request.getTraceId()); + event.setSessionId(request.getSessionId()); + event.setAgentId(request.getAgentDefinition().getAgentId()); + event.getMetadata().put("requestId", request.getRequestId()); + event.getMetadata().putAll(nullToEmpty(request.getMetadata())); + return event; + } + + /** + * 当来源元数据为空时返回空 Map。 + * + * @param map 来源 Map + * @return 非空 Map + */ + private Map nullToEmpty(Map map) { + return map == null ? new LinkedHashMap<>() : map; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSessionAdapter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSessionAdapter.java new file mode 100644 index 0000000..5d56385 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSessionAdapter.java @@ -0,0 +1,177 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.persistence.AgentPersistencePolicy; +import com.easyagents.agent.runtime.persistence.AgentRuntimeState; +import com.easyagents.agent.runtime.persistence.AgentSessionStore; +import io.agentscope.core.session.Session; +import io.agentscope.core.state.SessionKey; +import io.agentscope.core.state.State; +import io.agentscope.core.state.StatePersistence; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 将 AgentScope Session 桥接到运行时 AgentSessionStore。 + */ +public class AgentScopeSessionAdapter implements Session { + + private final AgentSessionStore sessionStore; + + /** + * 创建适配器。 + * + * @param sessionStore 运行时会话存储 + */ + public AgentScopeSessionAdapter(AgentSessionStore sessionStore) { + this.sessionStore = sessionStore; + } + + @Override + public void save(SessionKey sessionKey, String name, State state) { + sessionStore.save(toKey(sessionKey), name, AgentRuntimeState.of(name, state)); + } + + /** + * 将状态列表保存到运行时存储。 + * + * @param sessionKey 会话键 + * @param name 状态名称 + * @param states 状态列表 + */ + @Override + public void save(SessionKey sessionKey, String name, List states) { + List converted = new ArrayList<>(); + if (states != null) { + for (State state : states) { + converted.add(AgentRuntimeState.of(name, state)); + } + } + sessionStore.saveList(toKey(sessionKey), name, converted); + } + + /** + * 从运行时存储获取单个指定类型的状态。 + * + * @param sessionKey 会话键 + * @param name 状态名称 + * @param clazz 状态类型 + * @param 状态类型 + * @return 可选状态 + */ + @Override + public Optional get(SessionKey sessionKey, String name, Class clazz) { + return sessionStore.get(toKey(sessionKey), name) + .map(AgentRuntimeState::getValue) + .filter(clazz::isInstance) + .map(clazz::cast); + } + + /** + * 从运行时存储获取指定类型的状态列表。 + * + * @param sessionKey 会话键 + * @param name 状态名称 + * @param clazz 状态类型 + * @param 状态类型 + * @return 状态列表 + */ + @Override + public List getList(SessionKey sessionKey, String name, Class clazz) { + return sessionStore.getList(toKey(sessionKey), name).stream() + .map(AgentRuntimeState::getValue) + .filter(clazz::isInstance) + .map(clazz::cast) + .collect(Collectors.toList()); + } + + /** + * 返回会话键是否存在。 + * + * @param sessionKey 会话键 + * @return 存在时为 true + */ + @Override + public boolean exists(SessionKey sessionKey) { + return sessionStore.exists(toKey(sessionKey)); + } + + /** + * 删除会话键。 + * + * @param sessionKey 会话键 + */ + @Override + public void delete(SessionKey sessionKey) { + sessionStore.delete(toKey(sessionKey)); + } + + /** + * 列出已存储的会话键。 + * + * @return 会话键列表 + */ + @Override + public Set listSessionKeys() { + return sessionStore.listSessionKeys().stream() + .map(RuntimeSessionKey::new) + .collect(Collectors.toSet()); + } + + /** + * 将持久化策略转换为 AgentScope 持久化标记。 + * + * @param policy 运行时策略 + * @return AgentScope 状态持久化配置 + */ + public static StatePersistence toStatePersistence(AgentPersistencePolicy policy) { + if (policy == null || !policy.isEnabled()) { + return StatePersistence.none(); + } + return StatePersistence.builder() + .memoryManaged(policy.isMemoryManaged()) + .toolkitManaged(policy.isToolkitManaged()) + .planNotebookManaged(policy.isPlanNotebookManaged()) + .statefulToolsManaged(policy.isStatefulToolsManaged()) + .build(); + } + + /** + * 创建会话键。 + * + * @param value 键值 + * @return 会话键 + */ + public static SessionKey sessionKey(String value) { + return new RuntimeSessionKey(value); + } + + private String toKey(SessionKey sessionKey) { + return sessionKey == null ? "" : sessionKey.toIdentifier(); + } + + /** + * 运行时支撑的 AgentScope 会话键。 + */ + private static class RuntimeSessionKey implements SessionKey { + + private final String value; + + private RuntimeSessionKey(String value) { + this.value = value; + } + + /** + * 将键转换为稳定标识符。 + * + * @return 标识符 + */ + @Override + public String toIdentifier() { + return value; + } + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSkillAdapter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSkillAdapter.java new file mode 100644 index 0000000..80ba50d --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeSkillAdapter.java @@ -0,0 +1,108 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.skill.AgentSkillBoxSpec; +import com.easyagents.agent.runtime.skill.AgentSkillCompiler; +import com.easyagents.agent.runtime.skill.AgentSkillSpec; +import io.agentscope.core.skill.AgentSkill; +import io.agentscope.core.skill.SkillBox; +import io.agentscope.core.tool.AgentTool; +import io.agentscope.core.tool.Toolkit; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 将运行时 Skill 适配为 AgentScope SkillBox。 + */ +public class AgentScopeSkillAdapter implements AgentSkillCompiler { + + @Override + public AgentSkill compile(AgentSkillSpec skillSpec) { + Map metadata = new LinkedHashMap<>(skillSpec.getMetadata()); + metadata.put("skillId", skillSpec.getSkillId()); + metadata.put("name", skillSpec.getName()); + metadata.put("description", skillSpec.getDescription()); + return AgentSkill.builder() + .name(skillSpec.getName()) + .description(skillSpec.getDescription()) + .skillContent(skillSpec.getSkillContent()) + .metadata(metadata) + .resources(skillSpec.getResources()) + .source(skillSpec.getSource()) + .build(); + } + + /** + * 创建并绑定 AgentScope SkillBox。 + * + * @param spec SkillBox 声明 + * @param toolkit Toolkit 实例 + * @return SkillBox;未配置 Skill 时返回 null + */ + public SkillBox createSkillBox(AgentSkillBoxSpec spec, Toolkit toolkit) { + if (spec == null || spec.getSkills().isEmpty()) { + return null; + } + SkillBox skillBox = spec.getSkillBoxId() == null || spec.getSkillBoxId().isEmpty() + ? new SkillBox(toolkit) + : new SkillBox(toolkit, spec.getSkillBoxId()); + skillBox.setExposeAllSkillMetadata(spec.isExposeAllSkillMetadata()); + for (AgentSkillSpec skillSpec : spec.getSkills()) { + AgentSkill skill = compile(skillSpec); + SkillBox.SkillRegistration registration = skillBox.registration() + .skill(skill) + .toolkit(toolkit) + .enableTools(spec.getEnabledToolNames()) + .disableTools(spec.getDisabledToolNames()) + .presetParameters(spec.getPresetParameters()); + registration.apply(); + } + skillBox.syncToolGroupStates(); + return skillBox; + } + + /** + * 创建并绑定带 Skill 工具的 AgentScope SkillBox。 + * + * @param spec SkillBox 声明 + * @param toolkit Toolkit 实例 + * @param skillTools 按 Skill ID 分组的工具 + * @return SkillBox;未配置 Skill 时返回 null + */ + public SkillBox createSkillBox(AgentSkillBoxSpec spec, Toolkit toolkit, Map> skillTools) { + if (spec == null || spec.getSkills().isEmpty()) { + return null; + } + SkillBox skillBox = spec.getSkillBoxId() == null || spec.getSkillBoxId().isEmpty() + ? new SkillBox(toolkit) + : new SkillBox(toolkit, spec.getSkillBoxId()); + skillBox.setExposeAllSkillMetadata(spec.isExposeAllSkillMetadata()); + for (AgentSkillSpec skillSpec : spec.getSkills()) { + AgentSkill skill = compile(skillSpec); + List tools = skillTools == null ? List.of() : skillTools.getOrDefault(skillSpec.getSkillId(), List.of()); + if (tools.isEmpty()) { + skillBox.registration() + .skill(skill) + .toolkit(toolkit) + .enableTools(spec.getEnabledToolNames()) + .disableTools(spec.getDisabledToolNames()) + .presetParameters(spec.getPresetParameters()) + .apply(); + continue; + } + for (AgentTool tool : tools) { + skillBox.registration() + .skill(skill) + .toolkit(toolkit) + .enableTools(spec.getEnabledToolNames()) + .disableTools(spec.getDisabledToolNames()) + .presetParameters(spec.getPresetParameters()) + .agentTool(tool) + .apply(); + } + } + skillBox.syncToolGroupStates(); + return skillBox; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeToolAdapter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeToolAdapter.java new file mode 100644 index 0000000..475969a --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/agentscope/AgentScopeToolAdapter.java @@ -0,0 +1,459 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.AgentRunRequest; +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.hitl.AgentPendingState; +import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator; +import com.easyagents.agent.runtime.hitl.AgentToolApprovalRejectedException; +import com.easyagents.agent.runtime.skill.AgentSkillBinding; +import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext; +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 io.agentscope.core.message.TextBlock; +import io.agentscope.core.message.ToolResultBlock; +import io.agentscope.core.message.ToolUseBlock; +import io.agentscope.core.tool.AgentTool; +import io.agentscope.core.tool.ToolCallParam; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +/** + * 将运行时工具适配为 AgentScope 工具。 + */ +public class AgentScopeToolAdapter { + + /** + * 将运行时工具声明和调用器转换为 AgentScope AgentTool。 + * + * @param toolSpec 工具声明 + * @param invoker 工具调用器 + * @param request 运行请求 + * @return AgentScope 工具 + */ + public AgentTool adapt(AgentToolSpec toolSpec, AgentToolInvoker invoker, AgentRunRequest request) { + return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(), null); + } + + /** + * 将运行时工具声明和调用器转换为带事件 sink 的 AgentScope AgentTool。 + * + * @param toolSpec 工具声明 + * @param invoker 工具调用器 + * @param request 运行请求 + * @param eventSink 事件 sink + * @return AgentScope 工具 + */ + public AgentTool adapt(AgentToolSpec toolSpec, + AgentToolInvoker invoker, + AgentRunRequest request, + Sinks.Many eventSink) { + return adapt(toolSpec, invoker, request, AgentToolApprovalCoordinator.disabled(), eventSink); + } + + /** + * 将运行时工具声明和调用器转换为带审批协调器和事件 sink 的 AgentScope AgentTool。 + * + * @param toolSpec 工具声明 + * @param invoker 工具调用器 + * @param request 运行请求 + * @param approvalCoordinator 审批协调器 + * @param eventSink 事件 sink + * @return AgentScope 工具 + */ + public AgentTool adapt(AgentToolSpec toolSpec, + AgentToolInvoker invoker, + AgentRunRequest request, + AgentToolApprovalCoordinator approvalCoordinator, + Sinks.Many eventSink) { + return adapt(toolSpec, invoker, request, approvalCoordinator, eventSink, null); + } + + /** + * 将运行时工具声明和调用器转换为带 Skill 归属的 AgentScope AgentTool。 + * + * @param toolSpec 工具声明 + * @param invoker 工具调用器 + * @param request 运行请求 + * @param approvalCoordinator 审批协调器 + * @param eventSink 事件 sink + * @param skillBinding Skill 绑定关系 + * @return AgentScope 工具 + */ + public AgentTool adapt(AgentToolSpec toolSpec, + AgentToolInvoker invoker, + AgentRunRequest request, + AgentToolApprovalCoordinator approvalCoordinator, + Sinks.Many eventSink, + AgentSkillBinding skillBinding) { + return adapt(toolSpec, invoker, request, approvalCoordinator, eventSink, null, skillBinding); + } + + /** + * 将运行时工具声明和调用器转换为带 Skill 上下文的 AgentScope AgentTool。 + * + * @param toolSpec 工具声明 + * @param invoker 工具调用器 + * @param request 运行请求 + * @param approvalCoordinator 审批协调器 + * @param eventSink 事件 sink + * @param skillContext Skill 运行时上下文 + * @param skillBinding Skill 静态绑定关系 + * @return AgentScope 工具 + */ + public AgentTool adapt(AgentToolSpec toolSpec, + AgentToolInvoker invoker, + AgentRunRequest request, + AgentToolApprovalCoordinator approvalCoordinator, + Sinks.Many eventSink, + AgentSkillRuntimeContext skillContext, + AgentSkillBinding skillBinding) { + if (toolSpec == null || toolSpec.getName() == null) { + throw new AgentRuntimeException("Agent tool spec and name are required."); + } + if (invoker == null) { + throw new AgentRuntimeException("Agent tool invoker is required: " + toolSpec.getName()); + } + return new RuntimeAgentTool(toolSpec, invoker, request, approvalCoordinator, eventSink, skillContext, skillBinding); + } + + private record RuntimeAgentTool(AgentToolSpec toolSpec, + AgentToolInvoker invoker, + AgentRunRequest request, + AgentToolApprovalCoordinator approvalCoordinator, + Sinks.Many eventSink, + AgentSkillRuntimeContext skillContext, + AgentSkillBinding skillBinding) implements AgentTool { + + /** + * 获取工具名称。 + * + * @return 工具名称 + */ + @Override + public String getName() { + return toolSpec.getName(); + } + + /** + * 获取工具描述。 + * + * @return 工具描述 + */ + @Override + public String getDescription() { + return toolSpec.getDescription(); + } + + /** + * 获取工具参数 Schema。 + * + * @return 参数 Schema + */ + @Override + public Map getParameters() { + return toolSpec.getParametersSchema(); + } + + /** + * 获取输出 Schema。 + * + * @return 输出 Schema + */ + @Override + public Map getOutputSchema() { + return toolSpec.getOutputSchema(); + } + + /** + * 调用运行时工具。 + * + * @param param 工具调用参数 + * @return 工具结果块 + */ + @Override + public Mono callAsync(ToolCallParam param) { + emit(toolCallEvent(param == null ? null : param.getToolUseBlock())); + Map input = param == null || param.getInput() == null + ? new LinkedHashMap<>() + : new LinkedHashMap<>(param.getInput()); + if (toolSpec.isApprovalRequired()) { + if (approvalCoordinator == null) { + throw new AgentRuntimeException("Agent tool approval coordinator is required: " + toolSpec.getName()); + } + Instant expiresAt = Instant.now().plus( + toolSpec.getApprovalRequest() != null && toolSpec.getApprovalRequest().getTimeout() != null + ? toolSpec.getApprovalRequest().getTimeout() + : java.time.Duration.ofMinutes(30)); + AgentPendingState pendingState = approvalCoordinator.register( + request.getSessionId(), + request.getAgentDefinition().getAgentId(), + param != null && param.getToolUseBlock() != null ? param.getToolUseBlock().getId() : null, + toolSpec.getName(), + approvalPrompt(toolSpec.getApprovalRequest() == null ? null : toolSpec.getApprovalRequest().getApprovalPrompt()), + input, + toolSpec.getApprovalRequest() == null ? new LinkedHashMap<>() : toolSpec.getApprovalRequest().getMetadata(), + expiresAt); + AgentRuntimeEvent approvalEvent = toolApprovalRequiredEvent(param == null ? null : param.getToolUseBlock(), pendingState); + pendingState.setEventId(approvalEvent.getEventId()); + emit(approvalEvent); + return approvalCoordinator.await(pendingState.getResumeToken()) + .flatMap(response -> { + if (!response.isApproved()) { + return Mono.error(new AgentToolApprovalRejectedException( + response.getRejectReason())); + } + return Mono.fromCallable(() -> invokeTool(param, input)); + }); + } + return Mono.fromCallable(() -> invokeTool(param, input)); + } + + /** + * 构建中立工具上下文。 + * + * @param param 工具调用参数 + * @return 工具上下文 + */ + private AgentToolContext buildContext(ToolCallParam param) { + AgentToolContext context = new AgentToolContext(); + context.setRequestId(request.getRequestId()); + context.setTraceId(request.getTraceId()); + context.setSessionId(request.getSessionId()); + context.setAgentId(request.getAgentDefinition().getAgentId()); + context.setRuntimeContext(request.getRuntimeContext()); + if (param != null && param.getToolUseBlock() != null) { + context.setToolCallId(param.getToolUseBlock().getId()); + } + context.getMetadata().put("toolName", toolSpec.getName()); + context.getMetadata().put("category", toolSpec.getCategory()); + appendSkillPayload(context.getMetadata(), activeSkillBinding()); + context.getMetadata().putAll(toolSpec.getMetadata()); + return context; + } + + /** + * 将运行时结果转换为 AgentScope 结果块。 + * + * @param param 工具调用参数 + * @param result 运行时结果 + * @return AgentScope 结果块 + */ + private ToolResultBlock toToolResultBlock(ToolCallParam param, AgentToolResult result) { + AgentToolResult safeResult = result == null ? AgentToolResult.failure("Tool returned null result.") : result; + Map metadata = new LinkedHashMap<>(safeResult.getMetadata()); + metadata.put("success", safeResult.isSuccess()); + metadata.put("visibility", toolSpec.getVisibility().name()); + metadata.put("displayContent", safeResult.getDisplayContent()); + appendSkillPayload(metadata, activeSkillBinding()); + String output = safeResult.getModelContent(); + if (output == null || output.isEmpty()) { + output = safeResult.isSuccess() ? "" : safeResult.getErrorMessage(); + } + String id = param != null && param.getToolUseBlock() != null ? param.getToolUseBlock().getId() : null; + return ToolResultBlock.of(id, toolSpec.getName(), TextBlock.builder().text(output).build(), metadata); + } + + /** + * 执行真实工具。 + * + * @param param 工具调用参数 + * @param input 工具输入 + * @return 工具结果块 + */ + private ToolResultBlock invokeTool(ToolCallParam param, Map input) { + AgentToolContext context = buildContext(param); + AgentToolResult result = invoker.invoke(input, context); + ToolResultBlock block = toToolResultBlock(param, result); + emit(toolResultEvent(block)); + return block; + } + + /** + * 存在 sink 时发射一条事件。 + * + * @param event 事件 + */ + private void emit(AgentRuntimeEvent event) { + if (eventSink != null && event != null) { + Sinks.EmitResult result = eventSink.tryEmitNext(event); + if (result.isFailure()) { + throw new AgentRuntimeException("Failed to emit agent runtime event: " + result); + } + } + } + + /** + * 构建工具调用事件。 + * + * @param block 工具使用块 + * @return 运行时事件 + */ + private AgentRuntimeEvent toolCallEvent(ToolUseBlock block) { + AgentSkillBinding activeBinding = activeSkillBinding(); + AgentRuntimeEvent event = baseEvent(activeBinding == null + ? AgentRuntimeEventType.TOOL_CALL + : AgentRuntimeEventType.SKILL_STEP); + if (block != null) { + event.setToolCallId(block.getId()); + event.getPayload().put("name", block.getName()); + event.getPayload().put("toolName", block.getName()); + event.getPayload().put("input", block.getInput()); + event.getPayload().put("content", block.getContent()); + event.getMetadata().putAll(block.getMetadata() == null ? new LinkedHashMap<>() : block.getMetadata()); + } else { + event.getPayload().put("name", toolSpec.getName()); + event.getPayload().put("toolName", toolSpec.getName()); + } + if (activeBinding != null) { + appendSkillPayload(event.getPayload(), activeBinding); + event.getPayload().put("stepType", "TOOL_CALL"); + event.getPayload().put("stepName", toolSpec.getName()); + event.getPayload().put("status", "RUNNING"); + } + return event; + } + + /** + * 构建工具结果事件。 + * + * @param block 工具结果块 + * @return 运行时事件 + */ + private AgentRuntimeEvent toolResultEvent(ToolResultBlock block) { + AgentSkillBinding activeBinding = activeSkillBinding(); + AgentRuntimeEvent event = baseEvent(activeBinding == null + ? AgentRuntimeEventType.TOOL_RESULT + : AgentRuntimeEventType.SKILL_STEP); + if (block != null) { + event.setToolCallId(block.getId()); + event.getPayload().put("name", block.getName()); + event.getPayload().put("toolName", block.getName()); + event.getPayload().put("text", block.toString()); + event.getPayload().put("suspended", block.isSuspended()); + event.getMetadata().putAll(block.getMetadata() == null ? new LinkedHashMap<>() : block.getMetadata()); + } + if (activeBinding != null) { + appendSkillPayload(event.getPayload(), activeBinding); + event.getPayload().put("stepType", "TOOL_RESULT"); + event.getPayload().put("stepName", toolSpec.getName()); + event.getPayload().put("status", success(event) ? "SUCCESS" : "FAILED"); + event.getPayload().put("success", success(event)); + } + return event; + } + + /** + * 根据挂起的工具结果构建工具审批事件。 + * + * @param toolUseBlock 工具使用块 + * @param resultBlock 工具结果块 + * @return 工具审批事件 + */ + private AgentRuntimeEvent toolApprovalRequiredEvent(ToolUseBlock toolUseBlock, AgentPendingState pendingState) { + AgentRuntimeEvent event = baseEvent(AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED); + if (pendingState != null && pendingState.getToolCallId() != null) { + event.setToolCallId(pendingState.getToolCallId()); + } + Map payload = new LinkedHashMap<>(); + payload.put("resumeToken", pendingState == null || pendingState.getResumeToken() == null + ? UUID.randomUUID().toString() + : pendingState.getResumeToken().getValue()); + payload.put("sessionId", request.getSessionId()); + payload.put("agentId", request.getAgentDefinition().getAgentId()); + payload.put("approvalPrompt", approvalPrompt(pendingState == null ? null : pendingState.getApprovalPrompt())); + payload.put("approvalMetadata", pendingState == null ? new LinkedHashMap<>() : pendingState.getMetadata()); + payload.put("toolInput", pendingState == null ? new LinkedHashMap<>() : pendingState.getToolInput()); + payload.put("toolName", pendingState == null ? toolSpec.getName() : pendingState.getToolName()); + payload.put("expiresAt", pendingState == null || pendingState.getExpiresAt() == null ? null : pendingState.getExpiresAt().toString()); + appendSkillPayload(payload, activeSkillBinding()); + if (toolUseBlock != null) { + payload.put("toolCallId", toolUseBlock.getId()); + payload.put("toolName", toolUseBlock.getName()); + payload.put("input", toolUseBlock.getInput()); + } + event.setPayload(payload); + return event; + } + + /** + * 解析审批文案。 + * + * @param approvalPrompt 审批文案 + * @return 审批文案 + */ + private String approvalPrompt(String approvalPrompt) { + if (approvalPrompt != null && !approvalPrompt.isBlank()) { + return approvalPrompt; + } + return "是否批准执行该工具?"; + } + + /** + * 构建带请求身份信息的事件。 + * + * @param type 事件类型 + * @return 运行时事件 + */ + private AgentRuntimeEvent baseEvent(AgentRuntimeEventType type) { + AgentRuntimeEvent event = AgentRuntimeEvent.of(type); + event.setTraceId(request.getTraceId()); + event.setSessionId(request.getSessionId()); + event.setAgentId(request.getAgentDefinition().getAgentId()); + event.getMetadata().put("requestId", request.getRequestId()); + event.getMetadata().put("toolCategory", toolSpec.getCategory().name()); + event.getMetadata().put("visibility", toolSpec.getVisibility().name()); + appendSkillPayload(event.getMetadata(), activeSkillBinding()); + return event; + } + + /** + * 追加 Skill 归属字段。 + * + * @param target 目标 Map + * @param binding Skill 绑定关系 + */ + private void appendSkillPayload(Map target, AgentSkillBinding binding) { + if (binding == null || target == null) { + return; + } + target.put("skillId", binding.getSkillId()); + target.put("skillName", binding.getSkillName()); + target.put("skillBoxId", binding.getSkillBoxId()); + } + + /** + * 获取当前已激活的 Skill 绑定关系。 + * + * @return 已激活绑定关系 + */ + private AgentSkillBinding activeSkillBinding() { + if (skillBinding == null) { + return null; + } + if (skillContext == null) { + return skillBinding; + } + return skillContext.getActiveToolBinding(toolSpec.getName()); + } + + /** + * 从事件元数据判断工具结果是否成功。 + * + * @param event 运行时事件 + * @return 成功时为 true + */ + private boolean success(AgentRuntimeEvent event) { + Object success = event.getMetadata().get("success"); + return !(success instanceof Boolean) || Boolean.TRUE.equals(success); + } + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEvent.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEvent.java new file mode 100644 index 0000000..8c8a34f --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEvent.java @@ -0,0 +1,236 @@ +package com.easyagents.agent.runtime.event; + +import com.easyagents.agent.runtime.message.AgentMessage; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.UUID; + +/** + * 智能体执行过程中的中立运行时事件。 + */ +public class AgentRuntimeEvent { + + private String eventId = UUID.randomUUID().toString(); + private AgentRuntimeEventType eventType; + private String traceId; + private String sessionId; + private String agentId; + private String messageId; + private String toolCallId; + private AgentMessage message; + private Map payload = new LinkedHashMap<>(); + private Map metadata = new LinkedHashMap<>(); + private Instant createdAt = Instant.now(); + + /** + * 创建指定类型的事件。 + * + * @param eventType 事件类型 + * @return 新事件 + */ + public static AgentRuntimeEvent of(AgentRuntimeEventType eventType) { + AgentRuntimeEvent event = new AgentRuntimeEvent(); + event.setEventType(eventType); + return event; + } + + /** + * 获取事件ID。 + * + * @return 事件ID + */ + public String getEventId() { + return eventId; + } + + /** + * 设置事件ID。 + * + * @param eventId 事件ID + */ + public void setEventId(String eventId) { + this.eventId = eventId; + } + + /** + * 获取事件类型。 + * + * @return 事件类型 + */ + public AgentRuntimeEventType getEventType() { + return eventType; + } + + /** + * 设置事件类型。 + * + * @param eventType 事件类型 + */ + public void setEventType(AgentRuntimeEventType eventType) { + this.eventType = eventType; + } + + /** + * 获取链路ID。 + * + * @return 链路ID + */ + public String getTraceId() { + return traceId; + } + + /** + * 设置链路ID。 + * + * @param traceId 链路ID + */ + public void setTraceId(String traceId) { + this.traceId = traceId; + } + + /** + * 获取会话ID。 + * + * @return 会话ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * 设置会话ID。 + * + * @param sessionId 会话ID + */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** + * 获取智能体ID。 + * + * @return 智能体ID + */ + public String getAgentId() { + return agentId; + } + + /** + * 设置智能体ID。 + * + * @param agentId 智能体ID + */ + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + /** + * 获取消息ID。 + * + * @return 消息ID + */ + public String getMessageId() { + return messageId; + } + + /** + * 设置消息ID。 + * + * @param messageId 消息ID + */ + public void setMessageId(String messageId) { + this.messageId = messageId; + } + + /** + * 获取工具调用ID。 + * + * @return 工具调用ID + */ + public String getToolCallId() { + return toolCallId; + } + + /** + * 设置工具调用ID。 + * + * @param toolCallId 工具调用ID + */ + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + /** + * 获取结构化消息。 + * + * @return 结构化消息 + */ + public AgentMessage getMessage() { + return message; + } + + /** + * 设置结构化消息。 + * + * @param message 结构化消息 + */ + public void setMessage(AgentMessage message) { + this.message = message; + } + + /** + * 获取载荷。 + * + * @return 载荷 + */ + public Map getPayload() { + return payload; + } + + /** + * 设置载荷。 + * + * @param payload 载荷 + */ + public void setPayload(Map payload) { + this.payload = payload == null ? new LinkedHashMap<>() : payload; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } + + /** + * 获取创建时间。 + * + * @return 创建时间 + */ + public Instant getCreatedAt() { + return createdAt; + } + + /** + * 设置创建时间。 + * + * @param createdAt 创建时间 + */ + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt == null ? Instant.now() : createdAt; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventType.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventType.java new file mode 100644 index 0000000..0b6e20e --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/event/AgentRuntimeEventType.java @@ -0,0 +1,76 @@ +package com.easyagents.agent.runtime.event; + +/** + * 智能体运行输出的运行时事件类型。 + */ +public enum AgentRuntimeEventType { + /** + * 智能体运行已开始。 + */ + STARTED, + + /** + * 流式推理内容,用于展示思考过程。 + */ + REASONING_DELTA, + + /** + * 流式输出内容,用于流式展示聊天内容。 + */ + MESSAGE_DELTA, + + /** + * 智能体发起工具调用。 + */ + TOOL_CALL, + + /** + * 工具调用完成并返回结果。 + */ + TOOL_RESULT, + + /** + * 知识库检索完成并返回文档摘要。 + */ + KNOWLEDGE_RETRIEVAL, + + /** + * 工具执行前需要人工审批。 + */ + TOOL_APPROVAL_REQUIRED, + + /** + * 智能体开始加载并调用 Skill。 + */ + SKILL_CALL, + + /** + * Skill 内部执行步骤。 + */ + SKILL_STEP, + + /** + * Skill 加载或调用完成。 + */ + SKILL_RESULT, + + /** + * Skill 加载或调用失败。 + */ + SKILL_FAILED, + + /** + * 智能体处理完成。 + */ + COMPLETED, + + /** + * 智能体运行失败。 + */ + FAILED, + + /** + * 智能体运行被取消。 + */ + CANCELLED +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hir/.gitkeep b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hir/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hir/.gitkeep @@ -0,0 +1 @@ + diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentPendingState.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentPendingState.java new file mode 100644 index 0000000..2ccaf8d --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentPendingState.java @@ -0,0 +1,202 @@ +package com.easyagents.agent.runtime.hitl; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 等待工具执行审批的状态。 + */ +public class AgentPendingState { + + private AgentResumeToken resumeToken = AgentResumeToken.create(); + private String sessionId; + private String agentId; + private String eventId; + private String toolCallId; + private String toolName; + private String approvalPrompt; + private Map toolInput = new LinkedHashMap<>(); + private Instant expiresAt; + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取恢复令牌。 + * + * @return 恢复令牌 + */ + public AgentResumeToken getResumeToken() { + return resumeToken; + } + + /** + * 设置恢复令牌。 + * + * @param resumeToken 恢复令牌 + */ + public void setResumeToken(AgentResumeToken resumeToken) { + this.resumeToken = resumeToken == null ? AgentResumeToken.create() : resumeToken; + } + + /** + * 获取会话ID。 + * + * @return 会话ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * 设置会话ID。 + * + * @param sessionId 会话ID + */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** + * 获取智能体ID。 + * + * @return 智能体ID + */ + public String getAgentId() { + return agentId; + } + + /** + * 设置智能体ID。 + * + * @param agentId 智能体ID + */ + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + /** + * 获取事件ID。 + * + * @return 事件ID + */ + public String getEventId() { + return eventId; + } + + /** + * 设置事件ID。 + * + * @param eventId 事件ID + */ + public void setEventId(String eventId) { + this.eventId = eventId; + } + + /** + * 获取工具调用ID。 + * + * @return 工具调用ID + */ + public String getToolCallId() { + return toolCallId; + } + + /** + * 设置工具调用ID。 + * + * @param toolCallId 工具调用ID + */ + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + /** + * 获取工具名称。 + * + * @return 工具名称 + */ + public String getToolName() { + return toolName; + } + + /** + * 设置工具名称。 + * + * @param toolName 工具名称 + */ + public void setToolName(String toolName) { + this.toolName = toolName; + } + + /** + * 获取审批提示文案。 + * + * @return 审批提示文案 + */ + public String getApprovalPrompt() { + return approvalPrompt; + } + + /** + * 设置审批提示文案。 + * + * @param approvalPrompt 审批提示文案 + */ + public void setApprovalPrompt(String approvalPrompt) { + this.approvalPrompt = approvalPrompt; + } + + /** + * 获取工具调用参数。 + * + * @return 工具调用参数 + */ + public Map getToolInput() { + return toolInput; + } + + /** + * 设置工具调用参数。 + * + * @param toolInput 工具调用参数 + */ + public void setToolInput(Map toolInput) { + this.toolInput = toolInput == null ? new LinkedHashMap<>() : toolInput; + } + + /** + * 获取过期时间。 + * + * @return 过期时间 + */ + public Instant getExpiresAt() { + return expiresAt; + } + + /** + * 设置过期时间。 + * + * @param expiresAt 过期时间 + */ + public void setExpiresAt(Instant expiresAt) { + this.expiresAt = expiresAt; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentResumeToken.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentResumeToken.java new file mode 100644 index 0000000..3ad2c92 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentResumeToken.java @@ -0,0 +1,38 @@ +package com.easyagents.agent.runtime.hitl; + +import java.util.UUID; + +/** + * 用于恢复挂起运行的令牌。 + */ +public class AgentResumeToken { + + private String value = UUID.randomUUID().toString(); + + /** + * 创建a new token。 + * + * @return 令牌 + */ + public static AgentResumeToken create() { + return new AgentResumeToken(); + } + + /** + * 获取令牌值。 + * + * @return 令牌值 + */ + public String getValue() { + return value; + } + + /** + * 设置令牌值。 + * + * @param value 令牌值 + */ + public void setValue(String value) { + this.value = value; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentSuspendState.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentSuspendState.java new file mode 100644 index 0000000..b665b32 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentSuspendState.java @@ -0,0 +1,27 @@ +package com.easyagents.agent.runtime.hitl; + +/** + * 已挂起的运行时状态。 + */ +public class AgentSuspendState { + + private AgentPendingState pendingState; + + /** + * 获取挂起状态。 + * + * @return 挂起状态 + */ + public AgentPendingState getPendingState() { + return pendingState; + } + + /** + * 设置挂起状态。 + * + * @param pendingState 挂起状态 + */ + public void setPendingState(AgentPendingState pendingState) { + this.pendingState = pendingState; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalCoordinator.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalCoordinator.java new file mode 100644 index 0000000..7b1313b --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalCoordinator.java @@ -0,0 +1,161 @@ +package com.easyagents.agent.runtime.hitl; + +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 工具审批协调器。 + */ +public class AgentToolApprovalCoordinator { + + private final boolean enabled; + private final Map approvals = new ConcurrentHashMap<>(); + + /** + * 创建已启用的协调器。 + * + * @return 已启用的协调器 + */ + public static AgentToolApprovalCoordinator enabled() { + return new AgentToolApprovalCoordinator(true); + } + + /** + * 创建禁用的协调器。 + * + * @return 禁用的协调器 + */ + public static AgentToolApprovalCoordinator disabled() { + return new AgentToolApprovalCoordinator(false); + } + + /** + * 创建协调器。 + * + * @param enabled 是否启用 + */ + public AgentToolApprovalCoordinator(boolean enabled) { + this.enabled = enabled; + } + + /** + * 注册一个待审批请求。 + * + * @param sessionId 会话ID + * @param agentId 智能体ID + * @param toolCallId 工具调用ID + * @param toolName 工具名称 + * @param approvalPrompt 审批文案 + * @param toolInput 工具入参 + * @param metadata 元数据 + * @param expiresAt 过期时间 + * @return 待审批状态 + */ + public AgentPendingState register(String sessionId, + String agentId, + String toolCallId, + String toolName, + String approvalPrompt, + Map toolInput, + Map metadata, + Instant expiresAt) { + AgentPendingState state = new AgentPendingState(); + state.setSessionId(sessionId); + state.setAgentId(agentId); + state.setToolCallId(toolCallId); + state.setToolName(toolName); + state.setApprovalPrompt(approvalPrompt); + state.setToolInput(toolInput); + state.setMetadata(metadata); + state.setExpiresAt(expiresAt); + if (enabled) { + String token = state.getResumeToken().getValue(); + PendingApproval pendingApproval = new PendingApproval(state, new CompletableFuture<>()); + approvals.put(token, pendingApproval); + pendingApproval.future.whenComplete((response, error) -> approvals.remove(token)); + } + return state; + } + + /** + * 等待指定审批令牌的响应。 + * + * @param resumeToken 恢复令牌 + * @return 审批响应 + */ + public Mono await(AgentResumeToken resumeToken) { + if (!enabled) { + AgentToolApprovalResponse response = new AgentToolApprovalResponse(); + response.setResumeToken(resumeToken); + response.setApproved(true); + return Mono.just(response); + } + if (resumeToken == null || resumeToken.getValue() == null || resumeToken.getValue().isBlank()) { + return Mono.error(new AgentToolApprovalRejectedException("缺少审批令牌。")); + } + PendingApproval pendingApproval = approvals.get(resumeToken.getValue()); + if (pendingApproval == null) { + return Mono.error(new AgentToolApprovalRejectedException("审批请求已失效。")); + } + return Mono.fromFuture(pendingApproval.future); + } + + /** + * 提交审批响应。 + * + * @param response 审批响应 + */ + public void submit(AgentToolApprovalResponse response) { + if (!enabled || response == null || response.getResumeToken() == null + || response.getResumeToken().getValue() == null) { + return; + } + PendingApproval pendingApproval = approvals.get(response.getResumeToken().getValue()); + if (pendingApproval != null) { + pendingApproval.future.complete(response); + } + } + + /** + * 取消所有挂起审批。 + * + * @param reason 取消原因 + */ + public void cancelAll(String reason) { + if (!enabled) { + return; + } + for (PendingApproval pendingApproval : approvals.values()) { + AgentToolApprovalResponse response = new AgentToolApprovalResponse(); + response.setResumeToken(pendingApproval.state.getResumeToken()); + response.setApproved(false); + response.setRejectReason(reason); + pendingApproval.future.complete(response); + } + approvals.clear(); + } + + /** + * 是否已启用。 + * + * @return 启用标记 + */ + public boolean isEnabled() { + return enabled; + } + + private static class PendingApproval { + private final AgentPendingState state; + private final CompletableFuture future; + + private PendingApproval(AgentPendingState state, CompletableFuture future) { + this.state = Objects.requireNonNull(state, "state"); + this.future = Objects.requireNonNull(future, "future"); + } + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalRejectedException.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalRejectedException.java new file mode 100644 index 0000000..fefd8b2 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalRejectedException.java @@ -0,0 +1,30 @@ +package com.easyagents.agent.runtime.hitl; + +import com.easyagents.agent.runtime.AgentRuntimeException; + +/** + * 工具审批被拒绝时抛出的异常。 + */ +public class AgentToolApprovalRejectedException extends AgentRuntimeException { + + private final String rejectReason; + + /** + * 创建工具审批拒绝异常。 + * + * @param rejectReason 拒绝原因 + */ + public AgentToolApprovalRejectedException(String rejectReason) { + super(rejectReason == null || rejectReason.isBlank() ? "工具执行已被拒绝。" : rejectReason); + this.rejectReason = rejectReason; + } + + /** + * 获取拒绝原因。 + * + * @return 拒绝原因 + */ + public String getRejectReason() { + return rejectReason; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalRequest.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalRequest.java new file mode 100644 index 0000000..3c2fa43 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalRequest.java @@ -0,0 +1,69 @@ +package com.easyagents.agent.runtime.hitl; + +import java.time.Duration; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 工具执行审批请求。 + */ +public class AgentToolApprovalRequest { + + private String approvalPrompt; + private Duration timeout = Duration.ofMinutes(30); + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取审批提示文案。 + * + * @return 审批提示文案 + */ + public String getApprovalPrompt() { + return approvalPrompt; + } + + /** + * 设置审批提示文案。 + * + * @param approvalPrompt 审批提示文案 + */ + public void setApprovalPrompt(String approvalPrompt) { + this.approvalPrompt = approvalPrompt; + } + + /** + * 获取审批等待超时时间。 + * + * @return 超时时间 + */ + public Duration getTimeout() { + return timeout; + } + + /** + * 设置审批等待超时时间。 + * + * @param timeout 超时时间 + */ + public void setTimeout(Duration timeout) { + this.timeout = timeout == null ? Duration.ofMinutes(30) : timeout; + } + + /** + * 获取审批元数据。 + * + * @return 审批元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置审批元数据。 + * + * @param metadata 审批元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalResponse.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalResponse.java new file mode 100644 index 0000000..fadf97d --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/hitl/AgentToolApprovalResponse.java @@ -0,0 +1,87 @@ +package com.easyagents.agent.runtime.hitl; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 工具执行审批响应。 + */ +public class AgentToolApprovalResponse { + + private AgentResumeToken resumeToken; + private boolean approved; + private String rejectReason; + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取恢复令牌。 + * + * @return 恢复令牌 + */ + public AgentResumeToken getResumeToken() { + return resumeToken; + } + + /** + * 设置恢复令牌。 + * + * @param resumeToken 恢复令牌 + */ + public void setResumeToken(AgentResumeToken resumeToken) { + this.resumeToken = resumeToken; + } + + /** + * 返回是否批准执行工具。 + * + * @return 批准时为 true + */ + public boolean isApproved() { + return approved; + } + + /** + * 设置是否批准执行工具。 + * + * @param approved 批准标记 + */ + public void setApproved(boolean approved) { + this.approved = approved; + } + + /** + * 获取拒绝原因。 + * + * @return 拒绝原因 + */ + public String getRejectReason() { + return rejectReason; + } + + /** + * 设置拒绝原因。 + * + * @param rejectReason 拒绝原因 + */ + public void setRejectReason(String rejectReason) { + this.rejectReason = rejectReason; + } + + /** + * 获取审批元数据。 + * + * @return 审批元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置审批元数据。 + * + * @param metadata 审批元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeDocument.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeDocument.java new file mode 100644 index 0000000..0ca8c37 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeDocument.java @@ -0,0 +1,163 @@ +package com.easyagents.agent.runtime.knowledge; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 保留元数据的知识库检索文档。 + */ +public class AgentKnowledgeDocument { + + private String documentId; + private String documentName; + private String chunkId; + private String content; + private String sourceUri; + private Double score; + private Map knowledgeMetadata = new LinkedHashMap<>(); + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取文档ID。 + * + * @return 文档ID + */ + public String getDocumentId() { + return documentId; + } + + /** + * 设置文档ID。 + * + * @param documentId 文档ID + */ + public void setDocumentId(String documentId) { + this.documentId = documentId; + } + + /** + * 获取文档名称。 + * + * @return 文档名称 + */ + public String getDocumentName() { + return documentName; + } + + /** + * 设置文档名称。 + * + * @param documentName 文档名称 + */ + public void setDocumentName(String documentName) { + this.documentName = documentName; + } + + /** + * 获取分片ID。 + * + * @return 分片ID + */ + public String getChunkId() { + return chunkId; + } + + /** + * 设置分片ID。 + * + * @param chunkId 分片ID + */ + public void setChunkId(String chunkId) { + this.chunkId = chunkId; + } + + /** + * 获取内容。 + * + * @return 内容 + */ + public String getContent() { + return content; + } + + /** + * 设置内容。 + * + * @param content 内容 + */ + public void setContent(String content) { + this.content = content; + } + + /** + * 获取来源 URI。 + * + * @return 来源 URI + */ + public String getSourceUri() { + return sourceUri; + } + + /** + * 设置来源 URI。 + * + * @param sourceUri 来源 URI + */ + public void setSourceUri(String sourceUri) { + this.sourceUri = sourceUri; + } + + /** + * 获取分数。 + * + * @return 分数 + */ + public Double getScore() { + return score; + } + + /** + * 设置分数。 + * + * @param score 分数 + */ + public void setScore(Double score) { + this.score = score; + } + + /** + * 获取知识库级元数据。 + * + * @return 知识库级元数据 + */ + public Map getKnowledgeMetadata() { + return knowledgeMetadata; + } + + /** + * 设置知识库级元数据。 + * + * @param knowledgeMetadata 知识库级元数据 + */ + public void setKnowledgeMetadata(Map knowledgeMetadata) { + this.knowledgeMetadata = knowledgeMetadata == null ? new LinkedHashMap<>() : knowledgeMetadata; + } + + /** + * 获取文档级元数据。 + * + * @return 文档级元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置文档级元数据。 + * + * @param metadata 文档级元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgePolicy.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgePolicy.java new file mode 100644 index 0000000..5392eeb --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgePolicy.java @@ -0,0 +1,10 @@ +package com.easyagents.agent.runtime.knowledge; + +/** + * 知识库检索策略。 + */ +public enum AgentKnowledgePolicy { + AGENTIC, + GENERIC, + DISABLED +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeRetrievalRequest.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeRetrievalRequest.java new file mode 100644 index 0000000..f9892bf --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeRetrievalRequest.java @@ -0,0 +1,127 @@ +package com.easyagents.agent.runtime.knowledge; + +import com.easyagents.agent.runtime.AgentRuntimeContext; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 传递给知识库检索器的请求。 + */ +public class AgentKnowledgeRetrievalRequest { + + private String query; + private int limit; + private double scoreThreshold; + private AgentKnowledgeSpec knowledgeSpec; + private AgentRuntimeContext runtimeContext = new AgentRuntimeContext(); + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取查询。 + * + * @return 查询 + */ + public String getQuery() { + return query; + } + + /** + * 设置查询。 + * + * @param query 查询 + */ + public void setQuery(String query) { + this.query = query; + } + + /** + * 获取检索数量限制。 + * + * @return 检索数量限制 + */ + public int getLimit() { + return limit; + } + + /** + * 设置检索数量限制。 + * + * @param limit 检索数量限制 + */ + public void setLimit(int limit) { + this.limit = limit; + } + + /** + * 获取分数阈值。 + * + * @return 分数阈值 + */ + public double getScoreThreshold() { + return scoreThreshold; + } + + /** + * 设置分数阈值。 + * + * @param scoreThreshold 分数阈值 + */ + public void setScoreThreshold(double scoreThreshold) { + this.scoreThreshold = scoreThreshold; + } + + /** + * 获取知识库声明。 + * + * @return 知识库声明 + */ + public AgentKnowledgeSpec getKnowledgeSpec() { + return knowledgeSpec; + } + + /** + * 设置知识库声明。 + * + * @param knowledgeSpec 知识库声明 + */ + public void setKnowledgeSpec(AgentKnowledgeSpec knowledgeSpec) { + this.knowledgeSpec = knowledgeSpec; + } + + /** + * 获取运行时上下文。 + * + * @return 运行时上下文 + */ + public AgentRuntimeContext getRuntimeContext() { + return runtimeContext; + } + + /** + * 设置运行时上下文。 + * + * @param runtimeContext 运行时上下文 + */ + public void setRuntimeContext(AgentRuntimeContext runtimeContext) { + this.runtimeContext = runtimeContext == null ? new AgentRuntimeContext() : runtimeContext; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeRetrievalResult.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeRetrievalResult.java new file mode 100644 index 0000000..47a48b0 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeRetrievalResult.java @@ -0,0 +1,63 @@ +package com.easyagents.agent.runtime.knowledge; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 知识库检索结果。 + */ +public class AgentKnowledgeRetrievalResult { + + private List documents = new ArrayList<>(); + private Map metadata = new LinkedHashMap<>(); + + /** + * 根据检索文档创建结果。 + * + * @param documents 检索文档 + * @return 检索结果 + */ + public static AgentKnowledgeRetrievalResult of(List documents) { + AgentKnowledgeRetrievalResult result = new AgentKnowledgeRetrievalResult(); + result.setDocuments(documents); + return result; + } + + /** + * 获取文档列表。 + * + * @return 文档列表 + */ + public List getDocuments() { + return documents; + } + + /** + * 设置文档列表。 + * + * @param documents 文档列表 + */ + public void setDocuments(List documents) { + this.documents = documents == null ? new ArrayList<>() : documents; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeRetriever.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeRetriever.java new file mode 100644 index 0000000..7a6d87e --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeRetriever.java @@ -0,0 +1,16 @@ +package com.easyagents.agent.runtime.knowledge; + +/** + * 从单个知识源检索文档。 + */ +@FunctionalInterface +public interface AgentKnowledgeRetriever { + + /** + * 检索相关文档。 + * + * @param request 检索请求 + * @return 检索结果 + */ + AgentKnowledgeRetrievalResult retrieve(AgentKnowledgeRetrievalRequest request); +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeSpec.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeSpec.java new file mode 100644 index 0000000..75a2cb6 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/knowledge/AgentKnowledgeSpec.java @@ -0,0 +1,144 @@ +package com.easyagents.agent.runtime.knowledge; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 知识库声明。 + */ +public class AgentKnowledgeSpec { + + private String knowledgeId; + private String name; + private String description; + private AgentKnowledgePolicy retrievalMode = AgentKnowledgePolicy.AGENTIC; + private int limit = 5; + private double scoreThreshold = 0D; + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取知识库ID。 + * + * @return 知识库ID + */ + public String getKnowledgeId() { + return knowledgeId; + } + + /** + * 设置知识库ID。 + * + * @param knowledgeId 知识库ID + */ + public void setKnowledgeId(String knowledgeId) { + this.knowledgeId = knowledgeId; + } + + /** + * 获取知识库名称。 + * + * @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; + } + + /** + * 获取检索模式。 + * + * @return 检索模式 + */ + public AgentKnowledgePolicy getRetrievalMode() { + return retrievalMode; + } + + /** + * 设置检索模式。 + * + * @param retrievalMode 检索模式 + */ + public void setRetrievalMode(AgentKnowledgePolicy retrievalMode) { + this.retrievalMode = retrievalMode == null ? AgentKnowledgePolicy.AGENTIC : retrievalMode; + } + + /** + * 获取限制数量。 + * + * @return 限制数量 + */ + public int getLimit() { + return limit; + } + + /** + * 设置限制数量。 + * + * @param limit 限制数量 + */ + public void setLimit(int limit) { + this.limit = limit <= 0 ? 5 : limit; + } + + /** + * 获取分数阈值。 + * + * @return 分数阈值 + */ + public double getScoreThreshold() { + return scoreThreshold; + } + + /** + * 设置分数阈值。 + * + * @param scoreThreshold 分数阈值 + */ + public void setScoreThreshold(double scoreThreshold) { + this.scoreThreshold = Math.max(0D, scoreThreshold); + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryCompressionParameter.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryCompressionParameter.java new file mode 100644 index 0000000..d7cc12d --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryCompressionParameter.java @@ -0,0 +1,179 @@ +package com.easyagents.agent.runtime.memory; + +/** + * AutoContext 自动压缩参数。 + */ +public class AgentMemoryCompressionParameter { + + private boolean enabled = true; + private int msgThreshold = 20; + private int lastKeep = 8; + private double tokenRatio = 0.7D; + private long maxToken = 12000L; + private long largePayloadThreshold = 2048L; + private int minCompressionTokenThreshold = 1000; + private double currentRoundCompressionRatio = 0.5D; + private int minConsecutiveToolMessages = 4; + + /** + * 判断是否启用自动压缩。 + * + * @return 启用时返回 true + */ + public boolean isEnabled() { + return enabled; + } + + /** + * 设置压缩开关。 + * + * @param enabled 压缩开关 + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * 获取消息阈值。 + * + * @return 消息阈值 + */ + public int getMsgThreshold() { + return msgThreshold; + } + + /** + * 设置消息阈值。 + * + * @param msgThreshold 消息阈值 + */ + public void setMsgThreshold(int msgThreshold) { + this.msgThreshold = msgThreshold <= 0 ? 20 : msgThreshold; + } + + /** + * 获取最近保留消息数。 + * + * @return 最近保留消息数 + */ + public int getLastKeep() { + return lastKeep; + } + + /** + * 设置最近保留消息数。 + * + * @param lastKeep 最近保留消息数 + */ + public void setLastKeep(int lastKeep) { + this.lastKeep = Math.max(0, lastKeep); + } + + /** + * 获取Token 比例。 + * + * @return Token 比例 + */ + public double getTokenRatio() { + return tokenRatio; + } + + /** + * 设置Token 比例。 + * + * @param tokenRatio Token 比例 + */ + public void setTokenRatio(double tokenRatio) { + this.tokenRatio = tokenRatio <= 0D ? 0.7D : tokenRatio; + } + + /** + * 获取最大 Token 数。 + * + * @return 最大 Token 数 + */ + public long getMaxToken() { + return maxToken; + } + + /** + * 设置最大 Token 数。 + * + * @param maxToken 最大 Token 数 + */ + public void setMaxToken(long maxToken) { + this.maxToken = maxToken <= 0L ? 12000L : maxToken; + } + + /** + * 获取大负载阈值。 + * + * @return 大负载阈值 + */ + public long getLargePayloadThreshold() { + return largePayloadThreshold; + } + + /** + * 设置大负载阈值。 + * + * @param largePayloadThreshold 大负载阈值 + */ + public void setLargePayloadThreshold(long largePayloadThreshold) { + this.largePayloadThreshold = largePayloadThreshold <= 0L ? 2048L : largePayloadThreshold; + } + + /** + * 获取最小压缩 Token 阈值。 + * + * @return 最小压缩 Token 阈值 + */ + public int getMinCompressionTokenThreshold() { + return minCompressionTokenThreshold; + } + + /** + * 设置最小压缩 Token 阈值。 + * + * @param minCompressionTokenThreshold 最小压缩 Token 阈值 + */ + public void setMinCompressionTokenThreshold(int minCompressionTokenThreshold) { + this.minCompressionTokenThreshold = Math.max(0, minCompressionTokenThreshold); + } + + /** + * 获取当前轮压缩比例。 + * + * @return 当前轮压缩比例 + */ + public double getCurrentRoundCompressionRatio() { + return currentRoundCompressionRatio; + } + + /** + * 设置当前轮压缩比例。 + * + * @param currentRoundCompressionRatio 当前轮压缩比例 + */ + public void setCurrentRoundCompressionRatio(double currentRoundCompressionRatio) { + this.currentRoundCompressionRatio = currentRoundCompressionRatio <= 0D ? 0.5D : currentRoundCompressionRatio; + } + + /** + * 获取最小连续工具消息数。 + * + * @return 最小连续工具消息数 + */ + public int getMinConsecutiveToolMessages() { + return minConsecutiveToolMessages; + } + + /** + * 设置最小连续工具消息数。 + * + * @param minConsecutiveToolMessages 最小连续工具消息数 + */ + public void setMinConsecutiveToolMessages(int minConsecutiveToolMessages) { + this.minConsecutiveToolMessages = Math.max(0, minConsecutiveToolMessages); + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryPolicy.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryPolicy.java new file mode 100644 index 0000000..a45daa8 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryPolicy.java @@ -0,0 +1,89 @@ +package com.easyagents.agent.runtime.memory; + +/** + * 运行时记忆策略。 + */ +public class AgentMemoryPolicy { + + private AgentMemoryType type = AgentMemoryType.AUTO_CONTEXT; + private int maxAttachedMessageCount = 50; + private AgentMemoryCompressionParameter compressionParameter = new AgentMemoryCompressionParameter(); + + /** + * 创建默认 AutoContext 记忆策略。 + * + * @return AutoContext 记忆策略 + */ + public static AgentMemoryPolicy autoContext() { + AgentMemoryPolicy policy = new AgentMemoryPolicy(); + policy.setType(AgentMemoryType.AUTO_CONTEXT); + return policy; + } + + /** + * 创建内存策略。 + * + * @return 内存策略 + */ + public static AgentMemoryPolicy inMemory() { + AgentMemoryPolicy policy = new AgentMemoryPolicy(); + policy.setType(AgentMemoryType.IN_MEMORY); + return policy; + } + + /** + * 获取记忆类型。 + * + * @return 记忆类型 + */ + public AgentMemoryType getType() { + return type; + } + + /** + * 设置记忆类型。 + * + * @param type 记忆类型 + */ + public void setType(AgentMemoryType type) { + this.type = type == null ? AgentMemoryType.AUTO_CONTEXT : type; + } + + /** + * 获取最大附加消息数。 + * + * @return 最大附加消息数 + */ + public int getMaxAttachedMessageCount() { + return maxAttachedMessageCount; + } + + /** + * 设置最大附加消息数。 + * + * @param maxAttachedMessageCount 最大附加消息数 + */ + public void setMaxAttachedMessageCount(int maxAttachedMessageCount) { + this.maxAttachedMessageCount = maxAttachedMessageCount <= 0 ? 50 : maxAttachedMessageCount; + } + + /** + * 获取自动压缩参数。 + * + * @return 自动压缩参数 + */ + public AgentMemoryCompressionParameter getCompressionParameter() { + return compressionParameter; + } + + /** + * 设置自动压缩参数。 + * + * @param compressionParameter 自动压缩参数 + */ + public void setCompressionParameter(AgentMemoryCompressionParameter compressionParameter) { + this.compressionParameter = compressionParameter == null + ? new AgentMemoryCompressionParameter() + : compressionParameter; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemorySnapshot.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemorySnapshot.java new file mode 100644 index 0000000..6c03ade --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemorySnapshot.java @@ -0,0 +1,43 @@ +package com.easyagents.agent.runtime.memory; + +import com.easyagents.agent.runtime.message.AgentMessage; + +import java.util.ArrayList; +import java.util.List; + +/** + * 由调用方恢复的会话历史快照。 + */ +public class AgentMemorySnapshot { + + private List messages = new ArrayList<>(); + + /** + * 获取消息列表。 + * + * @return 消息列表 + */ + public List getMessages() { + return messages; + } + + /** + * 设置消息列表。 + * + * @param messages 消息列表 + */ + public void setMessages(List messages) { + this.messages = messages == null ? new ArrayList<>() : messages; + } + + /** + * 添加one message。 + * + * @param message 消息 + */ + public void addMessage(AgentMessage message) { + if (message != null) { + messages.add(message); + } + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryType.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryType.java new file mode 100644 index 0000000..d6727eb --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMemoryType.java @@ -0,0 +1,9 @@ +package com.easyagents.agent.runtime.memory; + +/** + * 记忆实现类型。 + */ +public enum AgentMemoryType { + IN_MEMORY, + AUTO_CONTEXT +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMessageRole.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMessageRole.java new file mode 100644 index 0000000..2d19b8c --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/memory/AgentMessageRole.java @@ -0,0 +1,11 @@ +package com.easyagents.agent.runtime.memory; + +/** + * 中立消息角色。 + */ +public enum AgentMessageRole { + SYSTEM, + USER, + ASSISTANT, + TOOL +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentContentBlock.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentContentBlock.java new file mode 100644 index 0000000..2f22c8e --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentContentBlock.java @@ -0,0 +1,40 @@ +package com.easyagents.agent.runtime.message; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 运行时消息内容块。 + */ +public abstract class AgentContentBlock { + + private final AgentContentBlockType type; + private final Map metadata = new LinkedHashMap<>(); + + /** + * 创建内容块。 + * + * @param type 块类型 + */ + protected AgentContentBlock(AgentContentBlockType type) { + this.type = type == null ? AgentContentBlockType.UNKNOWN : type; + } + + /** + * 获取块类型。 + * + * @return 块类型 + */ + public AgentContentBlockType getType() { + return type; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentContentBlockType.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentContentBlockType.java new file mode 100644 index 0000000..5f8286f --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentContentBlockType.java @@ -0,0 +1,15 @@ +package com.easyagents.agent.runtime.message; + +/** + * 消息块类型。 + */ +public enum AgentContentBlockType { + TEXT, + THINKING, + TOOL_USE, + TOOL_RESULT, + IMAGE, + AUDIO, + VIDEO, + UNKNOWN +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentKnowledgeReference.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentKnowledgeReference.java new file mode 100644 index 0000000..081cc3a --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentKnowledgeReference.java @@ -0,0 +1,163 @@ +package com.easyagents.agent.runtime.message; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 知识库引用信息。 + */ +public class AgentKnowledgeReference { + + private String knowledgeId; + private String knowledgeName; + private String documentId; + private String documentName; + private String chunkId; + private String sourceUri; + private Double score; + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取知识库ID。 + * + * @return 知识库ID + */ + public String getKnowledgeId() { + return knowledgeId; + } + + /** + * 设置知识库ID。 + * + * @param knowledgeId 知识库ID + */ + public void setKnowledgeId(String knowledgeId) { + this.knowledgeId = knowledgeId; + } + + /** + * 获取知识库名称。 + * + * @return 知识库名称 + */ + public String getKnowledgeName() { + return knowledgeName; + } + + /** + * 设置知识库名称。 + * + * @param knowledgeName 知识库名称 + */ + public void setKnowledgeName(String knowledgeName) { + this.knowledgeName = knowledgeName; + } + + /** + * 获取文档ID。 + * + * @return 文档ID + */ + public String getDocumentId() { + return documentId; + } + + /** + * 设置文档ID。 + * + * @param documentId 文档ID + */ + public void setDocumentId(String documentId) { + this.documentId = documentId; + } + + /** + * 获取文档名称。 + * + * @return 文档名称 + */ + public String getDocumentName() { + return documentName; + } + + /** + * 设置文档名称。 + * + * @param documentName 文档名称 + */ + public void setDocumentName(String documentName) { + this.documentName = documentName; + } + + /** + * 获取分片ID。 + * + * @return 分片ID + */ + public String getChunkId() { + return chunkId; + } + + /** + * 设置分片ID。 + * + * @param chunkId 分片ID + */ + public void setChunkId(String chunkId) { + this.chunkId = chunkId; + } + + /** + * 获取来源 URI。 + * + * @return 来源 URI + */ + public String getSourceUri() { + return sourceUri; + } + + /** + * 设置来源 URI。 + * + * @param sourceUri 来源 URI + */ + public void setSourceUri(String sourceUri) { + this.sourceUri = sourceUri; + } + + /** + * 获取分数。 + * + * @return 分数 + */ + public Double getScore() { + return score; + } + + /** + * 设置分数。 + * + * @param score 分数 + */ + public void setScore(Double score) { + this.score = score; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentMediaBlock.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentMediaBlock.java new file mode 100644 index 0000000..8835e51 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentMediaBlock.java @@ -0,0 +1,221 @@ +package com.easyagents.agent.runtime.message; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 媒体内容块。 + */ +public class AgentMediaBlock extends AgentContentBlock { + + private String mimeType; + private String url; + private String data; + private Integer minPixels; + private Integer maxPixels; + private Float fps; + private Integer maxFrames; + private Integer totalPixels; + private String mediaKind; + + /** + * 创建媒体块。 + * + * @param mediaKind 媒体类型 + */ + public AgentMediaBlock(String mediaKind) { + super(resolveType(mediaKind)); + this.mediaKind = mediaKind; + } + + /** + * 获取媒体类型。 + * + * @return 媒体类型 + */ + public String getMediaKind() { + return mediaKind; + } + + /** + * 设置媒体类型。 + * + * @param mediaKind 媒体类型 + */ + public void setMediaKind(String mediaKind) { + this.mediaKind = mediaKind; + } + + /** + * 获取 MIME 类型。 + * + * @return MIME 类型 + */ + public String getMimeType() { + return mimeType; + } + + /** + * 设置 MIME 类型。 + * + * @param mimeType MIME 类型 + */ + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + /** + * 获取 URL。 + * + * @return URL + */ + public String getUrl() { + return url; + } + + /** + * 设置 URL。 + * + * @param url URL + */ + public void setUrl(String url) { + this.url = url; + } + + /** + * 获取 Base64 数据。 + * + * @return Base64 数据 + */ + public String getData() { + return data; + } + + /** + * 设置 Base64 数据。 + * + * @param data Base64 数据 + */ + public void setData(String data) { + this.data = data; + } + + /** + * 获取最小像素。 + * + * @return 最小像素 + */ + public Integer getMinPixels() { + return minPixels; + } + + /** + * 设置最小像素。 + * + * @param minPixels 最小像素 + */ + public void setMinPixels(Integer minPixels) { + this.minPixels = minPixels; + } + + /** + * 获取最大像素。 + * + * @return 最大像素 + */ + public Integer getMaxPixels() { + return maxPixels; + } + + /** + * 设置最大像素。 + * + * @param maxPixels 最大像素 + */ + public void setMaxPixels(Integer maxPixels) { + this.maxPixels = maxPixels; + } + + /** + * 获取帧率。 + * + * @return 帧率 + */ + public Float getFps() { + return fps; + } + + /** + * 设置帧率。 + * + * @param fps 帧率 + */ + public void setFps(Float fps) { + this.fps = fps; + } + + /** + * 获取最大帧数。 + * + * @return 最大帧数 + */ + public Integer getMaxFrames() { + return maxFrames; + } + + /** + * 设置最大帧数。 + * + * @param maxFrames 最大帧数 + */ + public void setMaxFrames(Integer maxFrames) { + this.maxFrames = maxFrames; + } + + /** + * 获取总像素。 + * + * @return 总像素 + */ + public Integer getTotalPixels() { + return totalPixels; + } + + /** + * 设置总像素。 + * + * @param totalPixels 总像素 + */ + public void setTotalPixels(Integer totalPixels) { + this.totalPixels = totalPixels; + } + + private static AgentContentBlockType resolveType(String mediaKind) { + if ("audio".equalsIgnoreCase(mediaKind)) { + return AgentContentBlockType.AUDIO; + } + if ("video".equalsIgnoreCase(mediaKind)) { + return AgentContentBlockType.VIDEO; + } + return AgentContentBlockType.IMAGE; + } + + /** + * 额外元数据。 + * + * @return 额外元数据 + */ + public Map toMetadata() { + Map metadata = new LinkedHashMap<>(getMetadata()); + metadata.put("mediaKind", mediaKind); + metadata.put("mimeType", mimeType); + metadata.put("url", url); + metadata.put("data", data); + metadata.put("minPixels", minPixels); + metadata.put("maxPixels", maxPixels); + metadata.put("fps", fps); + metadata.put("maxFrames", maxFrames); + metadata.put("totalPixels", totalPixels); + return metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentMessage.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentMessage.java new file mode 100644 index 0000000..c214854 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentMessage.java @@ -0,0 +1,173 @@ +package com.easyagents.agent.runtime.message; + +import java.time.Instant; +import java.util.*; + +/** + * 智能体聊天消息 + */ +public class AgentMessage { + + private String messageId = UUID.randomUUID().toString(); + private String name; + /** + * 聊天发起者角色 + */ + private AgentMessageRole role = AgentMessageRole.USER; + /** + * 消息内容块 + */ + private List contentBlocks = new ArrayList<>(); + /** + * 知识库引用 + */ + private List knowledgeReferences = new ArrayList<>(); + /** + * 元数据 + */ + private Map metadata = new LinkedHashMap<>(); + /** + * 创建时间 + */ + private Instant createdAt = Instant.now(); + + /** + * 创建纯文本消息。 + * + * @param role 消息角色 + * @param content 文本内容 + * @return 消息 + */ + public static AgentMessage text(AgentMessageRole role, String content) { + AgentMessage message = new AgentMessage(); + message.setRole(role); + message.setContentBlocks(List.of(new AgentTextBlock(content))); + return message; + } + + /** + * 获取消息ID。 + * + * @return 消息ID + */ + public String getMessageId() { + return messageId; + } + + /** + * 设置消息ID。 + * + * @param messageId 消息ID + */ + public void setMessageId(String messageId) { + this.messageId = messageId; + } + + /** + * 获取消息名称。 + * + * @return 消息名称 + */ + public String getName() { + return name; + } + + /** + * 设置消息名称。 + * + * @param name 消息名称 + */ + public void setName(String name) { + this.name = name; + } + + /** + * 获取角色。 + * + * @return 角色 + */ + public AgentMessageRole getRole() { + return role; + } + + /** + * 设置角色。 + * + * @param role 角色 + */ + public void setRole(AgentMessageRole role) { + this.role = role == null ? AgentMessageRole.USER : role; + } + + /** + * 获取内容块列表。 + * + * @return 内容块列表 + */ + public List getContentBlocks() { + return contentBlocks; + } + + /** + * 设置内容块列表。 + * + * @param contentBlocks 内容块列表 + */ + public void setContentBlocks(List contentBlocks) { + this.contentBlocks = contentBlocks == null ? new ArrayList<>() : new ArrayList<>(contentBlocks); + } + + /** + * 获取知识库引用。 + * + * @return 知识库引用 + */ + public List getKnowledgeReferences() { + return knowledgeReferences; + } + + /** + * 设置知识库引用。 + * + * @param knowledgeReferences 知识库引用 + */ + public void setKnowledgeReferences(List knowledgeReferences) { + this.knowledgeReferences = knowledgeReferences == null ? new ArrayList<>() : new ArrayList<>(knowledgeReferences); + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(metadata); + } + + /** + * 获取创建时间。 + * + * @return 创建时间 + */ + public Instant getCreatedAt() { + return createdAt; + } + + /** + * 设置创建时间。 + * + * @param createdAt 创建时间 + */ + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt == null ? Instant.now() : createdAt; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentMessageRole.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentMessageRole.java new file mode 100644 index 0000000..1a34dcb --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentMessageRole.java @@ -0,0 +1,11 @@ +package com.easyagents.agent.runtime.message; + +/** + * 中立消息角色。 + */ +public enum AgentMessageRole { + SYSTEM, + USER, + ASSISTANT, + TOOL +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentTextBlock.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentTextBlock.java new file mode 100644 index 0000000..7934737 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentTextBlock.java @@ -0,0 +1,37 @@ +package com.easyagents.agent.runtime.message; + +/** + * 文本内容块。 + */ +public class AgentTextBlock extends AgentContentBlock { + + private String text; + + /** + * 创建文本块。 + * + * @param text 文本内容 + */ + public AgentTextBlock(String text) { + super(AgentContentBlockType.TEXT); + this.text = text; + } + + /** + * 获取文本。 + * + * @return 文本 + */ + public String getText() { + return text; + } + + /** + * 设置文本。 + * + * @param text 文本 + */ + public void setText(String text) { + this.text = text; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentThinkingBlock.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentThinkingBlock.java new file mode 100644 index 0000000..006dbf8 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentThinkingBlock.java @@ -0,0 +1,37 @@ +package com.easyagents.agent.runtime.message; + +/** + * 思考内容块。 + */ +public class AgentThinkingBlock extends AgentContentBlock { + + private String thinking; + + /** + * 创建思考块。 + * + * @param thinking 思考内容 + */ + public AgentThinkingBlock(String thinking) { + super(AgentContentBlockType.THINKING); + this.thinking = thinking; + } + + /** + * 获取思考内容。 + * + * @return 思考内容 + */ + public String getThinking() { + return thinking; + } + + /** + * 设置思考内容。 + * + * @param thinking 思考内容 + */ + public void setThinking(String thinking) { + this.thinking = thinking; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentToolResultBlock.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentToolResultBlock.java new file mode 100644 index 0000000..227e4f0 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentToolResultBlock.java @@ -0,0 +1,99 @@ +package com.easyagents.agent.runtime.message; + +import java.util.ArrayList; +import java.util.List; + +/** + * 工具结果内容块。 + */ +public class AgentToolResultBlock extends AgentContentBlock { + + private String id; + private String name; + private List output = new ArrayList<>(); + private boolean suspended; + + /** + * 创建工具结果块。 + * + * @param id 调用ID + * @param name 工具名 + */ + public AgentToolResultBlock(String id, String name) { + super(AgentContentBlockType.TOOL_RESULT); + this.id = id; + this.name = name; + } + + /** + * 获取调用ID。 + * + * @return 调用ID + */ + public String getId() { + return id; + } + + /** + * 设置调用ID。 + * + * @param id 调用ID + */ + public void setId(String id) { + this.id = id; + } + + /** + * 获取工具名。 + * + * @return 工具名 + */ + public String getName() { + return name; + } + + /** + * 设置工具名。 + * + * @param name 工具名 + */ + public void setName(String name) { + this.name = name; + } + + /** + * 获取输出块列表。 + * + * @return 输出块列表 + */ + public List getOutput() { + return output; + } + + /** + * 设置输出块列表。 + * + * @param output 输出块列表 + */ + public void setOutput(List output) { + this.output = output == null ? new ArrayList<>() : new ArrayList<>(output); + } + + /** + * 是否挂起。 + * + * @return 是否挂起 + */ + public boolean isSuspended() { + return suspended; + } + + /** + * 设置挂起状态。 + * + * @param suspended 挂起状态 + */ + public void setSuspended(boolean suspended) { + this.suspended = suspended; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentToolUseBlock.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentToolUseBlock.java new file mode 100644 index 0000000..35004ff --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentToolUseBlock.java @@ -0,0 +1,101 @@ +package com.easyagents.agent.runtime.message; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 工具调用内容块。 + */ +public class AgentToolUseBlock extends AgentContentBlock { + + private String id; + private String name; + private Map input = new LinkedHashMap<>(); + private String content; + + /** + * 创建工具调用块。 + * + * @param id 调用ID + * @param name 工具名 + * @param input 输入参数 + */ + public AgentToolUseBlock(String id, String name, Map input) { + super(AgentContentBlockType.TOOL_USE); + this.id = id; + this.name = name; + setInput(input); + } + + /** + * 获取调用ID。 + * + * @return 调用ID + */ + public String getId() { + return id; + } + + /** + * 设置调用ID。 + * + * @param id 调用ID + */ + public void setId(String id) { + this.id = id; + } + + /** + * 获取工具名。 + * + * @return 工具名 + */ + public String getName() { + return name; + } + + /** + * 设置工具名。 + * + * @param name 工具名 + */ + public void setName(String name) { + this.name = name; + } + + /** + * 获取输入参数。 + * + * @return 输入参数 + */ + public Map getInput() { + return input; + } + + /** + * 设置输入参数。 + * + * @param input 输入参数 + */ + public void setInput(Map input) { + this.input = input == null ? new LinkedHashMap<>() : new LinkedHashMap<>(input); + } + + /** + * 获取原始内容。 + * + * @return 原始内容 + */ + public String getContent() { + return content; + } + + /** + * 设置原始内容。 + * + * @param content 原始内容 + */ + public void setContent(String content) { + this.content = content; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentUnknownBlock.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentUnknownBlock.java new file mode 100644 index 0000000..57cd02e --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/message/AgentUnknownBlock.java @@ -0,0 +1,95 @@ +package com.easyagents.agent.runtime.message; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 未识别的内容消息块 + * 兜底表示。 + */ +public class AgentUnknownBlock extends AgentContentBlock { + + private String sourceClassName; + private String sourceTypeName; + private String rawText; + private Map rawMetadata = new LinkedHashMap<>(); + + /** + * 创建未知内容块。 + */ + public AgentUnknownBlock() { + super(AgentContentBlockType.UNKNOWN); + } + + /** + * 获取来源类名。 + * + * @return 来源类名 + */ + public String getSourceClassName() { + return sourceClassName; + } + + /** + * 设置来源类名。 + * + * @param sourceClassName 来源类名 + */ + public void setSourceClassName(String sourceClassName) { + this.sourceClassName = sourceClassName; + } + + /** + * 获取来源类型名。 + * + * @return 来源类型名 + */ + public String getSourceTypeName() { + return sourceTypeName; + } + + /** + * 设置来源类型名。 + * + * @param sourceTypeName 来源类型名 + */ + public void setSourceTypeName(String sourceTypeName) { + this.sourceTypeName = sourceTypeName; + } + + /** + * 获取原始文本表示。 + * + * @return 原始文本表示 + */ + public String getRawText() { + return rawText; + } + + /** + * 设置原始文本表示。 + * + * @param rawText 原始文本表示 + */ + public void setRawText(String rawText) { + this.rawText = rawText; + } + + /** + * 获取原始元数据。 + * + * @return 原始元数据 + */ + public Map getRawMetadata() { + return rawMetadata; + } + + /** + * 设置原始元数据。 + * + * @param rawMetadata 原始元数据 + */ + public void setRawMetadata(Map rawMetadata) { + this.rawMetadata = rawMetadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(rawMetadata); + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentGenerationOptions.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentGenerationOptions.java new file mode 100644 index 0000000..0c0dab9 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentGenerationOptions.java @@ -0,0 +1,239 @@ +package com.easyagents.agent.runtime.model; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 运行时模型适配器使用的生成参数。 + */ +public class AgentGenerationOptions { + + private Double temperature; + private Double topP; + private Integer topK; + private Integer maxTokens; + private Integer maxCompletionTokens; + private Integer thinkingBudget; + private String reasoningEffort; + private Boolean thinkingEnabled; + private Boolean stream = true; + private Map additionalBodyParams = new LinkedHashMap<>(); + private Map additionalHeaders = new LinkedHashMap<>(); + private Map additionalQueryParams = new LinkedHashMap<>(); + + /** + * 获取temperature。 + * + * @return temperature 参数 + */ + public Double getTemperature() { + return temperature; + } + + /** + * 设置temperature。 + * + * @param temperature temperature 参数 + */ + public void setTemperature(Double temperature) { + this.temperature = temperature; + } + + /** + * 获取top-p。 + * + * @return top-p 参数 + */ + public Double getTopP() { + return topP; + } + + /** + * 设置top-p。 + * + * @param topP top-p 参数 + */ + public void setTopP(Double topP) { + this.topP = topP; + } + + /** + * 获取top-k。 + * + * @return top-k 参数 + */ + public Integer getTopK() { + return topK; + } + + /** + * 设置top-k。 + * + * @param topK top-k 参数 + */ + public void setTopK(Integer topK) { + this.topK = topK; + } + + /** + * 获取最大 Token 数。 + * + * @return 最大 Token 数 + */ + public Integer getMaxTokens() { + return maxTokens; + } + + /** + * 设置最大 Token 数。 + * + * @param maxTokens 最大 Token 数 + */ + public void setMaxTokens(Integer maxTokens) { + this.maxTokens = maxTokens; + } + + /** + * 获取最大补全 Token 数。 + * + * @return 最大补全 Token 数 + */ + public Integer getMaxCompletionTokens() { + return maxCompletionTokens; + } + + /** + * 设置最大补全 Token 数。 + * + * @param maxCompletionTokens 最大补全 Token 数 + */ + public void setMaxCompletionTokens(Integer maxCompletionTokens) { + this.maxCompletionTokens = maxCompletionTokens; + } + + /** + * 获取思考预算。 + * + * @return 思考预算 + */ + public Integer getThinkingBudget() { + return thinkingBudget; + } + + /** + * 设置思考预算。 + * + * @param thinkingBudget 思考预算 + */ + public void setThinkingBudget(Integer thinkingBudget) { + this.thinkingBudget = thinkingBudget; + } + + /** + * 获取推理强度。 + * + * @return 推理强度 + */ + public String getReasoningEffort() { + return reasoningEffort; + } + + /** + * 设置推理强度。 + * + * @param reasoningEffort 推理强度 + */ + public void setReasoningEffort(String reasoningEffort) { + this.reasoningEffort = reasoningEffort; + } + + /** + * 返回是否provider thinking is enabled。 + * + * @return 思考开关 + */ + public Boolean getThinkingEnabled() { + return thinkingEnabled; + } + + /** + * 设置provider thinking flag。 + * + * @param thinkingEnabled 思考开关 + */ + public void setThinkingEnabled(Boolean thinkingEnabled) { + this.thinkingEnabled = thinkingEnabled; + } + + /** + * 返回是否streaming is requested。 + * + * @return 流式开关 + */ + public Boolean getStream() { + return stream; + } + + /** + * 设置流式开关。 + * + * @param stream 流式开关 + */ + public void setStream(Boolean stream) { + this.stream = stream; + } + + /** + * 获取额外请求体参数。 + * + * @return 额外请求体参数 + */ + public Map getAdditionalBodyParams() { + return additionalBodyParams; + } + + /** + * 设置额外请求体参数。 + * + * @param additionalBodyParams 额外请求体参数 + */ + public void setAdditionalBodyParams(Map additionalBodyParams) { + this.additionalBodyParams = additionalBodyParams == null ? new LinkedHashMap<>() : additionalBodyParams; + } + + /** + * 获取额外请求头。 + * + * @return 额外请求头 + */ + public Map getAdditionalHeaders() { + return additionalHeaders; + } + + /** + * 设置额外请求头。 + * + * @param additionalHeaders 额外请求头 + */ + public void setAdditionalHeaders(Map additionalHeaders) { + this.additionalHeaders = additionalHeaders == null ? new LinkedHashMap<>() : additionalHeaders; + } + + /** + * 获取额外查询参数。 + * + * @return 额外查询参数 + */ + public Map getAdditionalQueryParams() { + return additionalQueryParams; + } + + /** + * 设置额外查询参数。 + * + * @param additionalQueryParams 额外查询参数 + */ + public void setAdditionalQueryParams(Map additionalQueryParams) { + this.additionalQueryParams = additionalQueryParams == null ? new LinkedHashMap<>() : additionalQueryParams; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelFactory.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelFactory.java new file mode 100644 index 0000000..0c09223 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelFactory.java @@ -0,0 +1,18 @@ +package com.easyagents.agent.runtime.model; + +/** + * 供应商无关的模型工厂标记接口。 + * + * @param 由适配器持有的具体模型类型 + */ +public interface AgentModelFactory { + + /** + * 创建a concrete model。 + * + * @param modelSpec 模型声明 + * @param generationOptions 生成参数 + * @return 具体模型 + */ + T create(AgentModelSpec modelSpec, AgentGenerationOptions generationOptions); +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelProviderType.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelProviderType.java new file mode 100644 index 0000000..41c6896 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelProviderType.java @@ -0,0 +1,20 @@ +package com.easyagents.agent.runtime.model; + +/** + * 智能体运行时支持的模型供应商类型。 + */ +public enum AgentModelProviderType { + OPENAI, + OPENAI_COMPATIBLE, + ANTHROPIC, + GEMINI, + OLLAMA, + DEEPSEEK, + GLM, + MINIMAX, + MOONSHOT, + ARK, + SILICONFLOW, + DASHSCOPE, + CUSTOM +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelSpec.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelSpec.java new file mode 100644 index 0000000..e61ee99 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/model/AgentModelSpec.java @@ -0,0 +1,125 @@ +package com.easyagents.agent.runtime.model; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 供应商无关的模型声明。 + */ +public class AgentModelSpec { + + private AgentModelProviderType providerType = AgentModelProviderType.OPENAI_COMPATIBLE; + private String modelName; + private String baseUrl; + private String endpointPath; + private String apiKey; + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取供应商类型。 + * + * @return 供应商类型 + */ + public AgentModelProviderType getProviderType() { + return providerType; + } + + /** + * 设置供应商类型。 + * + * @param providerType 供应商类型 + */ + public void setProviderType(AgentModelProviderType providerType) { + this.providerType = providerType == null ? AgentModelProviderType.OPENAI_COMPATIBLE : providerType; + } + + /** + * 获取模型名称。 + * + * @return 模型名称 + */ + public String getModelName() { + return modelName; + } + + /** + * 设置模型名称。 + * + * @param modelName 模型名称 + */ + public void setModelName(String modelName) { + this.modelName = modelName; + } + + /** + * 获取基础 URL。 + * + * @return 基础 URL + */ + public String getBaseUrl() { + return baseUrl; + } + + /** + * 设置基础 URL。 + * + * @param baseUrl 基础 URL + */ + public void setBaseUrl(String baseUrl) { + this.baseUrl = baseUrl; + } + + /** + * 获取端点路径。 + * + * @return 端点路径 + */ + public String getEndpointPath() { + return endpointPath; + } + + /** + * 设置端点路径。 + * + * @param endpointPath 端点路径 + */ + public void setEndpointPath(String endpointPath) { + this.endpointPath = endpointPath; + } + + /** + * 获取API Key。 + * + * @return API Key 值 + */ + public String getApiKey() { + return apiKey; + } + + /** + * 设置API Key。 + * + * @param apiKey API Key 值 + */ + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentConversationRecorder.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentConversationRecorder.java new file mode 100644 index 0000000..2c93307 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentConversationRecorder.java @@ -0,0 +1,18 @@ +package com.easyagents.agent.runtime.persistence; + +import com.easyagents.agent.runtime.AgentRunRequest; +import com.easyagents.agent.runtime.event.AgentRuntimeEvent; + +/** + * 记录业务会话事件。 + */ +public interface AgentConversationRecorder { + + /** + * 记录一条运行时事件。 + * + * @param request 运行请求 + * @param event 运行时事件 + */ + void record(AgentRunRequest request, AgentRuntimeEvent event); +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentPersistencePolicy.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentPersistencePolicy.java new file mode 100644 index 0000000..42df9e4 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentPersistencePolicy.java @@ -0,0 +1,182 @@ +package com.easyagents.agent.runtime.persistence; + +/** + * 控制运行时状态持久化。 + */ +public class AgentPersistencePolicy { + + private boolean enabled; + private boolean memoryManaged; + private boolean toolkitManaged; + private boolean planNotebookManaged; + private boolean statefulToolsManaged; + private boolean hitlManaged; + private boolean sessionManaged; + private boolean conversationRecordingEnabled; + + /** + * 创建禁用策略。 + * + * @return 禁用策略 + */ + public static AgentPersistencePolicy disabled() { + return new AgentPersistencePolicy(); + } + + /** + * 创建仅记忆持久化策略。 + * + * @return 仅记忆持久化策略 + */ + public static AgentPersistencePolicy memoryOnly() { + AgentPersistencePolicy policy = new AgentPersistencePolicy(); + policy.setEnabled(true); + policy.setMemoryManaged(true); + policy.setSessionManaged(true); + return policy; + } + + /** + * 返回是否persistence is enabled。 + * + * @return 启用标记 + */ + public boolean isEnabled() { + return enabled; + } + + /** + * 设置启用标记。 + * + * @param enabled 启用标记 + */ + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + /** + * 返回是否memory is managed。 + * + * @return 记忆标记 + */ + public boolean isMemoryManaged() { + return memoryManaged; + } + + /** + * 设置记忆标记。 + * + * @param memoryManaged 记忆标记 + */ + public void setMemoryManaged(boolean memoryManaged) { + this.memoryManaged = memoryManaged; + } + + /** + * 返回是否toolkit is managed。 + * + * @return Toolkit 标记 + */ + public boolean isToolkitManaged() { + return toolkitManaged; + } + + /** + * 设置Toolkit 标记。 + * + * @param toolkitManaged Toolkit 标记 + */ + public void setToolkitManaged(boolean toolkitManaged) { + this.toolkitManaged = toolkitManaged; + } + + /** + * 返回是否plan notebook is managed。 + * + * @return 计划笔记标记 + */ + public boolean isPlanNotebookManaged() { + return planNotebookManaged; + } + + /** + * 设置计划笔记标记。 + * + * @param planNotebookManaged 计划笔记标记 + */ + public void setPlanNotebookManaged(boolean planNotebookManaged) { + this.planNotebookManaged = planNotebookManaged; + } + + /** + * 返回是否stateful tools are managed。 + * + * @return 有状态工具标记 + */ + public boolean isStatefulToolsManaged() { + return statefulToolsManaged; + } + + /** + * 设置有状态工具标记。 + * + * @param statefulToolsManaged 有状态工具标记 + */ + public void setStatefulToolsManaged(boolean statefulToolsManaged) { + this.statefulToolsManaged = statefulToolsManaged; + } + + /** + * 返回是否HITL pending state is managed。 + * + * @return HITL 标记 + */ + public boolean isHitlManaged() { + return hitlManaged; + } + + /** + * 设置HITL 标记。 + * + * @param hitlManaged HITL 标记 + */ + public void setHitlManaged(boolean hitlManaged) { + this.hitlManaged = hitlManaged; + } + + /** + * 返回是否session storage is managed。 + * + * @return 会话标记 + */ + public boolean isSessionManaged() { + return sessionManaged; + } + + /** + * 设置会话标记。 + * + * @param sessionManaged 会话标记 + */ + public void setSessionManaged(boolean sessionManaged) { + this.sessionManaged = sessionManaged; + } + + /** + * 返回是否conversation events should be recorded。 + * + * @return 会话记录标记 + */ + public boolean isConversationRecordingEnabled() { + return conversationRecordingEnabled; + } + + /** + * 设置会话记录标记。 + * + * @param conversationRecordingEnabled 会话记录标记 + */ + public void setConversationRecordingEnabled(boolean conversationRecordingEnabled) { + this.conversationRecordingEnabled = conversationRecordingEnabled; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentRuntimeCheckpoint.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentRuntimeCheckpoint.java new file mode 100644 index 0000000..ef5bfb3 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentRuntimeCheckpoint.java @@ -0,0 +1,107 @@ +package com.easyagents.agent.runtime.persistence; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 可恢复状态的运行时检查点。 + */ +public class AgentRuntimeCheckpoint { + + private String checkpointId; + private String sessionId; + private String agentId; + private Map state = new LinkedHashMap<>(); + private Instant createdAt = Instant.now(); + + /** + * 获取检查点ID。 + * + * @return 检查点ID + */ + public String getCheckpointId() { + return checkpointId; + } + + /** + * 设置检查点ID。 + * + * @param checkpointId 检查点ID + */ + public void setCheckpointId(String checkpointId) { + this.checkpointId = checkpointId; + } + + /** + * 获取会话ID。 + * + * @return 会话ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * 设置会话ID。 + * + * @param sessionId 会话ID + */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** + * 获取智能体ID。 + * + * @return 智能体ID + */ + public String getAgentId() { + return agentId; + } + + /** + * 设置智能体ID。 + * + * @param agentId 智能体ID + */ + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + /** + * 获取状态载荷。 + * + * @return 状态载荷 + */ + public Map getState() { + return state; + } + + /** + * 设置状态载荷。 + * + * @param state 状态载荷 + */ + public void setState(Map state) { + this.state = state == null ? new LinkedHashMap<>() : state; + } + + /** + * 获取创建时间。 + * + * @return 创建时间 + */ + public Instant getCreatedAt() { + return createdAt; + } + + /** + * 设置创建时间。 + * + * @param createdAt 创建时间 + */ + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt == null ? Instant.now() : createdAt; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentRuntimeState.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentRuntimeState.java new file mode 100644 index 0000000..bea03c0 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentRuntimeState.java @@ -0,0 +1,86 @@ +package com.easyagents.agent.runtime.persistence; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 运行时状态项。 + * + *

MVP 中 value 是 JVM 对象态,不是稳定的传输格式, + * 非内存 {@link AgentSessionStore} 实现必须显式完成序列化, + * 再写入外部存储。

+ */ +public class AgentRuntimeState { + + private String name; + private Object value; + private Map metadata = new LinkedHashMap<>(); + + /** + * 创建a 状态项。 + * + * @param name 状态名称 + * @param value 状态值 + * @return 状态项 + */ + public static AgentRuntimeState of(String name, Object value) { + AgentRuntimeState state = new AgentRuntimeState(); + state.setName(name); + state.setValue(value); + return state; + } + + /** + * 获取状态名称。 + * + * @return 状态名称 + */ + public String getName() { + return name; + } + + /** + * 设置状态名称。 + * + * @param name 状态名称 + */ + public void setName(String name) { + this.name = name; + } + + /** + * 获取状态值。 + * + * @return 状态值 + */ + public Object getValue() { + return value; + } + + /** + * 设置状态值。 + * + * @param value 状态值 + */ + public void setValue(Object value) { + this.value = value; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStore.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStore.java new file mode 100644 index 0000000..f2a8d2a --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStore.java @@ -0,0 +1,70 @@ +package com.easyagents.agent.runtime.persistence; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * 用于 AgentScope 类运行时会话状态的存储 SPI。 + * + */ +public interface AgentSessionStore { + + /** + * 保存one state value。 + * + * @param sessionKey 会话键 + * @param name 状态名称 + * @param state 状态值 + */ + void save(String sessionKey, String name, AgentRuntimeState state); + + /** + * 保存a state list。 + * + * @param sessionKey 会话键 + * @param name 状态名称 + * @param states 状态列表 + */ + void saveList(String sessionKey, String name, List states); + + /** + * 获取one state value。 + * + * @param sessionKey 会话键 + * @param name 状态名称 + * @return 可选状态 + */ + Optional get(String sessionKey, String name); + + /** + * 获取a state list。 + * + * @param sessionKey 会话键 + * @param name 状态名称 + * @return 状态列表 + */ + List getList(String sessionKey, String name); + + /** + * 返回是否session key exists。 + * + * @param sessionKey 会话键 + * @return 存在时为 true + */ + boolean exists(String sessionKey); + + /** + * 删除a session key。 + * + * @param sessionKey 会话键 + */ + void delete(String sessionKey); + + /** + * 列出会话键列表。 + * + * @return 会话键列表 + */ + Set listSessionKeys(); +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStoreException.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStoreException.java new file mode 100644 index 0000000..c743c8f --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/AgentSessionStoreException.java @@ -0,0 +1,28 @@ +package com.easyagents.agent.runtime.persistence; + +import com.easyagents.agent.runtime.AgentRuntimeException; + +/** + * 会话持久化异常。 + */ +public class AgentSessionStoreException extends AgentRuntimeException { + + /** + * 创建会话持久化异常。 + * + * @param message 错误消息 + */ + public AgentSessionStoreException(String message) { + super(message); + } + + /** + * 创建会话持久化异常。 + * + * @param message 错误消息 + * @param cause 原始异常 + */ + public AgentSessionStoreException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStateCodec.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStateCodec.java new file mode 100644 index 0000000..c9ff425 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStateCodec.java @@ -0,0 +1,62 @@ +package com.easyagents.agent.runtime.persistence.json; + +import com.easyagents.agent.runtime.persistence.AgentRuntimeState; +import com.easyagents.agent.runtime.persistence.AgentSessionStoreException; +import io.agentscope.core.state.State; +import io.agentscope.core.util.JsonUtils; + +/** + * AgentScope 状态的 JSON 编解码器。 + */ +public class AgentSessionStateCodec { + + /** + * 将状态对象编码为可持久化记录。 + * + * @param state 状态对象 + * @return 可持久化记录 + */ + public SerializedAgentRuntimeState encode(AgentRuntimeState state) { + if (state == null || state.getValue() == null) { + return null; + } + Object value = state.getValue(); + if (!(value instanceof State)) { + throw new AgentSessionStoreException("Only AgentScope State values can be serialized: " + + value.getClass().getName()); + } + SerializedAgentRuntimeState serialized = new SerializedAgentRuntimeState(); + serialized.setName(state.getName()); + serialized.setStateClassName(value.getClass().getName()); + serialized.setStateJson(JsonUtils.getJsonCodec().toJson(value)); + serialized.setMetadata(state.getMetadata()); + return serialized; + } + + /** + * 将持久化记录解码为运行时状态。 + * + * @param serialized 可持久化记录 + * @return 运行时状态 + */ + @SuppressWarnings("unchecked") + public AgentRuntimeState decode(SerializedAgentRuntimeState serialized) { + if (serialized == null) { + return null; + } + try { + Class clazz = Class.forName(serialized.getStateClassName()); + if (!State.class.isAssignableFrom(clazz)) { + throw new AgentSessionStoreException("Serialized state class is not AgentScope State: " + + serialized.getStateClassName()); + } + State value = JsonUtils.getJsonCodec().fromJson(serialized.getStateJson(), (Class) clazz); + AgentRuntimeState state = AgentRuntimeState.of(serialized.getName(), value); + state.setMetadata(serialized.getMetadata()); + return state; + } catch (ClassNotFoundException e) { + throw new AgentSessionStoreException("Serialized state class not found: " + + serialized.getStateClassName(), e); + } + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStoreBackend.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStoreBackend.java new file mode 100644 index 0000000..0cb27dd --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/AgentSessionStoreBackend.java @@ -0,0 +1,69 @@ +package com.easyagents.agent.runtime.persistence.json; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * JSON 会话状态的底层键值存储后端。 + */ +public interface AgentSessionStoreBackend { + + /** + * 保存单个状态记录。 + * + * @param sessionKey 会话键 + * @param name 状态名称 + * @param state 状态记录 + */ + void save(String sessionKey, String name, SerializedAgentRuntimeState state); + + /** + * 保存状态记录列表。 + * + * @param sessionKey 会话键 + * @param name 状态名称 + * @param states 状态记录列表 + */ + void saveList(String sessionKey, String name, List states); + + /** + * 获取单个状态记录。 + * + * @param sessionKey 会话键 + * @param name 状态名称 + * @return 状态记录 + */ + Optional get(String sessionKey, String name); + + /** + * 获取状态记录列表。 + * + * @param sessionKey 会话键 + * @param name 状态名称 + * @return 状态记录列表 + */ + List getList(String sessionKey, String name); + + /** + * 判断会话是否存在。 + * + * @param sessionKey 会话键 + * @return 存在时为 true + */ + boolean exists(String sessionKey); + + /** + * 删除会话。 + * + * @param sessionKey 会话键 + */ + void delete(String sessionKey); + + /** + * 列出全部会话键。 + * + * @return 会话键集合 + */ + Set listSessionKeys(); +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/FileAgentSessionStoreBackend.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/FileAgentSessionStoreBackend.java new file mode 100644 index 0000000..938c6b0 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/FileAgentSessionStoreBackend.java @@ -0,0 +1,172 @@ +package com.easyagents.agent.runtime.persistence.json; + +import com.easyagents.agent.runtime.persistence.AgentSessionStoreException; +import io.agentscope.core.util.JsonUtils; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.*; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 基于本地文件的 JSON 会话状态后端。 + */ +public class FileAgentSessionStoreBackend implements AgentSessionStoreBackend { + + private static final Pattern SAFE_NAME = Pattern.compile("[A-Za-z0-9._-]+"); + private final Path rootDirectory; + + /** + * 创建文件后端。 + * + * @param rootDirectory 根目录 + */ + public FileAgentSessionStoreBackend(Path rootDirectory) { + if (rootDirectory == null) { + throw new AgentSessionStoreException("Agent session root directory is required."); + } + this.rootDirectory = rootDirectory; + try { + Files.createDirectories(rootDirectory); + } catch (IOException e) { + throw new AgentSessionStoreException("Failed to create agent session root directory: " + rootDirectory, e); + } + } + + @Override + public void save(String sessionKey, String name, SerializedAgentRuntimeState state) { + if (state == null) { + return; + } + Path path = statePath(sessionKey, name); + createParent(path); + try { + Files.writeString(path, JsonUtils.getJsonCodec().toPrettyJson(state), StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); + } catch (IOException e) { + throw new AgentSessionStoreException("Failed to save agent session state: " + sessionKey + "/" + name, e); + } + } + + @Override + public void saveList(String sessionKey, String name, List states) { + Path path = listPath(sessionKey, name); + createParent(path); + try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + if (states == null) { + return; + } + for (SerializedAgentRuntimeState state : states) { + writer.write(JsonUtils.getJsonCodec().toJson(state)); + writer.newLine(); + } + } catch (IOException e) { + throw new AgentSessionStoreException("Failed to save agent session state list: " + sessionKey + "/" + name, e); + } + } + + @Override + public Optional get(String sessionKey, String name) { + Path path = statePath(sessionKey, name); + if (!Files.exists(path)) { + return Optional.empty(); + } + try { + String json = Files.readString(path, StandardCharsets.UTF_8); + return Optional.of(JsonUtils.getJsonCodec().fromJson(json, SerializedAgentRuntimeState.class)); + } catch (IOException e) { + throw new AgentSessionStoreException("Failed to load agent session state: " + sessionKey + "/" + name, e); + } + } + + @Override + public List getList(String sessionKey, String name) { + Path path = listPath(sessionKey, name); + if (!Files.exists(path)) { + return List.of(); + } + List states = new ArrayList<>(); + try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.isBlank()) { + states.add(JsonUtils.getJsonCodec().fromJson(line, SerializedAgentRuntimeState.class)); + } + } + return states; + } catch (IOException e) { + throw new AgentSessionStoreException("Failed to load agent session state list: " + sessionKey + "/" + name, e); + } + } + + @Override + public boolean exists(String sessionKey) { + return Files.isDirectory(sessionDirectory(sessionKey)); + } + + @Override + public void delete(String sessionKey) { + Path directory = sessionDirectory(sessionKey); + if (!Files.exists(directory)) { + return; + } + try (Stream paths = Files.walk(directory)) { + List ordered = paths.sorted(Comparator.reverseOrder()).collect(Collectors.toList()); + for (Path path : ordered) { + Files.deleteIfExists(path); + } + } catch (IOException e) { + throw new AgentSessionStoreException("Failed to delete agent session: " + sessionKey, e); + } + } + + @Override + public Set listSessionKeys() { + if (!Files.exists(rootDirectory)) { + return Set.of(); + } + try (Stream paths = Files.list(rootDirectory)) { + return paths.filter(Files::isDirectory) + .map(path -> path.getFileName().toString()) + .collect(Collectors.toCollection(LinkedHashSet::new)); + } catch (IOException e) { + throw new AgentSessionStoreException("Failed to list agent sessions.", e); + } + } + + private Path statePath(String sessionKey, String name) { + return sessionDirectory(sessionKey).resolve(safeName(name) + ".json"); + } + + private Path listPath(String sessionKey, String name) { + return sessionDirectory(sessionKey).resolve(safeName(name) + ".jsonl"); + } + + private Path sessionDirectory(String sessionKey) { + return rootDirectory.resolve(safeName(sessionKey)); + } + + private String safeName(String value) { + if (value == null || value.isBlank() || !SAFE_NAME.matcher(value).matches() + || value.contains("..")) { + throw new AgentSessionStoreException("Unsafe agent session storage key: " + value); + } + return value; + } + + private void createParent(Path path) { + try { + Files.createDirectories(path.getParent()); + } catch (IOException e) { + throw new AgentSessionStoreException("Failed to create agent session directory: " + path.getParent(), e); + } + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/JsonAgentSessionStore.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/JsonAgentSessionStore.java new file mode 100644 index 0000000..fc13d20 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/JsonAgentSessionStore.java @@ -0,0 +1,106 @@ +package com.easyagents.agent.runtime.persistence.json; + +import com.easyagents.agent.runtime.persistence.AgentRuntimeState; +import com.easyagents.agent.runtime.persistence.AgentSessionStore; +import com.easyagents.agent.runtime.persistence.AgentSessionStoreException; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * 基于 JSON 记录的默认非内存会话存储。 + */ +public class JsonAgentSessionStore implements AgentSessionStore { + + private final AgentSessionStoreBackend backend; + private final AgentSessionStateCodec codec; + + /** + * 创建文件型 JSON 会话存储。 + * + * @param rootDirectory 根目录 + */ + public JsonAgentSessionStore(Path rootDirectory) { + this(new FileAgentSessionStoreBackend(rootDirectory), new AgentSessionStateCodec()); + } + + /** + * 创建可替换后端的 JSON 会话存储。 + * + * @param backend 底层存储后端 + */ + public JsonAgentSessionStore(AgentSessionStoreBackend backend) { + this(backend, new AgentSessionStateCodec()); + } + + /** + * 创建可替换后端和编解码器的 JSON 会话存储。 + * + * @param backend 底层存储后端 + * @param codec 状态编解码器 + */ + public JsonAgentSessionStore(AgentSessionStoreBackend backend, AgentSessionStateCodec codec) { + if (backend == null) { + throw new AgentSessionStoreException("Agent session store backend is required."); + } + this.backend = backend; + this.codec = codec == null ? new AgentSessionStateCodec() : codec; + } + + @Override + public void save(String sessionKey, String name, AgentRuntimeState state) { + SerializedAgentRuntimeState serialized = codec.encode(state); + if (serialized != null) { + backend.save(sessionKey, name, serialized); + } + } + + @Override + public void saveList(String sessionKey, String name, List states) { + List serialized = new ArrayList<>(); + if (states != null) { + for (AgentRuntimeState state : states) { + SerializedAgentRuntimeState item = codec.encode(state); + if (item != null) { + serialized.add(item); + } + } + } + backend.saveList(sessionKey, name, serialized); + } + + @Override + public Optional get(String sessionKey, String name) { + return backend.get(sessionKey, name).map(codec::decode); + } + + @Override + public List getList(String sessionKey, String name) { + List states = new ArrayList<>(); + for (SerializedAgentRuntimeState item : backend.getList(sessionKey, name)) { + AgentRuntimeState state = codec.decode(item); + if (state != null) { + states.add(state); + } + } + return states; + } + + @Override + public boolean exists(String sessionKey) { + return backend.exists(sessionKey); + } + + @Override + public void delete(String sessionKey) { + backend.delete(sessionKey); + } + + @Override + public Set listSessionKeys() { + return backend.listSessionKeys(); + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/SerializedAgentRuntimeState.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/SerializedAgentRuntimeState.java new file mode 100644 index 0000000..2cb01fc --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/json/SerializedAgentRuntimeState.java @@ -0,0 +1,107 @@ +package com.easyagents.agent.runtime.persistence.json; + +import java.time.Instant; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 可跨进程持久化的 AgentScope 状态记录。 + */ +public class SerializedAgentRuntimeState { + + private String name; + private String stateClassName; + private String stateJson; + private Map metadata = new LinkedHashMap<>(); + private Instant createdAt = Instant.now(); + + /** + * 获取状态名称。 + * + * @return 状态名称 + */ + public String getName() { + return name; + } + + /** + * 设置状态名称。 + * + * @param name 状态名称 + */ + public void setName(String name) { + this.name = name; + } + + /** + * 获取状态类名。 + * + * @return 状态类名 + */ + public String getStateClassName() { + return stateClassName; + } + + /** + * 设置状态类名。 + * + * @param stateClassName 状态类名 + */ + public void setStateClassName(String stateClassName) { + this.stateClassName = stateClassName; + } + + /** + * 获取状态 JSON。 + * + * @return 状态 JSON + */ + public String getStateJson() { + return stateJson; + } + + /** + * 设置状态 JSON。 + * + * @param stateJson 状态 JSON + */ + public void setStateJson(String stateJson) { + this.stateJson = stateJson; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } + + /** + * 获取创建时间。 + * + * @return 创建时间 + */ + public Instant getCreatedAt() { + return createdAt; + } + + /** + * 设置创建时间。 + * + * @param createdAt 创建时间 + */ + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt == null ? Instant.now() : createdAt; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/memory/InMemoryAgentSessionStore.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/memory/InMemoryAgentSessionStore.java new file mode 100644 index 0000000..9470f89 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/memory/InMemoryAgentSessionStore.java @@ -0,0 +1,59 @@ +package com.easyagents.agent.runtime.persistence.memory; + +import com.easyagents.agent.runtime.persistence.AgentRuntimeState; +import com.easyagents.agent.runtime.persistence.AgentSessionStore; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 用于测试和单节点 MVP 的内存会话存储。 + */ +public class InMemoryAgentSessionStore implements AgentSessionStore { + + private final Map>> states = new ConcurrentHashMap<>(); + + @Override + public void save(String sessionKey, String name, AgentRuntimeState state) { + if (sessionKey == null || name == null || state == null) { + return; + } + saveList(sessionKey, name, List.of(state)); + } + + @Override + public void saveList(String sessionKey, String name, List states) { + if (sessionKey == null || name == null) { + return; + } + this.states.computeIfAbsent(sessionKey, key -> new ConcurrentHashMap<>()) + .put(name, states == null ? new ArrayList<>() : new ArrayList<>(states)); + } + + @Override + public Optional get(String sessionKey, String name) { + List list = getList(sessionKey, name); + return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(0)); + } + + @Override + public List getList(String sessionKey, String name) { + Map> byName = states.getOrDefault(sessionKey, new LinkedHashMap<>()); + return new ArrayList<>(byName.getOrDefault(name, new ArrayList<>())); + } + + @Override + public boolean exists(String sessionKey) { + return states.containsKey(sessionKey); + } + + @Override + public void delete(String sessionKey) { + states.remove(sessionKey); + } + + @Override + public Set listSessionKeys() { + return new LinkedHashSet<>(states.keySet()); + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentConversationRecorder.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentConversationRecorder.java new file mode 100644 index 0000000..433aba0 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentConversationRecorder.java @@ -0,0 +1,16 @@ +package com.easyagents.agent.runtime.persistence.noop; + +import com.easyagents.agent.runtime.AgentRunRequest; +import com.easyagents.agent.runtime.event.AgentRuntimeEvent; +import com.easyagents.agent.runtime.persistence.AgentConversationRecorder; + +/** + * 空操作会话记录器。 + */ +public enum NoopAgentConversationRecorder implements AgentConversationRecorder { + INSTANCE; + + @Override + public void record(AgentRunRequest request, AgentRuntimeEvent event) { + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentSessionStore.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentSessionStore.java new file mode 100644 index 0000000..36240e8 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/persistence/noop/NoopAgentSessionStore.java @@ -0,0 +1,48 @@ +package com.easyagents.agent.runtime.persistence.noop; + +import com.easyagents.agent.runtime.persistence.AgentRuntimeState; +import com.easyagents.agent.runtime.persistence.AgentSessionStore; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * 空操作会话存储。 + */ +public enum NoopAgentSessionStore implements AgentSessionStore { + INSTANCE; + + @Override + public void save(String sessionKey, String name, AgentRuntimeState state) { + } + + @Override + public void saveList(String sessionKey, String name, List states) { + } + + @Override + public Optional get(String sessionKey, String name) { + return Optional.empty(); + } + + @Override + public List getList(String sessionKey, String name) { + return Collections.emptyList(); + } + + @Override + public boolean exists(String sessionKey) { + return false; + } + + @Override + public void delete(String sessionKey) { + } + + @Override + public Set listSessionKeys() { + return Collections.emptySet(); + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillBinding.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillBinding.java new file mode 100644 index 0000000..f009fb0 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillBinding.java @@ -0,0 +1,52 @@ +package com.easyagents.agent.runtime.skill; + +/** + * Skill 与工具的运行时绑定关系。 + */ +public class AgentSkillBinding { + + private final String skillId; + private final String skillName; + private final String skillBoxId; + + /** + * 创建 Skill 绑定关系。 + * + * @param skillId Skill ID + * @param skillName Skill 名称 + * @param skillBoxId SkillBox ID + */ + public AgentSkillBinding(String skillId, String skillName, String skillBoxId) { + this.skillId = skillId; + this.skillName = skillName; + this.skillBoxId = skillBoxId; + } + + /** + * 获取 Skill ID。 + * + * @return Skill ID + */ + public String getSkillId() { + return skillId; + } + + /** + * 获取 Skill 名称。 + * + * @return Skill 名称 + */ + public String getSkillName() { + return skillName; + } + + /** + * 获取 SkillBox ID。 + * + * @return SkillBox ID + */ + public String getSkillBoxId() { + return skillBoxId; + } +} + diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillBoxSpec.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillBoxSpec.java new file mode 100644 index 0000000..0707a4b --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillBoxSpec.java @@ -0,0 +1,147 @@ +package com.easyagents.agent.runtime.skill; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 声明式 SkillBox 配置。 + */ +public class AgentSkillBoxSpec { + + private String skillBoxId; + private List skills = new ArrayList<>(); + private Map> toolBindings = new LinkedHashMap<>(); + private List enabledToolNames = new ArrayList<>(); + private List disabledToolNames = new ArrayList<>(); + private Map> presetParameters = new LinkedHashMap<>(); + private boolean exposeAllSkillMetadata; + + /** + * 获取SkillBox ID。 + * + * @return SkillBox ID 标识 + */ + public String getSkillBoxId() { + return skillBoxId; + } + + /** + * 设置SkillBox ID。 + * + * @param skillBoxId SkillBox ID 标识 + */ + public void setSkillBoxId(String skillBoxId) { + this.skillBoxId = skillBoxId; + } + + /** + * 获取Skill 列表。 + * + * @return Skill 列表 + */ + public List getSkills() { + return skills; + } + + /** + * 设置Skill 列表。 + * + * @param skills Skill 列表 + */ + public void setSkills(List skills) { + this.skills = skills == null ? new ArrayList<>() : new ArrayList<>(skills); + } + + /** + * 获取工具绑定。 + * + * @return 工具绑定 + */ + public Map> getToolBindings() { + return toolBindings; + } + + /** + * 设置工具绑定。 + * + * @param toolBindings 工具绑定 + */ + public void setToolBindings(Map> toolBindings) { + this.toolBindings = toolBindings == null ? new LinkedHashMap<>() : toolBindings; + } + + /** + * 获取启用的工具名称。 + * + * @return 启用的工具名称 + */ + public List getEnabledToolNames() { + return enabledToolNames; + } + + /** + * 设置启用的工具名称。 + * + * @param enabledToolNames 启用的工具名称 + */ + public void setEnabledToolNames(List enabledToolNames) { + this.enabledToolNames = enabledToolNames == null ? new ArrayList<>() : new ArrayList<>(enabledToolNames); + } + + /** + * 获取禁用的工具名称。 + * + * @return 禁用的工具名称 + */ + public List getDisabledToolNames() { + return disabledToolNames; + } + + /** + * 设置禁用的工具名称。 + * + * @param disabledToolNames 禁用的工具名称 + */ + public void setDisabledToolNames(List disabledToolNames) { + this.disabledToolNames = disabledToolNames == null ? new ArrayList<>() : new ArrayList<>(disabledToolNames); + } + + /** + * 获取预设参数。 + * + * @return 预设参数 + */ + public Map> getPresetParameters() { + return presetParameters; + } + + /** + * 设置预设参数。 + * + * @param presetParameters 预设参数 + */ + public void setPresetParameters(Map> presetParameters) { + this.presetParameters = presetParameters == null ? new LinkedHashMap<>() : presetParameters; + } + + /** + * 返回是否all skill metadata is exposed。 + * + * @return 所有元数据暴露时为 true + */ + public boolean isExposeAllSkillMetadata() { + return exposeAllSkillMetadata; + } + + /** + * 设置元数据暴露开关。 + * + * @param exposeAllSkillMetadata 元数据暴露开关 + */ + public void setExposeAllSkillMetadata(boolean exposeAllSkillMetadata) { + this.exposeAllSkillMetadata = exposeAllSkillMetadata; + } + +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillCompiler.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillCompiler.java new file mode 100644 index 0000000..ca4ce45 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillCompiler.java @@ -0,0 +1,17 @@ +package com.easyagents.agent.runtime.skill; + +/** + * 将声明式 Skill 编译为适配器持有的 Skill 类型。 + * + * @param 编译后的 Skill 类型 + */ +public interface AgentSkillCompiler { + + /** + * 编译单个 Skill。 + * + * @param skillSpec Skill 声明 + * @return 编译后的 Skill + */ + T compile(AgentSkillSpec skillSpec); +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillLoadCall.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillLoadCall.java new file mode 100644 index 0000000..5684a1a --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillLoadCall.java @@ -0,0 +1,95 @@ +package com.easyagents.agent.runtime.skill; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Skill 加载工具调用记录。 + */ +public class AgentSkillLoadCall { + + private final String toolCallId; + private final String skillId; + private final String skillName; + private final String skillBoxId; + private final String path; + private final Map input; + + /** + * 创建 Skill 加载工具调用记录。 + * + * @param toolCallId 工具调用 ID + * @param skillId Skill ID + * @param skillName Skill 名称 + * @param skillBoxId SkillBox ID + * @param path 资源路径 + * @param input 工具输入 + */ + public AgentSkillLoadCall(String toolCallId, + String skillId, + String skillName, + String skillBoxId, + String path, + Map input) { + this.toolCallId = toolCallId; + this.skillId = skillId; + this.skillName = skillName; + this.skillBoxId = skillBoxId; + this.path = path; + this.input = input == null ? new LinkedHashMap<>() : new LinkedHashMap<>(input); + } + + /** + * 获取工具调用 ID。 + * + * @return 工具调用 ID + */ + public String getToolCallId() { + return toolCallId; + } + + /** + * 获取 Skill ID。 + * + * @return Skill ID + */ + public String getSkillId() { + return skillId; + } + + /** + * 获取 Skill 名称。 + * + * @return Skill 名称 + */ + public String getSkillName() { + return skillName; + } + + /** + * 获取 SkillBox ID。 + * + * @return SkillBox ID + */ + public String getSkillBoxId() { + return skillBoxId; + } + + /** + * 获取资源路径。 + * + * @return 资源路径 + */ + public String getPath() { + return path; + } + + /** + * 获取工具输入。 + * + * @return 工具输入 + */ + public Map getInput() { + return input; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillRuntimeContext.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillRuntimeContext.java new file mode 100644 index 0000000..ca63f24 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillRuntimeContext.java @@ -0,0 +1,189 @@ +package com.easyagents.agent.runtime.skill; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Skill 运行时上下文。 + */ +public class AgentSkillRuntimeContext { + + /** + * AgentScope SkillBox 内置的 Skill 加载工具名称。 + */ + public static final String LOAD_SKILL_TOOL_NAME = "load_skill_through_path"; + + private final Map skillBindings; + private final Map toolBindings; + private final Map pendingLoadCalls = new ConcurrentHashMap<>(); + private final Map emittedLoadCalls = new ConcurrentHashMap<>(); + private final Map activeSkills = new ConcurrentHashMap<>(); + + private AgentSkillRuntimeContext(Map skillBindings, + Map toolBindings) { + this.skillBindings = skillBindings; + this.toolBindings = toolBindings; + } + + /** + * 从 SkillBox 配置创建运行时上下文。 + * + * @param spec SkillBox 配置 + * @return Skill 运行时上下文 + */ + public static AgentSkillRuntimeContext from(AgentSkillBoxSpec spec) { + if (spec == null) { + return new AgentSkillRuntimeContext(new LinkedHashMap<>(), new LinkedHashMap<>()); + } + Map skillBindings = new LinkedHashMap<>(); + for (AgentSkillSpec skillSpec : spec.getSkills()) { + if (skillSpec.getSkillId() == null || skillSpec.getSkillId().isBlank()) { + continue; + } + skillBindings.put(skillSpec.getSkillId(), + new AgentSkillBinding(skillSpec.getSkillId(), skillSpec.getName(), spec.getSkillBoxId())); + } + Map toolBindings = new LinkedHashMap<>(); + for (Map.Entry> entry : spec.getToolBindings().entrySet()) { + AgentSkillBinding binding = skillBindings.get(entry.getKey()); + if (binding == null || entry.getValue() == null) { + continue; + } + for (String toolName : entry.getValue()) { + if (toolName != null && !toolName.isBlank()) { + toolBindings.put(toolName, binding); + } + } + } + return new AgentSkillRuntimeContext(skillBindings, toolBindings); + } + + /** + * 判断是否为 Skill 加载工具。 + * + * @param toolName 工具名称 + * @return 是 Skill 加载工具时为 true + */ + public boolean isSkillLoadTool(String toolName) { + return LOAD_SKILL_TOOL_NAME.equals(toolName); + } + + /** + * 根据 Skill ID 获取绑定关系。 + * + * @param skillId Skill ID + * @return 绑定关系 + */ + public AgentSkillBinding getSkillBinding(String skillId) { + return skillBindings.get(skillId); + } + + /** + * 根据工具名称获取 Skill 绑定关系。 + * + * @param toolName 工具名称 + * @return 绑定关系 + */ + public AgentSkillBinding getToolBinding(String toolName) { + return toolBindings.get(toolName); + } + + /** + * 判断工具是否绑定到 Skill。 + * + * @param toolName 工具名称 + * @return 绑定到 Skill 时为 true + */ + public boolean isSkillBoundTool(String toolName) { + return toolBindings.containsKey(toolName); + } + + /** + * 激活指定 Skill。 + * + * @param skillId Skill ID + */ + public void activateSkill(String skillId) { + if (skillId != null && !skillId.isBlank()) { + activeSkills.put(skillId, true); + } + } + + /** + * 判断 Skill 是否已激活。 + * + * @param skillId Skill ID + * @return 已激活时为 true + */ + public boolean isSkillActive(String skillId) { + return Boolean.TRUE.equals(activeSkills.get(skillId)); + } + + /** + * 获取已激活 Skill 的工具绑定。 + * + * @param toolName 工具名称 + * @return 已激活 Skill 的绑定关系 + */ + public AgentSkillBinding getActiveToolBinding(String toolName) { + AgentSkillBinding binding = getToolBinding(toolName); + if (binding == null || !isSkillActive(binding.getSkillId())) { + return null; + } + return binding; + } + + /** + * 记录一次 Skill 加载工具调用。 + * + * @param toolCallId 工具调用 ID + * @param input 工具输入 + * @return Skill 加载调用 + */ + public AgentSkillLoadCall rememberLoadCall(String toolCallId, Map input) { + if (toolCallId == null || input == null) { + return null; + } + String skillId = stringValue(input.get("skillId")); + String path = stringValue(input.get("path")); + AgentSkillBinding binding = getSkillBinding(skillId); + AgentSkillLoadCall call = new AgentSkillLoadCall(toolCallId, skillId, + binding == null ? null : binding.getSkillName(), + binding == null ? null : binding.getSkillBoxId(), path, input); + pendingLoadCalls.put(toolCallId, call); + return call; + } + + /** + * 判断 Skill 加载调用事件是否已经发射。 + * + * @param toolCallId 工具调用 ID + * @return 首次发射时为 true + */ + public boolean markLoadCallEmitted(String toolCallId) { + if (toolCallId == null) { + return false; + } + return emittedLoadCalls.putIfAbsent(toolCallId, true) == null; + } + + /** + * 获取并移除一次 Skill 加载工具调用。 + * + * @param toolCallId 工具调用 ID + * @return Skill 加载调用 + */ + public AgentSkillLoadCall removeLoadCall(String toolCallId) { + if (toolCallId == null) { + return null; + } + emittedLoadCalls.remove(toolCallId); + return pendingLoadCalls.remove(toolCallId); + } + + private static String stringValue(Object value) { + return value == null ? null : String.valueOf(value); + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillSpec.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillSpec.java new file mode 100644 index 0000000..bb24763 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/skill/AgentSkillSpec.java @@ -0,0 +1,144 @@ +package com.easyagents.agent.runtime.skill; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 声明式 Skill 配置。 + */ +public class AgentSkillSpec { + + private String skillId; + private String name; + private String description; + private String skillContent; + private Map resources = new LinkedHashMap<>(); + private String source; + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取Skill ID。 + * + * @return Skill ID 标识 + */ + public String getSkillId() { + return skillId; + } + + /** + * 设置Skill ID。 + * + * @param skillId Skill ID 标识 + */ + public void setSkillId(String skillId) { + this.skillId = skillId; + } + + /** + * 获取Skill 名称。 + * + * @return Skill 名称 + */ + public String getName() { + return name; + } + + /** + * 设置Skill 名称。 + * + * @param name Skill 名称 + */ + public void setName(String name) { + this.name = name; + } + + /** + * 获取描述。 + * + * @return 描述 + */ + public String getDescription() { + return description; + } + + /** + * 设置描述。 + * + * @param description 描述 + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * 获取Skill 内容。 + * + * @return Skill 内容 + */ + public String getSkillContent() { + return skillContent; + } + + /** + * 设置Skill 内容。 + * + * @param skillContent Skill 内容 + */ + public void setSkillContent(String skillContent) { + this.skillContent = skillContent; + } + + /** + * 获取资源映射。 + * + * @return 资源映射 + */ + public Map getResources() { + return resources; + } + + /** + * 设置资源映射。 + * + * @param resources 资源映射 + */ + public void setResources(Map resources) { + this.resources = resources == null ? new LinkedHashMap<>() : new LinkedHashMap<>(resources); + } + + /** + * 获取来源。 + * + * @return 来源 + */ + public String getSource() { + return source; + } + + /** + * 设置来源。 + * + * @param source 来源 + */ + public void setSource(String source) { + this.source = source; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : new LinkedHashMap<>(metadata); + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolCategory.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolCategory.java new file mode 100644 index 0000000..9f95628 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolCategory.java @@ -0,0 +1,12 @@ +package com.easyagents.agent.runtime.tool; + +/** + * 动态工具分类。 + */ +public enum AgentToolCategory { + WORKFLOW, + PLUGIN, + MCP, + KNOWLEDGE, + CUSTOM +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolContext.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolContext.java new file mode 100644 index 0000000..357eb18 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolContext.java @@ -0,0 +1,146 @@ +package com.easyagents.agent.runtime.tool; + +import com.easyagents.agent.runtime.AgentRuntimeContext; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 传递给动态工具调用的上下文。 + */ +public class AgentToolContext { + + private String requestId; + private String traceId; + private String sessionId; + private String agentId; + private String toolCallId; + private AgentRuntimeContext runtimeContext = new AgentRuntimeContext(); + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取请求ID。 + * + * @return 请求ID + */ + public String getRequestId() { + return requestId; + } + + /** + * 设置请求ID。 + * + * @param requestId 请求ID + */ + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + /** + * 获取链路ID。 + * + * @return 链路ID + */ + public String getTraceId() { + return traceId; + } + + /** + * 设置链路ID。 + * + * @param traceId 链路ID + */ + public void setTraceId(String traceId) { + this.traceId = traceId; + } + + /** + * 获取会话ID。 + * + * @return 会话ID + */ + public String getSessionId() { + return sessionId; + } + + /** + * 设置会话ID。 + * + * @param sessionId 会话ID + */ + public void setSessionId(String sessionId) { + this.sessionId = sessionId; + } + + /** + * 获取智能体ID。 + * + * @return 智能体ID + */ + public String getAgentId() { + return agentId; + } + + /** + * 设置智能体ID。 + * + * @param agentId 智能体ID + */ + public void setAgentId(String agentId) { + this.agentId = agentId; + } + + /** + * 获取工具调用ID。 + * + * @return 工具调用ID + */ + public String getToolCallId() { + return toolCallId; + } + + /** + * 设置工具调用ID。 + * + * @param toolCallId 工具调用ID + */ + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + /** + * 获取运行时上下文。 + * + * @return 运行时上下文 + */ + public AgentRuntimeContext getRuntimeContext() { + return runtimeContext; + } + + /** + * 设置运行时上下文。 + * + * @param runtimeContext 运行时上下文 + */ + public void setRuntimeContext(AgentRuntimeContext runtimeContext) { + this.runtimeContext = runtimeContext == null ? new AgentRuntimeContext() : runtimeContext; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolInvoker.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolInvoker.java new file mode 100644 index 0000000..73057ab --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolInvoker.java @@ -0,0 +1,19 @@ +package com.easyagents.agent.runtime.tool; + +import java.util.Map; + +/** + * 执行动态工具。 + */ +@FunctionalInterface +public interface AgentToolInvoker { + + /** + * 使用模型生成的参数调用工具。 + * + * @param arguments 工具参数 + * @param context 调用上下文 + * @return 工具结果 + */ + AgentToolResult invoke(Map arguments, AgentToolContext context); +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolResult.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolResult.java new file mode 100644 index 0000000..f6745d6 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolResult.java @@ -0,0 +1,133 @@ +package com.easyagents.agent.runtime.tool; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 动态工具返回的结果。 + */ +public class AgentToolResult { + + private boolean success = true; + private String modelContent; + private Object displayContent; + private String errorMessage; + private Map metadata = new LinkedHashMap<>(); + + /** + * 创建成功的文本结果。 + * + * @param content 模型可见内容 + * @return 结果 + */ + public static AgentToolResult success(String content) { + AgentToolResult result = new AgentToolResult(); + result.setModelContent(content); + result.setDisplayContent(content); + return result; + } + + /** + * 创建失败结果。 + * + * @param errorMessage 错误消息 + * @return 结果 + */ + public static AgentToolResult failure(String errorMessage) { + AgentToolResult result = new AgentToolResult(); + result.setSuccess(false); + result.setErrorMessage(errorMessage); + result.setModelContent(errorMessage); + return result; + } + + /** + * 返回执行是否成功。 + * + * @return 成功时为 true + */ + public boolean isSuccess() { + return success; + } + + /** + * 设置成功标记。 + * + * @param success 成功标记 + */ + public void setSuccess(boolean success) { + this.success = success; + } + + /** + * 获取模型可见内容。 + * + * @return 模型可见内容 + */ + public String getModelContent() { + return modelContent; + } + + /** + * 设置模型可见内容。 + * + * @param modelContent 模型可见内容 + */ + public void setModelContent(String modelContent) { + this.modelContent = modelContent; + } + + /** + * 获取展示内容。 + * + * @return 展示内容 + */ + public Object getDisplayContent() { + return displayContent; + } + + /** + * 设置展示内容。 + * + * @param displayContent 展示内容 + */ + public void setDisplayContent(Object displayContent) { + this.displayContent = displayContent; + } + + /** + * 获取错误消息。 + * + * @return 错误消息 + */ + public String getErrorMessage() { + return errorMessage; + } + + /** + * 设置错误消息。 + * + * @param errorMessage 错误消息 + */ + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + /** + * 获取元数据。 + * + * @return 元数据 + */ + public Map getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolSpec.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolSpec.java new file mode 100644 index 0000000..9755333 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolSpec.java @@ -0,0 +1,184 @@ +package com.easyagents.agent.runtime.tool; + +import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 动态工具声明。 + */ +public class AgentToolSpec { + + private String name; + private String description; + private AgentToolCategory category = AgentToolCategory.CUSTOM; + private Map parametersSchema = new LinkedHashMap<>(); + private Map outputSchema = new LinkedHashMap<>(); + private AgentToolVisibility visibility = AgentToolVisibility.VISIBLE; + private boolean approvalRequired; + private AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest(); + private Map metadata = new LinkedHashMap<>(); + + /** + * 获取工具名称。 + * + * @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; + } + + /** + * 获取分类。 + * + * @return 分类 + */ + public AgentToolCategory getCategory() { + return category; + } + + /** + * 设置分类。 + * + * @param category 分类 + */ + public void setCategory(AgentToolCategory category) { + this.category = category == null ? AgentToolCategory.CUSTOM : category; + } + + /** + * 获取参数 JSON Schema。 + * + * @return 参数 Schema + */ + public Map getParametersSchema() { + return parametersSchema; + } + + /** + * 设置参数 JSON Schema。 + * + * @param parametersSchema 参数 Schema + */ + public void setParametersSchema(Map parametersSchema) { + this.parametersSchema = parametersSchema == null ? new LinkedHashMap<>() : parametersSchema; + } + + /** + * 获取输出 Schema。 + * + * @return 输出 Schema + */ + public Map getOutputSchema() { + return outputSchema; + } + + /** + * 设置输出 Schema。 + * + * @param outputSchema 输出 Schema + */ + public void setOutputSchema(Map outputSchema) { + this.outputSchema = outputSchema == null ? new LinkedHashMap<>() : outputSchema; + } + + /** + * 获取可见性策略。 + * + * @return 可见性策略 + */ + public AgentToolVisibility getVisibility() { + return visibility; + } + + /** + * 设置可见性策略。 + * + * @param visibility 可见性策略 + */ + public void setVisibility(AgentToolVisibility visibility) { + this.visibility = visibility == null ? AgentToolVisibility.VISIBLE : visibility; + } + + /** + * 返回工具执行前是否需要人工审批。 + * + * @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 getMetadata() { + return metadata; + } + + /** + * 设置元数据。 + * + * @param metadata 元数据 + */ + public void setMetadata(Map metadata) { + this.metadata = metadata == null ? new LinkedHashMap<>() : metadata; + } +} diff --git a/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolVisibility.java b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolVisibility.java new file mode 100644 index 0000000..39f09b2 --- /dev/null +++ b/easy-agents-agent-runtime/src/main/java/com/easyagents/agent/runtime/tool/AgentToolVisibility.java @@ -0,0 +1,10 @@ +package com.easyagents.agent.runtime.tool; + +/** + * 工具执行可见性策略。 + */ +public enum AgentToolVisibility { + VISIBLE, + HIDDEN, + MODEL_ONLY +} diff --git a/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeAdapterTest.java b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeAdapterTest.java new file mode 100644 index 0000000..ea713f1 --- /dev/null +++ b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/AgentScopeAdapterTest.java @@ -0,0 +1,951 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.AgentDefinition; +import com.easyagents.agent.runtime.AgentRunRequest; +import com.easyagents.agent.runtime.event.AgentRuntimeEvent; +import com.easyagents.agent.runtime.event.AgentRuntimeEventType; +import com.easyagents.agent.runtime.hitl.AgentResumeToken; +import com.easyagents.agent.runtime.hitl.AgentToolApprovalCoordinator; +import com.easyagents.agent.runtime.hitl.AgentToolApprovalRequest; +import com.easyagents.agent.runtime.hitl.AgentToolApprovalResponse; +import com.easyagents.agent.runtime.knowledge.AgentKnowledgeDocument; +import com.easyagents.agent.runtime.knowledge.AgentKnowledgeRetrievalResult; +import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec; +import com.easyagents.agent.runtime.memory.AgentMemoryCompressionParameter; +import com.easyagents.agent.runtime.memory.AgentMemoryPolicy; +import com.easyagents.agent.runtime.memory.AgentMemorySnapshot; +import com.easyagents.agent.runtime.message.*; +import com.easyagents.agent.runtime.model.AgentModelProviderType; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import com.easyagents.agent.runtime.persistence.AgentPersistencePolicy; +import com.easyagents.agent.runtime.persistence.AgentRuntimeState; +import com.easyagents.agent.runtime.persistence.json.JsonAgentSessionStore; +import com.easyagents.agent.runtime.persistence.memory.InMemoryAgentSessionStore; +import com.easyagents.agent.runtime.skill.AgentSkillBoxSpec; +import com.easyagents.agent.runtime.skill.AgentSkillSpec; +import com.easyagents.agent.runtime.tool.AgentToolResult; +import com.easyagents.agent.runtime.tool.AgentToolSpec; +import io.agentscope.core.ReActAgent; +import io.agentscope.core.agent.Event; +import io.agentscope.core.agent.EventType; +import io.agentscope.core.hook.ErrorEvent; +import io.agentscope.core.hook.HookEvent; +import io.agentscope.core.hook.ReasoningChunkEvent; +import io.agentscope.core.memory.autocontext.AutoContextConfig; +import io.agentscope.core.message.*; +import io.agentscope.core.model.AnthropicChatModel; +import io.agentscope.core.model.GeminiChatModel; +import io.agentscope.core.model.OpenAIChatModel; +import io.agentscope.core.rag.Knowledge; +import io.agentscope.core.rag.model.Document; +import io.agentscope.core.rag.model.RetrieveConfig; +import io.agentscope.core.session.Session; +import io.agentscope.core.skill.SkillBox; +import io.agentscope.core.tool.AgentTool; +import io.agentscope.core.tool.ToolCallParam; +import io.agentscope.core.tool.Toolkit; +import org.junit.Assert; +import org.junit.Test; +import reactor.core.publisher.Sinks; + +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +/** + * 在不发起真实模型网络调用的情况下测试 AgentScope 适配行为。 + */ +public class AgentScopeAdapterTest { + + @Test + public void shouldBuildReActAgentFromDefinition() { + AgentRunRequest request = request(); + AgentScopeReActRuntime runtime = fakeRuntime(); + + ReActAgent agent = runtime.buildAgent(request, Sinks.many().unicast().onBackpressureBuffer()); + + Assert.assertEquals("agent-name", agent.getName()); + Assert.assertEquals("system", agent.getSysPrompt()); + Assert.assertEquals(3, agent.getMaxIters()); + Assert.assertNotNull(agent.getMemory()); + Assert.assertNotNull(agent.getToolkit().getTool("echo")); + } + + @Test + public void shouldRegisterAndCallDynamicTool() { + AgentRunRequest request = request(); + AgentToolSpec toolSpec = request.getAgentDefinition().getToolSpecs().get(0); + AgentTool tool = new AgentScopeToolAdapter().adapt(toolSpec, request.getToolInvokers().get("echo"), request); + ToolUseBlock block = ToolUseBlock.builder() + .id("call-1") + .name("echo") + .input(Map.of("text", "hello")) + .build(); + + String result = ((TextBlock) tool.callAsync(ToolCallParam.builder() + .toolUseBlock(block) + .input(Map.of("text", "hello")) + .build()) + .block() + .getOutput() + .get(0)).getText(); + + Assert.assertTrue(result.contains("hello")); + } + + @Test + public void shouldEmitToolCallAndResultEventsFromToolkitCallback() throws Exception { + AgentRunRequest request = request(); + Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); + ReActAgent agent = fakeRuntime().buildAgent(request, sink); + CompletableFuture> eventsFuture = sink.asFlux().take(2).collectList().toFuture(); + ToolUseBlock block = ToolUseBlock.builder() + .id("call-2") + .name("echo") + .input(Map.of("text", "hello")) + .build(); + + agent.getToolkit().getTool("echo").callAsync(ToolCallParam.builder() + .toolUseBlock(block) + .input(Map.of("text", "hello")) + .build()) + .block(); + List events = eventsFuture.get(3, TimeUnit.SECONDS); + + Assert.assertTrue(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_CALL)); + Assert.assertTrue(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_RESULT)); + } + + @Test + public void shouldEmitNormalToolEventBeforeSkillActivated() throws Exception { + AgentRunRequest request = requestWithSkillBoundTool(); + Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); + ReActAgent agent = fakeRuntime().buildAgent(request, sink); + CompletableFuture> eventsFuture = sink.asFlux().take(2).collectList().toFuture(); + ToolUseBlock block = ToolUseBlock.builder() + .id("call-skill-tool") + .name("echo") + .input(Map.of("text", "hello")) + .build(); + + agent.getToolkit().getTool("echo").callAsync(ToolCallParam.builder() + .toolUseBlock(block) + .input(Map.of("text", "hello")) + .build()) + .block(); + List events = eventsFuture.get(3, TimeUnit.SECONDS); + + Assert.assertEquals(AgentRuntimeEventType.TOOL_CALL, events.get(0).getEventType()); + Assert.assertEquals(AgentRuntimeEventType.TOOL_RESULT, events.get(1).getEventType()); + Assert.assertFalse(events.get(0).getPayload().containsKey("skillId")); + } + + @Test + public void shouldEmitSkillStepAfterSkillActivated() throws Exception { + AgentRunRequest request = requestWithSkillBoundTool(); + com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext skillContext = + com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec()); + skillContext.activateSkill("skill-1"); + Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); + AgentTool tool = new AgentScopeToolAdapter().adapt(request.getAgentDefinition().getToolSpecs().get(0), + request.getToolInvokers().get("echo"), request, AgentToolApprovalCoordinator.disabled(), sink, + skillContext, skillContext.getToolBinding("echo")); + CompletableFuture> eventsFuture = sink.asFlux().take(2).collectList().toFuture(); + + tool.callAsync(ToolCallParam.builder() + .toolUseBlock(ToolUseBlock.builder() + .id("call-skill-tool") + .name("echo") + .input(Map.of("text", "hello")) + .build()) + .input(Map.of("text", "hello")) + .build()) + .block(); + List events = eventsFuture.get(3, TimeUnit.SECONDS); + + Assert.assertTrue(events.stream().allMatch(event -> event.getEventType() == AgentRuntimeEventType.SKILL_STEP)); + Assert.assertEquals("skill-1", events.get(0).getPayload().get("skillId")); + Assert.assertEquals("TOOL_CALL", events.get(0).getPayload().get("stepType")); + Assert.assertEquals("TOOL_RESULT", events.get(1).getPayload().get("stepType")); + } + + @Test + public void shouldEmitToolApprovalWhenToolRequiresApproval() throws Exception { + AgentRunRequest enabled = request(); + AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest(); + approvalRequest.setApprovalPrompt("确认执行?"); + enabled.getAgentDefinition().getToolSpecs().get(0).setApprovalRequired(true); + enabled.getAgentDefinition().getToolSpecs().get(0).setApprovalRequest(approvalRequest); + AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled(); + Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); + AgentTool tool = new AgentScopeToolAdapter().adapt(enabled.getAgentDefinition().getToolSpecs().get(0), + enabled.getToolInvokers().get("echo"), enabled, coordinator, sink); + java.util.concurrent.CountDownLatch approvalLatch = new java.util.concurrent.CountDownLatch(1); + java.util.concurrent.CopyOnWriteArrayList events = new java.util.concurrent.CopyOnWriteArrayList<>(); + sink.asFlux().subscribe(event -> { + events.add(event); + if (event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) { + approvalLatch.countDown(); + } + }); + tool.callAsync(ToolCallParam.builder() + .toolUseBlock(ToolUseBlock.builder() + .id("call-hitl") + .name("echo") + .input(Map.of("text", "hello")) + .build()) + .input(Map.of("text", "hello")) + .build()) + .subscribe(result -> { + }, error -> { + }); + Assert.assertTrue(approvalLatch.await(5, TimeUnit.SECONDS)); + AgentRuntimeEvent approval = events.stream() + .filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) + .findFirst() + .orElseThrow(AssertionError::new); + Assert.assertEquals("确认执行?", approval.getPayload().get("approvalPrompt")); + Assert.assertNotNull(approval.getPayload().get("resumeToken")); + } + + @Test + public void shouldContinueToolExecutionAfterApproval() throws Exception { + AgentRunRequest request = request(); + AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest(); + approvalRequest.setApprovalPrompt("确认执行?"); + request.getAgentDefinition().getToolSpecs().get(0).setApprovalRequired(true); + request.getAgentDefinition().getToolSpecs().get(0).setApprovalRequest(approvalRequest); + + AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled(); + Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); + AgentTool tool = new AgentScopeToolAdapter().adapt(request.getAgentDefinition().getToolSpecs().get(0), + request.getToolInvokers().get("echo"), request, coordinator, sink); + java.util.List events = new java.util.concurrent.CopyOnWriteArrayList<>(); + java.util.concurrent.CountDownLatch approvalLatch = new java.util.concurrent.CountDownLatch(1); + java.util.concurrent.atomic.AtomicReference errorRef = new java.util.concurrent.atomic.AtomicReference<>(); + sink.asFlux().subscribe(event -> { + events.add(event); + if (event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) { + approvalLatch.countDown(); + } + }); + java.util.concurrent.CompletableFuture future = tool.callAsync( + ToolCallParam.builder() + .toolUseBlock(ToolUseBlock.builder() + .id("call-hitl") + .name("echo") + .input(Map.of("text", "hello")) + .build()) + .input(Map.of("text", "hello")) + .build()).toFuture(); + Assert.assertTrue(approvalLatch.await(5, TimeUnit.SECONDS)); + AgentRuntimeEvent approval = events.stream() + .filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) + .findFirst() + .orElseThrow(AssertionError::new); + + AgentToolApprovalResponse response = new AgentToolApprovalResponse(); + AgentResumeToken token = AgentResumeToken.create(); + token.setValue(String.valueOf(approval.getPayload().get("resumeToken"))); + response.setResumeToken(token); + response.setApproved(true); + + coordinator.submit(response); + io.agentscope.core.message.ToolResultBlock resultBlock = future.get(5, TimeUnit.SECONDS); + Assert.assertTrue(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_RESULT)); + Assert.assertNotNull(resultBlock); + } + + @Test + public void shouldCancelToolExecutionAfterRejection() throws Exception { + AgentRunRequest request = request(); + AgentToolApprovalRequest approvalRequest = new AgentToolApprovalRequest(); + approvalRequest.setApprovalPrompt("确认执行?"); + request.getAgentDefinition().getToolSpecs().get(0).setApprovalRequired(true); + request.getAgentDefinition().getToolSpecs().get(0).setApprovalRequest(approvalRequest); + + AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled(); + Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); + AgentTool tool = new AgentScopeToolAdapter().adapt(request.getAgentDefinition().getToolSpecs().get(0), + request.getToolInvokers().get("echo"), request, coordinator, sink); + java.util.List events = new java.util.concurrent.CopyOnWriteArrayList<>(); + sink.asFlux().subscribe(events::add); + java.util.concurrent.CompletableFuture future = tool.callAsync( + ToolCallParam.builder() + .toolUseBlock(ToolUseBlock.builder() + .id("call-hitl") + .name("echo") + .input(Map.of("text", "hello")) + .build()) + .input(Map.of("text", "hello")) + .build()).toFuture(); + AgentRuntimeEvent approval = null; + long deadline = System.currentTimeMillis() + 5000L; + while (System.currentTimeMillis() < deadline) { + approval = events.stream() + .filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) + .findFirst() + .orElse(null); + if (approval != null) { + break; + } + Thread.sleep(50L); + } + if (approval == null) { + throw new AssertionError("未收到审批事件"); + } + + AgentToolApprovalResponse response = new AgentToolApprovalResponse(); + AgentResumeToken token = AgentResumeToken.create(); + token.setValue(String.valueOf(approval.getPayload().get("resumeToken"))); + response.setResumeToken(token); + response.setApproved(false); + response.setRejectReason("拒绝执行"); + + coordinator.submit(response); + try { + future.get(5, TimeUnit.SECONDS); + Assert.fail("拒绝后不应返回工具结果。"); + } catch (java.util.concurrent.ExecutionException exception) { + Assert.assertTrue(exception.getCause() instanceof com.easyagents.agent.runtime.hitl.AgentToolApprovalRejectedException); + } + Assert.assertFalse(events.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.TOOL_RESULT)); + } + + @Test + public void shouldNotDuplicateToolApprovalFromRuntimeEventMapper() throws Exception { + AgentRunRequest request = request(); + AgentScopeReActRuntime runtime = fakeRuntime(); + java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod( + "mapEvent", AgentRunRequest.class, Event.class); + method.setAccessible(true); + ToolResultBlock resultBlock = ToolResultBlock.builder() + .id("call-hitl") + .name("echo") + .output(TextBlock.builder().text("need confirm").build()) + .metadata(Map.of(ToolResultBlock.METADATA_SUSPENDED, true)) + .build(); + Msg message = Msg.builder() + .role(MsgRole.TOOL) + .content(resultBlock) + .build(); + Event event = new Event(EventType.TOOL_RESULT, message, true); + + @SuppressWarnings("unchecked") + List events = (List) method.invoke(runtime, request, event); + + Assert.assertTrue(events.isEmpty()); + } + + @Test + public void shouldMapSkillLoadEventsFromAgentScopeToolEvents() throws Exception { + AgentRunRequest request = requestWithSkillBoundTool(); + AgentScopeReActRuntime runtime = fakeRuntime(); + java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod( + "mapEvent", AgentRunRequest.class, Event.class, + com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext.class); + method.setAccessible(true); + com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext skillContext = + com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec()); + ToolUseBlock toolUseBlock = ToolUseBlock.builder() + .id("skill-call-1") + .name(com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext.LOAD_SKILL_TOOL_NAME) + .input(Map.of("skillId", "skill-1", "path", "SKILL.md")) + .build(); + Msg reasoning = Msg.builder() + .id("reasoning-msg") + .role(MsgRole.ASSISTANT) + .content(toolUseBlock) + .build(); + + @SuppressWarnings("unchecked") + List callEvents = (List) method.invoke( + runtime, request, new Event(EventType.REASONING, reasoning, false), skillContext); + + ToolResultBlock resultBlock = ToolResultBlock.of( + "skill-call-1", + com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext.LOAD_SKILL_TOOL_NAME, + TextBlock.builder().text("Successfully loaded skill: skill-1").build()); + Msg toolResult = Msg.builder() + .id("tool-msg") + .role(MsgRole.TOOL) + .content(resultBlock) + .build(); + @SuppressWarnings("unchecked") + List resultEvents = (List) method.invoke( + runtime, request, new Event(EventType.TOOL_RESULT, toolResult, true), skillContext); + + Assert.assertTrue(callEvents.stream().anyMatch(event -> event.getEventType() == AgentRuntimeEventType.SKILL_CALL)); + AgentRuntimeEvent resultEvent = resultEvents.get(0); + Assert.assertEquals(AgentRuntimeEventType.SKILL_RESULT, resultEvent.getEventType()); + Assert.assertEquals("skill-1", resultEvent.getPayload().get("skillId")); + Assert.assertEquals("SKILL.md", resultEvent.getPayload().get("path")); + Assert.assertTrue(skillContext.isSkillActive("skill-1")); + } + + @Test + public void shouldAttachStructuredMessageOnLastAgentResult() throws Exception { + AgentRunRequest request = request(); + AgentScopeReActRuntime runtime = fakeRuntime(); + java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod( + "mapEvent", AgentRunRequest.class, Event.class); + method.setAccessible(true); + Msg message = Msg.builder() + .id("assistant-msg") + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("final answer").build()) + .build(); + Event event = new Event(EventType.AGENT_RESULT, message, true); + + @SuppressWarnings("unchecked") + List events = (List) method.invoke(runtime, request, event); + + AgentRuntimeEvent delta = events.get(0); + Assert.assertEquals(AgentRuntimeEventType.MESSAGE_DELTA, delta.getEventType()); + Assert.assertNotNull(delta.getMessage()); + Assert.assertEquals("assistant-msg", delta.getMessage().getMessageId()); + } + + @Test + public void shouldNotEmitReasoningFromHook() { + AgentRunRequest request = request(); + AgentScopeEventHook hook = new AgentScopeEventHook(request); + ReActAgent agent = fakeRuntime().buildAgent(request, Sinks.many().unicast().onBackpressureBuffer()); + Msg chunk = Msg.builder() + .role(MsgRole.ASSISTANT) + .textContent("thinking") + .build(); + + hook.onEvent(new ReasoningChunkEvent(agent, "run-1", null, chunk, chunk)).block(); + Assert.assertNotNull(chunk); + } + + @Test + public void shouldAggregateKnowledgeAndPreserveMetadata() { + AgentRunRequest request = request(); + AgentKnowledgeSpec second = knowledge("kb-2", "KB2"); + second.getMetadata().put("permissionScope", "team"); + request.getAgentDefinition().getKnowledgeSpecs().add(second); + request.getKnowledgeRetrievers().put("kb-2", retrievalRequest -> { + AgentKnowledgeDocument document = doc("doc-2", "chunk-2", "content-2", 0.95D); + document.getMetadata().put("pageNo", 2); + return AgentKnowledgeRetrievalResult.of(List.of(document)); + }); + Knowledge knowledge = new AgentScopeKnowledgeAdapter().createAggregateKnowledge(request); + + List documents = knowledge.retrieve("query", RetrieveConfig.builder().limit(2).scoreThreshold(0D).build()).block(); + + Assert.assertEquals(2, documents.size()); + Map payload = documents.get(0).getPayload(); + Assert.assertTrue(payload.containsKey("knowledgeMetadata")); + Assert.assertTrue(payload.containsKey("documentMetadata")); + } + + @Test + public void shouldEmitKnowledgeRetrievalEvent() throws Exception { + AgentRunRequest request = request(); + Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); + Knowledge knowledge = new AgentScopeKnowledgeAdapter().createAggregateKnowledge(request, sink); + CompletableFuture> eventsFuture = sink.asFlux().take(1).collectList().toFuture(); + + knowledge.retrieve("query", RetrieveConfig.builder().limit(1).scoreThreshold(0.2D).build()).block(); + AgentRuntimeEvent event = eventsFuture.get(3, TimeUnit.SECONDS).get(0); + + Assert.assertEquals(AgentRuntimeEventType.KNOWLEDGE_RETRIEVAL, event.getEventType()); + Assert.assertEquals("kb-1", event.getPayload().get("knowledgeId")); + Assert.assertEquals(1, event.getPayload().get("documentCount")); + @SuppressWarnings("unchecked") + List> documents = (List>) event.getPayload().get("documents"); + Assert.assertFalse(documents.get(0).containsKey("content")); + } + + @Test + public void shouldFailWhenKnowledgeRetrieverMissing() { + AgentRunRequest request = request(); + request.getKnowledgeRetrievers().clear(); + Knowledge knowledge = new AgentScopeKnowledgeAdapter().createAggregateKnowledge(request); + + try { + knowledge.retrieve("query", RetrieveConfig.builder().limit(2).scoreThreshold(0D).build()).block(); + Assert.fail("Expected missing knowledge retriever to fail."); + } catch (Exception exception) { + Assert.assertTrue(exception.getMessage().contains("Knowledge retriever is required")); + } + } + + @Test + public void shouldUseKnowledgeSpecLimitAsAggregateDefault() { + AgentRunRequest request = request(); + AgentKnowledgeSpec second = knowledge("kb-2", "KB2"); + second.setLimit(3); + request.getAgentDefinition().getKnowledgeSpecs().get(0).setLimit(2); + request.getAgentDefinition().getKnowledgeSpecs().add(second); + ReActAgent agent = fakeRuntime().buildAgent(request, Sinks.many().unicast().onBackpressureBuffer()); + + Assert.assertNotNull(agent); + } + + @Test + public void shouldUseMinimumKnowledgeScoreThresholdAsAggregateDefault() throws Exception { + AgentRunRequest request = request(); + request.getAgentDefinition().getKnowledgeSpecs().get(0).setScoreThreshold(0.4D); + AgentKnowledgeSpec second = knowledge("kb-2", "KB2"); + second.setScoreThreshold(0.2D); + request.getAgentDefinition().getKnowledgeSpecs().add(second); + AgentScopeReActRuntime runtime = fakeRuntime(); + java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod( + "defaultRetrieveConfig", AgentDefinition.class); + method.setAccessible(true); + + RetrieveConfig config = (RetrieveConfig) method.invoke(runtime, request.getAgentDefinition()); + + Assert.assertEquals(0.2D, config.getScoreThreshold(), 0.0001D); + } + + @Test + public void shouldRoundTripStructuredMessageBlocks() { + AgentMessage message = new AgentMessage(); + message.setMessageId("msg-1"); + message.setRole(AgentMessageRole.ASSISTANT); + message.getMetadata().put("scene", "chat"); + message.getContentBlocks().add(new AgentTextBlock("hello")); + message.getContentBlocks().add(new AgentThinkingBlock("thinking")); + AgentToolUseBlock toolUse = new AgentToolUseBlock("call-1", "echo", Map.of("text", "hi")); + toolUse.setContent("raw-call"); + message.getContentBlocks().add(toolUse); + AgentToolResultBlock toolResult = new AgentToolResultBlock("call-1", "echo"); + toolResult.setSuspended(true); + toolResult.getMetadata().put("status", "ok"); + toolResult.getOutput().add(new AgentTextBlock("result")); + message.getContentBlocks().add(toolResult); + AgentMediaBlock media = new AgentMediaBlock("image"); + media.setUrl("https://example.com/a.png"); + media.setMimeType("image/png"); + message.getContentBlocks().add(media); + + AgentScopeMessageAdapter adapter = new AgentScopeMessageAdapter(); + Msg msg = adapter.toMsg(message); + AgentMessage converted = adapter.toAgentMessage(msg); + + Assert.assertEquals("msg-1", converted.getMessageId()); + Assert.assertEquals(5, converted.getContentBlocks().size()); + Assert.assertEquals("hello", ((AgentTextBlock) converted.getContentBlocks().get(0)).getText()); + Assert.assertEquals("thinking", ((AgentThinkingBlock) converted.getContentBlocks().get(1)).getThinking()); + Assert.assertEquals("call-1", ((AgentToolUseBlock) converted.getContentBlocks().get(2)).getId()); + Assert.assertEquals("echo", ((AgentToolResultBlock) converted.getContentBlocks().get(3)).getName()); + Assert.assertEquals("image", ((AgentMediaBlock) converted.getContentBlocks().get(4)).getMediaKind()); + } + + @Test + public void shouldPreserveInvalidTimestampAsRawMetadata() { + Msg msg = Msg.builder() + .id("msg-invalid-time") + .role(MsgRole.USER) + .textContent("hello") + .timestamp("invalid-time") + .build(); + + AgentMessage converted = new AgentScopeMessageAdapter().toAgentMessage(msg); + + Assert.assertEquals("invalid-time", converted.getMetadata().get("rawTimestamp")); + } + + @Test + public void shouldConvertUnknownBlockToExplicitFallbackText() { + AgentUnknownBlock unknownBlock = new AgentUnknownBlock(); + unknownBlock.setSourceClassName("com.example.CustomBlock"); + unknownBlock.setSourceTypeName("custom"); + + io.agentscope.core.message.ContentBlock converted = new AgentScopeMessageAdapter().toContentBlock(unknownBlock); + + Assert.assertTrue(converted instanceof TextBlock); + Assert.assertTrue(((TextBlock) converted).getText().contains("unsupported content block")); + } + + @Test + public void shouldCreateAutoContextConfigFromPolicy() { + AgentMemoryCompressionParameter parameter = new AgentMemoryCompressionParameter(); + parameter.setMsgThreshold(9); + parameter.setLastKeep(4); + parameter.setMaxToken(1234L); + + AutoContextConfig config = new AgentScopeMemoryAdapter().toAutoContextConfig(parameter); + + Assert.assertEquals(9, config.getMsgThreshold()); + Assert.assertEquals(4, config.getLastKeep()); + Assert.assertEquals(1234L, config.getMaxToken()); + } + + @Test + public void shouldMapOllamaGenerationOptions() throws Exception { + AgentModelSpec modelSpec = new AgentModelSpec(); + modelSpec.setProviderType(AgentModelProviderType.OLLAMA); + modelSpec.setModelName("llama"); + com.easyagents.agent.runtime.model.AgentGenerationOptions options = new com.easyagents.agent.runtime.model.AgentGenerationOptions(); + options.setTemperature(0.2D); + options.setTopP(0.8D); + options.setTopK(20); + options.setMaxTokens(128); + + Object model = new AgentScopeModelFactory().create(modelSpec, options); + Field field = model.getClass().getDeclaredField("defaultOptions"); + field.setAccessible(true); + io.agentscope.core.model.ollama.OllamaOptions ollamaOptions = (io.agentscope.core.model.ollama.OllamaOptions) field.get(model); + + Assert.assertEquals(0.2D, ollamaOptions.getTemperature(), 0.0001D); + Assert.assertEquals(0.8D, ollamaOptions.getTopP(), 0.0001D); + Assert.assertEquals(Integer.valueOf(20), ollamaOptions.getTopK()); + Assert.assertEquals(Integer.valueOf(128), ollamaOptions.getMaxTokens()); + } + + @Test + public void shouldCreateNativeProviderModels() { + Assert.assertTrue(createModel(AgentModelProviderType.OPENAI) instanceof OpenAIChatModel); + Assert.assertTrue(createModel(AgentModelProviderType.ANTHROPIC) instanceof AnthropicChatModel); + Assert.assertTrue(createModel(AgentModelProviderType.GEMINI) instanceof GeminiChatModel); + } + + @Test + public void shouldCreateDomesticProvidersAsOpenAiCompatibleModels() { + Assert.assertTrue(createModel(AgentModelProviderType.GLM) instanceof OpenAIChatModel); + Assert.assertTrue(createModel(AgentModelProviderType.MINIMAX) instanceof OpenAIChatModel); + Assert.assertTrue(createModel(AgentModelProviderType.MOONSHOT) instanceof OpenAIChatModel); + Assert.assertTrue(createModel(AgentModelProviderType.ARK) instanceof OpenAIChatModel); + Assert.assertTrue(createModel(AgentModelProviderType.SILICONFLOW) instanceof OpenAIChatModel); + } + + @Test + public void shouldStoreSessionThroughAdapter() { + InMemoryAgentSessionStore store = new InMemoryAgentSessionStore(); + Session session = new AgentScopeSessionAdapter(store); + + session.save(AgentScopeSessionAdapter.sessionKey("s1"), "state", new TestState("v1")); + + Assert.assertTrue(store.exists("s1")); + Assert.assertEquals("v1", session.get(AgentScopeSessionAdapter.sessionKey("s1"), "state", TestState.class).get().value); + Assert.assertTrue(AgentScopeSessionAdapter.toStatePersistence(AgentPersistencePolicy.memoryOnly()).memoryManaged()); + } + + @Test + public void shouldPersistSessionStateAsJsonFiles() throws Exception { + Path directory = Files.createTempDirectory("easy-agents-session-"); + JsonAgentSessionStore store = new JsonAgentSessionStore(directory); + Session session = new AgentScopeSessionAdapter(store); + Msg first = Msg.builder() + .role(MsgRole.USER) + .content(TextBlock.builder().text("hello").build()) + .build(); + Msg second = Msg.builder() + .role(MsgRole.ASSISTANT) + .content(TextBlock.builder().text("world").build()) + .build(); + + session.save(AgentScopeSessionAdapter.sessionKey("s1"), "message", first); + session.save(AgentScopeSessionAdapter.sessionKey("s1"), "messages", List.of(first, second)); + + JsonAgentSessionStore reloadedStore = new JsonAgentSessionStore(directory); + Session reloadedSession = new AgentScopeSessionAdapter(reloadedStore); + Msg reloaded = reloadedSession.get(AgentScopeSessionAdapter.sessionKey("s1"), "message", Msg.class).orElseThrow(); + List messages = reloadedSession.getList(AgentScopeSessionAdapter.sessionKey("s1"), "messages", Msg.class); + + Assert.assertTrue(reloadedStore.exists("s1")); + Assert.assertEquals("hello", reloaded.getTextContent()); + Assert.assertEquals(2, messages.size()); + Assert.assertEquals("world", messages.get(1).getTextContent()); + } + + @Test(expected = RuntimeException.class) + public void shouldRejectUnsafeSessionKeysForFileStore() throws Exception { + JsonAgentSessionStore store = new JsonAgentSessionStore(Files.createTempDirectory("easy-agents-session-")); + + store.save("../unsafe", "state", AgentRuntimeState.of("state", new TestState("v1"))); + } + + @Test + public void shouldCreateSkillBox() { + AgentSkillSpec skill = new AgentSkillSpec(); + skill.setSkillId("skill-1"); + skill.setName("skill"); + skill.setDescription("desc"); + skill.setSkillContent("content"); + AgentSkillBoxSpec spec = new AgentSkillBoxSpec(); + spec.setSkillBoxId("box"); + spec.setSkills(List.of(skill)); + + SkillBox skillBox = new AgentScopeSkillAdapter().createSkillBox(spec, new Toolkit()); + + Assert.assertNotNull(skillBox); + Assert.assertFalse(skillBox.getAllSkillIds().isEmpty()); + } + + @Test + public void shouldExposeSkillBoundToolThroughAgentToolkit() { + AgentRunRequest request = requestWithSkillBoundTool(); + ReActAgent agent = fakeRuntime().buildAgent(request, Sinks.many().unicast().onBackpressureBuffer()); + + Assert.assertNotNull(agent.getToolkit().getTool("echo")); + } + + @Test + public void shouldAttachSkillMetadataToApprovalEvent() throws Exception { + AgentRunRequest request = requestWithSkillBoundTool(); + request.getAgentDefinition().getToolSpecs().get(0).setApprovalRequired(true); + AgentToolApprovalCoordinator coordinator = AgentToolApprovalCoordinator.enabled(); + Sinks.Many sink = Sinks.many().unicast().onBackpressureBuffer(); + com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext skillContext = activatedSkillContext(request); + AgentTool tool = new AgentScopeToolAdapter().adapt(request.getAgentDefinition().getToolSpecs().get(0), + request.getToolInvokers().get("echo"), request, coordinator, sink, + skillContext, skillContext.getToolBinding("echo")); + java.util.List events = new java.util.concurrent.CopyOnWriteArrayList<>(); + java.util.concurrent.CountDownLatch approvalLatch = new java.util.concurrent.CountDownLatch(1); + sink.asFlux().subscribe(event -> { + events.add(event); + if (event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) { + approvalLatch.countDown(); + } + }); + + tool.callAsync(ToolCallParam.builder() + .toolUseBlock(ToolUseBlock.builder() + .id("call-skill-approval") + .name("echo") + .input(Map.of("text", "hello")) + .build()) + .input(Map.of("text", "hello")) + .build()) + .subscribe(result -> { + }, error -> { + }); + Assert.assertTrue(approvalLatch.await(5, TimeUnit.SECONDS)); + + AgentRuntimeEvent approval = events.stream() + .filter(event -> event.getEventType() == AgentRuntimeEventType.TOOL_APPROVAL_REQUIRED) + .findFirst() + .orElseThrow(AssertionError::new); + Assert.assertEquals("skill-1", approval.getPayload().get("skillId")); + Assert.assertEquals("skill", approval.getPayload().get("skillName")); + } + + @Test + public void shouldEmitCompletedAndFailedEvents() { + AgentRunRequest request = request(); + + Assert.assertTrue(fakeRuntime().stream(request) + .collectList() + .block() + .stream() + .anyMatch(event -> event.getEventType() == AgentRuntimeEventType.COMPLETED)); + + AgentRunRequest failed = request(); + failed.getAgentDefinition().setModelSpec(null); + Assert.assertTrue(fakeRuntime().stream(failed) + .collectList() + .block() + .stream() + .anyMatch(event -> event.getEventType() == AgentRuntimeEventType.FAILED)); + } + + @Test + public void shouldEmitCancelledEventFromRunHandle() throws Exception { + AgentRunRequest request = request(); + request.setCancelReason("user stopped"); + AgentScopeReActRuntime runtime = fakeRuntime(); + java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod( + "cancelled", AgentRunRequest.class); + method.setAccessible(true); + AgentRuntimeEvent cancelledEvent = (AgentRuntimeEvent) method.invoke(runtime, request); + List events = List.of(cancelledEvent); + Assert.assertTrue(events.stream().anyMatch(runtimeEvent -> runtimeEvent.getEventType() == AgentRuntimeEventType.CANCELLED)); + } + + @Test + public void shouldNotEmitFailedFromHook() { + AgentRunRequest request = request(); + AgentScopeEventHook hook = new AgentScopeEventHook(request); + ReActAgent agent = fakeRuntime().buildAgent(request, Sinks.many().unicast().onBackpressureBuffer()); + ErrorEvent errorEvent = new ErrorEvent(agent, new RuntimeException("boom")); + + HookEvent returned = hook.onEvent(errorEvent).block(); + + Assert.assertSame(errorEvent, returned); + } + + @Test + public void shouldAccumulateCompletedTextFromMessageDeltas() throws Exception { + AgentRunRequest request = request(); + AgentScopeReActRuntime runtime = fakeRuntime(); + java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod( + "updateFinalText", StringBuilder.class, AgentRuntimeEvent.class); + method.setAccessible(true); + StringBuilder builder = new StringBuilder(); + AgentRuntimeEvent first = AgentRuntimeEvent.of(AgentRuntimeEventType.MESSAGE_DELTA); + first.getPayload().put("text", "hello "); + AgentRuntimeEvent second = AgentRuntimeEvent.of(AgentRuntimeEventType.MESSAGE_DELTA); + second.getPayload().put("text", "world"); + + method.invoke(runtime, builder, first); + method.invoke(runtime, builder, second); + + Assert.assertEquals("hello world", builder.toString()); + } + + @Test + public void shouldAttachStructuredMessageOnCompletedEvent() throws Exception { + AgentRunRequest request = request(); + AgentMessage message = AgentMessage.text(AgentMessageRole.ASSISTANT, "final answer"); + AgentScopeReActRuntime runtime = fakeRuntime(); + java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod( + "completed", AgentRunRequest.class, String.class, AgentMessage.class); + method.setAccessible(true); + + AgentRuntimeEvent event = (AgentRuntimeEvent) method.invoke(runtime, request, "final answer", message); + + Assert.assertEquals(AgentRuntimeEventType.COMPLETED, event.getEventType()); + Assert.assertEquals("final answer", event.getPayload().get("text")); + Assert.assertSame(message, event.getMessage()); + } + + @Test + public void shouldAttachKnowledgeReferencesToCompletedMessage() throws Exception { + AgentRunRequest request = request(); + AgentMessage message = AgentMessage.text(AgentMessageRole.ASSISTANT, "final answer"); + AgentScopeReActRuntime runtime = fakeRuntime(); + java.lang.reflect.Method method = AgentScopeReActRuntime.class.getDeclaredMethod( + "completed", AgentRunRequest.class, String.class, AgentMessage.class, Map.class); + method.setAccessible(true); + Map refs = new java.util.LinkedHashMap<>(); + AgentKnowledgeReference ref = new AgentKnowledgeReference(); + ref.setKnowledgeId("kb-1"); + ref.setKnowledgeName("KB1"); + ref.setDocumentId("doc-1"); + ref.setDocumentName("doc-1.md"); + ref.setChunkId("chunk-1"); + refs.put("kb-1|doc-1|chunk-1", ref); + + AgentRuntimeEvent event = (AgentRuntimeEvent) method.invoke(runtime, request, "final answer", message, refs); + + Assert.assertEquals(AgentRuntimeEventType.COMPLETED, event.getEventType()); + List knowledgeRefs = event.getMessage().getKnowledgeReferences(); + Assert.assertEquals(1, knowledgeRefs.size()); + Assert.assertEquals("doc-1.md", knowledgeRefs.get(0).getDocumentName()); + Assert.assertEquals("kb-1", knowledgeRefs.get(0).getKnowledgeId()); + } + + private AgentScopeReActRuntime fakeRuntime() { + return new AgentScopeReActRuntime(new FakeAgentScopeModelFactory(), new AgentScopeToolAdapter(), + new AgentScopeKnowledgeAdapter(), new AgentScopeMemoryAdapter(), new AgentScopeSkillAdapter(), + new AgentScopeMessageAdapter()); + } + + private List invokeToolAndCollect(AgentRunRequest request, int count) throws Exception { + Sinks.Many sink = Sinks.many().replay().all(); + ReActAgent agent = fakeRuntime().buildAgent(request, sink); + CompletableFuture> eventsFuture = sink.asFlux().take(count).collectList().toFuture(); + ToolUseBlock block = ToolUseBlock.builder() + .id("call-hitl") + .name("echo") + .input(Map.of("text", "hello")) + .build(); + agent.getToolkit().getTool("echo").callAsync(ToolCallParam.builder() + .toolUseBlock(block) + .input(Map.of("text", "hello")) + .build()) + .block(); + return eventsFuture.get(3, TimeUnit.SECONDS); + } + + private AgentRunRequest request() { + AgentModelSpec modelSpec = new AgentModelSpec(); + modelSpec.setModelName("fake-model"); + AgentToolSpec tool = new AgentToolSpec(); + tool.setName("echo"); + tool.setDescription("Echo text"); + tool.setParametersSchema(Map.of("type", "object", "properties", Map.of("text", Map.of("type", "string")))); + + AgentDefinition definition = new AgentDefinition(); + definition.setAgentId("agent-1"); + definition.setAgentName("agent-name"); + definition.setSystemPrompt("system"); + definition.setModelSpec(modelSpec); + definition.getExecutionOptions().setMaxIters(3); + definition.setToolSpecs(List.of(tool)); + definition.setKnowledgeSpecs(List.of(knowledge("kb-1", "KB1"))); + definition.setMemoryPolicy(AgentMemoryPolicy.inMemory()); + + AgentMemorySnapshot snapshot = new AgentMemorySnapshot(); + snapshot.addMessage(AgentMessage.text(AgentMessageRole.USER, "history")); + + AgentRunRequest request = new AgentRunRequest(); + request.setRequestId("req-1"); + request.setTraceId("trace-1"); + request.setSessionId("session-1"); + request.setAgentDefinition(definition); + request.setMemorySnapshot(snapshot); + request.setUserMessage(AgentMessage.text(AgentMessageRole.USER, "hello")); + request.getToolInvokers().put("echo", (arguments, context) -> AgentToolResult.success(String.valueOf(arguments.get("text")))); + request.getKnowledgeRetrievers().put("kb-1", retrievalRequest -> AgentKnowledgeRetrievalResult.of( + List.of(doc("doc-1", "chunk-1", "content-1", 0.8D)))); + return request; + } + + private AgentRunRequest requestWithSkillBoundTool() { + AgentRunRequest request = request(); + AgentSkillSpec skill = new AgentSkillSpec(); + skill.setSkillId("skill-1"); + skill.setName("skill"); + skill.setDescription("desc"); + skill.setSkillContent("content"); + AgentSkillBoxSpec skillBoxSpec = new AgentSkillBoxSpec(); + skillBoxSpec.setSkillBoxId("box"); + skillBoxSpec.setSkills(List.of(skill)); + skillBoxSpec.setToolBindings(Map.of("skill-1", List.of("echo"))); + request.getAgentDefinition().setSkillBoxSpec(skillBoxSpec); + return request; + } + + private com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext activatedSkillContext(AgentRunRequest request) { + com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext skillContext = + com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext.from(request.getAgentDefinition().getSkillBoxSpec()); + skillContext.activateSkill("skill-1"); + return skillContext; + } + + private Object createModel(AgentModelProviderType providerType) { + AgentModelSpec modelSpec = new AgentModelSpec(); + modelSpec.setProviderType(providerType); + modelSpec.setModelName("test-model"); + modelSpec.setApiKey("test-key"); + return new AgentScopeModelFactory().create(modelSpec, new com.easyagents.agent.runtime.model.AgentGenerationOptions()); + } + + private AgentKnowledgeSpec knowledge(String id, String name) { + AgentKnowledgeSpec spec = new AgentKnowledgeSpec(); + spec.setKnowledgeId(id); + spec.setName(name); + spec.getMetadata().put("publishVersion", "v1"); + return spec; + } + + private AgentKnowledgeDocument doc(String documentId, String chunkId, String content, double score) { + AgentKnowledgeDocument document = new AgentKnowledgeDocument(); + document.setDocumentId(documentId); + document.setDocumentName(documentId + ".md"); + document.setChunkId(chunkId); + document.setContent(content); + document.setScore(score); + document.getMetadata().put("sectionTitle", "section"); + return document; + } + + private static class TestState implements io.agentscope.core.state.State { + private final String value; + + private TestState(String value) { + this.value = value; + } + } +} diff --git a/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/FakeAgentScopeModelFactory.java b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/FakeAgentScopeModelFactory.java new file mode 100644 index 0000000..e2f6a30 --- /dev/null +++ b/easy-agents-agent-runtime/src/test/java/com/easyagents/agent/runtime/agentscope/FakeAgentScopeModelFactory.java @@ -0,0 +1,47 @@ +package com.easyagents.agent.runtime.agentscope; + +import com.easyagents.agent.runtime.model.AgentGenerationOptions; +import com.easyagents.agent.runtime.model.AgentModelSpec; +import io.agentscope.core.message.Msg; +import io.agentscope.core.message.TextBlock; +import io.agentscope.core.model.ChatResponse; +import io.agentscope.core.model.GenerateOptions; +import io.agentscope.core.model.Model; +import io.agentscope.core.model.ToolSchema; +import reactor.core.publisher.Flux; + +import java.util.List; + +/** + * 避免网络调用的测试模型工厂。 + */ +class FakeAgentScopeModelFactory extends AgentScopeModelFactory { + + @Override + public Model create(AgentModelSpec modelSpec, AgentGenerationOptions generationOptions) { + return new FakeModel(modelSpec == null ? "fake" : modelSpec.getModelName()); + } + + private static class FakeModel implements Model { + + private final String modelName; + + private FakeModel(String modelName) { + this.modelName = modelName; + } + + @Override + public Flux stream(List messages, List toolSchemas, GenerateOptions options) { + return Flux.just(ChatResponse.builder() + .id("fake-response") + .content(List.of(TextBlock.builder().text("fake answer").build())) + .finishReason("stop") + .build()); + } + + @Override + public String getModelName() { + return modelName; + } + } +}