yxk-20260409-集成流程引擎bpm代码
This commit is contained in:
@@ -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>
|
||||
+32
@@ -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";
|
||||
}
|
||||
+28
@@ -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;
|
||||
}
|
||||
+106
@@ -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);
|
||||
}
|
||||
+38
@@ -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);
|
||||
|
||||
}
|
||||
+89
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -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模板注入问题 禁止解析ObjectConstructor,Execute和freemarker.template.utility.JythonRuntime。
|
||||
_tplConfig.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
|
||||
//update-end-author:scott date:2023-8-15 for: freemarker模板注入问题 禁止解析ObjectConstructor,Execute和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);
|
||||
// }
|
||||
}
|
||||
+36
@@ -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
|
||||
}
|
||||
}
|
||||
+77
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+109
@@ -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);
|
||||
}
|
||||
}
|
||||
+46
@@ -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;
|
||||
|
||||
}
|
||||
+56
@@ -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;
|
||||
|
||||
}
|
||||
+29
@@ -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;
|
||||
|
||||
}
|
||||
+33
@@ -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;
|
||||
}
|
||||
}
|
||||
+90
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+99
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+169
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+73
@@ -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);
|
||||
}
|
||||
}
|
||||
+469
@@ -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("&", "&");
|
||||
input = input.replaceAll("<", "<");
|
||||
input = input.replaceAll(">", ">");
|
||||
input = input.replaceAll("'", "'");
|
||||
input = input.replaceAll("/", "/");
|
||||
// 替换掉空格,否则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;
|
||||
}
|
||||
}
|
||||
+91
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+339
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+90
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
+53
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+96
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+163
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+170
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+85
@@ -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!");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+48
@@ -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;
|
||||
}
|
||||
}
|
||||
+12
@@ -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>
|
||||
+12
@@ -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>
|
||||
+12
@@ -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>
|
||||
+16
@@ -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>
|
||||
+10
@@ -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>
|
||||
+94
@@ -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(), "获取增强对象失败:类型不匹配");
|
||||
}
|
||||
|
||||
}
|
||||
+21
@@ -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);
|
||||
}
|
||||
+392
@@ -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";
|
||||
|
||||
|
||||
}
|
||||
+164
@@ -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;
|
||||
}
|
||||
}
|
||||
+305
@@ -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固定为blocking,inputParams通过请求参数获取
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
+123
@@ -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;
|
||||
}
|
||||
+33
@@ -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;
|
||||
}
|
||||
}
|
||||
+17
@@ -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);
|
||||
}
|
||||
+12
@@ -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>
|
||||
+19
@@ -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);
|
||||
|
||||
}
|
||||
+67
@@ -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);
|
||||
|
||||
}
|
||||
+113
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+384
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
}
|
||||
+21
@@ -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;
|
||||
|
||||
}
|
||||
+33
@@ -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;
|
||||
}
|
||||
+22
@@ -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;
|
||||
|
||||
}
|
||||
+66
@@ -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;
|
||||
|
||||
}
|
||||
+92
@@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
+37
@@ -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;
|
||||
|
||||
|
||||
|
||||
}
|
||||
+76
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
+36
@@ -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() {
|
||||
}
|
||||
}
|
||||
+45
@@ -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
|
||||
Reference in New Issue
Block a user