From 61824d695698f79c988893bf257aeb77c0c47b63 Mon Sep 17 00:00:00 2001 From: "everywhere.z" Date: Wed, 29 Apr 2026 19:12:29 +0800 Subject: [PATCH] feat(react-agent): add session factory infrastructure, memory storage config, and integration tests - Add MemoryStorageConfig/MemoryStorageMode and per-backend configs (Redis, MySQL, workspace file) - Add AgentSessionFactoryRegistry with NONE, JVM, WORKSPACE_FILE, REDIS, MYSQL implementations - Add integration test suite with EL-orchestrated Spring Boot tests - Remove per-module READMEs in favor of unified guide - Update POMs, CLAUDE.md, AGENTS.md Co-Authored-By: Claude Opus 4.7 --- .gitignore | 3 + AGENTS.md | 237 ++++++++++++++++++ CLAUDE.md | 27 +- .../2026-04-29-react-agent-model-spec.md | 19 +- .../liteflow/property/agent/AgentConfig.java | 3 + .../property/agent/MemoryStorageConfig.java | 40 +++ .../property/agent/MemoryStorageMode.java | 24 ++ .../property/agent/MysqlMemoryConfig.java | 30 +++ .../property/agent/RedisMemoryConfig.java | 28 +++ .../property/agent/SessionConfig.java | 3 + .../property/agent/WorkspaceMemoryConfig.java | 14 ++ .../property/agent/AgentConfigTest.java | 6 + .../liteflow-react-agent-anthropic/README.md | 9 - .../liteflow-react-agent-anthropic/pom.xml | 22 +- .../anthropic/AnthropicModelFactory.java | 10 +- .../liteflow-react-agent-core/README.md | 73 ------ .../liteflow-react-agent-core/pom.xml | 35 +-- .../yomahub/liteflow/agent/package-info.java | 2 +- .../agent/session/AgentSessionManager.java | 67 ++++- .../session/factory/AgentSessionFactory.java | 30 +++ .../factory/AgentSessionFactoryRegistry.java | 54 ++++ .../factory/InMemoryAgentSessionFactory.java | 26 ++ .../factory/MysqlAgentSessionFactory.java | 52 ++++ .../factory/NoneAgentSessionFactory.java | 22 ++ .../factory/RedisAgentSessionFactory.java | 79 ++++++ .../factory/WorkspaceAgentSessionFactory.java | 46 ++++ .../liteflow-react-agent-dashscope/README.md | 9 - .../liteflow-react-agent-dashscope/pom.xml | 22 +- .../liteflow-react-agent-gemini/README.md | 13 - .../liteflow-react-agent-gemini/pom.xml | 22 +- .../liteflow-react-agent-openai/README.md | 25 -- .../liteflow-react-agent-openai/pom.xml | 22 +- .../liteflow-testcase-el-react-agent/pom.xml | 91 +++++++ .../anthropic/AnthropicModelFactoryTest.java | 5 +- .../yomahub/liteflow/test/agent/ApiKeys.java | 20 ++ .../agent/ReActAgentELSpringbootTest.java | 170 +++++++++++++ .../test/agent/tool/CalculatorTool.java | 67 +++++ .../resources/agent/application.properties | 26 ++ .../src/test/resources/agent/flow.el.xml | 49 ++++ .../src/test/resources/logback-test.xml | 15 ++ liteflow-testcase-el/pom.xml | 13 + pom.xml | 139 ++++++---- 42 files changed, 1368 insertions(+), 301 deletions(-) create mode 100644 AGENTS.md create mode 100644 liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageConfig.java create mode 100644 liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageMode.java create mode 100644 liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MysqlMemoryConfig.java create mode 100644 liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/RedisMemoryConfig.java create mode 100644 liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/WorkspaceMemoryConfig.java delete mode 100644 liteflow-react-agent/liteflow-react-agent-anthropic/README.md delete mode 100644 liteflow-react-agent/liteflow-react-agent-core/README.md create mode 100644 liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/AgentSessionFactory.java create mode 100644 liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/AgentSessionFactoryRegistry.java create mode 100644 liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/InMemoryAgentSessionFactory.java create mode 100644 liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/MysqlAgentSessionFactory.java create mode 100644 liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/NoneAgentSessionFactory.java create mode 100644 liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/RedisAgentSessionFactory.java create mode 100644 liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/WorkspaceAgentSessionFactory.java delete mode 100644 liteflow-react-agent/liteflow-react-agent-dashscope/README.md delete mode 100644 liteflow-react-agent/liteflow-react-agent-gemini/README.md delete mode 100644 liteflow-react-agent/liteflow-react-agent-openai/README.md create mode 100644 liteflow-testcase-el/liteflow-testcase-el-react-agent/pom.xml create mode 100644 liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ApiKeys.java create mode 100644 liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentELSpringbootTest.java create mode 100644 liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/tool/CalculatorTool.java create mode 100644 liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/application.properties create mode 100644 liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/flow.el.xml create mode 100644 liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/logback-test.xml diff --git a/.gitignore b/.gitignore index 9a8ec89b0..6382cfe58 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ *.del *.pmd .tern-project +*.factorypath # Logs and databases # @@ -52,8 +53,10 @@ Thumbs.db # Build output directies /target */target +**/target /build */build +**/build # IntelliJ specific files/directories diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..87b0a885f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,237 @@ +# AGENTS.md + +This file provides guidance to Codex (Codex.ai/code) when working with code in this repository. + +## Overview + +LiteFlow (v2.15.3) is a lightweight rules engine framework for complex component-based business orchestration. It uses a DSL to drive workflows with support for hot reload and 11 scripting languages. The project targets Java 8+ (up to JDK 25) and has 2000+ test cases. + +**Official Documentation**: https://liteflow.cc/pages/5816c5/ + +## Commands + +### Build +```bash +# Build entire project (uses 'compile' profile by default, includes test modules) +mvn clean package -DskipTests + +# Build with tests +mvn clean package + +# Build specific module +mvn clean package -DskipTests -pl liteflow-core + +# Build for release (production modules only, excludes tests) +mvn clean package -DskipTests -P release +``` + +### Run Tests +```bash +# Run all tests +mvn test + +# Run tests for specific module (30+ test modules in liteflow-testcase-el/) +mvn test -pl liteflow-testcase-el/liteflow-testcase-el-springboot + +# Run single test class +mvn test -pl liteflow-core -Dtest=FlowExecutorTest + +# Run specific test method +mvn test -pl liteflow-core -Dtest=FlowExecutorTest#testExecute +``` + +### Other Commands +```bash +# Dependency tree +mvn dependency:tree + +# View module structure +ls liteflow-*/pom.xml +``` + +## High-Level Architecture + +### Core Execution Model + +**FlowExecutor** → **Chain** → **Condition Tree** → **Node Components** + +1. **FlowExecutor**: Entry point for executing workflows (`execute2Resp(chainId, param)`) +2. **FlowBus**: Central metadata registry for all chains and nodes (thread-safe, supports hot reload) +3. **Chain**: Named workflow composed of an EL expression that compiles to a Condition tree +4. **Slot/DataBus**: Thread-safe context management using slot pooling (configurable `slotSize`) +5. **NodeComponent**: Base class for all business logic components + +### Key Architectural Patterns + +#### 1. Two-Stage Parsing +Chains are built in two phases to handle circular dependencies: +- **Phase 1**: Register chain IDs (creates placeholder chains) +- **Phase 2**: Build complete condition trees with EL parsing + +#### 2. EL Expression Language +Uses QLExpress to parse declarative workflows. Example operators: +- **THEN(a, b, c)**: Sequential execution +- **WHEN(a, b, c)**: Parallel execution (async) +- **IF(condition, trueNode, falseNode)**: Conditional branching +- **SWITCH(selector).to(a, b, c)**: Multi-way branching +- **FOR(count).DO(loop)**: Fixed-count loop +- **WHILE(condition).DO(loop)**: Condition-based loop +- **ITERATOR(iterator).DO(loop)**: Iterator-based loop +- **RETRY(node).times(3).forException(Ex.class)**: Retry mechanism +- **CATCH(node).DO(handler)**: Exception handling +- **TIMEOUT(node).time(1000)**: Execution timeout (ms) +- **PRE(a, b)**: Pre-conditions (always run before chain, even on error) +- **FINALLY(a, b)**: Finally-conditions (always run after chain) +- **AND(a, b)**, **OR(a, b)**, **NOT(a)**: Boolean logic for IF conditions +- **node.tag("t")**, **.data("k","v")**, **.id("id")**: Per-node modifiers + +Rules are defined in XML/JSON/YML: +```xml + + THEN(a, WHEN(b, c).maxWaitSeconds(5), IF(e, f, g)); + +``` + +#### 3. Component Types +All extend `NodeComponent` but have specialized behaviors: +- **NodeComponent**: Standard synchronous component (`process()` method) +- **NodeBooleanComponent**: Returns boolean for IF conditions (`processBoolean()`) +- **NodeSwitchComponent**: Returns string for SWITCH routing (`processSwitch()`) +- **NodeIteratorComponent**: Provides iteration logic for ITERATOR construct +- **NodeForComponent**: Controls FOR loop behavior +- **ScriptComponent**: Script-based components (Groovy, JS, Python, etc.) + +**Declarative Component Pattern**: Any Spring bean can become a component without extending base classes by using `@LiteflowCmpDefine` (class-level, specifies `NodeTypeEnum`) and `@LiteflowMethod` (method-level, maps to `LiteFlowMethodEnum`). This avoids class hierarchy constraints. + +**Component Lifecycle Hooks** (override in NodeComponent subclasses or via `@LiteflowMethod`): +- `isAccess()` – gate check before execution; return `false` to skip +- `beforeProcess()` / `afterProcess()` – pre/post hooks per component +- `onSuccess()` / `onError()` – outcome callbacks +- `isContinueOnError()` – whether WHEN continues if this node fails +- `isEnd()` – signals chain should stop after this node +- `rollback()` – called in reverse order on failure + +**`@FallbackCmp`**: Annotate a fallback component that activates when the primary component is not found or throws. + +#### 4. Slot-Based Context +Thread-safe execution context: +- `DataBus.offerSlot(chainId)` acquires a slot from pool +- Slot contains execution metadata, context beans, and step tracking +- `DataBus.releaseSlot(slotIndex)` returns slot to pool +- Pool size configurable via `slotSize` property + +#### 5. Parse Mode Strategies +Three modes (`ParseModeEnum`): +- **PARSE_ALL_ON_START**: Parse all chains at startup (default) +- **PARSE_ONE_ON_FIRST_EXEC**: Lazy parse each chain on first use (faster startup) +- **PARSE_ALL_ON_FIRST_EXEC**: Parse all chains on first any chain execution + +### Module Structure + +#### Core Modules +- **liteflow-core**: Core engine (FlowExecutor, FlowBus, DataBus, Slot, Condition system, component model) +- **liteflow-el-builder**: Programmatic chain builder API using QLExpress + +#### Rule Source Plugins (6 implementations in `liteflow-rule-plugin/`) +- **liteflow-rule-zk**: ZooKeeper configuration source +- **liteflow-rule-sql**: SQL database configuration source +- **liteflow-rule-nacos**: Nacos configuration center +- **liteflow-rule-etcd**: etcd configuration source +- **liteflow-rule-apollo**: Apollo configuration center +- **liteflow-rule-redis**: Redis configuration source + +#### Script Plugins (11 languages in `liteflow-script-plugin/`) +- **liteflow-script-groovy**: Groovy scripting +- **liteflow-script-javascript**: Rhino JavaScript (JSR223) +- **liteflow-script-graaljs**: GraalVM JavaScript +- **liteflow-script-qlexpress**: Alibaba QLExpress +- **liteflow-script-python**: Jython (Python on JVM) +- **liteflow-script-lua**: LuaJ +- **liteflow-script-aviator**: Aviator expression language +- **liteflow-script-java**: Janino (Java compiler) +- **liteflow-script-javax**: JSR223 standard Java compiler +- **liteflow-script-javax-pro**: Liquor-based Java compiler (enhanced) +- **liteflow-script-kotlin**: Kotlin scripting + +#### Framework Integration +- **liteflow-spring**: Spring framework integration (component scanning, bean lifecycle, AOP) +- **liteflow-spring-boot-starter**: Spring Boot auto-configuration with `@ConfigurationProperties` +- **liteflow-solon-plugin**: Solon framework integration (lightweight alternative to Spring) + +#### Testing Infrastructure +30+ test modules in `liteflow-testcase-el/` organized by: +1. **Framework**: springboot, springnative, solon, nospring +2. **Config Sources**: zk, nacos, etcd, apollo, redis, sql +3. **Scripts**: One module per language + multi-language scenarios +4. **Features**: builder, declare, routechain, monitoring, timeout, etc. + +Test pattern: +```java +@SpringBootTest +@TestPropertySource(value = "classpath:/application.properties") +public class MyTest { + @Resource private FlowExecutor flowExecutor; + + @Test + public void test() { + LiteflowResponse response = flowExecutor.execute2Resp("chainId", "arg"); + Assertions.assertTrue(response.isSuccess()); + } +} +``` + +### Important Design Patterns + +#### SPI Pattern for Extensibility +Used extensively for: +- `ContextAware`: Framework abstraction (Spring vs non-Spring) +- `PathContentParser`: Custom file path resolution +- `CmpAroundAspect`: Global component lifecycle hooks +- `DeclComponentParser`: Declarative component parsing +- Script executors for each language + +#### Lifecycle Hooks +Multiple extension points: +- `PostProcessChainBuildLifeCycle`: Before/after chain building +- `PostProcessNodeBuildLifeCycle`: Before/after node building +- `PostProcessChainExecuteLifeCycle`: Before/after chain execution +- `PostProcessFlowExecuteLifeCycle`: Before/after flow execution + +#### Rollback Mechanism +Components implement `rollback()` for automatic rollback on failure (executed in reverse order via `executeSteps` deque). + +#### Hot Reload Support +- `MonitorFile` watches rule files for changes +- `reloadRule()` refreshes without restart +- Copy-on-write collections (unless `fastLoad=true`) prevent concurrent modification + +#### Metadata Caching +- `FlowBus` uses CopyOnWriteHashMap (or regular HashMap with `fastLoad`) +- EL MD5 caching for expression reuse +- Chain caching configurable (`chainCacheEnabled`, `chainCacheCapacity`) + +### Critical Configuration (`LiteflowConfig`) +- **ruleSource**: Rule file locations (supports XML, JSON, YML) +- **parseMode**: Parsing strategy (affects startup performance) +- **slotSize**: Context slot pool size +- **enableMonitorFile**: Hot reload of rule files +- **supportMultipleType**: Mix XML/JSON/YML rules +- **whenMaxWaitTime**: Timeout for WHEN parallel execution +- **fastLoad**: Disable CopyOnWrite for faster startup +- **enableVirtualThread**: Use virtual threads (JDK 21+) + +### Key File Locations +- Core engine: `liteflow-core/src/main/java/com/yomahub/liteflow/flow/` +- FlowExecutor: `liteflow-core/src/main/java/com/yomahub/liteflow/core/FlowExecutor.java` +- FlowBus: `liteflow-core/src/main/java/com/yomahub/liteflow/flow/FlowBus.java` +- Component base: `liteflow-core/src/main/java/com/yomahub/liteflow/core/NodeComponent.java` +- Condition system: `liteflow-core/src/main/java/com/yomahub/liteflow/flow/element/condition/` +- EL parser: `liteflow-core/src/main/java/com/yomahub/liteflow/parser/el/` + +### Important Conventions +- **Naming**: Components identified by `nodeId`, chains by `chainId` +- **Thread Safety**: Extensive ThreadLocal and concurrent collections +- **Fail-Fast**: Validation at parse time with detailed error messages +- **Fluent APIs**: Builder pattern for chain construction (EL builder) +- **Namespaces**: Chains can be organized into namespaces +- **Versioning**: Uses `${revision}` placeholder (currently 2.15.3) via flatten-maven-plugin diff --git a/CLAUDE.md b/CLAUDE.md index f9d2965b9..8783cc488 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,12 +71,19 @@ Chains are built in two phases to handle circular dependencies: #### 2. EL Expression Language Uses QLExpress to parse declarative workflows. Example operators: - **THEN(a, b, c)**: Sequential execution -- **WHEN(a, b, c)**: Parallel execution -- **IF(condition, THEN(x), ELSE(y))**: Conditional branching +- **WHEN(a, b, c)**: Parallel execution (async) +- **IF(condition, trueNode, falseNode)**: Conditional branching - **SWITCH(selector).to(a, b, c)**: Multi-way branching -- **FOR(count).DO(loop)**: Loop execution -- **RETRY(node).times(3).forException()**: Retry mechanism +- **FOR(count).DO(loop)**: Fixed-count loop +- **WHILE(condition).DO(loop)**: Condition-based loop +- **ITERATOR(iterator).DO(loop)**: Iterator-based loop +- **RETRY(node).times(3).forException(Ex.class)**: Retry mechanism - **CATCH(node).DO(handler)**: Exception handling +- **TIMEOUT(node).time(1000)**: Execution timeout (ms) +- **PRE(a, b)**: Pre-conditions (always run before chain, even on error) +- **FINALLY(a, b)**: Finally-conditions (always run after chain) +- **AND(a, b)**, **OR(a, b)**, **NOT(a)**: Boolean logic for IF conditions +- **node.tag("t")**, **.data("k","v")**, **.id("id")**: Per-node modifiers Rules are defined in XML/JSON/YML: ```xml @@ -94,6 +101,18 @@ All extend `NodeComponent` but have specialized behaviors: - **NodeForComponent**: Controls FOR loop behavior - **ScriptComponent**: Script-based components (Groovy, JS, Python, etc.) +**Declarative Component Pattern**: Any Spring bean can become a component without extending base classes by using `@LiteflowCmpDefine` (class-level, specifies `NodeTypeEnum`) and `@LiteflowMethod` (method-level, maps to `LiteFlowMethodEnum`). This avoids class hierarchy constraints. + +**Component Lifecycle Hooks** (override in NodeComponent subclasses or via `@LiteflowMethod`): +- `isAccess()` – gate check before execution; return `false` to skip +- `beforeProcess()` / `afterProcess()` – pre/post hooks per component +- `onSuccess()` / `onError()` – outcome callbacks +- `isContinueOnError()` – whether WHEN continues if this node fails +- `isEnd()` – signals chain should stop after this node +- `rollback()` – called in reverse order on failure + +**`@FallbackCmp`**: Annotate a fallback component that activates when the primary component is not found or throws. + #### 4. Slot-Based Context Thread-safe execution context: - `DataBus.offerSlot(chainId)` acquires a slot from pool diff --git a/docs/superpowers/plans/2026-04-29-react-agent-model-spec.md b/docs/superpowers/plans/2026-04-29-react-agent-model-spec.md index ce4328bf0..8d2fb319b 100644 --- a/docs/superpowers/plans/2026-04-29-react-agent-model-spec.md +++ b/docs/superpowers/plans/2026-04-29-react-agent-model-spec.md @@ -90,7 +90,7 @@ class ModelSpecTest { /** 仅用于测试的最小 ModelSpec 子类。 */ static class TestSpec extends ModelSpec { - @Override protected Model resolve(AgentConfig cfg) { return null; } + @Override public Model resolve(AgentConfig cfg) { return null; } } @Test @@ -192,11 +192,16 @@ public abstract class ModelSpec> { * 把本描述符解析为 agentscope {@link Model} 实例。 * 实现需从 {@link AgentConfig} 中读取对应平台的 credential, * 并把共性 + 个性参数翻译成 agentscope 的 GenerateOptions。 + *

