mirror of
https://gitee.com/dromara/liteFlow.git
synced 2026-06-10 03:07:32 +08:00
test(react-agent): migrate test cmps to ModelSpec API
Replaces buildModel(ctx) overrides with model(ctx) returning DeepSeek/OpenAI/Anthropic/Gemini/DashScope spec instances. Also updates TestAgent inner classes in ReActAgentComponentTest and MemoryStoragePersistenceTest. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package com.yomahub.liteflow.agent.component;
|
||||
|
||||
import com.yomahub.liteflow.agent.model.ModelSpec;
|
||||
import com.yomahub.liteflow.flow.element.Node;
|
||||
import com.yomahub.liteflow.property.LiteflowConfig;
|
||||
import com.yomahub.liteflow.property.LiteflowConfigGetter;
|
||||
import com.yomahub.liteflow.property.agent.AgentConfig;
|
||||
import com.yomahub.liteflow.property.agent.MemoryStorageMode;
|
||||
import com.yomahub.liteflow.property.agent.ShellMode;
|
||||
import com.yomahub.liteflow.slot.DataBus;
|
||||
import com.yomahub.liteflow.slot.Slot;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Verifies that {@link MemoryStorageMode#WORKSPACE} persists and restores
|
||||
* conversation history across simulated process restarts within the same JVM.
|
||||
*/
|
||||
class MemoryStoragePersistenceTest {
|
||||
|
||||
@TempDir Path tmp;
|
||||
|
||||
@AfterEach
|
||||
void cleanup() {
|
||||
ReActAgentComponent.AgentSessionManagerHolder.resetForTesting();
|
||||
LiteflowConfigGetter.clean();
|
||||
}
|
||||
|
||||
private void primeConfig(MemoryStorageMode mode) {
|
||||
AgentConfig agent = new AgentConfig();
|
||||
agent.getWorkspace().setRoot(tmp.toString());
|
||||
agent.getWorkspace().setCleanupOnSessionExpire(false);
|
||||
agent.getShell().setMode(ShellMode.DISABLED);
|
||||
agent.getSession().getMemory().setMode(mode);
|
||||
LiteflowConfig cfg = new LiteflowConfig();
|
||||
cfg.setAgent(agent);
|
||||
LiteflowConfigGetter.setLiteflowConfig(cfg);
|
||||
DataBus.init();
|
||||
}
|
||||
|
||||
static class TestAgent extends ReActAgentComponent {
|
||||
@Override
|
||||
@SuppressWarnings("rawtypes")
|
||||
protected ModelSpec model(ReActAgentContext ctx) {
|
||||
return new ModelSpec() {
|
||||
@Override public io.agentscope.core.model.Model resolve(AgentConfig cfg) {
|
||||
return new FakeEchoModel();
|
||||
}
|
||||
};
|
||||
}
|
||||
@Override protected String systemPrompt(ReActAgentContext ctx) { return "you are helpful"; }
|
||||
@Override protected String userPrompt(ReActAgentContext ctx) {
|
||||
return (String) ctx.getSlot().getChainReqData(ctx.getSlot().getChainId());
|
||||
}
|
||||
}
|
||||
|
||||
private void runOnce(String sid, String userInput) throws Exception {
|
||||
int slotIndex = DataBus.offerSlotByBean(Collections.emptyList());
|
||||
try {
|
||||
Slot slot = DataBus.getSlot(slotIndex);
|
||||
slot.putRequestId(sid);
|
||||
String chainId = "testChain";
|
||||
slot.setChainId(chainId);
|
||||
slot.setChainReqData(chainId, userInput);
|
||||
TestAgent agent = new TestAgent();
|
||||
agent.setNodeId("testAgent");
|
||||
Node node = new Node();
|
||||
node.setInstance(agent);
|
||||
node.setSlotIndex(slotIndex);
|
||||
node.setCurrChainId(chainId);
|
||||
agent.setRefNode(node);
|
||||
agent.process();
|
||||
} finally {
|
||||
DataBus.releaseSlot(slotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void workspace_mode_persists_messages_across_restart() throws Exception {
|
||||
primeConfig(MemoryStorageMode.WORKSPACE_FILE);
|
||||
|
||||
runOnce("sid-A", "hello");
|
||||
runOnce("sid-A", "goodbye");
|
||||
|
||||
// Persisted JSONL file should exist with at least the two user messages.
|
||||
Path persisted = tmp.resolve(".agent-session").resolve("sid-A").resolve("memory_messages.jsonl");
|
||||
assertTrue(Files.isRegularFile(persisted), "persisted memory file should exist");
|
||||
List<String> lines = Files.readAllLines(persisted);
|
||||
long userLines = lines.stream().filter(l -> l.contains("\"hello\"") || l.contains("\"goodbye\"")).count();
|
||||
assertEquals(2, userLines, "both user prompts must be persisted");
|
||||
|
||||
// Simulate a JVM restart by resetting the manager singleton.
|
||||
ReActAgentComponent.AgentSessionManagerHolder.resetForTesting();
|
||||
|
||||
// Acquire again with the same sid; loadIfExists should restore messages
|
||||
// before the next call appends new ones.
|
||||
runOnce("sid-A", "third");
|
||||
List<String> after = Files.readAllLines(persisted);
|
||||
assertTrue(after.size() >= lines.size() + 1,
|
||||
"after restart, new messages should append on top of restored history (was "
|
||||
+ lines.size() + ", now " + after.size() + ")");
|
||||
}
|
||||
|
||||
@Test
|
||||
void none_mode_skips_persistence() throws Exception {
|
||||
primeConfig(MemoryStorageMode.NONE);
|
||||
runOnce("sid-B", "hi");
|
||||
Path persistedDir = tmp.resolve(".agent-session");
|
||||
assertFalse(Files.exists(persistedDir),
|
||||
"NONE mode must not create the persistence sub-directory");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.yomahub.liteflow.agent.component;
|
||||
|
||||
import com.yomahub.liteflow.agent.model.ModelSpec;
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import com.yomahub.liteflow.flow.element.Node;
|
||||
import com.yomahub.liteflow.property.LiteflowConfig;
|
||||
@@ -8,7 +9,6 @@ import com.yomahub.liteflow.property.agent.AgentConfig;
|
||||
import com.yomahub.liteflow.property.agent.ShellMode;
|
||||
import com.yomahub.liteflow.slot.DataBus;
|
||||
import com.yomahub.liteflow.slot.Slot;
|
||||
import io.agentscope.core.model.Model;
|
||||
import org.junit.jupiter.api.*;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
@@ -45,7 +45,14 @@ class ReActAgentComponentTest {
|
||||
*/
|
||||
static class TestAgent extends ReActAgentComponent {
|
||||
@Override
|
||||
protected Model buildModel(ReActAgentContext ctx) { return new FakeEchoModel(); }
|
||||
@SuppressWarnings("rawtypes")
|
||||
protected ModelSpec model(ReActAgentContext ctx) {
|
||||
return new ModelSpec() {
|
||||
@Override public io.agentscope.core.model.Model resolve(AgentConfig cfg) {
|
||||
return new FakeEchoModel();
|
||||
}
|
||||
};
|
||||
}
|
||||
@Override
|
||||
protected String systemPrompt(ReActAgentContext ctx) { return "you are helpful"; }
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.yomahub.liteflow.test.agent.cmp;
|
||||
|
||||
import com.yomahub.liteflow.agent.anthropic.Anthropic;
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentComponent;
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentContext;
|
||||
import com.yomahub.liteflow.agent.model.ModelSpec;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("anthropicAgent")
|
||||
public class AnthropicAgentCmp extends ReActAgentComponent {
|
||||
|
||||
@Value("${test.anthropic.model:claude-sonnet-4-5}")
|
||||
private String modelName;
|
||||
|
||||
@Override
|
||||
protected ModelSpec<?> model(ReActAgentContext ctx) {
|
||||
return Anthropic.of(modelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String systemPrompt(ReActAgentContext ctx) {
|
||||
return "你是一名严谨的技术作者,回答简短而准确。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String userPrompt(ReActAgentContext ctx) {
|
||||
return String.valueOf(ctx.getSlot().getChainReqData(ctx.getSlot().getChainId()));
|
||||
}
|
||||
|
||||
@Override protected boolean enableShellTool() { return false; }
|
||||
@Override protected boolean enableWorkspaceFileTools() { return false; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.yomahub.liteflow.test.agent.cmp;
|
||||
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentComponent;
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentContext;
|
||||
import com.yomahub.liteflow.agent.dashscope.DashScope;
|
||||
import com.yomahub.liteflow.agent.model.ModelSpec;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("dashscopeAgent")
|
||||
public class DashScopeAgentCmp extends ReActAgentComponent {
|
||||
|
||||
@Value("${test.dashscope.model:qwen-plus}")
|
||||
private String modelName;
|
||||
|
||||
@Override
|
||||
protected ModelSpec<?> model(ReActAgentContext ctx) {
|
||||
return DashScope.of(modelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String systemPrompt(ReActAgentContext ctx) {
|
||||
return "你是阿里云通义千问助手,请用简短中文回答。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String userPrompt(ReActAgentContext ctx) {
|
||||
return String.valueOf(ctx.getSlot().getChainReqData(ctx.getSlot().getChainId()));
|
||||
}
|
||||
|
||||
@Override protected boolean enableShellTool() { return false; }
|
||||
@Override protected boolean enableWorkspaceFileTools() { return false; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.yomahub.liteflow.test.agent.cmp;
|
||||
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentComponent;
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentContext;
|
||||
import com.yomahub.liteflow.agent.model.ModelSpec;
|
||||
import com.yomahub.liteflow.agent.openai.DeepSeek;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* DeepSeek Agent(OpenAI 兼容协议)。从 application.properties 读模型名,
|
||||
* apiKey 由 liteflow.agent.openai-compatible.deepseek.api-key 注入到 AgentConfig。
|
||||
*/
|
||||
@Component("deepseekAgent")
|
||||
public class DeepSeekAgentCmp extends ReActAgentComponent {
|
||||
|
||||
@Value("${test.deepseek.model:deepseek-chat}")
|
||||
private String modelName;
|
||||
|
||||
@Override
|
||||
protected ModelSpec<?> model(ReActAgentContext ctx) {
|
||||
return DeepSeek.of(modelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String systemPrompt(ReActAgentContext ctx) {
|
||||
return "你是一名简洁的中文助理,回答严格控制在两句话以内。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String userPrompt(ReActAgentContext ctx) {
|
||||
return String.valueOf(ctx.getSlot().getChainReqData(ctx.getSlot().getChainId()));
|
||||
}
|
||||
|
||||
@Override protected boolean enableShellTool() { return false; }
|
||||
@Override protected boolean enableWorkspaceFileTools() { return false; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.yomahub.liteflow.test.agent.cmp;
|
||||
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentComponent;
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentContext;
|
||||
import com.yomahub.liteflow.agent.gemini.Gemini;
|
||||
import com.yomahub.liteflow.agent.model.ModelSpec;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("geminiAgent")
|
||||
public class GeminiAgentCmp extends ReActAgentComponent {
|
||||
|
||||
@Value("${test.gemini.model:gemini-2.5-flash}")
|
||||
private String modelName;
|
||||
|
||||
@Override
|
||||
protected ModelSpec<?> model(ReActAgentContext ctx) {
|
||||
return Gemini.of(modelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String systemPrompt(ReActAgentContext ctx) {
|
||||
return "You are a concise assistant. Answer in Chinese, one sentence.";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String userPrompt(ReActAgentContext ctx) {
|
||||
return String.valueOf(ctx.getSlot().getChainReqData(ctx.getSlot().getChainId()));
|
||||
}
|
||||
|
||||
@Override protected boolean enableShellTool() { return false; }
|
||||
@Override protected boolean enableWorkspaceFileTools() { return false; }
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.yomahub.liteflow.test.agent.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeBooleanComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 布尔节点:判定本次提问是否是计算题(用于 IF EL 路由演示)。
|
||||
*/
|
||||
@Component("isMath")
|
||||
public class IsMathCmp extends NodeBooleanComponent {
|
||||
|
||||
@Override
|
||||
public boolean processBoolean() {
|
||||
Object req = this.getRequestData();
|
||||
if (req == null) return false;
|
||||
String s = req.toString();
|
||||
return s.matches(".*\\d.*[+\\-*/].*\\d.*");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.yomahub.liteflow.test.agent.cmp;
|
||||
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentComponent;
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentContext;
|
||||
import com.yomahub.liteflow.agent.model.ModelSpec;
|
||||
import com.yomahub.liteflow.agent.openai.DeepSeek;
|
||||
import com.yomahub.liteflow.test.agent.tool.CalculatorTool;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数学 Agent:复用 DeepSeek 后端,但额外注入 {@link CalculatorTool}。
|
||||
* 演示通过覆写 tools() 把自定义 @Tool 对象交给 ReAct 推理。
|
||||
*/
|
||||
@Component("mathAgent")
|
||||
public class MathAgentCmp extends ReActAgentComponent {
|
||||
|
||||
@Value("${test.deepseek.model:deepseek-chat}")
|
||||
private String modelName;
|
||||
|
||||
@Override
|
||||
protected ModelSpec<?> model(ReActAgentContext ctx) {
|
||||
return DeepSeek.of(modelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String systemPrompt(ReActAgentContext ctx) {
|
||||
return "你是一名计算助理,凡是涉及算术,必须调用 calculator 工具,不要心算。";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String userPrompt(ReActAgentContext ctx) {
|
||||
return String.valueOf(ctx.getSlot().getChainReqData(ctx.getSlot().getChainId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Object> tools(ReActAgentContext ctx) {
|
||||
return List.of(new CalculatorTool());
|
||||
}
|
||||
|
||||
@Override protected boolean enableShellTool() { return false; }
|
||||
@Override protected boolean enableWorkspaceFileTools() { return false; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.yomahub.liteflow.test.agent.cmp;
|
||||
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentComponent;
|
||||
import com.yomahub.liteflow.agent.component.ReActAgentContext;
|
||||
import com.yomahub.liteflow.agent.model.ModelSpec;
|
||||
import com.yomahub.liteflow.agent.openai.OpenAI;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component("openaiAgent")
|
||||
public class OpenAIAgentCmp extends ReActAgentComponent {
|
||||
|
||||
@Value("${test.openai.model:gpt-4o-mini}")
|
||||
private String modelName;
|
||||
|
||||
@Override
|
||||
protected ModelSpec<?> model(ReActAgentContext ctx) {
|
||||
return OpenAI.of(modelName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String systemPrompt(ReActAgentContext ctx) {
|
||||
return "You are a concise assistant. Answer in Chinese, max two sentences.";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String userPrompt(ReActAgentContext ctx) {
|
||||
return String.valueOf(ctx.getSlot().getChainReqData(ctx.getSlot().getChainId()));
|
||||
}
|
||||
|
||||
@Override protected boolean enableShellTool() { return false; }
|
||||
@Override protected boolean enableWorkspaceFileTools() { return false; }
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.yomahub.liteflow.test.agent.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 准备节点:把链入参原样作为 chainReqData,
|
||||
* 同时模拟一些前置工作(打日志、生成 requestId 等)。
|
||||
*/
|
||||
@Component("prepare")
|
||||
public class PrepareCmp extends NodeComponent {
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
Object req = this.getRequestData();
|
||||
// 把 request data 写到 chainReqData,让 agent 通过 ctx.getSlot().getChainReqData(...) 取到
|
||||
this.getSlot().setChainReqData(this.getSlot().getChainId(), req);
|
||||
System.out.println("[prepare] question=" + req);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.yomahub.liteflow.test.agent.cmp;
|
||||
|
||||
import com.yomahub.liteflow.core.NodeComponent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 后处理节点:从 slot 取出 agent 写入的 responseData 打印出来,
|
||||
* 并把回复字符串塞回 slot.metaDataMap 以便测试断言(也可走 ContextBean)。
|
||||
*/
|
||||
@Component("recordReply")
|
||||
public class RecordReplyCmp extends NodeComponent {
|
||||
|
||||
public static final String NODE_ID = "recordReply";
|
||||
|
||||
@Override
|
||||
public void process() {
|
||||
Object reply = this.getSlot().getResponseData();
|
||||
System.out.println("[recordReply] reply=" + reply);
|
||||
// 把 reply 作为本节点 output 存入 slot,测试可通过 slot.getOutput(NODE_ID) 取出
|
||||
if (reply != null) {
|
||||
this.getSlot().setOutput(NODE_ID, reply);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user