From 0dfccc69cb2496ea3e069e4bf511ff5916bbc5fc Mon Sep 17 00:00:00 2001 From: Gogs Date: Fri, 8 May 2026 16:34:07 +0800 Subject: [PATCH 1/3] feat(agui): add context-aware agent factory with request input support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce InputContextualAgentFactory interface for dynamic agent creation - Extend AguiAgentRegistry to support context-aware factory registration - Add overloaded resolveAgent method in AgentResolver with RunAgentInput - Update AguiRequestProcessor to pass full request context to resolver - Add example implementation with UserContext and tabbed UI demo - Register new endpoint /agui/run/context with forwarded properties - Add Lombok dependency for agui example project This enables agents to be dynamically configured based on request context including thread ID, forwarded properties, tools, and runtime parameters. feat(agui): 添加支持请求输入的上下文感知 agent 工厂 - 引入 InputContextualAgentFactory 接口用于动态 agent 创建 - 扩展 AguiAgentRegistry 以支持上下文感知工厂注册 - 在 AgentResolver 中添加带 RunAgentInput 的重载 resolveAgent 方法 - 更新 AguiRequestProcessor 将完整请求上下文传递给解析器 - 添加包含 UserContext 和标签页 UI 演示的示例实现 - 注册带有转发属性的新端点 /agui/run/context - 为 agui 示例项目添加 Lombok 依赖 这使得 agent 能够基于请求上下文(包括线程 ID、转发属性、工具和运行时参数)进行动态配置。 --- .../agui/config/AgentConfiguration.java | 58 ++ .../examples/agui/tools/ExampleTools.java | 11 + .../examples/agui/tools/UserContext.java | 22 + .../agui/src/main/resources/static/index.html | 840 +++++++++++++----- .../core/agui/processor/AgentResolver.java | 14 + .../agui/processor/AguiRequestProcessor.java | 2 +- .../core/agui/registry/AguiAgentRegistry.java | 71 +- .../registry/InputContextualAgentFactory.java | 34 + .../agui/common/DefaultAgentResolver.java | 12 +- 9 files changed, 828 insertions(+), 236 deletions(-) create mode 100644 agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/UserContext.java create mode 100644 agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/registry/InputContextualAgentFactory.java diff --git a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java index dae080f07..1da316649 100644 --- a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java +++ b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java @@ -15,14 +15,21 @@ */ package io.agentscope.examples.agui.config; +import com.fasterxml.jackson.databind.ObjectMapper; import io.agentscope.core.ReActAgent; import io.agentscope.core.agent.Agent; +import io.agentscope.core.agui.converter.AguiToolConverter; +import io.agentscope.core.agui.model.RunAgentInput; import io.agentscope.core.formatter.dashscope.DashScopeChatFormatter; import io.agentscope.core.memory.InMemoryMemory; import io.agentscope.core.model.DashScopeChatModel; +import io.agentscope.core.model.ToolSchema; +import io.agentscope.core.tool.ToolExecutionContext; import io.agentscope.core.tool.Toolkit; import io.agentscope.examples.agui.tools.ExampleTools; +import io.agentscope.examples.agui.tools.UserContext; import io.agentscope.spring.boot.agui.common.AguiAgentRegistryCustomizer; +import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -54,6 +61,9 @@ public AguiAgentRegistryCustomizer aguiAgentRegistryCustomizer() { // Example: an agent specialized for calculations registry.registerFactory("calculator", this::createCalculatorAgent); + + // Example: a factory that creates a new agent instance with context + registry.registerFactoryWithInput("context", this::createAgentWithInput); }; System.out.println("Registered agents with AG-UI registry: default, chat, calculator"); @@ -61,6 +71,7 @@ public AguiAgentRegistryCustomizer aguiAgentRegistryCustomizer() { System.out.println(" - POST /agui/run (uses default-agent-id from config)"); System.out.println(" - POST /agui/run/chat (uses 'chat' agent)"); System.out.println(" - POST /agui/run with X-Agent-Id header"); + System.out.println(" - POST /agui/run/context with context"); return aguiAgentRegistryCustomizer; } @@ -152,6 +163,53 @@ private Agent createCalculatorAgent() { .build(); } + private static final String TOOL_GROUP_NAME = "agui_tools_group"; + + private Agent createAgentWithInput(RunAgentInput input) { + String apiKey = getRequiredApiKey(); + + // Create toolkit with example tools + Toolkit toolkit = new Toolkit(); + toolkit.registerTool(new ExampleTools()); + + AguiToolConverter toolConverter = new AguiToolConverter(); + List toolSchemas = toolConverter.toToolSchemaList(input.getTools()); + if (toolkit.getToolGroup(TOOL_GROUP_NAME) == null) { + toolkit.createToolGroup(TOOL_GROUP_NAME, "Tools for AG-UI", true); + } + + // if (!toolSchemas.isEmpty()) { + // toolkit.registerSchemas(toolSchemas); + // for (ToolSchema toolSchema : toolSchemas) { + // toolkit.addToolToGroup(TOOL_GROUP_NAME, toolSchema.getName()); + // } + // } + + ObjectMapper om = new ObjectMapper(); + ToolExecutionContext.Builder builder = ToolExecutionContext.builder(); + UserContext userContext = om.convertValue(input.getForwardedProps(), UserContext.class); + builder.register(userContext); + + // Create the agent + return ReActAgent.builder() + .name("AG-UI Assistant") + .sysPrompt( + "You are a helpful AI assistant exposed via the AG-UI protocol. " + + "You can help users with various tasks including weather queries " + + "and calculations. Be concise and helpful in your responses.") + .model( + DashScopeChatModel.builder().apiKey(apiKey).modelName("qwen-plus").stream( + true) + .enableThinking(false) + .formatter(new DashScopeChatFormatter()) + .build()) + .toolkit(toolkit) + .toolExecutionContext(builder.build()) + .memory(new InMemoryMemory()) + .maxIters(10) + .build(); + } + private String getRequiredApiKey() { String apiKey = System.getenv("DASHSCOPE_API_KEY"); if (apiKey == null || apiKey.isEmpty()) { diff --git a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java index 079190a4f..a823a74ca 100644 --- a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java +++ b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java @@ -93,6 +93,17 @@ public ToolResultBlock getCurrentTime() { return ToolResultBlock.text("Current time: " + now.format(formatter)); } + /** + * Get the current context info. + * + * @return Current context info + */ + @Tool(name = "get_current_context", description = "Get the current context") + public ToolResultBlock getCurrentContext( UserContext context) { + + return ToolResultBlock.text("Current context: " + context.getContext()+" Current user: "+context.getUser()); + } + /** * Simple expression evaluator. * Supports basic arithmetic: +, -, *, / diff --git a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/UserContext.java b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/UserContext.java new file mode 100644 index 000000000..58d62099a --- /dev/null +++ b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/UserContext.java @@ -0,0 +1,22 @@ +package io.agentscope.examples.agui.tools; + +public class UserContext { + private String context; + private String user; + + public String getContext() { + return context; + } + + public void setContext(String context) { + this.context = context; + } + + public String getUser() { + return user; + } + + public void setUser(String user) { + this.user = user; + } +} diff --git a/agentscope-examples/agui/src/main/resources/static/index.html b/agentscope-examples/agui/src/main/resources/static/index.html index 97c72c425..276c09a8a 100644 --- a/agentscope-examples/agui/src/main/resources/static/index.html +++ b/agentscope-examples/agui/src/main/resources/static/index.html @@ -20,6 +20,7 @@ AgentScope AG-UI Demo + -
-
-

AgentScope AG-UI Demo

-

Chat with an AI agent via the AG-UI protocol

-
+
+
+

AgentScope AG-UI Demo

+

Chat with an AI agent via the AG-UI protocol

+
+ +
+ + +
+
Ready @@ -301,242 +444,487 @@

AgentScope AG-UI Demo

- - - - + + input.focus(); + diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/processor/AgentResolver.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/processor/AgentResolver.java index 4f306b1ad..e3f44606d 100644 --- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/processor/AgentResolver.java +++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/processor/AgentResolver.java @@ -17,6 +17,7 @@ import io.agentscope.core.agent.Agent; import io.agentscope.core.agui.AguiException; +import io.agentscope.core.agui.model.RunAgentInput; /** * Interface for resolving agents from various sources. @@ -37,6 +38,19 @@ public interface AgentResolver { */ Agent resolveAgent(String agentId, String threadId); + /** + * Resolve an agent by its ID with full request context. + * + * @param agentId The agent ID to resolve + * @param threadId The thread ID for session management + * @param input The complete run agent input + * @return The resolved agent + * @throws AguiException.AgentNotFoundException if the agent is not found + */ + default Agent resolveAgent(String agentId, String threadId, RunAgentInput input) { + return resolveAgent(agentId, threadId); + } + /** * Check if a thread has existing memory/conversation history. * diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/processor/AguiRequestProcessor.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/processor/AguiRequestProcessor.java index 54c890492..b5f81ca42 100644 --- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/processor/AguiRequestProcessor.java +++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/processor/AguiRequestProcessor.java @@ -91,7 +91,7 @@ public ProcessResult process(RunAgentInput input, String headerAgentId, String p String agentId = resolveAgentId(input, headerAgentId, pathAgentId); // Resolve agent - Agent agent = agentResolver.resolveAgent(agentId, threadId); + Agent agent = agentResolver.resolveAgent(agentId, threadId, input); // Determine effective input based on server-side memory RunAgentInput effectiveInput = input; diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/registry/AguiAgentRegistry.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/registry/AguiAgentRegistry.java index 2a3878fd8..c29822e96 100644 --- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/registry/AguiAgentRegistry.java +++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/registry/AguiAgentRegistry.java @@ -16,6 +16,7 @@ package io.agentscope.core.agui.registry; import io.agentscope.core.agent.Agent; +import io.agentscope.core.agui.model.RunAgentInput; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -48,6 +49,8 @@ public class AguiAgentRegistry { private final Map singletonAgents = new ConcurrentHashMap<>(); private final Map> agentFactories = new ConcurrentHashMap<>(); + private final Map inputContextualFactories = + new ConcurrentHashMap<>(); /** * Register a singleton agent with the given ID. @@ -86,12 +89,43 @@ public void registerFactory(String agentId, Supplier factory) { agentFactories.put(agentId, factory); } + /** + * Register an agent factory with context access for the given ID. + * + *

This factory will receive the {@link RunAgentInput} when creating agent instances, + * allowing dynamic configuration based on request parameters such as thread ID, + * forwarded properties, or initial state. + * + *

Example: + *

{@code
+     *     registry.registerFactoryWithContext("dynamic-agent", (input) -> {
+     *         String apiKey = input.getForwardedProp("apiKey", "default-key");
+     *         return ReActAgent.builder()
+     *             .model(createModel(apiKey))
+     *             .build();
+     *     });
+     * }
+ * + * @param agentId The agent ID + * @param factory The factory that creates new agent instances with context + */ + public void registerFactoryWithInput(String agentId, InputContextualAgentFactory factory) { + if (agentId == null || agentId.isEmpty()) { + throw new IllegalArgumentException("Agent ID cannot be null or empty"); + } + if (factory == null) { + throw new IllegalArgumentException("Factory cannot be null"); + } + inputContextualFactories.put(agentId, factory); + } + /** * Get an agent by ID. * *

The agent is resolved in the following order: *

    - *
  1. Check for a registered factory and create a new instance
  2. + *
  3. Check for a registered contextual factory and create a new instance (requires input)
  4. + *
  5. Check for a regular factory and create a new instance
  6. *
  7. Return the singleton agent if registered
  8. *
* @@ -99,7 +133,31 @@ public void registerFactory(String agentId, Supplier factory) { * @return An Optional containing the agent, or empty if not found */ public Optional getAgent(String agentId) { - // First check for a factory + return getAgent(agentId, null); + } + + /** + * Get an agent by ID with optional request context. + * + *

If a contextual factory is registered and input is provided, it will be used. + * Otherwise falls back to regular factory or singleton. + * + * @param agentId The agent ID + * @param input The run agent input (may be null for backward compatibility) + * @return An Optional containing the agent, or empty if not found + */ + public Optional getAgent(String agentId, RunAgentInput input) { + // First check for contextual factory + InputContextualAgentFactory contextualFactory = inputContextualFactories.get(agentId); + if (contextualFactory != null) { + RunAgentInput effectiveInput = + input != null + ? input + : RunAgentInput.builder().threadId("default").runId("default").build(); + return Optional.of(contextualFactory.create(effectiveInput)); + } + + // Check for regular factory Supplier factory = agentFactories.get(agentId); if (factory != null) { return Optional.of(factory.get()); @@ -116,7 +174,9 @@ public Optional getAgent(String agentId) { * @return true if an agent is registered */ public boolean hasAgent(String agentId) { - return agentFactories.containsKey(agentId) || singletonAgents.containsKey(agentId); + return inputContextualFactories.containsKey(agentId) + || agentFactories.containsKey(agentId) + || singletonAgents.containsKey(agentId); } /** @@ -126,9 +186,10 @@ public boolean hasAgent(String agentId) { * @return true if an agent was unregistered */ public boolean unregister(String agentId) { + boolean removedContextual = inputContextualFactories.remove(agentId) != null; boolean removedFactory = agentFactories.remove(agentId) != null; boolean removedSingleton = singletonAgents.remove(agentId) != null; - return removedFactory || removedSingleton; + return removedContextual || removedFactory || removedSingleton; } /** @@ -145,6 +206,6 @@ public void clear() { * @return The total count of registered agents */ public int size() { - return agentFactories.size() + singletonAgents.size(); + return inputContextualFactories.size() + agentFactories.size() + singletonAgents.size(); } } diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/registry/InputContextualAgentFactory.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/registry/InputContextualAgentFactory.java new file mode 100644 index 000000000..752535517 --- /dev/null +++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/registry/InputContextualAgentFactory.java @@ -0,0 +1,34 @@ +package io.agentscope.core.agui.registry; + +import io.agentscope.core.agent.Agent; +import io.agentscope.core.agui.model.RunAgentInput; + +/** + * Factory interface for creating agents with access to the current request context. + * + *

This interface allows agent factories to access the {@link RunAgentInput} + * when creating agent instances, enabling dynamic agent configuration based on + * request parameters. + * + *

Example usage: + *

{@code
+ *     registry.registerFactoryWithContext("my-agent", (input) -> {
+ *         String threadId = input.getThreadId();
+ *         Map props = input.getForwardedProps();
+ *         return createCustomAgent(threadId, props);
+ *     });
+ * }
+ * + * @see AguiAgentRegistry #registerFactoryWithContext(String, ContextualAgentFactory) + */ +@FunctionalInterface +public interface InputContextualAgentFactory { + + /** + * Create a new agent instance with access to the request context. + * + * @param input The current run agent input containing thread ID, messages, etc. + * @return A new agent instance + */ + Agent create(RunAgentInput input); +} diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/common/DefaultAgentResolver.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/common/DefaultAgentResolver.java index 9970f8f9b..56942e889 100644 --- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/common/DefaultAgentResolver.java +++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/common/DefaultAgentResolver.java @@ -17,6 +17,7 @@ import io.agentscope.core.agent.Agent; import io.agentscope.core.agui.AguiException; +import io.agentscope.core.agui.model.RunAgentInput; import io.agentscope.core.agui.processor.AgentResolver; import io.agentscope.core.agui.registry.AguiAgentRegistry; import java.util.Objects; @@ -63,20 +64,23 @@ public DefaultAgentResolver( @Override public Agent resolveAgent(String agentId, String threadId) { + return resolveAgent(agentId, threadId, null); + } + + @Override + public Agent resolveAgent(String agentId, String threadId, RunAgentInput input) { if (serverSideMemory && sessionManager != null) { - // Server-side memory mode: use session manager return sessionManager.getOrCreateAgent( threadId, agentId, () -> - registry.getAgent(agentId) + registry.getAgent(agentId, input) .orElseThrow( () -> new AguiException.AgentNotFoundException( agentId))); } else { - // Standard mode: create new agent for each request - return registry.getAgent(agentId) + return registry.getAgent(agentId, input) .orElseThrow(() -> new AguiException.AgentNotFoundException(agentId)); } } From 2fc0bc6172450cd4ea41111cc23f49d391c9eabf Mon Sep 17 00:00:00 2001 From: XiangShuUncle <37469567+XiangShuUncle@users.noreply.github.com> Date: Fri, 8 May 2026 17:25:42 +0800 Subject: [PATCH 2/3] =?UTF-8?q?style(format):=20=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8C=96=E4=BB=A3=E7=A0=81=E5=B9=B6=E6=B7=BB=E5=8A=A0=E7=89=88?= =?UTF-8?q?=E6=9D=83=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 ExampleTools.java 中格式化 getCurrentContext 方法的返回语句 - 在 InputContextualAgentFactory.java 中添加 Apache 许可证版权声明 - 在 UserContext.java 中添加 Apache 许可证版权声明 - 统一代码格式和缩进规范 Signed-off-by: XiangShuUncle <37469567+XiangShuUncle@users.noreply.github.com> --- .../examples/agui/tools/ExampleTools.java | 5 +++-- .../examples/agui/tools/UserContext.java | 15 +++++++++++++++ .../registry/InputContextualAgentFactory.java | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java index a823a74ca..e3f5219af 100644 --- a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java +++ b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/ExampleTools.java @@ -99,9 +99,10 @@ public ToolResultBlock getCurrentTime() { * @return Current context info */ @Tool(name = "get_current_context", description = "Get the current context") - public ToolResultBlock getCurrentContext( UserContext context) { + public ToolResultBlock getCurrentContext(UserContext context) { - return ToolResultBlock.text("Current context: " + context.getContext()+" Current user: "+context.getUser()); + return ToolResultBlock.text( + "Current context: " + context.getContext() + " Current user: " + context.getUser()); } /** diff --git a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/UserContext.java b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/UserContext.java index 58d62099a..ea517e5c3 100644 --- a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/UserContext.java +++ b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/tools/UserContext.java @@ -1,3 +1,18 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.agentscope.examples.agui.tools; public class UserContext { diff --git a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/registry/InputContextualAgentFactory.java b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/registry/InputContextualAgentFactory.java index 752535517..86fc539b9 100644 --- a/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/registry/InputContextualAgentFactory.java +++ b/agentscope-extensions/agentscope-extensions-agui/src/main/java/io/agentscope/core/agui/registry/InputContextualAgentFactory.java @@ -1,3 +1,18 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package io.agentscope.core.agui.registry; import io.agentscope.core.agent.Agent; From b0901a6de896afcee57adc286f2e8559b43144b7 Mon Sep 17 00:00:00 2001 From: XiangShuUncle <37469567+XiangShuUncle@users.noreply.github.com> Date: Fri, 8 May 2026 17:51:55 +0800 Subject: [PATCH 3/3] feat(agui): add context-aware factory support and enhance tool group registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Introduce ToolGroup in AgentConfiguration and implement tool schema registration to tool groups - Add context-aware factory support to AguiAgentRegistry - Implement registerFactoryWithInput method for creating agents with input parameters - Add ContextAwareFactoryTests suite to verify context-aware factory behavior - Support passing threadId, runId, and forwarded properties to factories via RunAgentInput - Implement priority handling logic between context-aware factories and regular factories - Add comprehensive test coverage for null inputs, edge cases, and resource cleanup feat(agui): 添加上下文感知工厂支持并完善工具组注册 - 在 AgentConfiguration 中引入 ToolGroup 并实现工具模式注册到工具组 - 为 AguiAgentRegistry 添加 context-aware factory 功能 - 实现 registerFactoryWithInput 方法支持输入参数传递 - 添加完整的单元测试覆盖上下文感知工厂的各种场景 - 确保上下文感知工厂与普通工厂的优先级处理 - 实现上下文感知工厂的属性转发功能 - 完善工厂注销和计数逻辑以支持新的工厂类型 --- .../agui/config/AgentConfiguration.java | 6 +- .../agui/registry/AguiAgentRegistryTest.java | 200 ++++++++++++++++++ 2 files changed, 205 insertions(+), 1 deletion(-) diff --git a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java index 1da316649..819919f28 100644 --- a/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java +++ b/agentscope-examples/agui/src/main/java/io/agentscope/examples/agui/config/AgentConfiguration.java @@ -25,6 +25,7 @@ import io.agentscope.core.model.DashScopeChatModel; import io.agentscope.core.model.ToolSchema; import io.agentscope.core.tool.ToolExecutionContext; +import io.agentscope.core.tool.ToolGroup; import io.agentscope.core.tool.Toolkit; import io.agentscope.examples.agui.tools.ExampleTools; import io.agentscope.examples.agui.tools.UserContext; @@ -176,7 +177,10 @@ private Agent createAgentWithInput(RunAgentInput input) { List toolSchemas = toolConverter.toToolSchemaList(input.getTools()); if (toolkit.getToolGroup(TOOL_GROUP_NAME) == null) { toolkit.createToolGroup(TOOL_GROUP_NAME, "Tools for AG-UI", true); - } + } // 注册到工具组 + ToolGroup toolGroup = toolkit.getToolGroup(TOOL_GROUP_NAME); + toolSchemas.forEach(toolSchema -> toolGroup.addTool(toolSchema.getName())); + toolkit.registerSchemas(toolSchemas); // if (!toolSchemas.isEmpty()) { // toolkit.registerSchemas(toolSchemas); diff --git a/agentscope-extensions/agentscope-extensions-agui/src/test/java/io/agentscope/core/agui/registry/AguiAgentRegistryTest.java b/agentscope-extensions/agentscope-extensions-agui/src/test/java/io/agentscope/core/agui/registry/AguiAgentRegistryTest.java index 0e769d374..760de8250 100644 --- a/agentscope-extensions/agentscope-extensions-agui/src/test/java/io/agentscope/core/agui/registry/AguiAgentRegistryTest.java +++ b/agentscope-extensions/agentscope-extensions-agui/src/test/java/io/agentscope/core/agui/registry/AguiAgentRegistryTest.java @@ -24,6 +24,9 @@ import static org.mockito.Mockito.mock; import io.agentscope.core.agent.Agent; +import io.agentscope.core.agui.model.RunAgentInput; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; @@ -290,6 +293,203 @@ void testRegisterNullFactoryThrows() { } } + @Nested + @DisplayName("Context-Aware Factory Tests") + class ContextAwareFactoryTests { + + @Test + @DisplayName("Should register and retrieve agent with context-aware factory") + void testRegisterAndGetContextAwareFactory() { + RunAgentInput input = + RunAgentInput.builder().threadId("test-thread").runId("test-run").build(); + + registry.registerFactoryWithInput("context-agent", (inp) -> mock(Agent.class)); + + Optional result = registry.getAgent("context-agent", input); + + assertTrue(result.isPresent()); + } + + @Test + @DisplayName("Should pass input to context-aware factory") + void testContextAwareFactoryReceivesInput() { + RunAgentInput input = + RunAgentInput.builder() + .threadId("test-thread-123") + .runId("test-run-456") + .build(); + + final String[] capturedThreadId = new String[1]; + final String[] capturedRunId = new String[1]; + + registry.registerFactoryWithInput( + "context-agent", + (inp) -> { + capturedThreadId[0] = inp.getThreadId(); + capturedRunId[0] = inp.getRunId(); + return mock(Agent.class); + }); + + registry.getAgent("context-agent", input); + + assertEquals("test-thread-123", capturedThreadId[0]); + assertEquals("test-run-456", capturedRunId[0]); + } + + @Test + @DisplayName("Should create new instance for each call with context-aware factory") + void testContextAwareFactoryReturnsNewInstances() { + RunAgentInput input = + RunAgentInput.builder().threadId("test-thread").runId("test-run").build(); + + registry.registerFactoryWithInput("context-agent", (inp) -> mock(Agent.class)); + + Agent agent1 = registry.getAgent("context-agent", input).orElse(null); + Agent agent2 = registry.getAgent("context-agent", input).orElse(null); + + assertNotSame(agent1, agent2); + } + + @Test + @DisplayName("Should handle forwarded properties in context-aware factory") + void testContextAwareFactoryWithForwardedProps() { + Map forwardedProps = new HashMap<>(); + forwardedProps.put("user", "test-user"); + forwardedProps.put("apiKey", "test-key"); + + RunAgentInput input = + RunAgentInput.builder() + .threadId("test-thread") + .runId("test-run") + .forwardedProps(forwardedProps) + .build(); + + final String[] capturedUser = new String[1]; + final String[] capturedApiKey = new String[1]; + + registry.registerFactoryWithInput( + "context-agent", + (inp) -> { + capturedUser[0] = (String) inp.getForwardedProp("user"); + capturedApiKey[0] = (String) inp.getForwardedProp("apiKey"); + return mock(Agent.class); + }); + + registry.getAgent("context-agent", input); + + assertEquals("test-user", capturedUser[0]); + assertEquals("test-key", capturedApiKey[0]); + } + + @Test + @DisplayName("Should handle null input gracefully") + void testContextAwareFactoryWithNullInput() { + registry.registerFactoryWithInput("context-agent", (inp) -> mock(Agent.class)); + + Optional result = registry.getAgent("context-agent", null); + + assertTrue(result.isPresent()); + } + + @Test + @DisplayName("Context-aware factory should take priority over regular factory") + void testContextAwareFactoryTakesPriority() { + RunAgentInput input = + RunAgentInput.builder().threadId("test-thread").runId("test-run").build(); + + Agent regularAgent = mock(Agent.class); + registry.registerFactory("agent", () -> regularAgent); + registry.registerFactoryWithInput("agent", (inp) -> mock(Agent.class)); + + Agent result = registry.getAgent("agent", input).orElse(null); + + assertNotSame(regularAgent, result); + } + + @Test + @DisplayName("Should fall back to regular factory when no context factory exists") + void testFallbackToRegularFactory() { + RunAgentInput input = + RunAgentInput.builder().threadId("test-thread").runId("test-run").build(); + + Agent expectedAgent = mock(Agent.class); + registry.registerFactory("agent", () -> expectedAgent); + + Agent result = registry.getAgent("agent", input).orElse(null); + + assertSame(expectedAgent, result); + } + + @Test + @DisplayName("Should track context-aware factory invocation count") + void testContextAwareFactoryInvocationCount() { + AtomicInteger counter = new AtomicInteger(0); + RunAgentInput input = + RunAgentInput.builder().threadId("test-thread").runId("test-run").build(); + + registry.registerFactoryWithInput( + "counter-agent", + (inp) -> { + counter.incrementAndGet(); + return mock(Agent.class); + }); + + registry.getAgent("counter-agent", input); + registry.getAgent("counter-agent", input); + registry.getAgent("counter-agent", input); + + assertEquals(3, counter.get()); + } + + @Test + @DisplayName("Should unregister context-aware factory") + void testUnregisterContextAwareFactory() { + registry.registerFactoryWithInput("context-agent", (inp) -> mock(Agent.class)); + + assertTrue(registry.unregister("context-agent")); + assertFalse(registry.hasAgent("context-agent")); + } + + @Test + @DisplayName("Should include context-aware factories in size count") + void testSizeIncludesContextAwareFactories() { + assertEquals(0, registry.size()); + + registry.register("agent1", mock(Agent.class)); + assertEquals(1, registry.size()); + + registry.registerFactory("agent2", () -> mock(Agent.class)); + assertEquals(2, registry.size()); + + registry.registerFactoryWithInput("agent3", (inp) -> mock(Agent.class)); + assertEquals(3, registry.size()); + } + + @Test + @DisplayName("Should throw when registering null context-aware factory") + void testRegisterNullContextAwareFactoryThrows() { + assertThrows( + IllegalArgumentException.class, + () -> registry.registerFactoryWithInput("agent", null)); + } + + @Test + @DisplayName("Should throw when registering context-aware factory with null ID") + void testRegisterContextAwareFactoryNullIdThrows() { + assertThrows( + IllegalArgumentException.class, + () -> registry.registerFactoryWithInput(null, (inp) -> mock(Agent.class))); + } + + @Test + @DisplayName("Should throw when registering context-aware factory with empty ID") + void testRegisterContextAwareFactoryEmptyIdThrows() { + assertThrows( + IllegalArgumentException.class, + () -> registry.registerFactoryWithInput("", (inp) -> mock(Agent.class))); + } + } + @Nested @DisplayName("Thread Safety Tests") class ThreadSafetyTests {