+ * 本方法是框架 SPI:{@code ReActAgentComponent} 在不同包中调用, + * 因此必须为 {@code public}(subclass override 同样需要 {@code public})。 */ - protected abstract Model resolve(AgentConfig cfg); + public abstract Model resolve(AgentConfig cfg); } ``` +> **重要**:`resolve` 必须是 `public`,因为 Task 8 中 `ReActAgentComponent`(在 `com.yomahub.liteflow.agent.component` 包)会调用 `model(ctx).resolve(agentConfig())`。Java 不允许 override 缩小可见性,所以后续 Tasks 3-7 中所有 spec 子类的 `resolve` override 也必须是 `public`,测试中的 `TestSpec.resolve` 同理。 + - [ ] **Step 4: 再次运行测试,确认通过** ```bash @@ -513,7 +518,7 @@ public class OpenAISpec extends ModelSpec { public Double getPresencePenalty() { return presencePenalty; } @Override - protected Model resolve(AgentConfig cfg) { + public Model resolve(AgentConfig cfg) { PlatformCredential cred = CredentialResolver.requireFirstClass( cfg.getOpenai(), "liteflow.agent.openai"); return buildModel(cred.getApiKey(), cred.getBaseUrl()); @@ -740,7 +745,7 @@ public class OpenAICompatibleSpec extends OpenAISpec { } @Override - protected Model resolve(AgentConfig cfg) { + public Model resolve(AgentConfig cfg) { PlatformCredential cred = CredentialResolver.requireCompatible( cfg.getOpenaiCompatible(), configKey, "liteflow.agent.openai-compatible"); String baseUrl = (cred.getBaseUrl() != null && !cred.getBaseUrl().isBlank()) @@ -1018,7 +1023,7 @@ public class AnthropicSpec extends ModelSpec { public Boolean getThinkingEnabled() { return thinkingEnabled; } @Override - protected Model resolve(AgentConfig cfg) { + public Model resolve(AgentConfig cfg) { PlatformCredential cred; if (compatibleConfigKey == null) { cred = CredentialResolver.requireFirstClass( @@ -1239,7 +1244,7 @@ public class GeminiSpec extends ModelSpec { public Integer getThinkingBudget() { return thinkingBudget; } @Override - protected Model resolve(AgentConfig cfg) { + public Model resolve(AgentConfig cfg) { PlatformCredential cred = CredentialResolver.requireFirstClass( cfg.getGemini(), "liteflow.agent.gemini"); @@ -1429,7 +1434,7 @@ public class DashScopeSpec extends ModelSpec { public Integer getThinkingBudget() { return thinkingBudget; } @Override - protected Model resolve(AgentConfig cfg) { + public Model resolve(AgentConfig cfg) { PlatformCredential cred = CredentialResolver.requireFirstClass( cfg.getDashscope(), "liteflow.agent.dashscope"); diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java index 71f7d5684..631317dc8 100644 --- a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/AgentConfig.java @@ -13,6 +13,7 @@ public class AgentConfig { private PlatformCredential gemini = new PlatformCredential(); private PlatformCredential dashscope = new PlatformCredential(); private Map openaiCompatible = new LinkedHashMap<>(); + private Map anthropicCompatible = new LinkedHashMap<>(); public WorkspaceConfig getWorkspace() { return workspace; } public void setWorkspace(WorkspaceConfig v) { this.workspace = v; } @@ -32,4 +33,6 @@ public class AgentConfig { public void setDashscope(PlatformCredential v) { this.dashscope = v; } public Map getOpenaiCompatible() { return openaiCompatible; } public void setOpenaiCompatible(Map v) { this.openaiCompatible = v; } + public Map getAnthropicCompatible() { return anthropicCompatible; } + public void setAnthropicCompatible(Map v) { this.anthropicCompatible = v; } } diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageConfig.java new file mode 100644 index 000000000..870a02606 --- /dev/null +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageConfig.java @@ -0,0 +1,40 @@ +package com.yomahub.liteflow.property.agent; + +/** + * Memory persistence settings for ReActAgent sessions. + * + *

This config is intentionally orthogonal to {@link SessionConfig} (which + * controls JVM-side session caching, idle timeout, LRU eviction). Memory + * storage decides where the agent's conversation history is durably + * kept; session config decides how long a hot agent is held in memory. + */ +public class MemoryStorageConfig { + /** Default is {@link MemoryStorageMode#JVM} so existing deployments behave unchanged. */ + private MemoryStorageMode mode = MemoryStorageMode.JVM; + + private WorkspaceMemoryConfig workspace = new WorkspaceMemoryConfig(); + private RedisMemoryConfig redis = new RedisMemoryConfig(); + private MysqlMemoryConfig mysql = new MysqlMemoryConfig(); + + /** Whether to lazily load existing session state on first {@code process()}. */ + private boolean loadOnFirstUse = true; + /** Whether to save session state after a successful {@code process()}. */ + private boolean saveAfterCall = true; + /** Whether to save session state when {@code process()} throws. */ + private boolean saveOnError = true; + + public MemoryStorageMode getMode() { return mode; } + public void setMode(MemoryStorageMode mode) { this.mode = mode; } + public WorkspaceMemoryConfig getWorkspace() { return workspace; } + public void setWorkspace(WorkspaceMemoryConfig workspace) { this.workspace = workspace; } + public RedisMemoryConfig getRedis() { return redis; } + public void setRedis(RedisMemoryConfig redis) { this.redis = redis; } + public MysqlMemoryConfig getMysql() { return mysql; } + public void setMysql(MysqlMemoryConfig mysql) { this.mysql = mysql; } + public boolean isLoadOnFirstUse() { return loadOnFirstUse; } + public void setLoadOnFirstUse(boolean loadOnFirstUse) { this.loadOnFirstUse = loadOnFirstUse; } + public boolean isSaveAfterCall() { return saveAfterCall; } + public void setSaveAfterCall(boolean saveAfterCall) { this.saveAfterCall = saveAfterCall; } + public boolean isSaveOnError() { return saveOnError; } + public void setSaveOnError(boolean saveOnError) { this.saveOnError = saveOnError; } +} diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageMode.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageMode.java new file mode 100644 index 000000000..cfd2515c1 --- /dev/null +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MemoryStorageMode.java @@ -0,0 +1,24 @@ +package com.yomahub.liteflow.property.agent; + +/** + * Storage backend used to persist a ReActAgent's memory across executions + * within a session. + * + *

    + *
  • {@link #NONE} – do not persist or even hold memory; equivalent to a stateless agent
  • + *
  • {@link #JVM} – keep memory in JVM heap only (default; behaviour identical to pre-2.15.4 releases)
  • + *
  • {@link #WORKSPACE_FILE} – persist memory as JSON files under each session's workspace directory + * using AgentScope's JsonSession
  • + *
  • {@link #REDIS} – persist memory through AgentScope's RedisSession; requires the user to + * provide a {@code RedissonClient} / {@code UnifiedJedis} / {@code RedisClient} bean
  • + *
  • {@link #MYSQL} – persist memory through AgentScope's MysqlSession; requires the user to + * provide a {@code javax.sql.DataSource} bean
  • + *
+ */ +public enum MemoryStorageMode { + NONE, + JVM, + WORKSPACE_FILE, + REDIS, + MYSQL +} diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MysqlMemoryConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MysqlMemoryConfig.java new file mode 100644 index 000000000..5f491c27c --- /dev/null +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/MysqlMemoryConfig.java @@ -0,0 +1,30 @@ +package com.yomahub.liteflow.property.agent; + +/** + * Settings that only apply when {@link MemoryStorageMode#MYSQL} is selected. + * + *

The DataSource is supplied by the user via {@code beanName} and looked up + * through ContextAware. LiteFlow never builds a JDBC connection pool itself. + */ +public class MysqlMemoryConfig { + /** Bean name of the {@link javax.sql.DataSource} to look up via ContextAware. */ + private String dataSourceBeanName; + + /** Database name passed to {@code MysqlSession}. Empty means use AgentScope's default ({@code agentscope}). */ + private String databaseName; + + /** Table name passed to {@code MysqlSession}. Empty means use AgentScope's default ({@code agentscope_sessions}). */ + private String tableName; + + /** When true, AgentScope auto-creates database & table; defaults to false to respect production constraints. */ + private boolean createIfNotExist = false; + + public String getDataSourceBeanName() { return dataSourceBeanName; } + public void setDataSourceBeanName(String dataSourceBeanName) { this.dataSourceBeanName = dataSourceBeanName; } + public String getDatabaseName() { return databaseName; } + public void setDatabaseName(String databaseName) { this.databaseName = databaseName; } + public String getTableName() { return tableName; } + public void setTableName(String tableName) { this.tableName = tableName; } + public boolean isCreateIfNotExist() { return createIfNotExist; } + public void setCreateIfNotExist(boolean createIfNotExist) { this.createIfNotExist = createIfNotExist; } +} diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/RedisMemoryConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/RedisMemoryConfig.java new file mode 100644 index 000000000..fb8752231 --- /dev/null +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/RedisMemoryConfig.java @@ -0,0 +1,28 @@ +package com.yomahub.liteflow.property.agent; + +/** + * Settings that only apply when {@link MemoryStorageMode#REDIS} is selected. + * + *

Connections are not created by LiteFlow. Users must provide a Redis client + * bean (Redisson / Jedis / Lettuce) through the framework's {@code ContextAware} + * (Spring bean lookup, Solon container, etc.). + */ +public class RedisMemoryConfig { + /** Bean name of the Redis client to look up via ContextAware. */ + private String beanName; + + /** Type of the configured client; affects how AgentScope adapts it. */ + private RedisClientType clientType = RedisClientType.REDISSON; + + /** Key prefix used inside Redis. */ + private String keyPrefix = "liteflow:agent:session"; + + public String getBeanName() { return beanName; } + public void setBeanName(String beanName) { this.beanName = beanName; } + public RedisClientType getClientType() { return clientType; } + public void setClientType(RedisClientType clientType) { this.clientType = clientType; } + public String getKeyPrefix() { return keyPrefix; } + public void setKeyPrefix(String keyPrefix) { this.keyPrefix = keyPrefix; } + + public enum RedisClientType { REDISSON, JEDIS, LETTUCE } +} diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/SessionConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/SessionConfig.java index 78caa927c..af4d6955a 100644 --- a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/SessionConfig.java +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/SessionConfig.java @@ -6,6 +6,7 @@ public class SessionConfig { private Duration idleTimeout = Duration.ofMinutes(30); private Duration cleanupInterval = Duration.ofMinutes(1); private int maxSessions = 10_000; + private MemoryStorageConfig memory = new MemoryStorageConfig(); public Duration getIdleTimeout() { return idleTimeout; } public void setIdleTimeout(Duration v) { this.idleTimeout = v; } @@ -13,4 +14,6 @@ public class SessionConfig { public void setCleanupInterval(Duration v) { this.cleanupInterval = v; } public int getMaxSessions() { return maxSessions; } public void setMaxSessions(int v) { this.maxSessions = v; } + public MemoryStorageConfig getMemory() { return memory; } + public void setMemory(MemoryStorageConfig memory) { this.memory = memory; } } diff --git a/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/WorkspaceMemoryConfig.java b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/WorkspaceMemoryConfig.java new file mode 100644 index 000000000..fc12ae168 --- /dev/null +++ b/liteflow-core/src/main/java/com/yomahub/liteflow/property/agent/WorkspaceMemoryConfig.java @@ -0,0 +1,14 @@ +package com.yomahub.liteflow.property.agent; + +/** + * Settings that only apply when {@link MemoryStorageMode#WORKSPACE_FILE} is selected. + * + *

The persistence sub-directory is hard-coded to {@code .agent-session} so it + * stays out of the way of tool-produced files in the per-session workspace and + * is not configurable on purpose; users who want a custom location can plug in + * their own {@code AgentSessionFactory} via SPI. + */ +public class WorkspaceMemoryConfig { + /** Fixed sub-directory created under the workspace root that holds session JSON files. */ + public static final String SUB_DIR = ".agent-session"; +} diff --git a/liteflow-core/src/test/java/com/yomahub/liteflow/property/agent/AgentConfigTest.java b/liteflow-core/src/test/java/com/yomahub/liteflow/property/agent/AgentConfigTest.java index 9429d4c7e..2e5071b7f 100644 --- a/liteflow-core/src/test/java/com/yomahub/liteflow/property/agent/AgentConfigTest.java +++ b/liteflow-core/src/test/java/com/yomahub/liteflow/property/agent/AgentConfigTest.java @@ -15,6 +15,8 @@ class AgentConfigTest { assertNotNull(c.getDefaults()); assertNotNull(c.getOpenaiCompatible()); assertTrue(c.getOpenaiCompatible().isEmpty()); + assertNotNull(c.getAnthropicCompatible()); + assertTrue(c.getAnthropicCompatible().isEmpty()); WorkspaceConfig w = c.getWorkspace(); assertNull(w.getRoot()); @@ -43,8 +45,12 @@ class AgentConfigTest { void platform_credentials_are_independent_instances() { AgentConfig c = new AgentConfig(); c.getOpenai().setApiKey("k1"); + c.getOpenaiCompatible().put("compatible-openai", new PlatformCredential()); + c.getAnthropicCompatible().put("compatible-anthropic", new PlatformCredential()); assertNull(c.getAnthropic().getApiKey()); assertNull(c.getGemini().getApiKey()); assertNull(c.getDashscope().getApiKey()); + assertFalse(c.getOpenaiCompatible().containsKey("compatible-anthropic")); + assertFalse(c.getAnthropicCompatible().containsKey("compatible-openai")); } } diff --git a/liteflow-react-agent/liteflow-react-agent-anthropic/README.md b/liteflow-react-agent/liteflow-react-agent-anthropic/README.md deleted file mode 100644 index fd547f2df..000000000 --- a/liteflow-react-agent/liteflow-react-agent-anthropic/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# liteflow-react-agent-anthropic - -Anthropic Claude 模型支持。 - -## 使用 - -```java -AnthropicChatModel model = AnthropicModelFactory.of(apiKey, "claude-sonnet-4-6"); -``` \ No newline at end of file diff --git a/liteflow-react-agent/liteflow-react-agent-anthropic/pom.xml b/liteflow-react-agent/liteflow-react-agent-anthropic/pom.xml index 6da3601d8..11e0f6135 100644 --- a/liteflow-react-agent/liteflow-react-agent-anthropic/pom.xml +++ b/liteflow-react-agent/liteflow-react-agent-anthropic/pom.xml @@ -14,8 +14,8 @@ jar - 21 - 21 + 17 + 17 @@ -34,23 +34,5 @@ anthropic-java 1.2.0 - - org.junit.jupiter - junit-jupiter - ${junit.version} - test - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - false - - - - \ No newline at end of file diff --git a/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicModelFactory.java b/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicModelFactory.java index 7cbb2dc39..560989e96 100644 --- a/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicModelFactory.java +++ b/liteflow-react-agent/liteflow-react-agent-anthropic/src/main/java/com/yomahub/liteflow/agent/anthropic/AnthropicModelFactory.java @@ -11,4 +11,12 @@ public final class AnthropicModelFactory { .modelName(modelName) .build(); } -} \ No newline at end of file + + public static AnthropicChatModel custom(String apiKey, String baseUrl, String modelName) { + return AnthropicChatModel.builder() + .apiKey(apiKey) + .baseUrl(baseUrl) + .modelName(modelName) + .build(); + } +} diff --git a/liteflow-react-agent/liteflow-react-agent-core/README.md b/liteflow-react-agent/liteflow-react-agent-core/README.md deleted file mode 100644 index ff311dabc..000000000 --- a/liteflow-react-agent/liteflow-react-agent-core/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# liteflow-react-agent-core - -## 快速上手 - -### 1. 添加依赖(选择至少一个平台模块) - -```xml - - com.yomahub - liteflow-react-agent-openai - ${liteflow.version} - -``` - -### 2. 配置 - -```yaml -liteflow: - agent: - workspace: - root: /var/lib/liteflow/agent-workspaces - shell: - mode: whitelist - whitelist: [ls, cat, grep] - openai-compatible: - deepseek: - api-key: ${DEEPSEEK_API_KEY} - base-url: https://api.deepseek.com/v1 -``` - -### 3. 定义 Agent - -```java -@LiteflowComponent("reviewAgent") -public class ReviewAgent extends ReActAgentComponent { - @Override protected Model buildModel(ReActAgentContext ctx) { - return OpenAICompatiblePresets.deepseek( - agentConfig().getOpenaiCompatible().get("deepseek").getApiKey(), - "deepseek-chat" - ); - } - @Override protected String systemPrompt(ReActAgentContext ctx) { return "你是审核专家"; } - @Override protected String userPrompt(ReActAgentContext ctx) { - return ctx.getSlot().getRequestData(String.class); - } -} -``` - -### 4. EL 编排 - -```xml - - THEN(prepare, reviewAgent, notify); - -``` - -## 核心概念 - -- **Session**:由 `resolveSessionId` 决定;默认 `slot.getRequestId()`(一次性)。覆写后可复用 memory 与 workspace 实现多轮对话。 -- **Workspace**:每 session 一个目录,在 `liteflow.agent.workspace.root` 之下。内置 `WorkspaceFileTools` 强制路径围栏。 -- **Shell**:`ManagedShellCommandTool` 按 `liteflow.agent.shell.mode` 做 whitelist/blacklist/disabled 校验,首 token 不在策略内即拒绝。 - -## 可选覆写 - -| 方法 | 默认行为 | 说明 | -|------|----------|------| -| `tools(ctx)` | 空列表 | 注册自定义 agentscope @Tool 对象 | -| `resolveSessionId(slot)` | `slot.getRequestId()` | 覆写以实现多轮对话 | -| `maxIterations()` | 配置文件默认值 (15) | ReAct 最大推理轮数 | -| `enableShellTool()` | true | 是否启用受管 shell 工具 | -| `enableWorkspaceFileTools()` | true | 是否启用受管文件工具 | -| `hooks(ctx)` | 空列表 | agentscope Hook 列表 | -| `handleReply(reply, ctx)` | 写入 slot.responseData | 自定义回复处理 | \ No newline at end of file diff --git a/liteflow-react-agent/liteflow-react-agent-core/pom.xml b/liteflow-react-agent/liteflow-react-agent-core/pom.xml index baa49cbd8..4146a332b 100644 --- a/liteflow-react-agent/liteflow-react-agent-core/pom.xml +++ b/liteflow-react-agent/liteflow-react-agent-core/pom.xml @@ -16,8 +16,8 @@ jar - 21 - 21 + 17 + 17 @@ -43,36 +43,5 @@ hutool-core ${hutool.version} - - - org.junit.jupiter - junit-jupiter - ${junit.version} - test - - - org.mockito - mockito-core - 4.11.0 - test - - - org.junit.platform - junit-platform-console-standalone - 1.8.2 - test - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - false - - - - diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/package-info.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/package-info.java index 1e40fd8e3..5cbc5376f 100644 --- a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/package-info.java +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/package-info.java @@ -1,4 +1,4 @@ /** - * LiteFlow ReAct Agent core module. + * LiteFlow ReAct Agent 核心模块。 */ package com.yomahub.liteflow.agent; diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSessionManager.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSessionManager.java index 2b35fc2ec..30301e927 100644 --- a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSessionManager.java +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/AgentSessionManager.java @@ -1,7 +1,13 @@ package com.yomahub.liteflow.agent.session; import com.yomahub.liteflow.agent.exception.AgentConfigException; +import com.yomahub.liteflow.agent.session.factory.AgentSessionFactoryRegistry; import com.yomahub.liteflow.property.agent.AgentConfig; +import com.yomahub.liteflow.property.agent.MemoryStorageConfig; +import com.yomahub.liteflow.property.agent.MemoryStorageMode; +import io.agentscope.core.ReActAgent; +import io.agentscope.core.session.Session; +import io.agentscope.core.session.SessionManager; import java.io.IOException; import java.net.URLEncoder; @@ -18,6 +24,19 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; +/** + * 跟踪当前 JVM 中存活的 AgentSession,并桥接到可插拔的 + * {@link Session}(由 {@link AgentSessionFactoryRegistry} 提供)。 + * + *

这里将两类职责保持独立: + *

    + *
  • JVM 内缓存、加锁和 LRU 淘汰(本类负责)
  • + *
  • 持久化存储(委托给 AgentScope 的 Session 抽象)
  • + *
+ * + *

淘汰(空闲或超过容量)只移除缓存的 agent 实例。 + * workspace 文件以及磁盘、Redis、MySQL 中的持久化 session 数据会被保留。 + */ public class AgentSessionManager implements AutoCloseable { private static final Pattern SAFE = Pattern.compile("[a-zA-Z0-9_\\-]+"); @@ -26,6 +45,8 @@ public class AgentSessionManager implements AutoCloseable { private final Path root; private final Map sessions = new ConcurrentHashMap<>(); private final ScheduledExecutorService cleaner; + /** memory 模式为 NONE 时可能为 null。 */ + private final Session storage; public AgentSessionManager(AgentConfig config) { this.config = config; @@ -42,6 +63,7 @@ public class AgentSessionManager implements AutoCloseable { } else if (!Files.isDirectory(root)) { throw new AgentConfigException("workspace root does not exist: " + root); } + this.storage = AgentSessionFactoryRegistry.createSession(config); long every = Math.max(20, config.getSession().getCleanupInterval().toMillis()); this.cleaner = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "liteflow-agent-session-cleaner"); @@ -68,6 +90,35 @@ public class AgentSessionManager implements AutoCloseable { return sessions.containsKey(safeId(sessionId)); } + /** + * 将之前持久化的状态懒加载恢复到 agent 中。 + * 同一个 session id 在当前 JVM 生命周期内应只调用一次,并且应在 agent + * 构建完成后、首次 {@code agent.call(...)} 前调用。 + */ + public void loadIfExists(AgentSession session, ReActAgent agent) { + if (storage == null || agent == null) return; + MemoryStorageConfig mc = config.getSession().getMemory(); + if (!mc.isLoadOnFirstUse()) return; + if (mc.getMode() == MemoryStorageMode.NONE) return; + SessionManager.forSessionId(session.getSessionId()) + .withSession(storage) + .addComponent(agent) + .loadIfExists(); + } + + /** 持久化 agent 当前状态。失败会向调用方暴露。 */ + public void save(AgentSession session, ReActAgent agent) { + if (storage == null || agent == null) return; + MemoryStorageConfig mc = config.getSession().getMemory(); + if (mc.getMode() == MemoryStorageMode.NONE) return; + SessionManager.forSessionId(session.getSessionId()) + .withSession(storage) + .addComponent(agent) + .saveSession(); + } + + public Session storage() { return storage; } + static String safeId(String raw) { if (raw == null || raw.isEmpty()) return "_"; if (SAFE.matcher(raw).matches()) return raw; @@ -79,7 +130,8 @@ public class AgentSessionManager implements AutoCloseable { while (sessions.size() > max) { sessions.values().stream() .min(Comparator.comparing(AgentSession::getLastActive)) - .ifPresent(victim -> remove(victim, true)); + // LRU 淘汰只移除 JVM 内缓存;持久化数据保持不变。 + .ifPresent(victim -> evictFromCache(victim, false)); } } @@ -89,14 +141,20 @@ public class AgentSessionManager implements AutoCloseable { if (s.getLastActive().isAfter(cutoff)) continue; if (!s.getLock().tryLock()) continue; try { - remove(s, config.getWorkspace().isCleanupOnSessionExpire()); + evictFromCache(s, config.getWorkspace().isCleanupOnSessionExpire()); } finally { s.getLock().unlock(); } } } - private void remove(AgentSession s, boolean cleanWorkspace) { + /** + * @param cleanWorkspace 为 true 时,同时删除磁盘上的 workspace 目录 + * (保留历史行为)。存储在其他位置的持久化 session + * 状态(例如 workspaceRoot/.agent-session、Redis、 + * MySQL)不会在这里被删除。 + */ + private void evictFromCache(AgentSession s, boolean cleanWorkspace) { sessions.remove(s.getSessionId(), s); if (cleanWorkspace) { deleteRecursively(s.getWorkspaceDir()); @@ -118,6 +176,9 @@ public class AgentSessionManager implements AutoCloseable { if (config.getWorkspace().isCleanupOnJvmShutdown()) { sessions.values().forEach(s -> deleteRecursively(s.getWorkspaceDir())); } + if (storage != null) { + try { storage.close(); } catch (Exception ignored) {} + } sessions.clear(); } } diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/AgentSessionFactory.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/AgentSessionFactory.java new file mode 100644 index 000000000..f5c90469a --- /dev/null +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/AgentSessionFactory.java @@ -0,0 +1,30 @@ +package com.yomahub.liteflow.agent.session.factory; + +import com.yomahub.liteflow.property.agent.AgentConfig; +import com.yomahub.liteflow.property.agent.MemoryStorageMode; +import io.agentscope.core.session.Session; + +/** + * 用于为 {@link io.agentscope.core.session.Session} 接入额外持久化后端的 SPI。 + * + *

框架内置模式({@code JVM}、{@code WORKSPACE_FILE}、{@code REDIS}、 + * {@code MYSQL}、{@code NONE})。需要其他后端(例如 PostgreSQL、OSS、 + * 加密 JSON)的用户,可以在 {@code META-INF/services/}{@link AgentSessionFactory} + * 下注册自定义工厂。 + */ +public interface AgentSessionFactory { + + /** + * 当前工厂处理的模式。所有已注册工厂之间必须唯一。 + */ + MemoryStorageMode mode(); + + /** + * 根据 agent 配置构建底层 {@link Session}。该方法会在首次 + * {@code process()} 时懒调用,而不是在框架启动时调用。 + * + * @return 非 null 的 Session;如果需要跳过持久化则返回 {@code null} + * ({@link MemoryStorageMode#NONE} 对应的工厂会返回 {@code null})。 + */ + Session create(AgentConfig agentConfig); +} diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/AgentSessionFactoryRegistry.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/AgentSessionFactoryRegistry.java new file mode 100644 index 000000000..5628de0cf --- /dev/null +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/AgentSessionFactoryRegistry.java @@ -0,0 +1,54 @@ +package com.yomahub.liteflow.agent.session.factory; + +import com.yomahub.liteflow.agent.exception.AgentConfigException; +import com.yomahub.liteflow.property.agent.AgentConfig; +import com.yomahub.liteflow.property.agent.MemoryStorageMode; +import io.agentscope.core.session.Session; + +import java.util.EnumMap; +import java.util.Map; +import java.util.ServiceLoader; + +/** + * 根据指定模式解析合适的 {@link AgentSessionFactory}。 + * + *

解析顺序: + *

    + *
  1. 通过 {@link ServiceLoader} 注册的外部工厂
  2. + *
  3. 框架内置工厂(JVM、workspace、Redis、MySQL、none)
  4. + *
+ * 出现冲突时外部工厂优先,因此用户可以覆盖内置实现 + * (例如用自定义加密 JSON 工厂替换默认 workspace 工厂)。 + */ +public final class AgentSessionFactoryRegistry { + + private static final Map FACTORIES = new EnumMap<>(MemoryStorageMode.class); + + static { + // 先注册内置实现;如果存在 SPI 实现,再由 SPI 覆盖。 + register(new InMemoryAgentSessionFactory()); + register(new WorkspaceAgentSessionFactory()); + register(new RedisAgentSessionFactory()); + register(new MysqlAgentSessionFactory()); + register(new NoneAgentSessionFactory()); + for (AgentSessionFactory f : ServiceLoader.load(AgentSessionFactory.class)) { + register(f); + } + } + + private AgentSessionFactoryRegistry() { } + + private static void register(AgentSessionFactory f) { + FACTORIES.put(f.mode(), f); + } + + /** 根据配置模式构建 Session。{@link MemoryStorageMode#NONE} 可能返回 {@code null}。 */ + public static Session createSession(AgentConfig cfg) { + MemoryStorageMode mode = cfg.getSession().getMemory().getMode(); + AgentSessionFactory f = FACTORIES.get(mode); + if (f == null) { + throw new AgentConfigException("No AgentSessionFactory registered for mode: " + mode); + } + return f.create(cfg); + } +} diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/InMemoryAgentSessionFactory.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/InMemoryAgentSessionFactory.java new file mode 100644 index 000000000..c73b59f94 --- /dev/null +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/InMemoryAgentSessionFactory.java @@ -0,0 +1,26 @@ +package com.yomahub.liteflow.agent.session.factory; + +import com.yomahub.liteflow.property.agent.AgentConfig; +import com.yomahub.liteflow.property.agent.MemoryStorageMode; +import io.agentscope.core.session.InMemorySession; +import io.agentscope.core.session.Session; + +/** + * 使用 AgentScope 的内存存储支持 {@link MemoryStorageMode#JVM} 模式。 + * + *

注意:状态仍会在同一个 JVM 内跨调用保留(适合希望在单进程内保留多轮记忆的场景), + * 但进程退出后会丢失。如果需要跨重启持久化,请选择 + * {@code WORKSPACE_FILE}、{@code REDIS} 或 {@code MYSQL}。 + */ +public class InMemoryAgentSessionFactory implements AgentSessionFactory { + + @Override + public MemoryStorageMode mode() { + return MemoryStorageMode.JVM; + } + + @Override + public Session create(AgentConfig agentConfig) { + return new InMemorySession(); + } +} diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/MysqlAgentSessionFactory.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/MysqlAgentSessionFactory.java new file mode 100644 index 000000000..a86ba4b03 --- /dev/null +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/MysqlAgentSessionFactory.java @@ -0,0 +1,52 @@ +package com.yomahub.liteflow.agent.session.factory; + +import com.yomahub.liteflow.agent.exception.AgentConfigException; +import com.yomahub.liteflow.property.agent.AgentConfig; +import com.yomahub.liteflow.property.agent.MemoryStorageMode; +import com.yomahub.liteflow.property.agent.MysqlMemoryConfig; +import com.yomahub.liteflow.spi.holder.ContextAwareHolder; +import io.agentscope.core.session.Session; +import io.agentscope.core.session.mysql.MysqlSession; + +import javax.sql.DataSource; + +/** + * 支持 {@link MemoryStorageMode#MYSQL} 模式。使用用户提供的 + * {@link DataSource} bean;LiteFlow 不自行创建 JDBC 连接池。 + */ +public class MysqlAgentSessionFactory implements AgentSessionFactory { + + @Override + public MemoryStorageMode mode() { + return MemoryStorageMode.MYSQL; + } + + @Override + public Session create(AgentConfig cfg) { + MysqlMemoryConfig mc = cfg.getSession().getMemory().getMysql(); + if (mc.getDataSourceBeanName() == null || mc.getDataSourceBeanName().trim().isEmpty()) { + throw new AgentConfigException( + "liteflow.agent.session.memory.mysql.dataSourceBeanName is required when mode=MYSQL"); + } + Object bean = ContextAwareHolder.loadContextAware().getBean(mc.getDataSourceBeanName()); + if (bean == null) { + throw new AgentConfigException("DataSource bean not found: " + mc.getDataSourceBeanName()); + } + if (!(bean instanceof DataSource)) { + throw new AgentConfigException("Bean '" + mc.getDataSourceBeanName() + "' is not a DataSource; got " + + bean.getClass().getName()); + } + DataSource ds = (DataSource) bean; + String db = mc.getDatabaseName(); + String table = mc.getTableName(); + boolean hasCustom = (db != null && !db.isEmpty()) || (table != null && !table.isEmpty()); + try { + if (hasCustom) { + return new MysqlSession(ds, db, table, mc.isCreateIfNotExist()); + } + return new MysqlSession(ds, mc.isCreateIfNotExist()); + } catch (Exception e) { + throw new AgentConfigException("Failed to build MysqlSession", e); + } + } +} diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/NoneAgentSessionFactory.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/NoneAgentSessionFactory.java new file mode 100644 index 000000000..19b179565 --- /dev/null +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/NoneAgentSessionFactory.java @@ -0,0 +1,22 @@ +package com.yomahub.liteflow.agent.session.factory; + +import com.yomahub.liteflow.property.agent.AgentConfig; +import com.yomahub.liteflow.property.agent.MemoryStorageMode; +import io.agentscope.core.session.Session; + +/** + * 返回 {@code null} Session,用于通知 AgentSessionManager 跳过所有加载和保存操作。 + * 供 {@link MemoryStorageMode#NONE} 使用。 + */ +public class NoneAgentSessionFactory implements AgentSessionFactory { + + @Override + public MemoryStorageMode mode() { + return MemoryStorageMode.NONE; + } + + @Override + public Session create(AgentConfig agentConfig) { + return null; + } +} diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/RedisAgentSessionFactory.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/RedisAgentSessionFactory.java new file mode 100644 index 000000000..ad6254675 --- /dev/null +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/RedisAgentSessionFactory.java @@ -0,0 +1,79 @@ +package com.yomahub.liteflow.agent.session.factory; + +import com.yomahub.liteflow.agent.exception.AgentConfigException; +import com.yomahub.liteflow.property.agent.AgentConfig; +import com.yomahub.liteflow.property.agent.MemoryStorageMode; +import com.yomahub.liteflow.property.agent.RedisMemoryConfig; +import com.yomahub.liteflow.spi.holder.ContextAwareHolder; +import io.agentscope.core.session.Session; + +/** + * 通过把用户提供的 Redis 客户端 bean(Redisson、Jedis、Lettuce) + * 适配到 AgentScope 的 RedisSession 来支持 {@link MemoryStorageMode#REDIS}。 + * + *

Redis 客户端类通过反射查找,因此 core 模块不会对 Redisson、Jedis、Lettuce + * 产生硬性的编译期依赖。如果选择 REDIS 模式但 classpath 中缺少匹配驱动, + * 会在首次 {@code process()} 时失败,而不是在框架启动时失败。 + */ +public class RedisAgentSessionFactory implements AgentSessionFactory { + + private static final String REDIS_SESSION_CLASS = "io.agentscope.core.session.redis.RedisSession"; + + @Override + public MemoryStorageMode mode() { + return MemoryStorageMode.REDIS; + } + + @Override + public Session create(AgentConfig cfg) { + RedisMemoryConfig rc = cfg.getSession().getMemory().getRedis(); + if (rc.getBeanName() == null || rc.getBeanName().trim().isEmpty()) { + throw new AgentConfigException( + "liteflow.agent.session.memory.redis.beanName is required when mode=REDIS"); + } + Object client = ContextAwareHolder.loadContextAware().getBean(rc.getBeanName()); + if (client == null) { + throw new AgentConfigException("Redis client bean not found: " + rc.getBeanName()); + } + String builderMethod; + String clientFqn; + switch (rc.getClientType()) { + case REDISSON: + builderMethod = "redissonClient"; + clientFqn = "org.redisson.api.RedissonClient"; + break; + case JEDIS: + builderMethod = "jedisClient"; + clientFqn = "redis.clients.jedis.UnifiedJedis"; + break; + case LETTUCE: + builderMethod = "lettuceClient"; + clientFqn = "io.lettuce.core.RedisClient"; + break; + default: + throw new AgentConfigException("Unsupported redis client type: " + rc.getClientType()); + } + try { + Class sessionClass = Class.forName(REDIS_SESSION_CLASS); + Object builder = sessionClass.getMethod("builder").invoke(null); + Class clientType = Class.forName(clientFqn); + if (!clientType.isInstance(client)) { + throw new AgentConfigException("Bean '" + rc.getBeanName() + "' is not a " + + clientFqn + "; got " + client.getClass().getName()); + } + builder.getClass().getMethod(builderMethod, clientType).invoke(builder, client); + if (rc.getKeyPrefix() != null && !rc.getKeyPrefix().isEmpty()) { + builder.getClass().getMethod("keyPrefix", String.class).invoke(builder, rc.getKeyPrefix()); + } + return (Session) builder.getClass().getMethod("build").invoke(builder); + } catch (ClassNotFoundException e) { + throw new AgentConfigException( + "Class not found while building RedisSession: " + e.getMessage() + + ". Add the matching driver dependency (Redisson/Jedis/Lettuce).", e); + } catch (AgentConfigException e) { + throw e; + } catch (Exception e) { + throw new AgentConfigException("Failed to build RedisSession", e); + } + } +} diff --git a/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/WorkspaceAgentSessionFactory.java b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/WorkspaceAgentSessionFactory.java new file mode 100644 index 000000000..059b858b0 --- /dev/null +++ b/liteflow-react-agent/liteflow-react-agent-core/src/main/java/com/yomahub/liteflow/agent/session/factory/WorkspaceAgentSessionFactory.java @@ -0,0 +1,46 @@ +package com.yomahub.liteflow.agent.session.factory; + +import com.yomahub.liteflow.agent.exception.AgentConfigException; +import com.yomahub.liteflow.property.agent.AgentConfig; +import com.yomahub.liteflow.property.agent.MemoryStorageMode; +import com.yomahub.liteflow.property.agent.WorkspaceMemoryConfig; +import io.agentscope.core.session.JsonSession; +import io.agentscope.core.session.Session; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * 通过把 JSON 文件存储在 {@code workspace.root/.agent-session//} + * 下来支持 {@link MemoryStorageMode#WORKSPACE_FILE}。 + * + *

session 存储子目录特意与 {@code workspace.root//} + * 形式的单 session 工具 workspace 分离,避免 + * {@link com.yomahub.liteflow.agent.tool.WorkspaceFileTools} 读取或覆盖 + * agent 自身记忆。 + */ +public class WorkspaceAgentSessionFactory implements AgentSessionFactory { + + @Override + public MemoryStorageMode mode() { + return MemoryStorageMode.WORKSPACE_FILE; + } + + @Override + public Session create(AgentConfig cfg) { + if (cfg.getWorkspace() == null || cfg.getWorkspace().getRoot() == null) { + throw new AgentConfigException( + "liteflow.agent.workspace.root is required when session.memory.mode=WORKSPACE_FILE"); + } + Path root = Paths.get(cfg.getWorkspace().getRoot()).toAbsolutePath().normalize() + .resolve(WorkspaceMemoryConfig.SUB_DIR); + try { + Files.createDirectories(root); + } catch (IOException e) { + throw new AgentConfigException("cannot create session storage dir: " + root, e); + } + return new JsonSession(root); + } +} diff --git a/liteflow-react-agent/liteflow-react-agent-dashscope/README.md b/liteflow-react-agent/liteflow-react-agent-dashscope/README.md deleted file mode 100644 index 477ad7690..000000000 --- a/liteflow-react-agent/liteflow-react-agent-dashscope/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# liteflow-react-agent-dashscope - -阿里云 DashScope / Qwen 模型支持。 - -## 使用 - -```java -DashScopeChatModel model = DashScopeModelFactory.of(apiKey, "qwen3-max"); -``` \ No newline at end of file diff --git a/liteflow-react-agent/liteflow-react-agent-dashscope/pom.xml b/liteflow-react-agent/liteflow-react-agent-dashscope/pom.xml index 6b3f46054..a4445fdfa 100644 --- a/liteflow-react-agent/liteflow-react-agent-dashscope/pom.xml +++ b/liteflow-react-agent/liteflow-react-agent-dashscope/pom.xml @@ -12,8 +12,8 @@ jar - 21 - 21 + 17 + 17 @@ -22,23 +22,5 @@ liteflow-react-agent-core ${revision} - - org.junit.jupiter - junit-jupiter - ${junit.version} - test - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - false - - - - \ No newline at end of file diff --git a/liteflow-react-agent/liteflow-react-agent-gemini/README.md b/liteflow-react-agent/liteflow-react-agent-gemini/README.md deleted file mode 100644 index fd600bb64..000000000 --- a/liteflow-react-agent/liteflow-react-agent-gemini/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# liteflow-react-agent-gemini - -Google Gemini 模型支持。 - -## 使用 - -```java -// 基础 -GeminiChatModel m1 = GeminiModelFactory.of(apiKey, "gemini-3-flash-preview"); - -// 带 thinking level -GeminiChatModel m2 = GeminiModelFactory.of(apiKey, "gemini-3-flash-preview", "high"); -``` \ No newline at end of file diff --git a/liteflow-react-agent/liteflow-react-agent-gemini/pom.xml b/liteflow-react-agent/liteflow-react-agent-gemini/pom.xml index 6ebfd1b55..ecdd542a2 100644 --- a/liteflow-react-agent/liteflow-react-agent-gemini/pom.xml +++ b/liteflow-react-agent/liteflow-react-agent-gemini/pom.xml @@ -12,8 +12,8 @@ jar - 21 - 21 + 17 + 17 @@ -32,23 +32,5 @@ google-genai 0.2.0 - - org.junit.jupiter - junit-jupiter - ${junit.version} - test - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - false - - - - \ No newline at end of file diff --git a/liteflow-react-agent/liteflow-react-agent-openai/README.md b/liteflow-react-agent/liteflow-react-agent-openai/README.md deleted file mode 100644 index 7f424c417..000000000 --- a/liteflow-react-agent/liteflow-react-agent-openai/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# liteflow-react-agent-openai - -OpenAI 及 OpenAI 兼容协议厂商支持。 - -## 使用 - -```java -// 标准 OpenAI -OpenAIChatModel m1 = OpenAIModelFactory.openai(apiKey, "gpt-4o-mini"); - -// DeepSeek -OpenAIChatModel m2 = OpenAICompatiblePresets.deepseek(apiKey, "deepseek-chat"); - -// Kimi (Moonshot) -OpenAIChatModel m3 = OpenAICompatiblePresets.kimi(apiKey, "moonshot-v1-8k"); - -// GLM (智谱) -OpenAIChatModel m4 = OpenAICompatiblePresets.glm(apiKey, "glm-4"); - -// MiniMax -OpenAIChatModel m5 = OpenAICompatiblePresets.minimax(apiKey, "abab6.5s-chat"); - -// 自定义 OpenAI 兼容端点 -OpenAIChatModel m6 = OpenAIModelFactory.custom(apiKey, "https://your.own/v1", "your-model"); -``` \ No newline at end of file diff --git a/liteflow-react-agent/liteflow-react-agent-openai/pom.xml b/liteflow-react-agent/liteflow-react-agent-openai/pom.xml index 1094bbeb6..fb995fca4 100644 --- a/liteflow-react-agent/liteflow-react-agent-openai/pom.xml +++ b/liteflow-react-agent/liteflow-react-agent-openai/pom.xml @@ -14,8 +14,8 @@ jar - 21 - 21 + 17 + 17 @@ -24,23 +24,5 @@ liteflow-react-agent-core ${revision} - - org.junit.jupiter - junit-jupiter - ${junit.version} - test - - - - - - org.apache.maven.plugins - maven-surefire-plugin - - false - - - - \ No newline at end of file diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/pom.xml b/liteflow-testcase-el/liteflow-testcase-el-react-agent/pom.xml new file mode 100644 index 000000000..5cd603dae --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/pom.xml @@ -0,0 +1,91 @@ + + + + liteflow-testcase-el + com.yomahub + ${revision} + ../pom.xml + + 4.0.0 + + liteflow-testcase-el-react-agent + + + 17 + 17 + + + + + com.yomahub + liteflow-spring-boot-starter + ${revision} + + + + com.yomahub + liteflow-react-agent-openai + ${revision} + + + com.yomahub + liteflow-react-agent-anthropic + ${revision} + + + com.yomahub + liteflow-react-agent-gemini + ${revision} + + + com.yomahub + liteflow-react-agent-dashscope + ${revision} + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + 4.11.0 + test + + + ch.qos.logback + logback-classic + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 17 + 17 + + + + org.apache.maven.plugins + maven-surefire-plugin + + false + + + + + diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/agent/anthropic/AnthropicModelFactoryTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/agent/anthropic/AnthropicModelFactoryTest.java index abdcb088a..19690c345 100644 --- a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/agent/anthropic/AnthropicModelFactoryTest.java +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/agent/anthropic/AnthropicModelFactoryTest.java @@ -5,4 +5,7 @@ import static org.junit.jupiter.api.Assertions.*; class AnthropicModelFactoryTest { @Test void construct_ok() { assertNotNull(AnthropicModelFactory.of("k", "claude-sonnet-4-6")); } -} \ No newline at end of file + @Test void custom_base_url_ok() { + assertNotNull(AnthropicModelFactory.custom("k", "https://anthropic-proxy.example.com", "claude-sonnet-4-6")); + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ApiKeys.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ApiKeys.java new file mode 100644 index 000000000..6870e483e --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ApiKeys.java @@ -0,0 +1,20 @@ +package com.yomahub.liteflow.test.agent; + +/** + * 示例测试 API Key 工具:环境变量优先,再回退到 Spring 注入的 properties。 + * 留空时调用 {@link org.junit.jupiter.api.Assumptions#assumeTrue} 跳过用例。 + */ +public final class ApiKeys { + + private ApiKeys() {} + + public static String resolve(String envName, String configured) { + String env = System.getenv(envName); + if (env != null && !env.isBlank()) return env.trim(); + return configured == null ? "" : configured.trim(); + } + + public static boolean isPresent(String s) { + return s != null && !s.isBlank(); + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentELSpringbootTest.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentELSpringbootTest.java new file mode 100644 index 000000000..9531d55fc --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/ReActAgentELSpringbootTest.java @@ -0,0 +1,170 @@ +package com.yomahub.liteflow.test.agent; + +import com.yomahub.liteflow.core.FlowExecutor; +import com.yomahub.liteflow.flow.LiteflowResponse; +import com.yomahub.liteflow.property.LiteflowConfig; +import com.yomahub.liteflow.test.agent.cmp.RecordReplyCmp; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; +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; + +/** + * 走 LiteFlow EL 编排的 ReActAgent 示例测试。 + *

+ * 每个用例都通过 {@code flowExecutor.execute2Resp(chainId, ...)} 跑一条 chain, + * Agent 节点是 {@link com.yomahub.liteflow.agent.component.ReActAgentComponent} + * 的具体子类,apiKey 通过 {@code application.properties} 中的 + * {@code liteflow.agent..api-key} 注入。留空则用例自动 skip。 + */ +@TestPropertySource(value = "classpath:/agent/application.properties") +@SpringBootTest(classes = ReActAgentELSpringbootTest.class) +@EnableAutoConfiguration +@ComponentScan({ + "com.yomahub.liteflow.test.agent.cmp" +}) +public class ReActAgentELSpringbootTest { + + @Resource + private FlowExecutor flowExecutor; + + @Resource + private LiteflowConfig liteflowConfig; + + /** 真正运行链路并断言成功,回复非空。 */ + private void runChainExpectingReply(String chainId, String question) { + LiteflowResponse response = flowExecutor.execute2Resp(chainId, question); + if (!response.isSuccess() && response.getCause() != null) { + response.getCause().printStackTrace(); + } + Assertions.assertTrue(response.isSuccess(), + "chain failed: " + (response.getCause() == null ? "" : response.getCause().getMessage())); + Object reply = response.getSlot().getOutput(RecordReplyCmp.NODE_ID); + Assertions.assertNotNull(reply, "agent reply must be recorded"); + System.out.println(">>> [" + chainId + "] reply=" + reply); + } + + private String openaiKey() { return ApiKeys.resolve("OPENAI_API_KEY", liteflowConfig.getAgent().getOpenai().getApiKey()); } + private String anthropicKey() { return ApiKeys.resolve("ANTHROPIC_API_KEY", liteflowConfig.getAgent().getAnthropic().getApiKey()); } + private String geminiKey() { return ApiKeys.resolve("GEMINI_API_KEY", liteflowConfig.getAgent().getGemini().getApiKey()); } + private String dashscopeKey() { return ApiKeys.resolve("DASHSCOPE_API_KEY", liteflowConfig.getAgent().getDashscope().getApiKey()); } + private String deepseekKey() { + return ApiKeys.resolve("DEEPSEEK_API_KEY", + liteflowConfig.getAgent().getOpenaiCompatible() + .getOrDefault("deepseek", new com.yomahub.liteflow.property.agent.PlatformCredential()) + .getApiKey()); + } + + /** 上下文:在 BeforeAll 里把环境变量回写到 LiteflowConfig,避免 properties 留空时无 key 可用。 */ + private void syncEnvKeys() { + if (ApiKeys.isPresent(openaiKey())) liteflowConfig.getAgent().getOpenai().setApiKey(openaiKey()); + if (ApiKeys.isPresent(anthropicKey())) liteflowConfig.getAgent().getAnthropic().setApiKey(anthropicKey()); + if (ApiKeys.isPresent(geminiKey())) liteflowConfig.getAgent().getGemini().setApiKey(geminiKey()); + if (ApiKeys.isPresent(dashscopeKey())) liteflowConfig.getAgent().getDashscope().setApiKey(dashscopeKey()); + if (ApiKeys.isPresent(deepseekKey())) { + liteflowConfig.getAgent().getOpenaiCompatible() + .computeIfAbsent("deepseek", k -> { + com.yomahub.liteflow.property.agent.PlatformCredential c = + new com.yomahub.liteflow.property.agent.PlatformCredential(); + c.setBaseUrl("https://api.deepseek.com/v1"); + return c; + }) + .setApiKey(deepseekKey()); + } + } + + /* ===================================================================== + * 单平台单 Agent 链路:THEN(prepare, , recordReply) + * ===================================================================== */ + + @Test + public void testDeepSeekChain() { + syncEnvKeys(); + Assumptions.assumeTrue(ApiKeys.isPresent(deepseekKey()), + "deepseek api-key 未配置,跳过 deepseekChain"); + runChainExpectingReply("deepseekChain", "用一句话总结 ReAct 模式的核心思想。"); + } + + @Test + public void testOpenAIChain() { + syncEnvKeys(); + Assumptions.assumeTrue(ApiKeys.isPresent(openaiKey()), + "openai api-key 未配置,跳过 openaiChain"); + runChainExpectingReply("openaiChain", "用一句话介绍 LiteFlow。"); + } + + @Test + public void testAnthropicChain() { + syncEnvKeys(); + Assumptions.assumeTrue(ApiKeys.isPresent(anthropicKey()), + "anthropic api-key 未配置,跳过 anthropicChain"); + runChainExpectingReply("anthropicChain", "什么是规则引擎?一句话回答。"); + } + + @Test + public void testDashScopeChain() { + syncEnvKeys(); + Assumptions.assumeTrue(ApiKeys.isPresent(dashscopeKey()), + "dashscope api-key 未配置,跳过 dashscopeChain"); + runChainExpectingReply("dashscopeChain", "用一句话介绍通义千问。"); + } + + @Test + public void testGeminiChain() { + syncEnvKeys(); + Assumptions.assumeTrue(ApiKeys.isPresent(geminiKey()), + "gemini api-key 未配置,跳过 geminiChain"); + runChainExpectingReply("geminiChain", "用一句中文介绍 Gemini 模型。"); + } + + /* ===================================================================== + * 自定义工具:mathChain → mathAgent 注册 CalculatorTool + * ===================================================================== */ + + @Test + public void testMathChainWithCustomTool() { + syncEnvKeys(); + Assumptions.assumeTrue(ApiKeys.isPresent(deepseekKey()), + "deepseek api-key 未配置,跳过 mathChain(mathAgent 后端用 deepseek)"); + runChainExpectingReply("mathChain", + "请用 calculator 工具计算 (123 + 456) * 7 - 89,并用一句话给出答案。"); + } + + /* ===================================================================== + * IF 路由:routerChain,根据 isMath 选 mathAgent 或 deepseekAgent + * ===================================================================== */ + + @Test + public void testRouterChainPicksMathAgent() { + syncEnvKeys(); + Assumptions.assumeTrue(ApiKeys.isPresent(deepseekKey()), + "deepseek api-key 未配置,跳过 routerChain"); + runChainExpectingReply("routerChain", "12*34 等于多少?"); + } + + @Test + public void testRouterChainPicksChatAgent() { + syncEnvKeys(); + Assumptions.assumeTrue(ApiKeys.isPresent(deepseekKey()), + "deepseek api-key 未配置,跳过 routerChain"); + runChainExpectingReply("routerChain", "你好,简单介绍下你自己。"); + } + + /* ===================================================================== + * WHEN 并行:parallelChain 同时跑 deepseek + dashscope,谁后到 recordReply 取谁 + * ===================================================================== */ + + @Test + public void testParallelChain() { + syncEnvKeys(); + Assumptions.assumeTrue( + ApiKeys.isPresent(deepseekKey()) && ApiKeys.isPresent(dashscopeKey()), + "需要同时配置 deepseek 和 dashscope 的 api-key 才能运行 parallelChain"); + runChainExpectingReply("parallelChain", "用一句话介绍 LiteFlow 的应用场景。"); + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/tool/CalculatorTool.java b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/tool/CalculatorTool.java new file mode 100644 index 000000000..7f5b8e0e1 --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/java/com/yomahub/liteflow/test/agent/tool/CalculatorTool.java @@ -0,0 +1,67 @@ +package com.yomahub.liteflow.test.agent.tool; + +import io.agentscope.core.tool.Tool; +import io.agentscope.core.tool.ToolParam; + +/** + * 自定义工具示例:计算器。被 ReActAgentComponent.tools() 注册后, + * agent 在推理时会被 toolkit 自动发现并按需调用。 + */ +public class CalculatorTool { + + @Tool(name = "calculator", description = "Evaluate a basic arithmetic expression like '1+2*3'") + public String calc(@ToolParam(name = "expression", + description = "Arithmetic expression with +, -, *, /, and parentheses") String expression) { + try { + return String.valueOf(Eval.run(expression)); + } catch (Exception e) { + return "ERROR: " + e.getMessage(); + } + } + + /** 极简递归下降表达式求值器(仅供示例,不保证健壮性)。 */ + static final class Eval { + private final String s; + private int pos; + private Eval(String s) { this.s = s; } + + static double run(String s) { + Eval e = new Eval(s.replaceAll("\\s+", "")); + double v = e.expr(); + if (e.pos < e.s.length()) throw new IllegalArgumentException("Unexpected: " + e.s.substring(e.pos)); + return v; + } + private double expr() { + double v = term(); + while (pos < s.length() && (s.charAt(pos) == '+' || s.charAt(pos) == '-')) { + char op = s.charAt(pos++); + double r = term(); + v = (op == '+') ? v + r : v - r; + } + return v; + } + private double term() { + double v = factor(); + while (pos < s.length() && (s.charAt(pos) == '*' || s.charAt(pos) == '/')) { + char op = s.charAt(pos++); + double r = factor(); + v = (op == '*') ? v * r : v / r; + } + return v; + } + private double factor() { + if (pos < s.length() && s.charAt(pos) == '(') { + pos++; + double v = expr(); + if (pos >= s.length() || s.charAt(pos) != ')') throw new IllegalArgumentException("Missing )"); + pos++; + return v; + } + int start = pos; + if (pos < s.length() && (s.charAt(pos) == '+' || s.charAt(pos) == '-')) pos++; + while (pos < s.length() && (Character.isDigit(s.charAt(pos)) || s.charAt(pos) == '.')) pos++; + if (start == pos) throw new IllegalArgumentException("Number expected at " + pos); + return Double.parseDouble(s.substring(start, pos)); + } + } +} diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/application.properties b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/application.properties new file mode 100644 index 000000000..33af5fe49 --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/application.properties @@ -0,0 +1,26 @@ +liteflow.rule-source=agent/flow.el.xml +liteflow.print-banner=false + +# ============================================================================= +# ReAct Agent 测试 API Key —— 用户在此填入,留空则对应链路测试被 Assumptions 跳过 +# 也可通过环境变量覆盖(OPENAI_API_KEY / ANTHROPIC_API_KEY / GEMINI_API_KEY / +# DASHSCOPE_API_KEY / DEEPSEEK_API_KEY),环境变量优先级更高 +# ============================================================================= +liteflow.agent.workspace.root=${java.io.tmpdir}/liteflow-react-agent-test +liteflow.agent.shell.mode=disabled + +liteflow.agent.openai.api-key= +liteflow.agent.anthropic.api-key= +liteflow.agent.gemini.api-key= +liteflow.agent.dashscope.api-key= +liteflow.agent.openai-compatible.deepseek.api-key= +liteflow.agent.openai-compatible.deepseek.base-url=https://api.deepseek.com/v1 +liteflow.agent.anthropic-compatible.gateway.api-key= +liteflow.agent.anthropic-compatible.gateway.base-url=https://anthropic-gateway.example.com + +# 模型名(可按需覆盖) +test.openai.model=gpt-4o-mini +test.anthropic.model=claude-sonnet-4-5 +test.gemini.model=gemini-2.5-flash +test.dashscope.model=qwen-plus +test.deepseek.model=deepseek-chat diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/flow.el.xml b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/flow.el.xml new file mode 100644 index 000000000..f24ca3f9d --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/agent/flow.el.xml @@ -0,0 +1,49 @@ + + + + + + + THEN(prepare, deepseekAgent, recordReply); + + + + THEN(prepare, openaiAgent, recordReply); + + + + THEN(prepare, anthropicAgent, recordReply); + + + + THEN(prepare, dashscopeAgent, recordReply); + + + + THEN(prepare, geminiAgent, recordReply); + + + + + THEN(prepare, mathAgent, recordReply); + + + + + THEN( + prepare, + IF(isMath, mathAgent, deepseekAgent), + recordReply + ); + + + + + THEN( + prepare, + WHEN(deepseekAgent, dashscopeAgent).maxWaitSeconds(60), + recordReply + ); + + + diff --git a/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/logback-test.xml b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/logback-test.xml new file mode 100644 index 000000000..300fa02e9 --- /dev/null +++ b/liteflow-testcase-el/liteflow-testcase-el-react-agent/src/test/resources/logback-test.xml @@ -0,0 +1,15 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{40} - %msg%n + + + + + + + + + + diff --git a/liteflow-testcase-el/pom.xml b/liteflow-testcase-el/pom.xml index cba831c06..0fdd56d51 100644 --- a/liteflow-testcase-el/pom.xml +++ b/liteflow-testcase-el/pom.xml @@ -46,4 +46,17 @@ liteflow-testcase-el-sql-springboot-sharding-jdbc liteflow-testcase-el-script-javaxpro-springboot + + + + + testcase-react-agent + + [17,) + + + liteflow-testcase-el-react-agent + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0d76ac914..77619109c 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,7 @@ - 2.15.3.2 + 2.16.0 UTF-8 UTF-8 8 @@ -82,7 +82,7 @@ 4.1.1 1.14.0 2.9.3 - 1.0.9 + 1.0.11 1.38.0 @@ -425,11 +425,64 @@ + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.4 + + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.7 + + + verify + + sign + + + + + ${gpg.passphrase} + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.11.3 + + + package + + jar + + + -Xdoclint:none + + + + + + org.sonatype.central + central-publishing-maven-plugin + 0.8.0 + true + + oss + true + + + + - compile + compile-8-to-16 liteflow-core liteflow-script-plugin @@ -442,12 +495,12 @@ liteflow-benchmark - true + [1.8,17) - release + release-main liteflow-core liteflow-script-plugin @@ -463,53 +516,46 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.4 - org.apache.maven.plugins maven-gpg-plugin - 3.2.7 - - - verify - - sign - - - - - ${gpg.passphrase} - - - org.apache.maven.plugins maven-javadoc-plugin - 3.11.3 - - - package - - jar - - - -Xdoclint:none - - - - org.sonatype.central central-publishing-maven-plugin - 0.8.0 - true - - oss - true - + + + + + + + release-react-agent + + liteflow-react-agent + + + + + + org.apache.maven.plugins + maven-deploy-plugin + + + org.apache.maven.plugins + maven-gpg-plugin + + + org.apache.maven.plugins + maven-javadoc-plugin + + + org.sonatype.central + central-publishing-maven-plugin @@ -517,11 +563,20 @@ - react-agent + compile-17+ - [21,) + [17,) + liteflow-core + liteflow-script-plugin + liteflow-rule-plugin + liteflow-spring-boot-starter + liteflow-spring + liteflow-solon-plugin + liteflow-testcase-el + liteflow-el-builder + liteflow-benchmark liteflow-react-agent