diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentComponent.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentComponent.java index 8c533d7c2..e64dd0d9b 100644 --- a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentComponent.java +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentComponent.java @@ -1,29 +1,32 @@ package com.yomahub.liteflow.agent.component; import com.yomahub.liteflow.agent.exception.AgentConfigException; -import com.yomahub.liteflow.agent.exception.AgentInvocationException; import com.yomahub.liteflow.agent.model.ModelSpec; import com.yomahub.liteflow.core.NodeComponent; import com.yomahub.liteflow.property.LiteflowConfigGetter; import com.yomahub.liteflow.property.agent.AgentConfig; import com.yomahub.liteflow.slot.Slot; import com.yomahub.liteflow.util.ConversationIdGenerator; +import io.agentscope.core.ReActAgent; +import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.hook.Hook; import io.agentscope.core.message.Msg; +import io.agentscope.core.message.UserMessage; import io.agentscope.core.model.Model; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.Map; /** * 封装 agentscope agent 的 LiteFlow 抽象组件。 * - *
状态:v2 迁移进行中(Task 0 green-commit 基底)。 - * 本类已删除所有依赖 agentscope 1.0 已删类型(session 管理 / skill / 工具 / - * 流式事件桥接)的实现,{@link #process()} 当前抛出 - * {@link AgentInvocationException}。后续 Task 2.x 会基于 v2 - * {@code HarnessAgent} 重建 {@code process()}。受保护方法签名保持不变, - * 子类实现与业务侧代码无需改动。 + *
状态:v2(RC3)非流式实现。 + * {@link #process()} 通过 {@link ReactAgentFactory#getOrCreate} 取按组件类缓存的无状态 + * {@code ReActAgent} 单例,按 {@code (conversationId, agentKey)} 构造 + * {@code RuntimeContext(userId, sessionId)},调 {@code agent.call(List<Msg>, RuntimeContext).block()} + * 拿到回复后交给 {@link #handleReply(Msg)} 写回 slot。流式({@code streamEvents})留给 Task 6.1。 * *
子类必须提供 {@link #model()}、{@link #systemPrompt()} 和 {@link #userPrompt()}。 * 可选覆写方法用于自定义工具、钩子和生命周期回调;当前均为空实现或读 @@ -34,8 +37,10 @@ import java.util.Map; * *
会话标识被拆为两层: *
{@link #process()} 方法被声明为 {@code final},由框架统一保证。 @@ -209,14 +214,72 @@ public abstract class ReActAgentComponent extends NodeComponent { /* ===== 框架 final 执行体 ===== */ /** - * v2 迁移期 stub。当前抛出 {@link AgentInvocationException}, - * 由 Task 2.x 基于 v2 {@code HarnessAgent} 重建。 + * 端到端非流式执行(RC3)。 + * + *
流程: + *
非流式:本方法始终用 {@code call(...)}。流式({@code streamEvents})桥接 + * 由 Task 6.1 单独实现,不在本方法范围内。 * *
签名保持 {@code final},与 1.0 一致。 */ @Override public final void process() { - throw new AgentInvocationException( - "ReActAgentComponent v2 migration in progress; process() is rebuilt in Task 2.3"); + AgentConfig cfg = agentConfig(); + ReActAgent agent = ReactAgentFactory.getOrCreate(this, cfg); + + Slot slot = getSlot(); + String cid = resolveConversationId(); + slot.setConversationId(cid); + String akey = agentKey(); + RuntimeContext rc = runtimeContext(cid, akey); + + ReActAgentContext ctx = new ReActAgentContext(slot, cid, akey, workspaceRoot(cfg, cid)); + ctx.setRuntimeContext(rc); + slot.setAttachment(ctxKey(), ctx); + try { + Msg reply = agent.call(List.of(new UserMessage(userPrompt())), rc).block(); + handleReply(reply); + } finally { + slot.removeAttachment(ctxKey()); + } + } + + /** + * 构造本次调用的 {@link RuntimeContext}。标识映射(spec §4.1): + * {@code userId = conversationId}、{@code sessionId = agentKey}。 + * + *
声明为 {@code protected} 便于子类在需要时覆写(例如注入额外 typed extras)。 + */ + protected RuntimeContext runtimeContext(String conversationId, String agentKey) { + return RuntimeContext.builder().userId(conversationId).sessionId(agentKey).build(); + } + + /** + * 返回本次调用使用的 workspace 根目录(仅用于 {@link ReActAgentContext#getWorkspaceDir()} + * 暴露给业务/未来工具;RC3 核心 {@code ReActAgent} 无 {@code .workspace()}, + * 此值不传给 agent builder)。 + * + *
RC3-core 暂用 cfg 配置的单根目录;workspace 用户分桶由后续 GA/HarnessAgent 处理。 + */ + protected Path workspaceRoot(AgentConfig cfg, String conversationId) { + String root = cfg.getWorkspace().getRoot(); + if (root == null || root.isBlank()) { + return Paths.get(System.getProperty("java.io.tmpdir"), "liteflow-agent-workspace"); + } + return Paths.get(root); } } diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentContext.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentContext.java index 09ad8a509..535e7f5af 100644 --- a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentContext.java +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/component/ReActAgentContext.java @@ -1,6 +1,7 @@ package com.yomahub.liteflow.agent.component; import com.yomahub.liteflow.slot.Slot; +import io.agentscope.core.agent.RuntimeContext; import io.agentscope.core.model.ChatUsage; import java.nio.file.Path; @@ -30,6 +31,7 @@ public class ReActAgentContext { private final String conversationId; private final String agentKey; private final Path workspaceDir; + private RuntimeContext runtimeContext; public ReActAgentContext(Slot slot, String conversationId, String agentKey, Path workspaceDir) { this.slot = Objects.requireNonNull(slot, "slot"); @@ -46,6 +48,22 @@ public class ReActAgentContext { public Path getWorkspaceDir() { return workspaceDir; } + /** + * 本次 {@code process()} 调用注入给底层 {@code ReActAgent.call(...)} 的 + * {@link RuntimeContext}。由 {@link ReActAgentComponent#process()} 在构造 ctx 后、 + * 调 {@code agent.call(...)} 前设置。 + * + *
标识映射:{@code runtimeContext.userId = conversationId}、 + * {@code runtimeContext.sessionId = agentKey}(spec §4.1)。 + * + * @return 本次调用使用的 {@link RuntimeContext};在 {@code process()} 生命周期外为 {@code null} + */ + public RuntimeContext getRuntimeContext() { return runtimeContext; } + + public void setRuntimeContext(RuntimeContext runtimeContext) { + this.runtimeContext = runtimeContext; + } + /** * 由框架注入:本次 {@code process()} 调用使用的 token 累加 hook。 * diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/support/LiveTestSupport.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/support/LiveTestSupport.java index 617af0d6b..a73afc5b5 100644 --- a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/support/LiveTestSupport.java +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/support/LiveTestSupport.java @@ -1,5 +1,6 @@ package com.yomahub.liteflow.test.agent.support; +import com.yomahub.liteflow.agent.component.ReactAgentFactory; import com.yomahub.liteflow.agent.model.ModelSpec; import com.yomahub.liteflow.agent.openai.OpenAICompatible; import com.yomahub.liteflow.property.LiteflowConfig; @@ -7,18 +8,17 @@ import com.yomahub.liteflow.property.agent.AgentConfig; import com.yomahub.liteflow.property.agent.PlatformCredential; import org.junit.jupiter.api.Assumptions; -import java.lang.reflect.Method; - /** * 整个模块唯一共享的「凭据/skip/重置」插管。 * - *
按用户约定:不同 package 之间只共享这一层(凭据解析、无 key 即 skip、SessionManager 重置); + *
按用户约定:不同 package 之间只共享这一层(凭据解析、无 key 即 skip、agent 运行时缓存重置); * 其余 agent 组件、辅助节点、探针、flow xml、application.properties 一律每个 package 各自冗余。 * *
提供: *
方法名保留 {@code resetAgentSessionManager} 以兼容现有调用点(如 + * {@code BaseAgentLiveTest.resetAgentRuntime})——RC3 会话状态已不在独立 SessionManager, + * 而由 {@code AgentStateStore}(按 {@code (userId, sessionId)} 寻址)承担,重置单例缓存即可 + * 让下一个测试用全新的 cfg 重新构建 agent。 */ - public static void resetAgentSessionManager() throws Exception { - Class> holder = Class.forName( - "com.yomahub.liteflow.agent.component.ReActAgentComponent$AgentSessionManagerHolder"); - Method reset = holder.getDeclaredMethod("resetForTesting"); - reset.setAccessible(true); - reset.invoke(null); + public static void resetAgentSessionManager() { + ReactAgentFactory.resetForTesting(); } /** diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/v2/CannedReplyModel.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/v2/CannedReplyModel.java new file mode 100644 index 000000000..215c16ad6 --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/v2/CannedReplyModel.java @@ -0,0 +1,51 @@ +package com.yomahub.liteflow.test.agent.v2; + +import io.agentscope.core.message.ContentBlock; +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.ToolSchema; +import reactor.core.publisher.Flux; + +import java.util.List; + +/** + * 确定性的「罐头回复」{@link io.agentscope.core.model.Model} 实现,专供 v2 + * {@code process()} 集成测试在无网络、无真实 LLM 的前提下端到端跑通。 + * + *
{@link #stream(List, List, GenerateOptions)} 无论输入如何,固定 emit 一个 + * {@link ChatResponse}(单 {@link TextBlock} + {@code finishReason="stop"})。 + * ReActAgent 在第一轮拿到带 stop 的文本回复即收敛,{@code call(...).block()} 返回该回复, + * 使 {@code ReActAgentComponent.process()} 能完整走通「factory 取 agent → call → handleReply」 + * 链路而无需任何凭据。 + * + *
实现 {@link io.agentscope.core.model.Model} 全部抽象方法({@code stream} + {@code getModelName});
+ * {@code supportsNativeStructuredOutput} 走 default。无状态、线程安全。
+ */
+final class CannedReplyModel implements io.agentscope.core.model.Model {
+
+ static final String CANNED_REPLY = "[canned-reply] hello from mock model";
+
+ private final String modelName;
+
+ CannedReplyModel(String modelName) {
+ this.modelName = modelName;
+ }
+
+ @Override
+ public Flux 无真实 LLM:{@link ProcessAgentCmp} 覆写 {@code buildModel()} 返回
+ * {@link CannedReplyModel}(确定性罐头回复),整个测试不需要任何 apikey/baseUrl,
+ * 不会因缺失凭据被 {@code Assumptions.assumeTrue} 跳过。
+ *
+ * 断言:
+ *
+ *
+ */
+@Component("processAgent")
+public class ProcessAgentCmp extends ReActAgentComponent {
+
+ public static final AtomicInteger USER_PROMPT_COUNT = new AtomicInteger();
+ public static final AtomicInteger HANDLE_REPLY_COUNT = new AtomicInteger();
+
+ public static void reset() {
+ USER_PROMPT_COUNT.set(0);
+ HANDLE_REPLY_COUNT.set(0);
+ }
+
+ /** 仅满足抽象方法签名;buildModel() 被覆写后这里不会被调用。 */
+ @Override
+ @SuppressWarnings("rawtypes")
+ protected ModelSpec model() {
+ return new ModelSpec() {
+ @Override
+ public Model resolve(AgentConfig c) {
+ return new CannedReplyModel("canned");
+ }
+ };
+ }
+
+ @Override
+ protected Model buildModel() {
+ return new CannedReplyModel("canned");
+ }
+
+ @Override
+ protected String systemPrompt() {
+ return "test system prompt";
+ }
+
+ @Override
+ protected String userPrompt() {
+ USER_PROMPT_COUNT.incrementAndGet();
+ Object reqData = getSlot().getChainReqData(getSlot().getChainId());
+ return reqData == null ? "hi" : reqData.toString();
+ }
+
+ @Override
+ protected boolean enableShellTool() {
+ return false;
+ }
+
+ @Override
+ protected boolean enableWorkspaceFileTools() {
+ return false;
+ }
+
+ @Override
+ protected boolean enableReActLogging() {
+ return false;
+ }
+
+ @Override
+ protected void handleReply(io.agentscope.core.message.Msg reply) {
+ HANDLE_REPLY_COUNT.incrementAndGet();
+ super.handleReply(reply);
+ }
+}
diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/v2/ProcessIntegrationTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/v2/ProcessIntegrationTest.java
new file mode 100644
index 000000000..bfb417239
--- /dev/null
+++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/v2/ProcessIntegrationTest.java
@@ -0,0 +1,92 @@
+package com.yomahub.liteflow.test.agent.v2;
+
+import com.yomahub.liteflow.agent.component.ReActAgentComponent;
+import com.yomahub.liteflow.flow.LiteflowResponse;
+import com.yomahub.liteflow.property.LiteflowConfig;
+import com.yomahub.liteflow.test.agent.support.LiveTestSupport;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.test.context.TestPropertySource;
+
+import javax.annotation.Resource;
+
+/**
+ * Task 2.3 端到端集成测试:验证重写后的 {@link ReActAgentComponent#process()}
+ * 在 Spring Boot 上下文 + 最简 EL chain 下能完整跑通「factory 取无状态 ReActAgent 单例 →
+ * 按 (conversationId, agentKey) 构造 RuntimeContext → agent.call(UserMessage, rc).block() →
+ * handleReply 写回 slot.responseData」链路。
+ *
+ *
+ *
+ */
+@TestPropertySource("classpath:/feature/process/application.properties")
+@SpringBootTest(classes = ProcessIntegrationTest.class)
+@EnableAutoConfiguration
+@ComponentScan("com.yomahub.liteflow.test.agent.v2")
+public class ProcessIntegrationTest {
+
+ @Resource
+ private com.yomahub.liteflow.core.FlowExecutor flowExecutor;
+
+ @Resource
+ private LiteflowConfig liteflowConfig;
+
+ @BeforeEach
+ public void resetRuntime() {
+ // 清空 ReactAgentFactory 进程内单例缓存,保证本类用本测试的 cfg 重新构建 agent。
+ LiveTestSupport.resetAgentSessionManager();
+ ProcessAgentCmp.reset();
+ }
+
+ @Test
+ public void testProcessRunsEndToEndWithMockModel() {
+ String prompt = "ping";
+ LiteflowResponse response = flowExecutor.execute2Resp("processAgentChain", prompt);
+
+ Assertions.assertTrue(response.isSuccess(),
+ "chain failed: " + (response.getCause() == null
+ ? "