yxk-20260409-集成流程引擎bpm代码

This commit is contained in:
ye1023
2026-04-09 15:55:31 +08:00
parent d94e9c9bc4
commit 9a65463105
582 changed files with 364516 additions and 0 deletions
@@ -0,0 +1,194 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-platform</artifactId>
<version>3.8.0</version>
</parent>
<artifactId>jeecg-boot-module-airag-flow</artifactId>
<description>此模块属于商业功能范畴,需要购买商业授权才可使用,禁止盗卖、泄露、传播商业源码和商业资料,双方签订合同有保密协议,违者收回商业授权,并追究法律责任</description>
<url>http://www.jeecg.com</url>
<licenses>
<license>
<name>商业授权说明:商业版本只针对企业用户授权,提供更专业更强大的商业版本</name>
<url>http://jeecg.com/vip</url>
<distribution>禁止盗版买卖,违者必究,举报邮箱: jeecgos@163.com</distribution>
<comments>Copyright:北京国炬信息技术有限公司</comments>
</license>
</licenses>
<developers>
<developer>
<name>北京国炬信息技术有限公司</name>
<email>jeecgos@163.com</email>
</developer>
<developer>
<name>授权对象(合作企业): 中国船舶集团有限公司第七〇四研究所</name>
<email>授权范围:贵司通过商业合作,获得商业产品的使用权,但商业版源码、资料禁止传播、盗卖,违者必究!!!</email>
</developer>
</developers>
<scm>
<connection>http://www.jeecg.com</connection>
<developerConnection>http://www.guojusoft.com</developerConnection>
<url>http://www.jeecg.com/vip</url>
</scm>
<properties>
<kotlin.version>1.6.21</kotlin.version>
<liteflow.version>2.12.4.1</liteflow.version>
</properties>
<dependencies>
<!-- system单体 api-->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- liteflow -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
<version>${liteflow.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-lang</artifactId>
<groupId>commons-lang</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-rule-sql</artifactId>
<version>${liteflow.version}</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-graaljs</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-kotlin</artifactId>
<version>${liteflow.version}</version>
<exclusions>
<exclusion>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
</exclusion>
</exclusions>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-aviator</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
<exclusions>
<exclusion>
<artifactId>aviator</artifactId>
<groupId>com.googlecode.aviator</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<!-- ProGuard混淆插件-->
<plugin>
<groupId>com.github.wvengen</groupId>
<artifactId>proguard-maven-plugin</artifactId>
<version>2.6.0</version>
<executions>
<execution>
<!-- 打包的时候开始混淆-->
<phase>package</phase>
<goals><goal>proguard</goal></goals>
</execution>
</executions>
<configuration>
<putLibraryJarsInTempDir>true</putLibraryJarsInTempDir>
<!--指定混淆的jar,如springboot打出来的jar基于它混淆-->
<injar>${project.build.finalName}.jar</injar>
<!--输出的jar-->
<outjar>${project.build.finalName}-pg.jar</outjar>
<!-- 是否混淆-->
<obfuscate>true</obfuscate>
<options>
<option>-target ${java.version}</option> <!--指定java版本号-->
<option>-keepdirectories</option> <!-- 保持目录结构,否则会导致spring自动装配失败 -->
<option>-dontshrink</option> <!--默认开启,不做收缩(删除注释、未被引用代码)-->
<option>-dontoptimize</option><!--默认是开启的,这里关闭字节码级别的优化-->
<option>-adaptclassstrings</option><!--混淆类名之后,对使用Class.forName('className')之类的地方进行相应替代-->
<option>-ignorewarnings</option><!-- 忽略warn消息,如果提示org.apache.http.* 这个包里的类有问题,那么就加入下述代码:-keep class org.apache.http.** { *; } -dontwarn org.apache.http.**-->
<option>-keep class org.apache.logging.log4j.util.* { *; }</option>
<option>-dontwarn org.apache.logging.log4j.util.**</option>
<option>-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod</option><!--对异常、注解信息在runtime予以保留,不然影响springboot启动-->
<!--不混淆所有interface接口-->
<option>-keepnames interface org.jeecg.modules.**.service.* { *; }</option>
<option>-keepnames interface org.jeecg.modules.**.mapper.* { *; }</option>
<option>-keepnames enum org.jeecg.modules.**.enums.* { *; }</option>
<option>-keep enum org.jeecg.modules.airag.flow.component.code.CodeNodeTypeEnum {*;}</option>
<option>-keepnames interface org.jeecg.modules.airag.common.handler.IAIChatHandler { *; }</option>
<option>-keepnames interface org.jeecg.modules.airag.common.handler.IEmbeddingHandler { *; }</option>
<option>-keepnames interface org.jeecg.modules.airag.common.handler.AIChatParams { *; }</option>
<option>-keepnames interface org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava { *; }</option>
<option>-keep class org.jeecg.modules.airag.common.consts.AiragConsts {*;}</option>
<option>-keep class org.jeecg.modules.airag.common.vo.** {*;}</option>
<option>-keep class org.jeecg.modules.airag.flow.vo.** {*;}</option>
<option>-keep class org.jeecg.modules.airag.flow.entity.** {*;}</option>
<option>-keep class org.jeecg.modules.airag.flow.context {*;}</option>
<option>-keep class org.jeecg.modules.airag.flow.consts.FlowConsts {*;}</option>
<option>-keep class org.jeecg.modules.airag.common.utils.AiragLocalCache {*;}</option>
<option>-keep class org.jeecg.modules.airag.common.handler.AIChatParams {*;}</option>
<option>-keep class org.jeecg.modules.airag.flow.context.JeecgFlowContext {*;}</option>
<option>-keepparameternames</option>
<option>-keepclasseswithmembers public class * {
public static void main(java.lang.String[]);}</option> <!--保留main方法的类及其方法名-->
<!--忽略note消息,如果提示javax.annotation有问题,那麽就加入以下代码-->
<option>-dontnote javax.annotation.**</option>
<option>-dontnote sun.applet.**</option>
<option>-dontnote sun.tools.jar.**</option>
<option>-dontnote org.apache.commons.logging.**</option>
<option>-dontnote javax.inject.**</option>
<option>-dontnote org.aopalliance.intercept.**</option>
<option>-dontnote org.aopalliance.aop.**</option>
<option>-dontnote org.apache.logging.log4j.**</option>
<option>-dontnote module-info</option>
<!-- 不混淆所有的set/get方法 -->
<option>-keepclassmembers public class * {void set*(***);***
get*();}</option>
<option>-keepclassmembers class * {
@org.springframework.beans.factory.annotation.Autowired *;
@org.springframework.beans.factory.annotation.Value *;
}
</option>
</options>
<libs>
<lib>${java.home}/</lib>
</libs>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,32 @@
package org.jeecg.modules.airag.common.consts;
/**
* @Description: airag 公用常量
* @Author: chenrui
* @Date: 2025/3/26 16:39
*/
public class AiragConsts {
/**
* sse缓存key
*/
public static final String CACHE_TYPE_SSE = "CHAT:TYPE:SSE";
/**
* sse send Time 缓存key
*/
public static final String CACHE_TYPE_SSE_SEND_TIME = "CHAT:TYPE:SSE_SEND_TIME";
/**
* 消息类型: 系统
*/
public static final String MESSAGE_ROLE_SYSTEM = "system";
/**
* 消息类型: 用户
*/
public static final String MESSAGE_ROLE_USER = "user";
/**
* 消息类型: AI
*/
public static final String MESSAGE_ROLE_AI = "ai";
}
@@ -0,0 +1,28 @@
package org.jeecg.modules.airag.common.handler;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.jeecg.ai.handler.AIParams;
import java.util.List;
/**
* @Description: 自定义ai聊天参数
* @Author: chenrui
* @Date: 2025/3/11 17:45
*/
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
public class AIChatParams extends AIParams {
/**
* 知识库ids
*/
List<String> knowIds;
/**
* 无需思考
*/
Boolean noThinking;
}
@@ -0,0 +1,106 @@
package org.jeecg.modules.airag.common.handler;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.service.TokenStream;
import java.util.List;
/**
* @Description: AI聊天处理器接口
* @Author: chenrui
* @Date: 2025/3/26 17:10
*/
public interface IAIChatHandler {
/**
* 使用默认模型问答
*
* @param messages
* @param params
* @return
* @author chenrui
* @date 2025/3/12 15:13
*/
String completionsByDefaultModel(List<ChatMessage> messages, AIChatParams params);
/**
* AI问答
*
* @param modelId 模型id
* @param messages
* @return
* @author chenrui
* @date 2025/3/26 17:18
*/
String completions(String modelId, List<ChatMessage> messages);
/**
* AI问答
*
* @param modelId 模型id
* @param messages
* @param params
* @return
* @author chenrui
* @date 2025/3/26 17:12
*/
String completions(String modelId, List<ChatMessage> messages, AIChatParams params);
/**
* 聊天(流式)
*
* @param modelId
* @param messages
* @return
* @author chenrui
* @date 2025/2/20 21:06
*/
TokenStream chat(String modelId, List<ChatMessage> messages);
/**
* 聊天(流式)
*
* @param modelId
* @param messages
* @param params
* @return
* @author chenrui
* @date 2025/2/18 21:03
*/
TokenStream chat(String modelId, List<ChatMessage> messages, AIChatParams params);
/**
* 使用默认模型聊天
*
* @param messages
* @param params
* @return
* @author chenrui
* @date 2025/3/12 15:13
*/
TokenStream chatByDefaultModel(List<ChatMessage> messages, AIChatParams params);
/**
* 构造用户消息
* @param content
* @param images
* @return
* @author chenrui
* @date 2025/3/26 18:27
*/
UserMessage buildUserMessage(String content, List<String> images);
/**
* 构造图片消息内容
*
* @param images
* @return
* @author chenrui
* @date 2025/3/18 17:56
*/
List<ImageContent> buildImageContents(List<String> images);
}
@@ -0,0 +1,38 @@
package org.jeecg.modules.airag.common.handler;
import dev.langchain4j.rag.query.router.QueryRouter;
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
import java.util.List;
/**
* @Description: 向量工具类
* @Author: chenrui
* @Date: 2025/3/26 17:03
*/
public interface IEmbeddingHandler {
/**
* 向量搜索
* @param knowIds
* @param queryText
* @param topNumber
* @param similarity
* @return
* @author chenrui
* @date 2025/4/16 19:10
*/
public KnowledgeSearchResult embeddingSearch(List<String> knowIds, String queryText, Integer topNumber, Double similarity);
/**
* 获取知识库查询路由
* @param knowIds
* @param topNumber
* @param similarity
* @return
* @author chenrui
* @date 2025/4/16 19:09
*/
public QueryRouter getQueryRouter(List<String> knowIds, Integer topNumber, Double similarity);
}
@@ -0,0 +1,89 @@
package org.jeecg.modules.airag.common.utils;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* @Description: 本地缓存
* @Author: chenrui
* @Date: 2025/2/24 18:10
*/
public class AiragLocalCache {
private static final Map<String, Cache<String, Object>> CACHE_MAP = new ConcurrentHashMap<>();
/**
* 获取缓存值
*
* @param cacheType 缓存类型
* @param key 缓存键
* @param <T> 返回值类型
* @return 缓存值
*/
public static <T> T get(String cacheType, String key) {
Cache<String, Object> cache = getCache(cacheType);
return (T) cache.getIfPresent(key);
}
/**
* 添加或更新缓存值
*
* @param cacheType 缓存类型
* @param key 缓存键
* @param value 缓存值
*/
public static void put(String cacheType, String key, Object value) {
Cache<String, Object> cache = getCache(cacheType);
cache.put(key, value);
}
/**
* 删除缓存值
*
* @param cacheType 缓存类型
* @param key 缓存键
*/
public static void remove(String cacheType, String key) {
if (CACHE_MAP.containsKey(cacheType)) {
Cache<String, Object> cache = CACHE_MAP.get(cacheType);
cache.invalidate(key);
}
}
/**
* 清空指定缓存类型的所有缓存值
*
* @param cacheType 缓存类型
*/
public static void clear(String cacheType) {
if (CACHE_MAP.containsKey(cacheType)) {
Cache<String, Object> cache = CACHE_MAP.get(cacheType);
cache.invalidateAll();
}
}
/**
* 获取缓存
*
* @param cacheType
* @author chenrui
* @date 2025/2/24 18:50
*/
private static Cache<String, Object> getCache(String cacheType) {
if (!CACHE_MAP.containsKey(cacheType)) {
Cache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(300)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
CACHE_MAP.put(cacheType, cache);
}
return CACHE_MAP.get(cacheType);
}
}
@@ -0,0 +1,60 @@
package org.jeecg.modules.airag.common.utils;
import freemarker.core.TemplateClassResolver;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import java.io.StringWriter;
import java.util.Map;
/**
*
* @description:Freemarker引擎工具类
* @Author scott
* @Date 2019-03-22
* @Version V1.0
*/
@Slf4j
public class FreemarkerHelper {
private static Configuration _tplConfig = new Configuration(Configuration.VERSION_2_3_28);
static{
//模板对于数字超过1000,会自动格式为1,,000(禁止转换)
_tplConfig.setNumberFormat("0.#####################");
_tplConfig.setClassForTemplateLoading(FreemarkerHelper.class, "/");
//update-begin-author:scott date:2023-8-15 for: freemarker模板注入问题 禁止解析ObjectConstructorExecute和freemarker.template.utility.JythonRuntime。
_tplConfig.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
//update-end-author:scott date:2023-8-15 for: freemarker模板注入问题 禁止解析ObjectConstructorExecute和freemarker.template.utility.JythonRuntime。
}
/**
* 解析ftl
* @param tplName 模板名
* @param encoding 编码
* @param paras 参数
* @return
*/
private static String parseTemplate(String tplName, String encoding,
Map<String, Object> paras) {
try {
StringWriter swriter = new StringWriter();
Template mytpl = null;
mytpl = _tplConfig.getTemplate(tplName, encoding);
mytpl.process(paras, swriter);
return swriter.toString();
} catch (Exception e) {
log.error(e.getMessage(), e);
return e.toString();
}
}
public static String parseTemplate(String tplName, Map<String, Object> paras) {
return parseTemplate(tplName, "utf-8", paras);
}
// public static void main(String[] args) {
// String html = FreemarkerHelper.parseTemplate("org/jeecg/modules/online/config/engine/tableTemplate.ftl", null);
// System.out.println(html);
// }
}
@@ -0,0 +1,36 @@
package org.jeecg.modules.airag.common.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Condition;
import org.springframework.core.type.AnnotatedTypeMetadata;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* JDK 版本判断条件
* <p>
* 该类用于判断当前运行环境的 JDK 版本是否为 21。
* 如果是 JDK 21,则返回 false,表示不满足条件。
* 否则返回 true,表示满足条件。
* </p>
*/
public class JdkVersionCondition implements Condition {
private static final Logger logger = LoggerFactory.getLogger(JdkVersionCondition.class);
private static final String JAVA_VERSION = System.getProperty("java.version");
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 判断 JDK 版本是否为 21
Pattern pattern = Pattern.compile("^(1\\.)?21\\..*");
Matcher matcher = pattern.matcher(JAVA_VERSION);
if (matcher.matches()) {
logger.warn("【::AI环境要求::】当前运行环境是 JDK 21 无法使用AI流程编排,目前 AI流程 只支持 JDK 8 ~ JDK 17");
return false; // 如果是 JDK 21,返回 false
}
return true; // 如果不是 JDK 21,返回 true
}
}
@@ -0,0 +1,77 @@
package org.jeecg.modules.airag.common.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.net.URI;
import java.util.List;
/**
* 聊天记录
*
* @author chenrui
* @date 2025/2/25 14:49
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageHistory implements Serializable {
private static final long serialVersionUID = 3238429500037511283L;
/**
* 会话id
*/
String conversationId;
/**
* 话题id
*/
String topicId;
/**
* 角色
*
* @see org.jeecg.modules.airag.common.consts.AiragConsts
*/
String role;
/**
* 聊天记录内容
*/
String content;
/**
* 图片列表
*/
List<ImageHistory> images;
/**
* 发送时间
*/
String datetime;
@Data
public static class ImageHistory {
private URI url;
private String base64Data;
private String mimeType;
public ImageHistory() {
}
public ImageHistory(URI url, String base64Data, String mimeType) {
this.url = url;
this.base64Data = base64Data;
this.mimeType = mimeType;
}
public static ImageHistory from(URI url, String base64Data, String mimeType) {
return new ImageHistory(url, base64Data, mimeType);
}
}
}
@@ -0,0 +1,109 @@
package org.jeecg.modules.airag.common.vo.event;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 流程事件数据
*
* @author chenrui
* @date 2025/2/19 17:56
*/
@Data
@NoArgsConstructor
public class EventData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 事件类型:节点开始
*/
public static final String EVENT_NODE_STARTED = "NODE_STARTED";
/**
* 事件类型:节点结束
*/
public static final String EVENT_NODE_FINISHED = "NODE_FINISHED";
/**
* 事件类型:消息
*/
public static final String EVENT_MESSAGE = "MESSAGE";
/**
* 事件类型:消息完结
*/
public static final String EVENT_MESSAGE_END = "MESSAGE_END";
/**
* 事件类型:流程开始
*/
public static final String EVENT_FLOW_STARTED = "FLOW_STARTED";
/**
* 事件类型:流程结束
*/
public static final String EVENT_FLOW_FINISHED = "FLOW_FINISHED";
/**
* 事件类型:异常
*/
public static final String EVENT_FLOW_ERROR = "ERROR";
/**
* 事件类型:推送初始化requestId
*/
public static final String EVENT_INIT_REQUEST_ID = "INIT_REQUEST_ID";
public EventData(String requestId, String flowId, String event) {
this.requestId = requestId;
this.flowId = flowId;
this.event = event;
}
public EventData(String requestId, String flowId, String event, String conversationId, String topicId) {
this.event = event;
this.flowId = flowId;
this.requestId = requestId;
this.conversationId = conversationId;
this.topicId = topicId;
}
/**
* 事件
*/
private String event;
/**
* 流程id
*/
private String flowId;
/**
* 请求id
*/
private String requestId;
/**
* 会话id
*/
private String conversationId;
/**
* 话题id
*/
private String topicId;
/**
* 数据
*/
private Object data;
@Override
public String toString() {
return JSONObject.toJSONString(this);
}
}
@@ -0,0 +1,46 @@
package org.jeecg.modules.airag.common.vo.event;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* 流程事件数据:流程开始结束
*
* @author chenrui
* @date 2025/2/19 17:56
*/
@Builder
@Data
public class EventFlowData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 是否成功
*/
private boolean success;
/**
* 消息
*/
private String message;
/**
* 执行的节点
*/
private String executeSteps;
/**
* 流程入参
*/
private Map<String, Object> inputs = new HashMap<>();
/**
* 流程结果
*/
private Object outputs;
}
@@ -0,0 +1,56 @@
package org.jeecg.modules.airag.common.vo.event;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* 流程事件数据:节点
*
* @author chenrui
* @date 2025/2/19 17:56
*/
@Builder
@Data
public class EventFlowNodeData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 节点id
*/
private String id;
/**
* 节点类型
*/
private String type;
/**
* 节点名称(描述)
*/
private String text;
/**
* 入参
*/
private Map<String, Object> inputs = new HashMap<>();
/**
* 出参
*/
private Object outputs;
/**
* 是否成功
*/
private boolean success;
/**
* 消息
*/
private String message;
}
@@ -0,0 +1,29 @@
package org.jeecg.modules.airag.common.vo.event;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* 流程事件数据: 消息
*
* @author chenrui
* @date 2025/2/19 17:56
*/
@Builder
@Data
public class EventMessageData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 节点id
*/
private String fromNodeId;
/**
* 消息(实时)
*/
private String message;
}
@@ -0,0 +1,33 @@
package org.jeecg.modules.airag.common.vo.knowledge;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* 知识库查询返回结果
*
* @Author: chenrui
* @Date: 2025/2/18 17:53
*/
@Data
@NoArgsConstructor
public class KnowledgeSearchResult {
/**
* 命中的文档内容
*/
String data;
/**
* 命中的文档列表
*/
List<Map<String, Object>> documents;
public KnowledgeSearchResult(String data, List<Map<String, Object>> documents) {
this.data = data;
this.documents = documents;
}
}
@@ -0,0 +1,90 @@
package org.jeecg.modules.airag.flow.aspect;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.flow.LiteflowResponse;
import com.yomahub.liteflow.lifecycle.PostProcessFlowExecuteLifeCycle;
import com.yomahub.liteflow.slot.Slot;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.common.vo.event.EventData;
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
/**
* 流程切面
*
* @author chenrui
* @date 2025/2/20 15:08
*/
@Slf4j
@Component("flowExecuteAspect")
public class FlowExecuteAspect implements PostProcessFlowExecuteLifeCycle {
@Override
public void postProcessBeforeFlowExecute(String chainId, Slot slot) {
JeecgFlowContext context = slot.getContextBean(JeecgFlowContext.class);
EventData eventData = new EventData(context.getRequestId(),
slot.getChainId(), EventData.EVENT_FLOW_STARTED, context.getConversationId(), context.getTopicId());
EventFlowData data = EventFlowData.builder()
.success(true)
.inputs(context.getRequestDatas())
.build();
eventData.setData(data);
String flowEventDataStr = JSONObject.toJSONString(eventData);
sendEmitter(context, flowEventDataStr, false);
log.info("[Flow-Process-Before]Event:{}", flowEventDataStr);
}
@Override
public void postProcessAfterFlowExecute(String chainId, Slot slot) {
LiteflowResponse response = LiteflowResponse.newMainResponse(slot);
JeecgFlowContext context = slot.getContextBean(JeecgFlowContext.class);
EventData eventData = new EventData(context.getRequestId(),
slot.getChainId(), EventData.EVENT_FLOW_FINISHED, context.getConversationId(), context.getTopicId());
EventFlowData data = EventFlowData.builder()
.success(response.isSuccess())
.executeSteps(response.getExecuteStepStr())
.message(response.getMessage())
.inputs(context.getRequestDatas())
.outputs(context.getResult())
.build();
eventData.setData(data);
if (oConvertUtils.isObjectNotEmpty(context.getEventCallback())) {
try {
context.getEventCallback().accept(eventData);
} catch (Throwable e) {
log.error("[flow]run event callback fail:{}", e.getMessage());
}
}
String flowEventDataStr = JSONObject.toJSONString(eventData);
sendEmitter(context, flowEventDataStr, true);
log.info("[Flow-Process-after]Event:{}", flowEventDataStr);
}
/**
* 发送sse消息
*
* @param context
* @param eventData
* @author chenrui
* @date 2025/2/20 16:55
*/
private static void sendEmitter(JeecgFlowContext context, String eventData, boolean isFinished) {
SseEmitter emitter = context.getEmitter();
if (null != emitter) {
try {
emitter.send(SseEmitter.event().data(eventData));
if (isFinished) {
emitter.complete();
}
} catch (IOException e) {
log.error("SSE send error", e);
emitter.completeWithError(e);
}
}
}
}
@@ -0,0 +1,99 @@
package org.jeecg.modules.airag.flow.aspect;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.aop.ICmpAroundAspect;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.jeecg.modules.airag.common.vo.event.EventData;
import org.jeecg.modules.airag.common.vo.event.EventFlowNodeData;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
/**
* 流程节点切面
*
* @author chenrui
* @date 2025/2/20 15:08
*/
@Slf4j
@Component("nodeProcessAspect")
public class NodeProcessAspect implements ICmpAroundAspect {
@Override
public void beforeProcess(NodeComponent cmp) {
EventData flowEventData = buildEventData(cmp, EventData.EVENT_NODE_STARTED);
String eventData = JSONObject.toJSONString(flowEventData);
sendEmitter(cmp, eventData);
log.info("[Node-Process-Before]Event:{}", eventData);
}
@Override
public void afterProcess(NodeComponent cmp) {
}
@Override
public void onSuccess(NodeComponent cmp) {
EventData flowEventData = buildEventData(cmp, EventData.EVENT_NODE_FINISHED);
EventFlowNodeData eventFlowNodeData = (EventFlowNodeData) flowEventData.getData();
eventFlowNodeData.setSuccess(true);
String eventData = JSONObject.toJSONString(flowEventData);
sendEmitter(cmp, eventData);
log.info("[Node-Process-After]Event:{}", JSONObject.toJSONString(flowEventData));
}
@Override
public void onError(NodeComponent cmp, Exception e) {
EventData flowEventData = buildEventData(cmp, EventData.EVENT_NODE_FINISHED);
EventFlowNodeData eventFlowNodeData = (EventFlowNodeData) flowEventData.getData();
eventFlowNodeData.setSuccess(false);
eventFlowNodeData.setMessage(e.getMessage());
String eventData = JSONObject.toJSONString(flowEventData);
sendEmitter(cmp, eventData);
log.info("[Node-Process-Error]Event:{}", JSONObject.toJSONString(flowEventData));
}
/**
* 构造事件数据
*
* @param cmp
* @param event
* @author chenrui
* @date 2025/2/19 19:25
*/
private static EventData buildEventData(NodeComponent cmp, String event) {
JeecgFlowContext context = cmp.getContextBean(JeecgFlowContext.class);
EventData eventData = new EventData(context.getRequestId(),
cmp.getChainId(), event, context.getConversationId(), context.getTopicId());
FlowNodeConfig nodeConfig = JeecgBaseFlowNode.getNodeConfig(cmp);
EventFlowNodeData eventFlowNodeData = EventFlowNodeData.builder()
.id(nodeConfig.getNodeId())
.type(nodeConfig.getNodeType())
.text(nodeConfig.getText())
.inputs(JeecgBaseFlowNode.getInputs(nodeConfig, context))
.outputs(JeecgBaseFlowNode.getOutputs(nodeConfig, context))
.build();
eventData.setData(eventFlowNodeData);
return eventData;
}
private static void sendEmitter(NodeComponent cmp, String eventData) {
JeecgFlowContext context = cmp.getContextBean(JeecgFlowContext.class);
SseEmitter emitter = context.getEmitter();
if(null != emitter){
try {
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
log.error("SSE send error", e);
emitter.completeWithError(e);
}
}
}
}
@@ -0,0 +1,169 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.jeecg.modules.airag.flow.consts.FlowConsts.*;
/**
* 分类器节点
*
* @author chenrui
* @date 2025/02/17 11:20
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_CLASSIFIER)
public class ClassifierNode extends JeecgBaseFlowNode {
@Autowired
IAIChatHandler aiChatHandler;
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_SWITCH, nodeType = NodeTypeEnum.SWITCH)
public String process(NodeComponent component) {
log.info("节点开始-classifier");
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
JSONObject modelOption = options.getJSONObject(FLOW_NODE_OPTION_MODEL);
if (!modelOption.containsKey(FLOW_NODE_OPTION_MODEL_MODEID) || oConvertUtils.isEmpty(modelOption.containsKey(FLOW_NODE_OPTION_MODEL_MODEID))) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "请选择模型");
}
String modelId = modelOption.getString(FLOW_NODE_OPTION_MODEL_MODEID);
// 整理消息
JSONArray categories = options.getJSONArray(FLOW_NODE_OPTION_MODEL_CATEGORIES);
AssertUtils.assertNotEmpty("[classifier]节点至少配置一个分类", categories);
List<ChatMessage> chatMessages = collateMessage(component, categories, inputParams);
AIChatParams aiChatParams = buildChatParams(modelOption);
// 发送消息,让AI选择
String chooseNodeTag = null;
try {
chooseNodeTag = aiChatHandler.completions(modelId, chatMessages, aiChatParams);
} catch (Exception e) {
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
if (e.getMessage().contains("timeout")) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), FlowConsts.FLOW_ERROR_MSG_LLM_TIMEOUT);
}
throw e;
//update-end---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
}
JeecgFlowContext contextBean = getContext(component);
for (int i = 0; i < categories.size(); i++) {
JSONObject categoryObj = categories.getJSONObject(i);
String next = oConvertUtils.getString(categoryObj.getString(FLOW_NODE_OPTION_NEXT), "");
if (chooseNodeTag.equalsIgnoreCase(next)) {
String categoryDescr = oConvertUtils.getString(categoryObj.getString(FLOW_NODE_OPTION_MODEL_CATEGORY), "");
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_INDEX, i);
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_CONTENT, categoryDescr);
log.info("[classifier]已选择节点:[{}]", chooseNodeTag);
return getNextTag(flowNodeConfig, chooseNodeTag);
}
}
// else
Map<String, Object> elseOption = (Map<String, Object>) options.get(FLOW_NODE_OPTION_ELSE);
if (null != elseOption && !elseOption.isEmpty()) {
log.info("[classifier]选择else:{}", elseOption.get(FLOW_NODE_OPTION_NEXT));
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_INDEX, -1);
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_CONTENT, "都不符合");
return getNextTag(flowNodeConfig, (String) elseOption.get(FLOW_NODE_OPTION_NEXT));
}
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "没有找到匹配的结果");
}
/**
* 构造AI问答参数
* @param modelOption
* @return
* @author chenrui
* @date 2025/3/27 16:09
*/
private static AIChatParams buildChatParams(JSONObject modelOption) {
AIChatParams aiChatParams = new AIChatParams();
JSONObject modelParams = modelOption.getJSONObject(FLOW_NODE_OPTION_MODEL_PARAMS);
if (null != modelParams) {
Double temperature = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_TEMPERATURE);
if (null != temperature) {
aiChatParams.setTemperature(temperature);
}
Double topP = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_TOP);
if (null != topP) {
aiChatParams.setTopP(topP);
}
Double presencePenalty = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_PRESENCE_PENALTY);
if (null != presencePenalty) {
aiChatParams.setPresencePenalty(presencePenalty);
}
Double frequencyPenalty = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_FREQUENCY_PENALTY);
if (null != frequencyPenalty) {
aiChatParams.setFrequencyPenalty(frequencyPenalty);
}
Integer maxTokens = modelParams.getInteger(FLOW_NODE_OPTION_MODEL_MAX_TOKENS);
if (null != maxTokens) {
aiChatParams.setMaxTokens(maxTokens);
}
}
return aiChatParams;
}
private List<ChatMessage> collateMessage(NodeComponent component, JSONArray categories, List<FlowNodeConfig.NodeParam> inputParams) {
JeecgFlowContext contextBean = getContext(component);
List<ChatMessage> chatMessages = new ArrayList<>();
AssertUtils.assertNotEmpty("分类器节点,请选择输入变量", inputParams);
AssertUtils.assertNotEmpty("分类器节点,至少需要配置一个分类", categories);
for (int i = 0; i < categories.size(); i++) {
JSONObject categoryObj = (JSONObject) categories.get(i);
String categoryDescr = oConvertUtils.getString(categoryObj.getString(FLOW_NODE_OPTION_MODEL_CATEGORY), "");
AssertUtils.assertNotEmpty("" + (i + 1) + "个分类未填写描述", categoryDescr);
AssertUtils.assertNotEmpty("分类" + categoryDescr + "未选择跳转节点", categoryObj.getString(FLOW_NODE_OPTION_NEXT));
}
// 系统消息
String systemMsgContent = "请根据用户的问题,从以下分类数组中选择最符合的分类对象。 \n" +
"回复该分类对象的 `next` 属性值。 \n" +
"如果 **找不到最符合的**,请返回 `-1`。 " +
"分类数据如下:\n" +
"---------------------------\n" +
categories.toJSONString() + "\n" +
"---------------------------\n";
chatMessages.add(new SystemMessage(systemMsgContent));
// 用户消息
FlowNodeConfig.NodeParam firstInputParams = inputParams.get(0);
Object inputData = contextBean.getContextData(firstInputParams.getNodeId(), firstInputParams.getField());
chatMessages.add(new UserMessage(inputData.toString()));
return chatMessages;
}
}
@@ -0,0 +1,73 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_END)
public class EndNode extends JeecgBaseFlowNode {
public static final Pattern VAR_PLACEHOLDER_PATTERN = Pattern.compile("\\{\\{(.*?)}}");
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-end");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
// 整理输出参数
List<FlowNodeConfig.NodeParam> outputParams = flowNodeConfig.getOutputParams();
Map<String, Object> outputMap = new HashMap<>();
for (FlowNodeConfig.NodeParam outputParam : outputParams) {
Object data = contextBean.getContextData(outputParam.getNodeId(), outputParam.getField());
outputMap.put(outputParam.getName(), data);
}
// 判断是否输出文本
boolean outputText = false;
JSONObject options = null;
if (oConvertUtils.isObjectNotEmpty(flowNodeConfig.getOptions())) {
options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
outputText = options.getBooleanValue(FlowConsts.FLOW_NODE_OPTION_OUTPUT_TEXT);
}
if (outputText) {
// 输出文本: 构建文本内容
String outputContent = options.getString(FlowConsts.FLOW_NODE_OPTION_OUTPUT_CONTENT);
if (oConvertUtils.isNotEmpty(outputContent)) {
Matcher matcher = VAR_PLACEHOLDER_PATTERN.matcher(outputContent);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String key = matcher.group(1);
Object val = (oConvertUtils.isObjectNotEmpty(outputMap)) ? outputMap.get(key) : null;
matcher.appendReplacement(sb, (val != null) ? Matcher.quoteReplacement(val.toString()) : "");
}
matcher.appendTail(sb);
contextBean.setResult(sb.toString());
}
} else {
contextBean.setResult(outputMap);
}
// 结束流程
component.setIsEnd(true);
}
}
@@ -0,0 +1,469 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.SysUserCacheInfo;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.RestUtil;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.context.annotation.Conditional;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
/**
* http节点
*
* @author chenrui
* @date 2025/2/11 17:25
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_HTTP)
public class HttpNode extends JeecgBaseFlowNode {
/**
* 输出参数:body
*/
public static final String OUTPUT_PARAM_NAME_BODY = "body";
/**
* 输出参数:statusCode
*/
public static final String OUTPUT_PARAM_NAME_STATUS_CODE = "statusCode";
/**
* 请求体类型: none
*/
private static final String OPTIONS_BODY_TYPE_NONE = "none";
/**
* 请求体类型: binary
*/
private static final String OPTIONS_BODY_TYPE_BINARY = "binary";
/**
* 请求体类型: raw
*/
private static final String OPTIONS_BODY_TYPE_RAW = "raw";
/**
* 请求体类型: x-www-form-urlencoded
*/
private static final String OPTIONS_BODY_TYPE_X_WWW_FORM_URLENCODED = "x-www-form-urlencoded";
/**
* 请求体类型: form-data
*/
private static final String OPTIONS_BODY_TYPE_FORM_DATA = "form-data";
/**
* 请求体类型: json
*/
private static final String OPTIONS_BODY_TYPE_JSON = "json";
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-http");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
// 节点配置
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
HttpOptions httpOptions = options.getJSONObject(FlowConsts.FLOW_NODE_OPTION_HTTP).toJavaObject(HttpOptions.class);
// 检查必填参数
if (oConvertUtils.isEmpty(httpOptions.getMethod()) || oConvertUtils.isEmpty(httpOptions.getUrl())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "缺少必要参数");
}
// 防御SSRF: 检查domain是否在允许的范围内
// TODO author: chenrui for:防御SSRF: 检查domain是否在允许的范围内 date:2025/3/13
// 处理请求参数
ReqParam reqParam = preprocessReq(httpOptions, flowNodeConfig, contextBean, inputParams);
// 发送请求
ResponseEntity<String> responseEntity;
try {
// 发送请求
log.info("[FLOW_HTTP]发送http请求:url:[{}]", reqParam.getUrl());
responseEntity = RestUtil.request(reqParam.getUrl(), reqParam.getHttpMethod(), reqParam.getHttpHeaders(), reqParam.getRequestParams(), reqParam.getBody(), String.class);
// 这是返回参数
contextBean.setContextData(flowNodeConfig.getNodeId(), OUTPUT_PARAM_NAME_STATUS_CODE, responseEntity.getStatusCodeValue());
// 获取响应体
String respBody = responseEntity.getBody();
contextBean.setContextData(flowNodeConfig.getNodeId(), OUTPUT_PARAM_NAME_BODY, respBody);
if(oConvertUtils.isNotEmpty(respBody)) {
processOutputParams(flowNodeConfig,respBody,contextBean);
}
} catch (Exception e) {
String errMessage = e.getMessage();
String detailMessage = errMessage;
if (e instanceof HttpClientErrorException) {
HttpClientErrorException httpClientException = (HttpClientErrorException) e;
errMessage = httpClientException.getStatusCode().toString();
}
log.error("[FLOW_HTTP]发送http请求失败:" + detailMessage,e);
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "发送http请求失败:" + errMessage);
}
}
/**
* 预处理请求
*
* @param httpOptions
* @param flowNodeConfig
* @param contextBean
* @param inputParams
* @return
* @author chenrui
* @date 2025/3/25 11:07
*/
private ReqParam preprocessReq(HttpOptions httpOptions, FlowNodeConfig flowNodeConfig, JeecgFlowContext contextBean, List<FlowNodeConfig.NodeParam> inputParams) {
// method
HttpMethod httpMethod = HttpMethod.resolve(httpOptions.getMethod().toUpperCase());
if (httpMethod == null) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "无效的HTTP方法");
}
// 流程上下文参数以及系统上下文变量
HttpServletRequest httpRequest = contextBean.getHttpRequest();
SysUserCacheInfo user = contextBean.getCacheUser();
Map<String, Object> inputs = collateInputParams(inputParams, contextBean);
// 处理请求地址
String url = httpOptions.getUrl();
if (url.contains("{") && url.contains("}")) {
// 替换变量
if (url.contains("{{domainURL}}")) {
String baseUrl = CommonUtils.getBaseUrl(httpRequest);
url = url.replaceFirst("\\{\\{domainURL}}", baseUrl);
}
url = replaceVar(url, inputs, user);
}
// 获取请求体
JSONObject requestBody = httpOptions.getRequestBody();
String bodyType = requestBody.getString("type");
String bodyStr = requestBody.getString("body");
// 处理输入内容
bodyStr = sanitizeInput(bodyStr);
// 替换变量
bodyStr = replaceVar(bodyStr, inputs, user);
Object body = bodyStr;
if (!bodyType.equalsIgnoreCase(OPTIONS_BODY_TYPE_RAW) && !bodyType.equalsIgnoreCase(OPTIONS_BODY_TYPE_NONE)) {
body = JSONObject.parseObject(bodyStr);
}
// 处理params
JSONObject requestParams = httpOptions.getRequestParams();
if (null != requestParams) {
requestParams.keySet().forEach(key -> {
Object val = requestParams.get(key);
if (val instanceof String) {
// 替换变量
String strVal = (String) val;
strVal = sanitizeInput(strVal);
strVal = replaceVar(strVal, inputs, user);
requestParams.put(key, strVal);
}
});
}
// 组装请求头,格式化body
HttpHeaders httpHeaders = buildHeader(bodyType, contextBean, httpOptions, inputs, user);
return new ReqParam(httpMethod, url, body, requestParams, httpHeaders);
}
/**
* 替换变量
*
* @param bodyStr
* @param inputs
* @return
* @author chenrui
* @date 2025/3/13 17:37
*/
private static String replaceVar(String bodyStr, Map<String, Object> inputs, SysUserCacheInfo user) {
Matcher matcher = FlowConsts.VAR_PLACEHOLDER_PATTERN.matcher(bodyStr);
StringBuffer resMessage = new StringBuffer();
while (matcher.find()) {
String key = matcher.group(1);
Object val = (oConvertUtils.isObjectNotEmpty(inputs)) ? inputs.get(key) : null;
if (null == val) {
val = JwtUtil.getUserSystemData("#{" + key + "}", user);
}
matcher.appendReplacement(resMessage, (val != null) ? Matcher.quoteReplacement(val.toString()) : "");
}
matcher.appendTail(resMessage);
return resMessage.toString();
}
/**
* 整理输入变量
*
* @param inputParams
* @param contextBean
* @return
* @author chenrui
* @date 2025/3/13 15:59
*/
private Map<String, Object> collateInputParams(List<FlowNodeConfig.NodeParam> inputParams, JeecgFlowContext contextBean) {
Map<String, Object> inputs = new HashMap<>();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
// 从context中获取
Object inputData = contextBean.getContextData(inputParam.getNodeId(), inputParam.getField());
inputs.put(inputParam.getName(), inputData);
}
return inputs;
}
/**
* 构造header
*
* @param bodyType
* @param contextBean
* @param httpOptions
* @param inputs
* @param user
* @return
* @author chenrui
* @date 2025/3/13 17:37
*/
private static HttpHeaders buildHeader(String bodyType, JeecgFlowContext contextBean, HttpOptions httpOptions, Map<String, Object> inputs, SysUserCacheInfo user) {
HttpHeaders httpHeaders;
switch (bodyType) {
case OPTIONS_BODY_TYPE_NONE:
httpHeaders = new HttpHeaders();
break;
case OPTIONS_BODY_TYPE_JSON:
httpHeaders = RestUtil.getHeader(MediaType.APPLICATION_JSON_UTF8_VALUE);
break;
case OPTIONS_BODY_TYPE_FORM_DATA:
httpHeaders = RestUtil.getHeader(MediaType.MULTIPART_FORM_DATA_VALUE);
break;
case OPTIONS_BODY_TYPE_X_WWW_FORM_URLENCODED:
httpHeaders = RestUtil.getHeader(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
break;
case OPTIONS_BODY_TYPE_RAW:
httpHeaders = RestUtil.getHeader(MediaType.TEXT_PLAIN_VALUE);
break;
case OPTIONS_BODY_TYPE_BINARY:
httpHeaders = RestUtil.getHeader(MediaType.APPLICATION_OCTET_STREAM_VALUE);
break;
default:
httpHeaders = RestUtil.getHeaderApplicationJson();
break;
}
HttpServletRequest httpRequest = contextBean.getHttpRequest();
// token
String token = TokenUtils.getTokenByRequest(httpRequest);
if (oConvertUtils.isNotEmpty(token)) {
httpHeaders.set("X-Access-Token", token);
}
// tenantId
String tenantId = TokenUtils.getTenantIdByRequest(httpRequest);
if (oConvertUtils.isNotEmpty(tenantId)) {
httpHeaders.set("X-Tenant-Id", tenantId);
}
// 自定义请求头
JSONObject headers = httpOptions.getHeaders();
for (String key : headers.keySet()) {
String headerVal = headers.getString(key);
headerVal = replaceVar(headerVal, inputs, user);
httpHeaders.add(key, headerVal);
}
return httpHeaders;
}
/**
* 检查请求内容,防止XSS等攻击
*
* @param input 输入字符串
* @return 清理后的字符串
*/
private String sanitizeInput(String input) {
// 防御XSS: 对特殊字符进行HTML编码
input = input.replaceAll("&", "&amp;");
input = input.replaceAll("<", "&lt;");
input = input.replaceAll(">", "&gt;");
input = input.replaceAll("'", "&#x27;");
input = input.replaceAll("/", "&#x2F;");
// 替换掉空格,否则json解析失败
input = input.replace("\u00A0", " ");
return input;
}
/**
* 处理输出参数
*
* @param flowNodeConfig
* @param bodyString
* @param contextBean
*/
public void processOutputParams(FlowNodeConfig flowNodeConfig, String bodyString, JeecgFlowContext contextBean) {
List<FlowNodeConfig.NodeParam> outputParams = flowNodeConfig.getOutputParams();
if (oConvertUtils.isEmpty(bodyString)) {
return;
}
JSONObject body = JSONObject.parseObject(bodyString);
for (FlowNodeConfig.NodeParam outputParam : outputParams) {
String field = outputParam.getField();
if (oConvertUtils.isEmpty(field) || OUTPUT_PARAM_NAME_BODY.equalsIgnoreCase(field) || OUTPUT_PARAM_NAME_STATUS_CODE.equalsIgnoreCase(field)) {
continue;
}
Object value = getValueFromBody(field, body);
if (value != null) {
try {
value = convertToType(value, outputParam.getType());
}catch (Exception e) {
log.error("转换类型失败,field:{},value:{}", field, value);
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "转换类型失败");
}
}
contextBean.setContextData(flowNodeConfig.getNodeId(), field, value != null ? value : "");
}
}
/**
* 从响应体中获取值
*
* @param field 字段名
* @param body 响应体
* @return 值
*/
private Object getValueFromBody(String field, JSONObject body) {
if(field.startsWith(OUTPUT_PARAM_NAME_BODY+".")) {
field = field.replaceFirst(OUTPUT_PARAM_NAME_BODY+".", "");
}
String[] fieldParts = field.split("\\.");
Object value = body;
for (String part : fieldParts) {
if (value instanceof JSONObject) {
if (part.contains("[") && part.contains("]")) {
String arrayField = part.substring(0, part.indexOf("["));
int index = Integer.parseInt(part.substring(part.indexOf("[") + 1, part.indexOf("]")));
JSONArray arrayData = ((JSONObject) value).getJSONArray(arrayField);
if(arrayData.size()>index+1) {
value = arrayData.get(index);
}else{
value = null;
}
} else {
value = ((JSONObject) value).get(part);
}
} else {
return null;
}
}
return value;
}
private Object convertToType(Object value, String type) {
if (FlowConsts.FLOW_PARAM_TYPE_STRING.equals(type)) {
return value.toString();
} else if (FlowConsts.FLOW_PARAM_TYPE_NUMBER.equals(type)) {
return value instanceof Number ? value : (Object) Double.parseDouble(value.toString());
} else if (FlowConsts.FLOW_PARAM_TYPE_PICTURE.equals(type)) {
// Assuming picture is a base64 encoded string
return value.toString();
} else if (FlowConsts.FLOW_PARAM_TYPE_OBJECT.equals(type)) {
return value instanceof JSONObject ? value : JSONObject.parseObject(value.toString());
} else if (FlowConsts.FLOW_PARAM_TYPE_STRING_ARRAY.equals(type)) {
return value instanceof List ? ((List<?>) value).toArray(new Object[0]) : new String[]{value.toString()};
} else if (FlowConsts.FLOW_PARAM_TYPE_NUMBER_ARRAY.equals(type)) {
return value instanceof List ? ((List<?>) value).stream().mapToDouble(v -> Double.parseDouble(v.toString())).toArray() : new double[]{Double.parseDouble(value.toString())};
} else if (FlowConsts.FLOW_PARAM_TYPE_OBJECT_ARRAY.equals(type)) {
return value instanceof List ? ((List<?>) value).toArray(new Object[0]) : new JSONObject[]{JSONObject.parseObject(value.toString())};
} else {
return value;
}
}
/**
* 请求参数类
*
* @author chenrui
* @date 2025/3/25 11:09
*/
@Data
private static class ReqParam {
private final HttpMethod httpMethod;
private final String url;
private final Object body;
private final JSONObject requestParams;
private final HttpHeaders httpHeaders;
public ReqParam(HttpMethod httpMethod, String url, Object body, JSONObject requestParams, HttpHeaders httpHeaders) {
this.httpMethod = httpMethod;
this.url = url;
this.body = body;
this.requestParams = requestParams;
this.httpHeaders = httpHeaders;
}
}
/**
* HTTP选项类
*/
@Data
public static class HttpOptions {
/**
* HTTP方法(例如:GET, POST
*/
private String method;
/**
* 发送请求的URL
*/
private String url;
/**
* 请求参数,JSON对象
*/
private JSONObject requestParams;
/**
* 请求体
*/
private JSONObject requestBody;
/**
* HTTP头,JSON对象
*/
private JSONObject headers;
/**
* 请求超时时间,单位为毫秒
*/
private int timeout;
}
}
@@ -0,0 +1,91 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import dev.langchain4j.rag.query.router.QueryRouter;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.handler.IEmbeddingHandler;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
import static org.jeecg.modules.airag.flow.consts.FlowConsts.*;
/**
* 知识库节点
*
* @author chenrui
* @date 2025/2/18 20:49
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_KNOWLEDGE)
public class KnowledgeNode extends JeecgBaseFlowNode {
/**
* 输出参数:data (拼接的命中内容)
*/
public static final String OUTPUT_PARAM_NAME_DATA = "data";
/**
* 输出参数:documents (命中的文档片段)
*/
public static final String OUTPUT_PARAM_NAME_DOCUMENTS = "documents";
/**
* 输出参数:queryRouter (查询路由,供llm节点使用)
*/
public static final String OUTPUT_PARAM_NAME_QUERY_ROUTER = "queryRouter";
@Autowired
IEmbeddingHandler embeddingHandler;
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-knowledge");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
// 获取参数
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
if (oConvertUtils.isObjectEmpty(inputParams)) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "必须选择输入变量");
}
FlowNodeConfig.NodeParam inputParam = inputParams.get(0);
Object question = contextBean.getContextData(inputParam.getNodeId(), inputParam.getField());
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
Integer topNumber = options.getInteger(FLOW_NODE_OPTION_TOP_NUMBER);
Double similarity = options.getDouble(FLOW_NODE_OPTION_SIMILARITY);
JSONArray knowIdArray = options.getJSONArray(FLOW_NODE_OPTION_KNOW_IDS);
if (oConvertUtils.isObjectEmpty(knowIdArray)) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "请选择知识库");
}
List<String> knowIds = knowIdArray.stream().map(Object::toString).collect(Collectors.toList());
// 向量查询
KnowledgeSearchResult resultData = embeddingHandler.embeddingSearch(knowIds, question.toString(), topNumber, similarity);
QueryRouter queryRouter = embeddingHandler.getQueryRouter(knowIds, topNumber, similarity);
contextBean.setContextData(flowNodeConfig.getNodeId(), OUTPUT_PARAM_NAME_DATA, resultData.getData());
contextBean.setContextData(flowNodeConfig.getNodeId(), OUTPUT_PARAM_NAME_DOCUMENTS, resultData.getDocuments());
contextBean.setContextData(flowNodeConfig.getNodeId(), OUTPUT_PARAM_NAME_QUERY_ROUTER, queryRouter);
}
}
@@ -0,0 +1,339 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import dev.langchain4j.data.message.*;
import dev.langchain4j.rag.query.router.QueryRouter;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.consts.AiragConsts;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.common.vo.MessageHistory;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNode;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static org.jeecg.modules.airag.flow.consts.FlowConsts.*;
/**
* 大模型节点
*
* @author chenrui
* @date 2025/2/11 17:25
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_LLM)
public class LlmNode extends JeecgBaseFlowNode {
@Autowired
IAIChatHandler aiChatHandler;
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-llm");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
// 整理入参
CollatedInputParams collatedParams = collateInputParams(component, inputParams);
// 整理消息
String messagesOpStr = options.getString(FLOW_NODE_OPTION_MODEL_MESSAGES);
List<MessageHistory> messages = JSONArray.parseArray(messagesOpStr, MessageHistory.class);
AssertUtils.assertNotEmpty("LLM节点至少配置一条用户消息", messages);
// 历史消息
Object historyMessageObj = contextBean.getRequestData(FLOW_INPUT_PARAM_HISTORY);
if (historyMessageObj instanceof List) {
List<MessageHistory> historyMessages = (List) historyMessageObj;
if (oConvertUtils.isObjectNotEmpty(historyMessages)) {
messages.addAll(0, historyMessages);
}
}
List<ChatMessage> chatMessages = convertToChatMessage(messages, collatedParams);
// AI发送消息
String response = null;
try {
response = sendMessage2AI(component, options.getJSONObject(FLOW_NODE_OPTION_MODEL),
collatedParams.queryRouter, contextBean, flowNodeConfig, chatMessages);
} catch (Exception e) {
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
if (e.getMessage().contains("timeout")) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), FlowConsts.FLOW_ERROR_MSG_LLM_TIMEOUT);
}
throw e;
//update-end---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
}
List<FlowNodeConfig.NodeParam> outputParams = flowNodeConfig.getOutputParams();
FlowNodeConfig.NodeParam outputParam = outputParams.get(0);
contextBean.setContextData(flowNodeConfig.getNodeId(), outputParam.getField(), response);
}
/**
* 给AI发消息
*
* @param component
* @param modelOption
* @param queryRouter
* @param contextBean
* @param flowNodeConfig
* @param chatMessages
* @return
* @author chenrui
* @date 2025/2/27 19:40
*/
private String sendMessage2AI(NodeComponent component, JSONObject modelOption, QueryRouter queryRouter,
JeecgFlowContext contextBean, FlowNodeConfig flowNodeConfig, List<ChatMessage> chatMessages) {
// 构造ai请求参数
if (!modelOption.containsKey(FLOW_NODE_OPTION_MODEL_MODEID) || oConvertUtils.isEmpty(modelOption.containsKey(FLOW_NODE_OPTION_MODEL_MODEID))) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "请选择模型");
}
String modelId = modelOption.getString(FLOW_NODE_OPTION_MODEL_MODEID);
AIChatParams aiParams = buildAiParams(modelOption, queryRouter, flowNodeConfig);
return aiChatHandler.completions(modelId, chatMessages, aiParams);
}
/**
* 构造ai参数
*
* @param modelOption
* @param queryRouter
* @param flowNodeConfig
* @return
* @author chenrui
* @date 2025/3/3 20:10
*/
private static AIChatParams buildAiParams(JSONObject modelOption, QueryRouter queryRouter, FlowNodeConfig flowNodeConfig) {
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
// 历史消息数量
Integer historyNum = oConvertUtils.getInt(options.getInteger(FLOW_NODE_OPTION_HISTORY_NUM), 3);
AIChatParams aiChatParams = new AIChatParams();
aiChatParams.setQueryRouter(queryRouter);
aiChatParams.setMaxMsgNumber(historyNum);
JSONObject modelParams = modelOption.getJSONObject(FLOW_NODE_OPTION_MODEL_PARAMS);
if (null != modelParams) {
Double temperature = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_TEMPERATURE);
if (null != temperature) {
aiChatParams.setTemperature(temperature);
}
Double topP = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_TOP);
if (null != topP) {
aiChatParams.setTopP(topP);
}
Double presencePenalty = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_PRESENCE_PENALTY);
if (null != presencePenalty) {
aiChatParams.setPresencePenalty(presencePenalty);
}
Double frequencyPenalty = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_FREQUENCY_PENALTY);
if (null != frequencyPenalty) {
aiChatParams.setFrequencyPenalty(frequencyPenalty);
}
Integer maxTokens = modelParams.getInteger(FLOW_NODE_OPTION_MODEL_MAX_TOKENS);
if (null != maxTokens) {
aiChatParams.setMaxTokens(maxTokens);
}
}
return aiChatParams;
}
/**
* 整理入参
*
* @param component
* @param inputParams
* @return
* @author chenrui
* @date 2025/2/18 21:13
*/
private CollatedInputParams collateInputParams(NodeComponent component, List<FlowNodeConfig.NodeParam> inputParams) {
JeecgFlowContext contextBean = getContext(component);
// 知识库查询路由
QueryRouter queryRouter = null;
// 消息替换map
Map<String, Object> messageReplaceMap = new HashMap<>();
List<String> images = new ArrayList<>();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
Object inputData = contextBean.getContextData(inputParam.getNodeId(), inputParam.getField());
if (oConvertUtils.isEmpty(inputData)) {
continue;
}
// 判断入参是不是knowledge节点
FlowNode paramFlowNode = getFlowNodeByTag(component, inputParam.getNodeId());
if (null != paramFlowNode) {
if (FLOW_NODE_TYPE_KNOWLEDGE.equalsIgnoreCase(paramFlowNode.getType())) {
queryRouter = contextBean.getContextData(paramFlowNode.getId(), KnowledgeNode.OUTPUT_PARAM_NAME_QUERY_ROUTER);
continue;
}
}
boolean isImage = isParamImage(component, inputParam);
if (isImage) {
// 图片类型
if (inputData instanceof List) {
List<String> imageList = (List<String>) inputData;
if (oConvertUtils.isObjectNotEmpty(imageList)) {
images.addAll(imageList);
}
} else if (inputData instanceof String) {
if (((String) inputData).contains(",")) {
// 逗号分隔的图片
String[] imageArray = ((String) inputData).split(",");
if (oConvertUtils.isObjectNotEmpty(imageArray)) {
images.addAll(Arrays.asList(imageArray));
}
} else {
// 单个图片
images.add((String) inputData);
}
}
continue;
}
String placeholder = "{{" + inputParam.getName() + "}}";
messageReplaceMap.put(placeholder, inputData);
}
return new CollatedInputParams(queryRouter, messageReplaceMap, images);
}
protected static boolean isParamImage(NodeComponent component, FlowNodeConfig.NodeParam param) {
FlowNode paramFlowNode = getFlowNodeByTag(component, param.getNodeId());
if (null == paramFlowNode) {
return false;
}
FlowNodeConfig properties = paramFlowNode.getProperties();
if (FLOW_NODE_TYPE_START.equalsIgnoreCase(paramFlowNode.getType())) {
// 变量来源于开始节点
List<FlowNodeConfig.NodeParam> beginNodeInputParams = properties.getInputParams();
if (null != beginNodeInputParams) {
for (FlowNodeConfig.NodeParam inputParam : beginNodeInputParams) {
if (inputParam.getField().equalsIgnoreCase(param.getField())) {
if (FlowConsts.FLOW_PARAM_TYPE_PICTURE.equalsIgnoreCase(inputParam.getType())) {
// 参数是图片类型
return true;
}
}
}
}
} else {
// 不来源于开始
if (oConvertUtils.isNotEmpty(properties.getOutputParams())) {
for (FlowNodeConfig.NodeParam outputParam : properties.getOutputParams()) {
if (outputParam.getField().equalsIgnoreCase(param.getField())) {
if (FlowConsts.FLOW_PARAM_TYPE_PICTURE.equalsIgnoreCase(param.getType())) {
// 参数是图片类型
return true;
}
}
}
}
}
return false;
}
/**
* 整理后的入参
*
* @author chenrui
* @date 2025/2/18 21:12
*/
private static class CollatedInputParams {
public final QueryRouter queryRouter;
public final Map<String, Object> messageReplaceMap;
public final List<String> images;
public CollatedInputParams(QueryRouter queryRouter, Map<String, Object> messageReplaceMap, List<String> images) {
this.queryRouter = queryRouter;
this.messageReplaceMap = messageReplaceMap;
this.images = images;
}
}
/**
* 类型转换并替换占位符
*
* @param messages
* @param collatedInputParams
* @return
* @author chenrui
* @date 2025/2/20 20:28
*/
private List<ChatMessage> convertToChatMessage(List<MessageHistory> messages, CollatedInputParams collatedInputParams) {
Map<String, Object> messageReplaceMap = collatedInputParams.messageReplaceMap;
AtomicInteger userCount = new AtomicInteger(0);
List<ChatMessage> chatMessages = messages.stream()
.filter(Objects::nonNull)
.filter(message -> oConvertUtils.isNotEmpty(message.getContent()))
.map(message -> {
// 替换占位符
String content = message.getContent();
for (String placeholder : messageReplaceMap.keySet()) {
content = content.replace(placeholder, messageReplaceMap.get(placeholder).toString());
}
if (AiragConsts.MESSAGE_ROLE_SYSTEM.equalsIgnoreCase(message.getRole())) {
// 系统消息
return new SystemMessage(content);
} else if (AiragConsts.MESSAGE_ROLE_USER.equalsIgnoreCase(message.getRole())) {
// 用户消息,组装图片和文本
List<Content> contents = new ArrayList<>();
List<MessageHistory.ImageHistory> images = message.getImages();
if (oConvertUtils.isObjectNotEmpty(images)
&& !images.isEmpty()) {
contents.addAll(images.stream().map(imageHistory -> {
if (oConvertUtils.isNotEmpty(imageHistory.getUrl())) {
return ImageContent.from(imageHistory.getUrl());
} else {
return ImageContent.from(imageHistory.getBase64Data(), imageHistory.getMimeType());
}
}).collect(Collectors.toList()));
}
contents.add(TextContent.from(content));
userCount.getAndIncrement();
return UserMessage.from(contents);
} else {
// 助手消息
return new AiMessage(content);
}
}).collect(Collectors.toList());
AssertUtils.assertGt("LLM节点至少配置一条用户消息", userCount.get(), 0);
// 如果入参有图片,则将图片添加到最后一条用户消息中
if (oConvertUtils.isObjectNotEmpty(collatedInputParams.images)) {
for (int i = chatMessages.size() - 1; i >= 0; i--) {
ChatMessage message = chatMessages.get(i);
if (ChatMessageType.USER.equals(message.type())) {
UserMessage lastUserMessage = (UserMessage) message;
List<ImageContent> imageContents = aiChatHandler.buildImageContents(collatedInputParams.images);
List<Content> contents = new ArrayList<>(lastUserMessage.contents());
contents.addAll(imageContents);
chatMessages.set(i, UserMessage.from(contents));
break;
}
}
}
return chatMessages;
}
}
@@ -0,0 +1,90 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.common.vo.event.EventData;
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
/**
* @Description: 直接回复节点
* @Author: chenrui
* @Date: 2025/3/13 10:26
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_REPLY)
public class ReplyNode extends JeecgBaseFlowNode {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-http");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
Map<String, Object> inputs = new HashMap<>();
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
// 从context中获取
Object inputData = contextBean.getContextData(inputParam.getNodeId(), inputParam.getField());
inputs.put(inputParam.getName(), inputData);
}
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
// sse
SseEmitter emitter = contextBean.getEmitter();
if (null != emitter) {
// 回复内容
String content = options.getString(FlowConsts.FLOW_NODE_OPTION_CONTENT);
if (oConvertUtils.isNotEmpty(content)) {
Matcher matcher = FlowConsts.VAR_PLACEHOLDER_PATTERN.matcher(content);
StringBuffer resMessage = new StringBuffer();
while (matcher.find()) {
String key = matcher.group(1);
Object val = (oConvertUtils.isObjectNotEmpty(inputs)) ? inputs.get(key) : null;
matcher.appendReplacement(resMessage, (val != null) ? Matcher.quoteReplacement(val.toString()) : "");
}
matcher.appendTail(resMessage);
// 流式回复消息
EventData eventData = new EventData(contextBean.getRequestId(),
component.getChainId(), EventData.EVENT_MESSAGE, contextBean.getConversationId(), contextBean.getTopicId());
EventMessageData messageEventData = EventMessageData.builder()
.fromNodeId(flowNodeConfig.getNodeId())
.message(resMessage.toString())
.build();
eventData.setData(messageEventData);
try {
emitter.send(SseEmitter.event().data(JSONObject.toJSONString(eventData)));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
@@ -0,0 +1,53 @@
package org.jeecg.modules.airag.flow.component;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 开始节点
*
* @author chenrui
* @date 2025/2/11 17:13
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_START)
public class StartNode extends JeecgBaseFlowNode {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-begin");
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
JeecgFlowContext context = getContext(component);
// 初始化租户
if(oConvertUtils.isNotEmpty(context.getTenantId())){
TenantContext.setTenant(context.getTenantId());
}
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
Object requestData = context.getRequestData(inputParam.getField());
if(oConvertUtils.isEmpty(requestData)){
if (inputParam.getRequired() && !inputParam.getField().equals(FlowConsts.FLOW_INPUT_PARAM_HISTORY)) {
throw new AiRagFlowRuntimeException("缺少必要参数:" + inputParam.getName() + "(" + inputParam.getField() + ")");
}
}
context.setContextData(flowNodeConfig.getNodeId(), inputParam.getField(), requestData);
}
}
}
@@ -0,0 +1,96 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
import org.jeecg.modules.airag.flow.vo.api.FlowRunInnerParams;
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.jeecg.modules.airag.flow.consts.FlowConsts.FLOW_NODE_OPTION_SUBFLOW_ID;
import static org.jeecg.modules.airag.flow.consts.FlowConsts.FLOW_RESPONSE_MODE_BLOCKING;
/**
* 子流程节点
*
* @author chenrui
* @date 2025/2/26 17:25
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_SUBFLOW)
public class SubflowNode extends JeecgBaseFlowNode {
@Autowired
IAiragFlowService airagFlowService;
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-sub");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
String subflowId = options.getString(FLOW_NODE_OPTION_SUBFLOW_ID);
if (oConvertUtils.isEmpty(subflowId)) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "请选择子流程");
}
FlowRunInnerParams flowRunParams = new FlowRunInnerParams();
// 子流程id
flowRunParams.setFlowId(subflowId);
// 阻塞式返回
flowRunParams.setResponseMode(FLOW_RESPONSE_MODE_BLOCKING);
// 子流程入参
Map<String, Object> subInputs = new HashMap<>();
// 默认参数:历史记录
Object history = contextBean.getRequestData(FlowConsts.FLOW_INPUT_PARAM_HISTORY);
subInputs.put(FlowConsts.FLOW_INPUT_PARAM_HISTORY,history);
// 自定义入参
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
// 从context中获取
Object inputData = contextBean.getContextData(inputParam.getNodeId(), inputParam.getField());
subInputs.put(inputParam.getName(), inputData);
}
flowRunParams.setInputParams(subInputs);
flowRunParams.setHttpRequest(contextBean.getHttpRequest());
flowRunParams.setCacheUser(contextBean.getCacheUser());
Result<?> subFlowResp = (Result<?>) airagFlowService.runFlow(flowRunParams);
if (!subFlowResp.isSuccess()) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "子流程执行失败:" + subFlowResp.getMessage());
}
Object respData = subFlowResp.getResult();
if(respData instanceof String){
// 输出文本
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_OUTPUT_TEXT, respData);
}else {
Map<String,Object> respDataMap = (Map<String, Object>) respData;
respDataMap.forEach((key, val) -> {
contextBean.setContextData(flowNodeConfig.getNodeId(), key, val);
});
}
}
}
@@ -0,0 +1,163 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowCondition;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowSwitchRule;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.jeecg.modules.airag.flow.consts.FlowConsts.*;
/**
* 流程节点:条件选择
*
* @author chenrui
* @date 2025/2/18 20:38
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_SWITCH)
public class SwitchNode extends JeecgBaseFlowNode {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_SWITCH, nodeType = NodeTypeEnum.SWITCH)
public String process(NodeComponent component) {
log.info("节点开始-switch");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
Map<String, Object> options = flowNodeConfig.getOptions();
List<Map<String, Object>> ifRules = (List<Map<String, Object>>) options.get(FLOW_NODE_OPTION_IF);
List<FlowSwitchRule> rules = new ArrayList<>();
if (null != ifRules && !ifRules.isEmpty()) {
rules = ifRules.stream()
.map(map -> JSONObject.parseObject(JSONObject.toJSONString(map), FlowSwitchRule.class))
.collect(Collectors.toList());
}
// if & elif
for (int i = 0; i < rules.size(); i++) {
FlowSwitchRule rule = rules.get(i);
List<FlowCondition> conditions = rule.getConditions();
String logic = rule.getLogic();
boolean ruleResult = false;
for (FlowCondition condition : conditions) {
Object conditionData = contextBean.getContextData(condition.getNodeId(), condition.getField());
ruleResult = evaluateCondition(conditionData, condition.getOperator(), condition.getValue());
if (ruleResult && FLOW_NODE_OPTION_LOGIC_OR.equals(logic) || !ruleResult && FLOW_NODE_OPTION_LOGIC_AND.equals(logic)) {
break;
}
}
if (ruleResult) {
log.info("[switch]跳转到{}", rule.getNext());
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_INDEX, i);
return getNextTag(flowNodeConfig, rule.getNext());
}
}
// else
Map<String, Object> elseOption = (Map<String, Object>) options.get(FLOW_NODE_OPTION_ELSE);
if (null != elseOption && !elseOption.isEmpty()) {
log.info("[switch]跳转到{}", elseOption.get(FLOW_NODE_OPTION_NEXT));
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_INDEX, -1);
return getNextTag(flowNodeConfig, (String) elseOption.get("next"));
}
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "没有找到匹配的结果");
}
/**
* 执行表达式
*
* @param conditionData
* @param operator
* @param value
* @return
* @author chenrui
* @date 2025/2/13 14:11
*/
private boolean evaluateCondition(Object conditionData, String operator, String value) {
switch (operator) {
case FLOW_NODE_OPTION_OP_CONTAINS:
// 包含
return conditionData != null && conditionData.toString().contains(value);
case FLOW_NODE_OPTION_OP_NOT_CONTAINS:
// 不包含
return conditionData == null || !conditionData.toString().contains(value);
case FLOW_NODE_OPTION_OP_EQUALS:
// 相等
return conditionData != null && conditionData.toString().equals(value);
case FLOW_NODE_OPTION_OP_GTE:
// 大于等于
return conditionData != null && compareAsNumber(conditionData, value) >= 0;
case FLOW_NODE_OPTION_OP_GT:
// 大于
return conditionData != null && compareAsNumber(conditionData, value) > 0;
case FLOW_NODE_OPTION_OP_LTE:
// 小于等于
return conditionData != null && compareAsNumber(conditionData, value) <= 0;
case FLOW_NODE_OPTION_OP_LT:
// 小于
return conditionData != null && compareAsNumber(conditionData, value) < 0;
case FLOW_NODE_OPTION_OP_LEN_EQ:
// 长度比较:相等
return conditionData != null && conditionData.toString().length() == Integer.parseInt(value);
case FLOW_NODE_OPTION_OP_LEN_GTE:
// 长度比较:大于等于
return conditionData != null && conditionData.toString().length() >= Integer.parseInt(value);
case FLOW_NODE_OPTION_OP_LEN_GT:
// 长度比较:大于
return conditionData != null && conditionData.toString().length() > Integer.parseInt(value);
case FLOW_NODE_OPTION_OP_LEN_LTE:
// 长度比较:小于等于
return conditionData != null && conditionData.toString().length() <= Integer.parseInt(value);
case FLOW_NODE_OPTION_OP_LEN_LT:
// 长度比较:小于
return conditionData != null && conditionData.toString().length() < Integer.parseInt(value);
case FLOW_NODE_OPTION_OP_EMPTY:
// 是否为空
return oConvertUtils.isObjectEmpty(conditionData);
case FLOW_NODE_OPTION_OP_NOT_EMPTY:
// 是否不为空
return oConvertUtils.isObjectNotEmpty(conditionData);
default:
return false;
}
}
/**
* 数字比较
*
* @param conditionData
* @param value
* @return
* @author chenrui
* @date 2025/3/13 17:10
*/
private int compareAsNumber(Object conditionData, String value) {
try {
double conditionNumber = Double.parseDouble(conditionData.toString());
double valueNumber = Double.parseDouble(value);
return Double.compare(conditionNumber, valueNumber);
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid number format for comparison", e);
}
}
}
@@ -0,0 +1,170 @@
package org.jeecg.modules.airag.flow.component.base;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNode;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 开始节点
*
* @author chenrui
* @date 2025/2/11 17:13
*/
@Slf4j
public abstract class JeecgBaseFlowNode {
public static FlowNode getFlowNode(NodeComponent component) {
String tag = component.getTag();
if (StringUtils.isEmpty(tag)) {
tag = component.getNodeId();
}
// 通过tag获取节点配置
return getFlowNodeByTag(component, tag);
}
/**
* 通过tag获取节点信息
*
* @param component
* @param tag
* @return
* @author chenrui
* @date 2025/2/18 20:33
*/
public static FlowNode getFlowNodeByTag(NodeComponent component, String tag) {
Map<String, FlowNode> flowNodes = getContext(component).getFlowNodes();
if (null != flowNodes && !flowNodes.isEmpty()) {
return flowNodes.get(tag);
}
return null;
}
/**
* 获取节点配置
*
* @param component
* @return
* @author chenrui
* @date 2025/2/19 16:52
*/
public static FlowNodeConfig getNodeConfig(NodeComponent component) {
FlowNode flowNode = getFlowNode(component);
if (null == flowNode) {
log.error("获取节点配置信息失败:nodeId:{},nodeTag::{},nodeTag:{}", component.getNodeId(), component.getTag());
throw new AiRagFlowRuntimeException(component.getNodeId(), "", "获取节点配置信息失败");
}
FlowNodeConfig properties = flowNode.getProperties();
if (null == properties) {
log.error("获取节点配置信息失败:nodeId:{},nodeTag:{}", component.getNodeId(), component.getTag());
throw new AiRagFlowRuntimeException(component.getNodeId(), "", "获取节点配置信息失败");
} else {
properties.setNodeId(flowNode.getId());
properties.setNodeType(flowNode.getType());
}
return properties;
}
/**
* 获取请求数据
*
* @param component
* @return
* @author chenrui
* @date 2025/2/19 16:52
*/
public static Map<String, Object> getRequestData(NodeComponent component) {
return getContext(component).getRequestDatas();
}
/**
* 获取上下文环境
*
* @param component
* @return
* @author chenrui
* @date 2025/2/19 16:53
*/
public static JeecgFlowContext getContext(NodeComponent component) {
return component.getContextBean(JeecgFlowContext.class);
}
/**
* @param nodeConfig
* @param context
* @return
* @author chenrui
* @date 2025/2/19 19:26
*/
public static Map<String, Object> getInputs(FlowNodeConfig nodeConfig, JeecgFlowContext context) {
Map<String, Object> inputs = new HashMap<>();
List<FlowNodeConfig.NodeParam> inputParams = nodeConfig.getInputParams();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
if (FlowConsts.FLOW_NODE_TYPE_START.equalsIgnoreCase(nodeConfig.getNodeType())) {
// 从requestData中获取
Object inputData = context.getRequestData(inputParam.getField());
inputs.put(inputParam.getField(), inputData);
} else {
// 从context中获取
Object inputData = context.getContextData(inputParam.getNodeId(), inputParam.getField());
inputs.put(inputParam.getNodeId() + FlowConsts.FLOW_CONTEXT_PARAM_SEPARATOR + inputParam.getField(), inputData);
}
}
return inputs;
}
public static Object getOutputs(FlowNodeConfig nodeConfig, JeecgFlowContext context) {
if (FlowConsts.FLOW_NODE_TYPE_END.equalsIgnoreCase(nodeConfig.getNodeType())) {
// 从result中获取
return context.getResult();
} else {
Map<String, Object> outputs = new HashMap<>();
List<FlowNodeConfig.NodeParam> outputParams = nodeConfig.getOutputParams();
for (FlowNodeConfig.NodeParam outputParam : outputParams) {
// 从context中获取
Map<String, Object> contextDatas = context.getContextDatas();
String prefix = nodeConfig.getNodeId() + FlowConsts.FLOW_CONTEXT_PARAM_SEPARATOR;
contextDatas.forEach((key, value) -> {
if (key.startsWith(prefix)) {
outputs.put(key, value);
}
});
}
return outputs;
}
}
/**
* 获取next配置
*
* @param flowNodeConfig
* @param next
* @return
* @author chenrui
* @date 2025/2/21 19:53
*/
protected String getNextTag(FlowNodeConfig flowNodeConfig, String next) {
if (oConvertUtils.isEmpty(next)) {
log.error("{},{}未指定下一节点", flowNodeConfig.getNodeId(), flowNodeConfig.getText());
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "未指定下一节点");
}
if (next.startsWith("tag:")) {
return next;
} else {
return "tag:" + next;
}
}
}
@@ -0,0 +1,85 @@
package org.jeecg.modules.airag.flow.component.code;
import com.yomahub.liteflow.builder.LiteFlowNodeBuilder;
import com.yomahub.liteflow.enums.ScriptTypeEnum;
import com.yomahub.liteflow.flow.FlowBus;
import com.yomahub.liteflow.script.validator.ScriptValidator;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.FreemarkerHelper;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNode;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class CodeNode {
/**
* 动态构造并装载代码节点
*
* @param flowNodeObj
* @author chenrui
* @date 2025/2/14 16:44
*/
public static void buildCodeFLowNode(FlowNode flowNodeObj) {
FlowNodeConfig flowNodeConfig = flowNodeObj.getProperties();
if (!FlowConsts.FLOW_NODE_TYPE_CODE.equals(flowNodeObj.getType()) || oConvertUtils.isEmpty(flowNodeConfig)) {
return ;
}
Map<String, Object> options = flowNodeConfig.getOptions();
if (oConvertUtils.isEmpty(options) || !options.containsKey(FlowConsts.FLOW_NODE_OPTION_CODE)) {
return ;
}
String nodeId = flowNodeObj.getId();
if (!FlowBus.containNode(nodeId)) {
String codeType = (String) options.get(FlowConsts.FLOW_NODE_OPTION_CODE_TYPE);
AssertUtils.assertNotEmpty("请选择代码类型",codeType);
CodeNodeTypeEnum codeTypeEnum = CodeNodeTypeEnum.valueOf(codeType.toUpperCase());
if (codeTypeEnum == CodeNodeTypeEnum.PYTHON) {
log.warn("暂不支持python脚本节点");
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点[" + flowNodeConfig.getText() + "]:暂不支持python脚本,请使用其他脚本语言");
}
// 获取代码
String script = (String) options.get(FlowConsts.FLOW_NODE_OPTION_CODE);
AssertUtils.assertNotEmpty("请输入代码", script);
Map<String, Object> templateData = new HashMap<>();
templateData.put("inputParams", flowNodeConfig.getInputParams());
templateData.put("outputParams", flowNodeConfig.getOutputParams());
templateData.put("flowNodeObjId", nodeId);
templateData.put("scriptContent", script);
script = FreemarkerHelper.parseTemplate(codeTypeEnum.getTplPath(), templateData);
log.info("类型:{},脚本:{}", codeTypeEnum.getScriptType(), script);
if(ScriptValidator.validate(script, ScriptTypeEnum.getEnumByDisplayName(codeTypeEnum.getScriptType()))){
// 动态创建节点
LiteFlowNodeBuilder.createScriptNode().setId(nodeId)
.setScript(script)
.setLanguage(codeTypeEnum.getScriptType())
.build();
}else{
log.error("脚本校验失败,请检查脚本语法");
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点["+flowNodeConfig.getText()+"]:脚本校验失败,请检查脚本语法");
}
}
}
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("kotlin");
if (engine != null) {
System.out.println("Kotlin ScriptEngine loaded: " + engine);
} else {
System.err.println("Kotlin ScriptEngine not found!");
}
}
}
@@ -0,0 +1,48 @@
package org.jeecg.modules.airag.flow.component.code;
import lombok.Getter;
/**
* @Description: 代码节点类型枚举
* @Author: chenrui
* @Date: 2025/2/14 19:00
*/
@Getter
public enum CodeNodeTypeEnum {
/**
* js类型
*/
JAVASCRIPT("javascript", "js", "org/jeecg/modules/airag/flow/component/code/code_node_javascript.ftl"),
/**
* python类型
*/
PYTHON("python", "python", "org/jeecg/modules/airag/flow/component/code/code_node_python.ftl"),
/**
* groovy类型
*/
GROOVY("groovy", "groovy", "org/jeecg/modules/airag/flow/component/code/code_node_groovy.ftl"),
/**
* kotlin类型
*/
KOTLIN("kotlin", "kotlin", "org/jeecg/modules/airag/flow/component/code/code_node_kotlin.ftl"),
/**
* aviatorScript类型
*/
AVIATOR("aviator", "aviator", "org/jeecg/modules/airag/flow/component/code/code_node_aviator.ftl");
String name;
/**
* 对应liteFlow的脚本类型
*/
String scriptType;
String tplPath;
CodeNodeTypeEnum(String name,String type, String tplPath) {
this.name = name;
this.scriptType = type;
this.tplPath = tplPath;
}
}
@@ -0,0 +1,12 @@
let params = seq.map();
<#list inputParams as inputParam>
seq.put(params, "${inputParam.name}",getContextData(jeecgFlowContext, "${inputParam.nodeId}", "${inputParam.field}"));
</#list>
println(params);
${scriptContent}
println(resp);
<#list outputParams as outputParam>
setContextData(jeecgFlowContext, "${flowNodeObjId}", "${outputParam.field}", seq.get(resp,"${outputParam.field}"));
</#list>
@@ -0,0 +1,12 @@
${scriptContent}
def params = [:]
<#list inputParams as inputParam>
params["${inputParam.name}"] = jeecgFlowContext.getContextData("${inputParam.nodeId}", "${inputParam.field}")
</#list>
def resp = main(params)
<#list outputParams as outputParam>
jeecgFlowContext.setContextData("${flowNodeObjId}", "${outputParam.field}", resp["${outputParam.field}"])
</#list>
@@ -0,0 +1,12 @@
${scriptContent}
var params = {};
<#list inputParams as inputParam>
params["${inputParam.name}"] = jeecgFlowContext.getContextData("${inputParam.nodeId}", "${inputParam.field}");
</#list>
var resp = main(params);
<#list outputParams as outputParam>
jeecgFlowContext.setContextData("${flowNodeObjId}","${outputParam.field}", resp["${outputParam.field}"]);
</#list>
@@ -0,0 +1,16 @@
import org.jeecg.modules.airag.flow.context.JeecgFlowContext
val jeecgFlowContext = bindings["jeecgFlowContext"] as JeecgFlowContext
val params = mutableMapOf<String, Any?>()
<#list inputParams as inputParam>
params["${inputParam.name}"] = jeecgFlowContext.getContextData("${inputParam.nodeId}", "${inputParam.field}")
</#list>
${scriptContent}
val resp = main(params)
<#list outputParams as outputParam>
jeecgFlowContext.setContextData("${flowNodeObjId}", "${outputParam.field}", resp["${outputParam.field}"])
</#list>
@@ -0,0 +1,10 @@
params = {}
<#list inputParams as inputParam>
params["${inputParam.name}"] = jeecgFlowContext.getContextData("${inputParam.nodeId}", "${inputParam.field}")
</#list>
${scriptContent}
<#list outputParams as outputParam>
jeecgFlowContext.setContextData("${flowNodeObjId}", "${outputParam.field}", resp.get("${outputParam.field}"))
</#list>
@@ -0,0 +1,94 @@
package org.jeecg.modules.airag.flow.component.enhance;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.MyClassLoader;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* java增强节点
*
* @author chenrui
* @date 2025/2/26 21:25
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_ENHANCE_JAVA)
public class EnhanceJavaNode extends JeecgBaseFlowNode {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-enhanceJava");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
JSONObject enhanceConf = options.getJSONObject(FlowConsts.FLOW_NODE_OPTION_ENHANCE);
IAiRagEnhanceJava enhanceObject = getEnhanceObject(flowNodeConfig, enhanceConf);
// 增强类入参
Map<String, Object> enhanceInputs = new HashMap<>();
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
// 从context中获取
Object inputData = contextBean.getContextData(inputParam.getNodeId(), inputParam.getField());
enhanceInputs.put(inputParam.getName(), inputData);
}
// 执行
Map<String, Object> enhanceResp;
try {
enhanceResp = enhanceObject.process(enhanceInputs);
}catch(Throwable t){
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "运行java增强失败:" + t.getMessage());
}
// 设置出参
enhanceResp.forEach((key, val) -> {
contextBean.setContextData(flowNodeConfig.getNodeId(), key, val);
});
}
private IAiRagEnhanceJava getEnhanceObject(FlowNodeConfig flowNodeConfig, JSONObject enhanceConf) {
Object obj = null;
if (enhanceConf != null) {
String type = enhanceConf.getString("type");
String javaPath = enhanceConf.getString("path");
if (oConvertUtils.isNotEmpty(javaPath)) {
if ("class".equals(type)) {
try {
obj = MyClassLoader.getClassByScn(javaPath).newInstance();
} catch (InstantiationException | IllegalAccessException e) {
log.error(e.getMessage(), e);
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "获取增强对象失败:" + e.getMessage());
}
} else if ("spring".equals(type)) {
obj = SpringContextUtils.getBean(javaPath);
}
}
}
if (obj instanceof IAiRagEnhanceJava) {
return (IAiRagEnhanceJava) obj;
}
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "获取增强对象失败:类型不匹配");
}
}
@@ -0,0 +1,21 @@
package org.jeecg.modules.airag.flow.component.enhance;
import java.util.Map;
/**
* AiRag流程java增强节点接口
* @author chenrui
* @date 2025/2/26 17:34
*/
public interface IAiRagEnhanceJava {
/**
* 执行流程
* @param inputParams 入参,内容与流程编排中配置的一致
* @return Map<String, Object> 返回值,返回值的key可以在编排中获取到.
* @author chenrui
* @date 2025/2/26 17:33
*/
Map<String, Object> process(Map<String, Object> inputParams);
}
@@ -0,0 +1,392 @@
package org.jeecg.modules.airag.flow.consts;
import java.util.regex.Pattern;
/**
* @Description: airag流程常量类
* @Author: chenrui
* @Date: 2025/2/12 17:35
*/
public class FlowConsts {
/**
* 流程状态:启用
*/
public static final String FLOW_STATUS_ENABLE = "enable";
/**
* 流程状态:禁用
*/
public static final String FLOW_STATUS_DISABLE = "disable";
/**
* 内置的输入参数: 历史记录
*/
public static final String FLOW_INPUT_PARAM_HISTORY = "history";
/**
* 内置的输入参数: 用户提问
*/
public static final String FLOW_INPUT_PARAM_QUESTION = "content";
/**
* 内置的输入参数: 图片列表
*/
public static final String FLOW_INPUT_PARAM_IMAGES = "images";
/**
* 参数类型:String
*/
public static final String FLOW_PARAM_TYPE_STRING = "string";
/**
* 参数类型:Number
*/
public static final String FLOW_PARAM_TYPE_NUMBER = "number";
/**
* 参数类型:Picture
*/
public static final String FLOW_PARAM_TYPE_PICTURE = "picture";
/**
* 参数类型:Object
*/
public static final String FLOW_PARAM_TYPE_OBJECT = "object";
/**
* 参数类型:String Array
*/
public static final String FLOW_PARAM_TYPE_STRING_ARRAY = "string[]";
/**
* 参数类型:Number Array
*/
public static final String FLOW_PARAM_TYPE_NUMBER_ARRAY = "number[]";
/**
* 参数类型:Object Array
*/
public static final String FLOW_PARAM_TYPE_OBJECT_ARRAY = "object[]";
/**
* 流程返回模式:流式
*/
public static final String FLOW_RESPONSE_MODE_STREAMING = "streaming";
/**
* 流程返回模式:阻塞式
*/
public static final String FLOW_RESPONSE_MODE_BLOCKING = "blocking";
/**
* 流程设计:节点列表
*/
public static final String FLOW_DESIGN_NODES = "nodes";
/**
* 流程元数据:输入
*/
public static final String FLOW_METADATA_INPUTS = "inputs";
/**
* 流程元数据:输出
*/
public static final String FLOW_METADATA_OUTPUTS = "outputs";
/**
* 流程节点类型:start
*/
public static final String FLOW_NODE_TYPE_START = "start";
/**
* 流程节点类型:switch
*/
public static final String FLOW_NODE_TYPE_SWITCH = "switch";
/**
* 流程节点类型:llm
*/
public static final String FLOW_NODE_TYPE_LLM = "llm";
/**
* 流程节点类型:knowledge
*/
public static final String FLOW_NODE_TYPE_KNOWLEDGE = "knowledge";
/**
* 流程节点类型:classifier
*/
public static final String FLOW_NODE_TYPE_CLASSIFIER = "classifier";
/**
* 流程节点类型:end
*/
public static final String FLOW_NODE_TYPE_END = "end";
/**
* 流程节点类型:code
*/
public static final String FLOW_NODE_TYPE_CODE = "code";
/**
* 流程节点类型:subflow
*/
public static final String FLOW_NODE_TYPE_SUBFLOW = "subflow";
/**
* 流程节点类型:java增强节点
*/
public static final String FLOW_NODE_TYPE_ENHANCE_JAVA = "enhanceJava";
/**
* 流程节点类型:http 请求
*/
public static final String FLOW_NODE_TYPE_HTTP = "http";
/**
* 流程节点类型:reply 直接回复
*/
public static final String FLOW_NODE_TYPE_REPLY = "reply";
/**
* 流程节点Options:代码类型
*/
public static final String FLOW_NODE_OPTION_CODE_TYPE = "codeType";
/**
* 流程节点Options:代码
*/
public static final String FLOW_NODE_OPTION_CODE = "code";
/**
* 流程节点Options:if
*/
public static final String FLOW_NODE_OPTION_IF = "if";
/**
* 流程节点Options:else
*/
public static final String FLOW_NODE_OPTION_ELSE = "else";
/**
* 流程节点Options:下一个节点(next)
*/
public static final String FLOW_NODE_OPTION_NEXT = "next";
/**
* 流程节点Options:结果条数
*/
public static final String FLOW_NODE_OPTION_TOP_NUMBER = "topNumber";
/**
* 流程节点Options:相似度
*/
public static final String FLOW_NODE_OPTION_SIMILARITY = "similarity";
/**
* 流程节点Options:知识库id集合
*/
public static final String FLOW_NODE_OPTION_KNOW_IDS = "knowIds";
/**
* 流程节点Options:http ( http)
*/
public static final String FLOW_NODE_OPTION_HTTP = "http";
/**
* 流程节点Options:model ( 模型)
*/
public static final String FLOW_NODE_OPTION_MODEL = "model";
/**
* 流程节点Options:modeId ( 模型id)
*/
public static final String FLOW_NODE_OPTION_MODEL_MODEID = "modeId";
/**
* 流程节点Options:params ( 模型参数)
*/
public static final String FLOW_NODE_OPTION_MODEL_PARAMS= "params";
/**
* 流程节点Options:温度
*/
public static final String FLOW_NODE_OPTION_MODEL_TEMPERATURE = "temperature";
/**
* 流程节点Options:多样性
*/
public static final String FLOW_NODE_OPTION_MODEL_TOP = "topP";
/**
* 流程节点Options:存在惩罚
*/
public static final String FLOW_NODE_OPTION_MODEL_PRESENCE_PENALTY = "presencePenalty";
/**
* 流程节点Options:频率惩罚
*/
public static final String FLOW_NODE_OPTION_MODEL_FREQUENCY_PENALTY = "frequencyPenalty";
/**
* 流程节点Options:最大标记
*/
public static final String FLOW_NODE_OPTION_MODEL_MAX_TOKENS = "maxTokens";
/**
* 流程节点Options:messages ( 消息列表)
*/
public static final String FLOW_NODE_OPTION_MODEL_MESSAGES = "messages";
/**
* 流程节点Options:历史消息数量
*/
public static final String FLOW_NODE_OPTION_HISTORY_NUM = "history";
/**
* 流程节点Options:categories ( 分类列表)
*/
public static final String FLOW_NODE_OPTION_MODEL_CATEGORIES = "categories";
/**
* 流程节点Options:category ( 分类)
*/
public static final String FLOW_NODE_OPTION_MODEL_CATEGORY = "category";
/**
* 流程节点Options:子流程id
*/
public static final String FLOW_NODE_OPTION_SUBFLOW_ID = "subflowId";
/**
* 流程节点Options:增强
*/
public static final String FLOW_NODE_OPTION_ENHANCE = "enhance";
/**
* 流程节点Options:是否输出文本
*/
public static final String FLOW_NODE_OPTION_OUTPUT_TEXT = "outputText";
/**
* 流程节点Options:输出内容
*/
public static final String FLOW_NODE_OPTION_OUTPUT_CONTENT = "outputContent";
/**
* 条件逻辑:与
*/
public static final String FLOW_NODE_OPTION_LOGIC_AND = "AND";
/**
* 条件逻辑:或
*/
public static final String FLOW_NODE_OPTION_LOGIC_OR = "OR";
/**
* 包含
*/
public static final String FLOW_NODE_OPTION_OP_CONTAINS = "CONTAINS";
/**
* 不包含
*/
public static final String FLOW_NODE_OPTION_OP_NOT_CONTAINS = "NOT_CONTAINS";
/**
* 等于
*/
public static final String FLOW_NODE_OPTION_OP_EQUALS = "EQUALS";
/**
* 大于等于
*/
public static final String FLOW_NODE_OPTION_OP_GTE = "GTE";
/**
* 大于
*/
public static final String FLOW_NODE_OPTION_OP_GT = "GT";
/**
* 小于等于
*/
public static final String FLOW_NODE_OPTION_OP_LTE = "LTE";
/**
* 小于
*/
public static final String FLOW_NODE_OPTION_OP_LT = "LT";
/**
* 长度等于
*/
public static final String FLOW_NODE_OPTION_OP_LEN_EQ = "LEN_EQ";
/**
* 长度大于等于
*/
public static final String FLOW_NODE_OPTION_OP_LEN_GTE = "LEN_GTE";
/**
* 长度大于
*/
public static final String FLOW_NODE_OPTION_OP_LEN_GT = "LEN_GT";
/**
* 长度小于等于
*/
public static final String FLOW_NODE_OPTION_OP_LEN_LTE = "LEN_LTE";
/**
* 长度小于
*/
public static final String FLOW_NODE_OPTION_OP_LEN_LT = "LEN_LT";
/**
* 为空
*/
public static final String FLOW_NODE_OPTION_OP_EMPTY = "EMPTY";
/**
* 不为空
*/
public static final String FLOW_NODE_OPTION_OP_NOT_EMPTY = "NOT_EMPTY";
/**
* 回复内容
*/
public static final String FLOW_NODE_OPTION_CONTENT = "content";
/**
* 上下文变量分隔符
*/
public static final String FLOW_CONTEXT_PARAM_SEPARATOR = ".";
/**
* 流程节点输出字段名:outputText <br/>
* 只有子流程用了
*/
public static final String FLOW_OUTPUT_FIELD_OUTPUT_TEXT = "outputText";
/**
* 分支节点:序号(选择项的下标)
*/
public static final String FLOW_OUTPUT_FIELD_INDEX = "index";
/**
* 分支节点:回复内容(选择项的描述)
*/
public static final String FLOW_OUTPUT_FIELD_CONTENT = "content";
/**
* 变量占位符正则表达式
*/
public static final Pattern VAR_PLACEHOLDER_PATTERN = Pattern.compile("\\{\\{(.*?)}}");
/**
* 流程错误信息:大模型调用超时
* for [QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示
*/
public static final String FLOW_ERROR_MSG_LLM_TIMEOUT = "llm request timeout";
}
@@ -0,0 +1,164 @@
package org.jeecg.modules.airag.flow.context;
import lombok.Getter;
import lombok.Setter;
import org.jeecg.common.system.vo.SysUserCacheInfo;
import org.jeecg.modules.airag.common.vo.event.EventData;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNode;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
/**
* @Description: jeecg流程上下文
* @Author: chenrui
* @Date: 2025/2/11 19:06
*/
public class JeecgFlowContext implements Serializable {
private static final long serialVersionUID = -3630250155343804583L;
/**
* 请求id
*/
@Getter
@Setter
private String requestId = "";
/**
* 会话id
* 不一定有,如果是从会话过来的就有值
*/
@Getter
@Setter
private String conversationId;
/**
* 话题id
* 不一定有,如果是从会话过来的就有值
*/
@Getter
@Setter
private String topicId;
/**
* 结果数据 <br/>
* 类型:String or Map
* 取决于end节点配置
*/
@Getter
@Setter
private Object result = null;
/**
* 上下文数据
*/
private final ConcurrentHashMap<String, Object> contextData = new ConcurrentHashMap<>();
/**
* 请求参数
*/
Map<String, Object> requestData = new ConcurrentHashMap<>();
/**
* 流程节点map id:flowNode
*/
Map<String, FlowNode> flowNodes = new ConcurrentHashMap<>();
/**
* SSE emitter
*/
@Getter
@Setter
private SseEmitter emitter;
/**
* 事件回调
*/
@Getter
@Setter
private Consumer<EventData> eventCallback;
/**
* 调用流程的http请求
*/
@Getter
@Setter
HttpServletRequest httpRequest;
/**
* 用户信息
*/
@Setter
@Getter
SysUserCacheInfo cacheUser;
/**
* 租户信息
*/
@Getter
@Setter
String tenantId;
public JeecgFlowContext() {
}
/*****************************************上下文数据*********************************************/
public Map<String, Object> getContextDatas() {
return contextData;
}
public <T> T getContextData(String key) {
return contextData.containsKey(key) ? (T) contextData.get(key) : null;
}
public <T> T getContextData(String prefix, String key) {
key = prefix + FlowConsts.FLOW_CONTEXT_PARAM_SEPARATOR + key;
return getContextData(key);
}
public <T> void setContextData(String key, T t) {
if (null != t) {
contextData.put(key, t);
}
}
public <T> void setContextData(String prefix, String key, T t) {
key = prefix + FlowConsts.FLOW_CONTEXT_PARAM_SEPARATOR + key;
setContextData(key, t);
}
/*****************************************请求参数*********************************************/
public Map<String, Object> getRequestDatas() {
return requestData;
}
public void setRequestDatas(Map<String, Object> requestData) {
this.requestData = requestData;
}
public <T> T getRequestData(String key) {
return requestData.containsKey(key) ? (T) requestData.get(key) : null;
}
/*****************************************节点详情*********************************************/
public Map<String, FlowNode> getFlowNodes() {
return flowNodes;
}
public void setFlowNodes(Map<String, FlowNode> flowNodes) {
this.flowNodes = flowNodes;
}
}
@@ -0,0 +1,305 @@
package org.jeecg.modules.airag.flow.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.FlowBus;
import com.yomahub.liteflow.springboot.LiteflowProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.config.shiro.IgnoreAuth;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.service.IAiragFlowAiGenService;
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
import org.jeecg.modules.airag.flow.vo.api.AigcTestDataParams;
import org.jeecg.modules.airag.flow.vo.api.FlowDebugParams;
import org.jeecg.modules.airag.flow.vo.api.FlowDesignParams;
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
import org.jeecg.modules.airag.flow.vo.api.SubFlowResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: AiRag流程
* @Author: jeecg-boot
* @Date: 2025-02-12
* @Version: V1.0
*/
@Tag(name = "AiRag流程")
@RestController("airagFlowController")
@RequestMapping("/airag/flow")
@Slf4j
public class AiragFlowController extends JeecgController<AiragFlow, IAiragFlowService> {
@Autowired
private IAiragFlowService airagFlowService;
@Autowired
private IAiragFlowAiGenService airagFlowAiGenService;
@Autowired
LiteflowProperty liteflowProperty;
@Autowired
FlowExecutor flowExecutor;
/**
* 分页列表查询
*
* @param airagFlow
* @param pageNo
* @param pageSize
* @param req
* @return
*/
@GetMapping(value = "/list")
public Result<IPage<AiragFlow>> queryPageList(AiragFlow airagFlow,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<AiragFlow> queryWrapper = QueryGenerator.initQueryWrapper(airagFlow, req.getParameterMap());
Page<AiragFlow> page = new Page<AiragFlow>(pageNo, pageSize);
IPage<AiragFlow> pageList = airagFlowService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 添加
*
* @param airagFlow
* @return
*/
@PostMapping(value = "/add")
@RequiresPermissions("airag:flow:add")
public Result<String> add(@RequestBody AiragFlow airagFlow) {
Map<String, String> ruleSourceExtDataMap = liteflowProperty.getRuleSourceExtDataMap();
String appName = "jeecg";
if (null != ruleSourceExtDataMap) {
appName = oConvertUtils.getString(ruleSourceExtDataMap.get("applicationName"), "jeecg");
}
airagFlow.setApplicationName(appName);
airagFlow.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
airagFlowService.save(airagFlow);
return Result.OK("添加成功!", airagFlow.getId());
}
/**
* 编辑
*
* @param airagFlow
* @return
*/
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:flow:edit")
public Result<String> edit(@RequestBody AiragFlow airagFlow) {
AiragFlow existsFlow = airagFlowService.getById(airagFlow.getId());
AssertUtils.assertNotEmpty("流程不存在", existsFlow);
airagFlow.setChain(null);
airagFlow.setDesign(null);
airagFlow.setApplicationName(null);
airagFlowService.updateById(airagFlow);
return Result.OK("编辑成功!");
}
/**
* 编排设计保存
*
* @param flowDesignParams
* @return
* @author chenrui
* @date 2025/2/19 11:37
*/
@RequestMapping(value = "/design/save", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:flow:designSave")
public Result<String> saveDesign(@RequestBody FlowDesignParams flowDesignParams) {
return airagFlowService.saveDesign(flowDesignParams);
}
/**
* 通过id删除
*
* @param id
* @return
*/
@DeleteMapping(value = "/delete")
@RequiresPermissions("airag:flow:delete")
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
AiragFlow flow = airagFlowService.getById(id);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
if (null == flow || !flow.getTenantId().equals(currentTenantId)) {
return Result.error("删除AI流程失败,不能删除其他租户的AI流程!");
}
}
FlowBus.removeChain(id);
airagFlowService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 通过id查询
*
* @param id
* @return
*/
@GetMapping(value = "/queryById")
public Result<AiragFlow> queryById(@RequestParam(name = "id", required = true) String id) {
AiragFlow airagFlow = airagFlowService.getById(id);
if (airagFlow == null) {
return Result.error("未找到对应数据");
}
return Result.OK(airagFlow);
}
/**
* 获取子流程以及子流程的开始节点的参数列表
*
* @param id 主流程ID(用于排除自身)
* @param keywords 查询关键字(默认仅返回10条,可通过查询显示其他流程)
* @return
*/
@GetMapping(value = "/subflowList")
public Result<IPage<SubFlowResult>> subflowList(
@RequestParam(name = "id", required = false) String id,
@RequestParam(name = "keywords", required = false) String keywords,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize
) {
IPage<AiragFlow> page = new Page<>(pageNo, pageSize);
IPage<SubFlowResult> result = airagFlowService.subflowPage(page, id, keywords);
return Result.OK(result);
}
/**
* 根据ID获取子流程以及子流程的开始节点的参数列表
*
* @param subflowId 子流程ID
* @return
*/
@GetMapping(value = "/querySubflowById")
public Result<SubFlowResult> querySubflowById(@RequestParam(name = "subflowId") String subflowId) {
SubFlowResult result = airagFlowService.querySubflowById(subflowId);
return Result.OK(result);
}
/**
* 导出excel
*
* @param request
* @param airagFlow
*/
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, AiragFlow airagFlow) {
return super.exportXls(request, airagFlow, AiragFlow.class, "AiRag流程");
}
/**
* 通过excel导入数据
*
* @param request
* @param response
* @return
*/
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, AiragFlow.class);
}
/**
* 调试流程 <br/>
* 实时返回,通过SSE返回数据 <br/>
* 前端使用EventSource接收数据
*
* @param flowDebugParams
* @return 取决于responseMode的值: <br/>
* streaming : 流式模式(推荐),基于SSE实现类似打字机输出方式的流式返回。<br/>
* blocking : 阻塞模式,等待执行完毕后返回结果。
* @author chenrui
* @date 2025/3/04 11:17
*/
@PostMapping(value = "/debug")
public Object debugFlow(@RequestBody FlowDebugParams flowDebugParams) {
return airagFlowService.debugFlow(flowDebugParams);
}
/**
* 运行流程 <br/>
* 实时返回,通过SSE返回数据 <br/>
* 前端使用EventSource接收数据
*
* @param flowRunParams
* @return 取决于responseMode的值: <br/>
* streaming : 流式模式(推荐),基于SSE实现类似打字机输出方式的流式返回。<br/>
* blocking : 阻塞模式,等待执行完毕后返回结果。
* @author chenrui
* @date 2025/2/13 15:19
*/
@IgnoreAuth
@PostMapping(value = "/run")
public Object runFlow(@RequestBody FlowRunParams flowRunParams) {
return airagFlowService.runFlow(flowRunParams);
}
/**
* 运行流程 - GET版本 <br/>
* 将id作为路径变量,responseMode固定为blockinginputParams通过请求参数获取
* for [QQYUN-11718]【AI】积木报表对接AI流程编排接口展示报表
* @param id 流程ID(作为路径变量)
* @return 以阻塞模式返回流程执行结果
* @author chenrui
* @date 2025/4/30 11:15
*/
@IgnoreAuth
@GetMapping(value = "/run/{flowId}")
public Object runFlowGetSimple(@PathVariable(name = "flowId") String flowId, HttpServletRequest request) {
FlowRunParams flowRunParams = new FlowRunParams();
flowRunParams.setFlowId(flowId);
// 固定为blocking模式
flowRunParams.setResponseMode(FlowConsts.FLOW_RESPONSE_MODE_BLOCKING);
// 从请求参数中获取所有参数作为inputParams
Map<String, Object> inputParams = new HashMap<>();
Map<String, String[]> parameterMap = request.getParameterMap();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String key = entry.getKey();
String[] values = entry.getValue();
// 单值参数直接使用第一个值,多值参数使用数组
if (values.length == 1) {
inputParams.put(key, values[0]);
} else {
inputParams.put(key, values);
}
}
flowRunParams.setInputParams(inputParams);
return airagFlowService.runFlow(flowRunParams);
}
/**
* 根据提示生成调试运行的测试数据
*
* @author sunjianlei
* @date 2025-4-9
*/
@PostMapping(value = "/aigc/test-data")
public Result<?> aigcTestData(@RequestBody AigcTestDataParams params) {
return airagFlowAiGenService.genTestData(params);
}
}
@@ -0,0 +1,123 @@
package org.jeecg.modules.airag.flow.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* @Description: AiRag流程
* @Author: jeecg-boot
* @Date: 2025-02-12
* @Version: V1.0
*/
@Data
@TableName("airag_flow")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description="AiRag流程")
public class AiragFlow implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private String id;
/**
* 创建人
*/
@Schema(description = "创建人")
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
private String createBy;
/**
* 创建日期
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建日期")
private Date createTime;
/**
* 更新人
*/
@Schema(description = "更新人")
private String updateBy;
/**
* 更新日期
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新日期")
private Date updateTime;
/**
* 所属部门
*/
@Schema(description = "所属部门")
private String sysOrgCode;
/**
* 租户id
*/
@Excel(name = "租户id", width = 15)
@Schema(description = "租户id")
private String tenantId;
/**
* 应用名称
*/
@Excel(name = "应用名称", width = 15)
@Schema(description = "应用名称")
private String applicationName;
/**
* 名称
*/
@Excel(name = "名称", width = 15)
@Schema(description = "名称")
private String name;
/**
* 描述
*/
@Excel(name = "描述", width = 15)
@Schema(description = "描述")
private String descr;
/**
* 应用图标
*/
@Excel(name = "应用图标", width = 15)
@Schema(description = "应用图标")
private String icon;
/**
* 编排规则
*/
@Excel(name = "编排规则", width = 15)
@Schema(description = "编排规则")
private String chain;
/**
* 编排设计
*/
@Excel(name = "编排设计", width = 15)
@Schema(description = "编排设计")
private String design;
/**
* 状态(enable=启用、disable=禁用、release=发布)
*/
@Excel(name = "状态", width = 15)
@Schema(description = "状态")
private String status;
/**
* 元数据
*/
@Excel(name = "元数据", width = 15)
@Schema(description = "元数据")
private String metadata;
}
@@ -0,0 +1,33 @@
package org.jeecg.modules.airag.flow.exception;
import org.jeecg.common.exception.JeecgBootException;
/**
* 流程运行时异常
* @Author: chenrui
* @Date: 2025/2/14 11:24
*/
public class AiRagFlowRuntimeException extends JeecgBootException {
private static final long serialVersionUID = -9008987328133246231L;
String nodeId;
String nodeName;
public AiRagFlowRuntimeException(String message) {
super(message);
}
public AiRagFlowRuntimeException(String nodeId, String nodeName, String message) {
super(message);
this.nodeName = nodeName;
}
public String getNodeId() {
return nodeId;
}
public String getNodeName() {
return nodeName;
}
}
@@ -0,0 +1,17 @@
package org.jeecg.modules.airag.flow.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
/**
* @Description: AiRag流程
* @Author: jeecg-boot
* @Date: 2025-02-12
* @Version: V1.0
*/
public interface AiragFlowMapper extends BaseMapper<AiragFlow> {
@InterceptorIgnore(tenantLine = "true")
public AiragFlow getFlowByIdIgnoreTenant(String id);
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.airag.flow.mapper.AiragFlowMapper">
<select id="getFlowByIdIgnoreTenant" resultType="org.jeecg.modules.airag.flow.entity.AiragFlow" parameterType="string">
SELECT
*
FROM
airag_flow
WHERE id = #{id}
</select>
</mapper>
@@ -0,0 +1,19 @@
package org.jeecg.modules.airag.flow.service;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.flow.vo.api.AigcTestDataParams;
/**
* Aiflow 生成数据
*/
public interface IAiragFlowAiGenService {
/**
* 根据提示生成调试运行的测试数据
*
* @param params 提示词
* @return
*/
Result<?> genTestData(AigcTestDataParams params);
}
@@ -0,0 +1,67 @@
package org.jeecg.modules.airag.flow.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.vo.api.FlowDebugParams;
import org.jeecg.modules.airag.flow.vo.api.FlowDesignParams;
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
import org.jeecg.modules.airag.flow.vo.api.SubFlowResult;
/**
* @Description: AiRag流程
* @Author: jeecg-boot
* @Date: 2025-02-12
* @Version: V1.0
*/
public interface IAiragFlowService extends IService<AiragFlow> {
/**
* 运行流程 <br/>
* 实时返回,通过SSE返回数据 <br/>
* 前端使用EventSource接收数据
*
* @param flowRunParams
* @return
* @author chenrui
* @date 2025/2/20 16:11
*/
Object runFlow(FlowRunParams flowRunParams);
/**
* 保存设计
* @param flowDesignParams
* @author chenrui
* @date 2025/2/21 14:56
*/
Result<String> saveDesign(FlowDesignParams flowDesignParams);
/**
* 调试流程
* @param flowDebugParams
* @return
* @author chenrui
* @date 2025/3/4 11:18
*/
Object debugFlow(FlowDebugParams flowDebugParams);
/**
* 获取子流程以及子流程的开始节点的参数列表
*
* @param page
* @param id 主流程ID(用于排除自身)
* @param keywords 查询关键字(默认仅返回10条,可通过查询显示其他流程)
* @return
*/
IPage<SubFlowResult> subflowPage(IPage<AiragFlow> page, String id, String keywords);
/**
* 根据ID获取子流程以及子流程的开始节点的参数列表
*
* @param subflowId 子流程ID
* @return
*/
SubFlowResult querySubflowById(String subflowId);
}
@@ -0,0 +1,113 @@
package org.jeecg.modules.airag.flow.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.chatgpt.dto.chat.MultiChatMessage;
import org.jeecg.chatgpt.service.AiChatService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.service.IAiragFlowAiGenService;
import org.jeecg.modules.airag.flow.vo.api.AigcTestDataParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@Service("airagFlowAiGenServiceImpl")
public class AiragFlowAiGenServiceImpl implements IAiragFlowAiGenService {
@Autowired
AiChatService aiChatService;
@Override
public Result<?> genTestData(AigcTestDataParams params) {
if (params == null || CollectionUtils.isEmpty(params.getFields())) {
return Result.error("没有待生成的字段");
} else {
JeecgBootBizTipException aiException = new JeecgBootBizTipException("Ai开小差了,请稍后再试");
// 遍历字段列表,拼接提示词
List<AigcTestDataParams.FieldItem> fields = params.getFields();
StringBuilder promptSB = new StringBuilder();
for (AigcTestDataParams.FieldItem field : fields) {
if (oConvertUtils.isEmpty(field.getField())) {
continue;
}
promptSB.append(field.getField());
if (oConvertUtils.isNotEmpty(field.getName())) {
promptSB.append("").append(field.getName()).append("");
}
promptSB.append("").append(field.getType());
if (field.isRequired()) {
promptSB.append(":必填");
}
promptSB.append("\n");
}
String prompt = promptSB.toString();
if (oConvertUtils.isEmpty(prompt)) {
return Result.error("没有待生成的字段");
}
// 从AI获取表设计数据
String schemaStr = genTestDataFromAi(prompt);
if (oConvertUtils.isEmpty(schemaStr)) {
throw aiException;
}
// 解析AI返回数据
JSONObject schemaObj;
try {
schemaObj = JSONArray.parseObject(schemaStr);
} catch (JSONException e) {
throw aiException;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw aiException;
}
JSONObject res = new JSONObject();
res.put("data", schemaObj);
return Result.OK("生成成功", res);
}
}
/**
* 通过AI生成测试数据
*/
private String genTestDataFromAi(String prompt) {
// 示例JSON
String demoJson = "{\"name\":\"张三\",\"age\":23}";
String sysMsgContent = "" +
"根据字段列表帮我生成一个测试数据JSON," +
"测试数据参考JSON如下:" + demoJson + "\n" +
"在该JSON数组中,key为字段的field,value为你生成的数据," +
"要严格按照字段类型,如果是文本需要保持在10个字左右";
List<MultiChatMessage> messages = new LinkedList<>();
messages.add(MultiChatMessage.builder().role(MultiChatMessage.Role.SYSTEM).content("请严格按照参考json数组的格式输出,不要有其他任何描述,输出内容应以{开头,以}结尾").build());
messages.add(MultiChatMessage.builder().role(MultiChatMessage.Role.USER).content(sysMsgContent).build());
messages.add(MultiChatMessage.builder().role(MultiChatMessage.Role.USER).content("\n\n我的字段如下:\n> " + prompt + "\n").build());
String gptResp = aiChatService.multiCompletions(messages);
if (StringUtils.isEmpty(gptResp)) {
throw new JeecgBootBizTipException("如果您想使用AI助手,请先设置相应配置!");
}
// 兼容 think 思考过程
if (gptResp.contains("</think>")) {
String[] thinkSplit = gptResp.split("</think>");
gptResp = thinkSplit[thinkSplit.length - 1];
}
log.debug("Ai返回结果:" + gptResp);
Pattern pattern = Pattern.compile("\\{.*}", Pattern.DOTALL);
Matcher matcher = pattern.matcher(gptResp);
String returnData = "";
if (matcher.find()) {
returnData = matcher.group(0);
}
return returnData;
}
}
@@ -0,0 +1,384 @@
package org.jeecg.modules.airag.flow.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.FlowBus;
import com.yomahub.liteflow.flow.LiteflowResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.SysUserCacheInfo;
import org.jeecg.common.util.*;
import org.jeecg.modules.airag.common.consts.AiragConsts;
import org.jeecg.modules.airag.common.utils.AiragLocalCache;
import org.jeecg.modules.airag.flow.component.code.CodeNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.mapper.AiragFlowMapper;
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
import org.jeecg.modules.airag.flow.vo.api.*;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNode;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**
* @Description: AiRag流程
* @Author: jeecg-boot
* @Date: 2025-02-12
* @Version: V1.0
*/
@Slf4j
@Service("airagFlowService")
public class AiragFlowServiceImpl extends ServiceImpl<AiragFlowMapper, AiragFlow> implements IAiragFlowService {
@Autowired
FlowExecutor flowExecutor;
@Autowired
AiragFlowMapper airagFlowMapper;
@Autowired
private ISysBaseAPI sysBaseAPI;
@Override
public Result<String> saveDesign(FlowDesignParams flowDesignParams) {
AiragFlow existsFlow = getById(flowDesignParams.getId());
AssertUtils.assertNotEmpty("流程不存在", existsFlow);
AssertUtils.assertNotEmpty("流程规则不能为空", flowDesignParams.getChain());
AssertUtils.assertNotEmpty("设计数据不能为空", flowDesignParams.getDesign());
String design = existsFlow.getDesign();
String metadataStr = existsFlow.getMetadata();
JSONObject metadata = new JSONObject();
if (oConvertUtils.isNotEmpty(metadataStr)) {
metadata = JSONObject.parseObject(metadataStr);
}
if (StringUtils.isNotEmpty(design)) {
JSONObject designJson = JSONObject.parseObject(design);
JSONArray nodes = designJson.getJSONArray(FlowConsts.FLOW_DESIGN_NODES);
// 移除旧的code节点
removeCodeNodeCache(nodes);
updateMetadata(nodes, metadata);
} else {
metadata.put(FlowConsts.FLOW_METADATA_INPUTS, null);
metadata.put(FlowConsts.FLOW_METADATA_OUTPUTS, null);
}
// 移除旧的流程
if (FlowBus.containChain(existsFlow.getId())) {
FlowBus.removeChain(existsFlow.getId());
}
// 更新数据
existsFlow.setName(flowDesignParams.getName());
existsFlow.setChain(flowDesignParams.getChain());
existsFlow.setDesign(flowDesignParams.getDesign());
existsFlow.setMetadata(metadata.toJSONString());
updateById(existsFlow);
return Result.OK("保存成功");
}
/**
* 更新源数据
*
* @param nodes
* @param metadata
* @author chenrui
* @date 2025/3/12 17:52
*/
private static void updateMetadata(JSONArray nodes, JSONObject metadata) {
// 更新metadata
Map<String, FlowNodeConfig.NodeParam> outputParamMap = new HashMap<>();
for (Object node : nodes) {
FlowNode flowNodeObj = ((JSONObject) node).toJavaObject(FlowNode.class);
FlowNodeConfig properties = flowNodeObj.getProperties();
if (FlowConsts.FLOW_NODE_TYPE_START.equals(flowNodeObj.getType())) {
// 开始节点,更新元数据:输入参数
List<FlowNodeConfig.NodeParam> inputParams = properties.getInputParams();
metadata.put(FlowConsts.FLOW_METADATA_INPUTS, inputParams);
} else if (FlowConsts.FLOW_NODE_TYPE_END.equals(flowNodeObj.getType())) {
// 结束节点,更新元数据:输出参数
boolean outputText = false;
if (oConvertUtils.isObjectNotEmpty(properties.getOptions())) {
outputText = (boolean) properties.getOptions().get(FlowConsts.FLOW_NODE_OPTION_OUTPUT_TEXT);
}
if (outputText) {
FlowNodeConfig.NodeParam nodeParam = new FlowNodeConfig.NodeParam();
nodeParam.setField(FlowConsts.FLOW_OUTPUT_FIELD_OUTPUT_TEXT);
nodeParam.setType("string");
outputParamMap.put(nodeParam.getField(), nodeParam);
} else {
properties.getOutputParams().forEach(nodeParam -> {
outputParamMap.put(nodeParam.getField(), nodeParam);
});
}
}
}
metadata.put(FlowConsts.FLOW_METADATA_OUTPUTS, outputParamMap.values());
}
@Override
public Object runFlow(FlowRunParams flowRunParams) {
AiragFlow flow = airagFlowMapper.getFlowByIdIgnoreTenant(flowRunParams.getFlowId());
JeecgFlowContext context = initFlowNodes(flow, flowRunParams);
return executeFlow(flowRunParams, context);
}
@Override
public Object debugFlow(FlowDebugParams flowDebugParams) {
AiragFlow flow = flowDebugParams.getFlow();
String tempFLowId = UUIDGenerator.generate();
flow.setId(tempFLowId);
String appName = "jeecg";
flow.setApplicationName(appName);
flow.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
flowDebugParams.setFlowId(tempFLowId);
flowDebugParams.setEventCallback(eventData -> {
// 移除临时流程
String eventFlowId = eventData.getFlowId();
if (FlowBus.containChain(eventFlowId)) {
FlowBus.removeChain(eventFlowId);
}
// 移除code
if (oConvertUtils.isNotEmpty(flow.getDesign())) {
JSONObject designJson = JSONObject.parseObject(flow.getDesign());
JSONArray nodes = designJson.getJSONArray(FlowConsts.FLOW_DESIGN_NODES);
removeCodeNodeCache(nodes);
}
});
JeecgFlowContext context = initFlowNodes(flow, flowDebugParams);
return executeFlow(flowDebugParams, context);
}
/**
* 初始化流程节点
*
* @param flowRunParams
* @return PAIR K : 临时新建的节点id, V : context
* @author chenrui
* @date 2025/2/19 16:00
*/
private JeecgFlowContext initFlowNodes(AiragFlow flow, FlowRunParams flowRunParams) {
AssertUtils.assertNotEmpty("流程不存在", flow);
AssertUtils.assertNotEmpty("请先设计流程", flow.getChain());
String design = flow.getDesign();
Map<String, FlowNode> flowNode;
if (StringUtils.isNotEmpty(design)) {
JSONObject designJson = JSONObject.parseObject(design);
JSONArray nodes = designJson.getJSONArray(FlowConsts.FLOW_DESIGN_NODES);
flowNode = new HashMap<>();
nodes.forEach(node -> {
FlowNode flowNodeObj = ((JSONObject) node).toJavaObject(FlowNode.class);
if (oConvertUtils.isEmpty(flowNodeObj)) {
return;
}
// 动态构造代码节点
CodeNode.buildCodeFLowNode(flowNodeObj);
flowNode.put(flowNodeObj.getId(), flowNodeObj);
});
} else {
flowNode = null;
}
JeecgFlowContext context = new JeecgFlowContext();
context.setFlowNodes(flowNode);
context.setRequestDatas(flowRunParams.getInputParams());
// 请求id
if (oConvertUtils.isNotEmpty(flowRunParams.getRequestId())) {
context.setRequestId(flowRunParams.getRequestId());
} else {
context.setRequestId(UUIDGenerator.generate());
}
// 会话id
if (oConvertUtils.isNotEmpty(flowRunParams.getConversationId())) {
context.setConversationId(flowRunParams.getConversationId());
}
// 话题
if (oConvertUtils.isNotEmpty(flowRunParams.getTopicId())) {
context.setTopicId(flowRunParams.getTopicId());
}
// 回调函数
if (oConvertUtils.isObjectNotEmpty(flowRunParams.getEventCallback())) {
context.setEventCallback(flowRunParams.getEventCallback());
}
// 设置request
if (oConvertUtils.isObjectNotEmpty(flowRunParams.getHttpRequest())) {
context.setHttpRequest(flowRunParams.getHttpRequest());
} else {
try {
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
context.setHttpRequest(request);
} catch (Exception ignored) {
}
}
SysUserCacheInfo cacheUserInfo = null;
// 当前登录用户
if (flowRunParams instanceof FlowRunInnerParams) {
cacheUserInfo = ((FlowRunInnerParams) flowRunParams).getCacheUser();
}
if (oConvertUtils.isObjectNotEmpty(cacheUserInfo)) {
context.setCacheUser(cacheUserInfo);
} else {
try {
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
if (null != sysUser) {
context.setCacheUser(sysBaseAPI.getCacheUser(sysUser.getUsername()));
}
} catch (Exception e) {
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
}
}
// 当前租户
String tenantId = TenantContext.getTenant();
if(oConvertUtils.isNotEmpty(tenantId)){
context.setTenantId(tenantId);
}
// 加载流程
log.info("流程编排规则:{}", flow.getChain());
if (!FlowBus.containChain(flow.getId())) {
FlowBus.reloadChain(flow.getId(), flow.getChain());
}
return context;
}
/**
* 移除脚本节点的缓存
*
* @param nodes
* @author chenrui
* @date 2025/3/5 19:24
*/
private static void removeCodeNodeCache(JSONArray nodes) {
nodes.forEach(node -> {
FlowNode flowNodeObj = ((JSONObject) node).toJavaObject(FlowNode.class);
if (oConvertUtils.isEmpty(flowNodeObj) || !FlowConsts.FLOW_NODE_TYPE_CODE.equals(flowNodeObj.getType())) {
return;
}
// 移除节点
if (FlowBus.containNode(flowNodeObj.getId())) {
FlowBus.removeNode(flowNodeObj.getId());
}
});
}
/**
* 运行流程
*
* @param flowRunParams
* @param context
* @return
* @author chenrui
* @date 2025/3/4 11:34
*/
private Object executeFlow(FlowRunParams flowRunParams, JeecgFlowContext context) {
if (FlowConsts.FLOW_RESPONSE_MODE_BLOCKING.equalsIgnoreCase(flowRunParams.getResponseMode())) {
// 阻塞式运行
LiteflowResponse flowResponse = flowExecutor.execute2Resp(flowRunParams.getFlowId(), null, context);
if (flowResponse.isSuccess()) {
JeecgFlowContext resultContext = flowResponse.getContextBean(JeecgFlowContext.class);
return Result.OK(resultContext.getResult());
} else {
if (null != flowResponse.getCause()) {
log.error(flowResponse.getCause().getMessage(), flowResponse.getCause());
}
return Result.error(flowResponse.getMessage());
}
} else {
SseEmitter emitter;
if (oConvertUtils.isNotEmpty(flowRunParams.getRequestId())) {
emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, flowRunParams.getRequestId());
} else {
emitter = new SseEmitter(-0L);
emitter.onError(throwable -> {
log.warn("SEE向客户端发送消息失败: {}", throwable.getMessage());
try {
emitter.complete();
} catch (Exception ignore) {}
});
}
context.setEmitter(emitter);
// 异步运行(流式)
flowExecutor.execute2Future(flowRunParams.getFlowId(), null, context);
return emitter;
}
}
@Override
public IPage<SubFlowResult> subflowPage(IPage<AiragFlow> page, String id, String keywords) {
LambdaQueryWrapper<AiragFlow> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(AiragFlow::getCreateTime);
queryWrapper.select(Arrays.asList(
AiragFlow::getId,
AiragFlow::getName,
AiragFlow::getDescr,
AiragFlow::getStatus,
AiragFlow::getDesign
));
if (oConvertUtils.isNotEmpty(id)) {
queryWrapper.ne(AiragFlow::getId, id);
}
// 如果传递了 keywords 就根据 keywords 进行模糊查询
if (oConvertUtils.isNotEmpty(keywords)) {
queryWrapper.like(AiragFlow::getName, "%" + keywords + "%");
}
IPage<AiragFlow> pageList = baseMapper.selectPage(page, queryWrapper);
List<AiragFlow> flowList = pageList.getRecords();
// 拼接返回结果
IPage<SubFlowResult> results = new Page<>();
results.setPages(pageList.getPages());
results.setTotal(pageList.getTotal());
results.setCurrent(page.getCurrent());
results.setSize(page.getSize());
results.setRecords(new ArrayList<>());
if (CollectionUtils.isEmpty(flowList)) {
return results;
}
for (AiragFlow flow : flowList) {
SubFlowResult result = new SubFlowResult(flow);
if (result.getInputParams() == null) {
continue;
}
results.getRecords().add(result);
}
return results;
}
@Override
public SubFlowResult querySubflowById(String subflowId) {
if (oConvertUtils.isEmpty(subflowId)) {
throw new JeecgBootBizTipException("subflowId 不能为空");
}
AiragFlow flow = this.getById(subflowId);
if (flow == null) {
throw new JeecgBootBizTipException("流程不存在");
}
SubFlowResult result = new SubFlowResult(flow);
if (result.getInputParams() == null) {
throw new JeecgBootBizTipException("流程配置异常");
}
return result;
}
}
@@ -0,0 +1,23 @@
package org.jeecg.modules.airag.flow.vo.api;
import lombok.Data;
import java.util.List;
/**
* Ai生成测试数据所需参数
*/
@Data
public class AigcTestDataParams {
// 待生成的字段
private List<FieldItem> fields;
@Data
public static class FieldItem {
private String field;
private String name;
private String type;
private boolean required;
}
}
@@ -0,0 +1,21 @@
package org.jeecg.modules.airag.flow.vo.api;
import lombok.Data;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
/**
* 流程调试入参
*
* @author chenrui
* @date 2025/3/4 11:39
*/
@Data
public class FlowDebugParams extends FlowRunParams {
private static final long serialVersionUID = 1L;
/**
* 流程实体
*/
private AiragFlow flow;
}
@@ -0,0 +1,33 @@
package org.jeecg.modules.airag.flow.vo.api;
import lombok.Data;
import java.io.Serializable;
/**
* 流程设计
*
* @author chenrui
* @date 2025/2/19 11:39
*/
@Data
public class FlowDesignParams implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private String id;
/**
* 编排规则
*/
private String chain;
/**
* 编排设计
*/
private String design;
/**
* 流程名称
*/
private String name;
}
@@ -0,0 +1,22 @@
package org.jeecg.modules.airag.flow.vo.api;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.jeecg.common.system.vo.SysUserCacheInfo;
/**
* 流程运行入参(内部使用)
*
* @author sjlei
* @date 2025-5-23 20:22:29
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class FlowRunInnerParams extends FlowRunParams {
/**
* 用户信息
*/
private SysUserCacheInfo cacheUser;
}
@@ -0,0 +1,66 @@
package org.jeecg.modules.airag.flow.vo.api;
import lombok.Data;
import org.jeecg.modules.airag.common.vo.event.EventData;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Consumer;
/**
* 流程运行入参
*
* @author chenrui
* @date 2025/2/19 11:39
*/
@Data
public class FlowRunParams implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 流程id
*/
private String flowId;
/**
* 输入参数
*/
private Map<String, Object> inputParams;
/**
* 返回模式: <br/>
* streaming : 流式模式(推荐)。基于SSE实现类似打字机输出方式的流式返回。<br/>
* blocking : 阻塞模式,等待执行完毕后返回结果。
*/
private String responseMode;
/**
* 请求id
*/
private String requestId;
/**
* 事件回调 <br/>
* 流程的执行节点会调用该回调.
*/
private Consumer<EventData> eventCallback;
/**
* 会话id <br/>
* app调用流程时有值
*/
private String conversationId;
/**
* 话题id <br/>
* app调用流程时有值
*/
private String topicId;
/**
* http请求 <br/>
*/
private HttpServletRequest httpRequest;
}
@@ -0,0 +1,92 @@
package org.jeecg.modules.airag.flow.vo.api;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNode;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 查询子流程的返回对象
*/
@Data
public class SubFlowResult {
private String id;
private String name;
private String descr;
private String status;
/**
* 开始节点的输入参数列表
*/
private List<FlowNodeConfig.NodeParam> inputParams;
/**
* 流程的输出参数列表
*/
private List<FlowNodeConfig.NodeParam> outputParams;
public SubFlowResult(AiragFlow flow) {
this.id = flow.getId();
this.name = flow.getName();
this.descr = flow.getDescr();
this.status = flow.getStatus();
this.inputParams = new ArrayList<>();
this.outputParams = new ArrayList<>();
this.initParams(flow);
}
private void initParams(AiragFlow flow) {
JSONObject design;
try {
design = JSONObject.parseObject(flow.getDesign());
} catch (Exception e) {
return;
}
if (null == design || !design.containsKey(FlowConsts.FLOW_DESIGN_NODES)) {
return;
}
JSONArray nodes = design.getJSONArray(FlowConsts.FLOW_DESIGN_NODES);
if (CollectionUtils.isEmpty(nodes)) {
return;
}
// 开始和结束节点
Map<String, FlowNodeConfig.NodeParam> outputParamMap = new HashMap<>();
for (Object node : nodes) {
FlowNode flowNodeObj = ((JSONObject) node).toJavaObject(FlowNode.class);
FlowNodeConfig properties = flowNodeObj.getProperties();
if (FlowConsts.FLOW_NODE_TYPE_START.equals(flowNodeObj.getType())) {
this.inputParams = properties.getInputParams();
} else if (FlowConsts.FLOW_NODE_TYPE_END.equals(flowNodeObj.getType())) {
boolean outputText = false;
if (oConvertUtils.isObjectNotEmpty(properties.getOptions())) {
outputText = (boolean) properties.getOptions().get(FlowConsts.FLOW_NODE_OPTION_OUTPUT_TEXT);
}
if (outputText) {
FlowNodeConfig.NodeParam nodeParam = new FlowNodeConfig.NodeParam();
nodeParam.setName(FlowConsts.FLOW_OUTPUT_FIELD_OUTPUT_TEXT);
nodeParam.setField(FlowConsts.FLOW_OUTPUT_FIELD_OUTPUT_TEXT);
outputParamMap.put(nodeParam.getField(),nodeParam);
} else {
properties.getOutputParams().forEach(nodeParam -> {
outputParamMap.put(nodeParam.getName(), nodeParam);
});
}
}
}
this.outputParams = new ArrayList<>(outputParamMap.values());
}
}
@@ -0,0 +1,23 @@
package org.jeecg.modules.airag.flow.vo.flow.config;
import lombok.Data;
@Data
public class FlowCondition {
/**
* 节点id
*/
private String nodeId;
/**
* 字段名
*/
private String field;
/**
* 操作符
*/
private String operator;
/**
* 值
*/
private String value;
}
@@ -0,0 +1,37 @@
package org.jeecg.modules.airag.flow.vo.flow.config;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 流程节点
*
* @Author: chenrui
* @Date: 2025/2/12 10:47
*/
@Data
@AllArgsConstructor
public class FlowNode {
public FlowNode() {
}
/**
* id 节点唯一标识
*/
private String id;
/**
* 节点类型
*/
private String type;
/**
* 节点配置
*/
private FlowNodeConfig properties;
}
@@ -0,0 +1,76 @@
package org.jeecg.modules.airag.flow.vo.flow.config;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
/**
* 节点参数
* @author chenrui
* @date 2025/2/11 18:22
*/
@Data
@AllArgsConstructor
public class FlowNodeConfig {
public FlowNodeConfig() {
}
/**
* 节点id
*/
private String nodeId;
/**
* 节点类型
*/
private String nodeType;
/**
* 入参
*/
private List<NodeParam> inputParams;
/**
* 出参
*/
private List<NodeParam> outputParams;
/**
* 选项
*/
private Map<String, Object> options;
/**
* 节点名称
*/
private String text;
@Getter
@Setter
public static class NodeParam {
/**
* 节点id
*/
private String nodeId;
/**
* 参数显示名称
*/
private String name;
/**
* 参数名
*/
private String field;
/**
* 类型
*/
private String type;
/**
* 是否必填
*/
private Boolean required;
}
}
@@ -0,0 +1,36 @@
package org.jeecg.modules.airag.flow.vo.flow.config;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* 分支流程规则
* @author chenrui
* @date 2025/2/13 11:25
*/
@Getter
@Setter
@AllArgsConstructor
public class FlowSwitchRule {
/**
* 逻辑 AND , OR
*/
private String logic;
/**
* 条件表达式
*/
private List<FlowCondition> conditions;
/**
* 跳转节点 tag:tagName
*/
private String next;
// 构造函数
public FlowSwitchRule() {
}
}
@@ -0,0 +1,45 @@
liteflow:
print-banner: false
parse-mode: PARSE_ONE_ON_FIRST_EXEC
rule-source-ext-data-map:
# url: jdbc:mysql://localhost:3306/poseidon
# driverClassName: com.mysql.cj.jdbc.Driver
# username: root
# password: 123456
applicationName: jeecg
#是否开启SQL日志
sqlLogEnabled: true
#是否开启SQL数据轮询自动刷新机制 默认不开启
# pollingEnabled: false
#SQL数据轮询时间间隔(s) 默认为60s
# pollingIntervalSeconds: 60
#规则配置后首次轮询的起始时间(s) 默认为60s
# pollingStartSeconds: 60
#以下是chain表的配置,这个一定得有
#编排规则表的表名
chainTableName: airag_flow
#编排规则表中应用名称存储字段名
chainApplicationNameField: application_name
#规则名称存储的字段名
chainNameField: id
#EL表达式的字段(只存EL)
elDataField: chain
#以下是决策路由字段的配置,如果你没用到决策路由,可以不配置
# routeField: route
# namespaceField: namespace
# #是否启用这条规则
# chainEnableField: enable
# #规则表自定义过滤SQL
chainCustomSql: select id, application_name, chain from airag_flow where status = 'enable' and chain is not null
#以下是script表的配置,如果你没使用到脚本,下面可以不配置
# scriptTableName: script
# scriptApplicationNameField: application_name
# scriptIdField: script_id
# scriptNameField: script_name
# scriptDataField: script_data
# scriptTypeField: script_type
# scriptLanguageField: script_language
# #是否启用这条脚本
# scriptEnableField: enable
# #脚本表自定义过滤SQL
# scriptCustomSql: 这里设置自定义脚本表SQL