Compare commits

...

10 Commits

Author SHA1 Message Date
edc250847b 1 2025-11-08 18:14:44 +08:00
785134bd68 feat: 修改聊天角色枚举类型 2025-05-05 01:55:01 +08:00
041a908180 feat: 参数类型与提示模板的角色全部改为枚举类型 2025-05-05 01:42:43 +08:00
077166e1a6 feat: llm基本功能已实现 2025-05-05 01:14:39 +08:00
bef6849e45 feat: node 和 edge id切换为String类型 2025-05-04 23:04:12 +08:00
7a72358fc7 feat: llm节点进行调试 2025-05-04 22:56:58 +08:00
f783658227 feat : swagger 字段描述 2025-05-04 15:44:48 +08:00
e72dfab9fe refactor(metis-starter): 修改获取流程定义接口的参数名称
- 将 ProcessDefinitionController 中的 deploymentId 参数重命名为 workflowId
- 相应地修改 ProcessDefinitionFacade 中的方法名和参数名
- 这个改动统一了接口参数名称,使其更加清晰和一致
2025-04-26 22:46:05 +08:00
61047720ab feat(metis-starter): 添加模型平台相关功能
- 新增日期格式枚举类 DateFormattersEnum
- 添加 Jackson 自动配置类 JacksonAutoConfiguration
- 创建基础实体类 Model 和 ModelPlatform
- 实现 ModelPlatform 相关的转换类、信息类、Mapper 和 Service
- 添加 LLM 模型服务接口和实现类
- 新增 LLM 模型类型枚举 ModelTypeEnum
- 定义平台模型列表接口 PlatformModeList
2025-04-26 22:36:52 +08:00
ddb5f4e36c ci: 模板 2025-04-26 18:40:05 +08:00
83 changed files with 2737 additions and 156 deletions

View File

@@ -45,34 +45,32 @@ steps:
- name: notify
kind: template
load: notify_template.yaml
# image: 10.7.127.190:38080/plugins/webhook:latest
# environment:
# NOTIFY_WX_URL:
# from_secret: notify_wx_url
# when:
# status: [ success,failure ]
# settings:
# urls:
# from_secret: notify_wx_url
# content_type: application/json
# template: |
# {
# "msgtype": "markdown",
# "markdown": {
# "content": "{{#success build.status}}<font color=\"green\">✅ 构建成功</font>{{else}}<font color=\"red\">❌ 构建失败</font>{{/success}}
# >**项目名称**: #{{ repo.name }}
# >**构建编号**: #{{build.number}}
# >**构建状态**: {{build.status}}
# >**代码分支**: {{build.branch}}
# >**提交哈希**: {{build.commit}}
# >**提交作者**: {{build.author}}
# >**提交信息**: {{build.message}}
# >[查看构建详情]({{build.link}})
# >{{^success build.status}}[查看失败日志]({{build.link}}/logs){{/success}}"
# }
# }
image: 10.7.127.190:38080/plugins/webhook:latest
environment:
NOTIFY_WX_URL:
from_secret: notify_wx_url
when:
status: [ success,failure ]
settings:
urls:
from_secret: notify_wx_url
content_type: application/json
template: |
{
"msgtype": "markdown",
"markdown": {
"content": "{{#success build.status}}<font color=\"green\">✅ 构建成功</font>{{else}}<font color=\"red\">❌ 构建失败</font>{{/success}}
>**项目名称**: #{{ repo.name }}
>**构建编号**: #{{build.number}}
>**构建状态**: {{build.status}}
>**代码分支**: {{build.branch}}
>**提交哈希**: {{build.commit}}
>**提交作者**: {{build.author}}
>**提交信息**: {{build.message}}
>[查看构建详情]({{build.link}})
>{{^success build.status}}[查看失败日志]({{build.link}}/logs){{/success}}"
}
}
volumes:

View File

@@ -31,14 +31,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
@@ -77,6 +69,24 @@
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
</dependency>
<!-- velocity 模板渲染 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
</dependency>
<!-- langchain4j -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
</dependency>
</dependencies>

View File

@@ -0,0 +1,43 @@
package com.metis.config;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.stream.Stream;
@Getter
@AllArgsConstructor
public enum DateFormatterEnum {
/**
* 时间格式
*/
COMMON_DATE_TIME(0, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault())),
COMMON_SHORT_DATE(1, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault())),
COMMON_MONTH_DAY(2, DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(ZoneId.systemDefault())),
COMMON_MONTH(3, DateTimeFormatter.ofPattern("yyyy-MM").withZone(ZoneId.systemDefault())),
YEAR(4, DateTimeFormatter.ofPattern("yyyy").withZone(ZoneId.systemDefault())),
COLON_DELIMITED_TIME(5, DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault())),
COLON_DELIMITED_SHORT_TIME(6, DateTimeFormatter.ofPattern("HH:mm").withZone(ZoneId.systemDefault())),
CHINESE_DATE_TIME(7, DateTimeFormatter.ofPattern("yyyy年MM月dd日HH时mm分ss秒").withZone(ZoneId.systemDefault())),
CHINESE_SHORT_DATE(8, DateTimeFormatter.ofPattern("yyyy年MM月dd日HH时mm分").withZone(ZoneId.systemDefault())),
CHINESE_DATE(9, DateTimeFormatter.ofPattern("yyyy年MM月dd日").withZone(ZoneId.systemDefault())),
CHINESE_MONTH(10, DateTimeFormatter.ofPattern("yyyy年MM月").withZone(ZoneId.systemDefault())),
CHINESE_YEAR(11, DateTimeFormatter.ofPattern("yyyy年").withZone(ZoneId.systemDefault())),
CHINESE_TIME(12, DateTimeFormatter.ofPattern("HH时mm分ss秒").withZone(ZoneId.systemDefault())),
CHINESE_SHORT_TIME(13, DateTimeFormatter.ofPattern("HH时mm分").withZone(ZoneId.systemDefault()));
private final Integer code;
private final DateTimeFormatter dateTimeFormatter;
public static DateFormatterEnum findByCode(int code){
return Stream.of(values())
.filter(e -> e.getCode().equals(code))
.findAny()
.orElse(null);
}
}

View File

@@ -0,0 +1,144 @@
package com.metis.config;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.deser.std.NumberDeserializers;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import lombok.AllArgsConstructor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.util.Locale;
import java.util.TimeZone;
/**
* 杰克逊自动配置
*
* @author clay
* @date 2025/04/26
*/
@Order(Integer.MIN_VALUE)
@AutoConfiguration
@AllArgsConstructor
@ConditionalOnClass(ObjectMapper.class)
@AutoConfigureBefore(org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration.class)
public class JacksonAutoConfiguration {
/**
* Jackson2ObjectMapperBuilder
*
* @return {@link Jackson2ObjectMapperBuilder}
*/
@Bean
@Primary
public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.locale(Locale.CHINA);
builder.timeZone(TimeZone.getTimeZone(ZoneId.systemDefault()));
//时间类型支持
JavaTimeModule javaTimeModule = buildJavaTimeModule();
//其他类型支持
SimpleModule simpleModule = buildSimpleModule();
builder.modules(javaTimeModule,simpleModule);
return builder;
}
/**
* ObjectMapper
*
* @param builder 建设者
* @return {@link ObjectMapper}
*/
@Bean
@Primary
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
objectMapper.findAndRegisterModules();
// 其他配置
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true);
objectMapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true);
objectMapper.configure(JsonReadFeature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER.mappedFeature(), true);
objectMapper.configure(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION, true);
//开启整数反序列化时使用长整型long的选项。确保处理的数据不会因类型限制丢失信息
objectMapper.configure(DeserializationFeature.USE_LONG_FOR_INTS, true);
//开启输出缩进功能。这通常用于美化JSON格式输出使结构更清晰易读
objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
//禁用报错
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
return objectMapper;
}
/**
* 构建简单模块
*
* @return {@link SimpleModule}
*/
private SimpleModule buildSimpleModule() {
SimpleModule simpleModule = new SimpleModule();
//BigInteger
simpleModule.addSerializer(BigInteger.class, ToStringSerializer.instance);
simpleModule.addDeserializer(BigInteger.class, new NumberDeserializers.BigIntegerDeserializer());
//Long
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
simpleModule.addDeserializer(Long.class, new NumberDeserializers.LongDeserializer(Long.class, null));
//BigDecimal
simpleModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
simpleModule.addDeserializer(BigDecimal.class, new NumberDeserializers.BigDecimalDeserializer());
return simpleModule;
}
/**
* 构建Java时间模块
*
* @return {@link JavaTimeModule}
*/
private JavaTimeModule buildJavaTimeModule() {
// 添加自定义序列化和反序列化器
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateFormatterEnum.COMMON_DATE_TIME.getDateTimeFormatter()));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateFormatterEnum.COMMON_DATE_TIME.getDateTimeFormatter()));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateFormatterEnum.COMMON_MONTH_DAY.getDateTimeFormatter()));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateFormatterEnum.COLON_DELIMITED_TIME.getDateTimeFormatter()));
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateFormatterEnum.COMMON_MONTH_DAY.getDateTimeFormatter()));
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateFormatterEnum.COLON_DELIMITED_TIME.getDateTimeFormatter()));
//不提供 java.util.Date 的序列化处理,全项目禁用 java.util.Date 类
return javaTimeModule;
}
}

View File

@@ -3,4 +3,12 @@ package com.metis.constant;
public interface BaseConstant {
Integer DEFAULT_VERSION = 1;
String TEXT = "text";
String FINISH_REASON = "finishReason";
String USAGE = "usage";
}

View File

@@ -23,9 +23,9 @@ public class ProcessDefinitionController {
@Operation(summary = "根据部署ID获取流程定义")
@GetMapping("/{deploymentId}")
public Result<AppVo> getByDeploymentId(@PathVariable Long deploymentId) {
AppVo app = processDefinitionFacade.getByDeploymentId(deploymentId);
@GetMapping("/{workflowId}")
public Result<AppVo> getByWorkflowId(@PathVariable Long workflowId) {
AppVo app = processDefinitionFacade.getByWorkflowId(workflowId);
return Result.ok(app);
}

View File

@@ -0,0 +1,19 @@
package com.metis.convert;
import com.metis.domain.entity.ModelPlatform;
import com.metis.domain.entity.ModelPlatformInfo;
import com.metis.domain.entity.base.Model;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface ModelPlatformConvert {
ModelPlatformConvert INSTANCE = Mappers.getMapper(ModelPlatformConvert.class);
ModelPlatformInfo toInfo(ModelPlatform modelPlatform);
Model toModel(ModelPlatform modelPlatform);
}

View File

@@ -1,19 +1,26 @@
package com.metis.domain.bo;
import com.metis.enums.YesOrNoEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "应用")
public class AppBO {
@Schema(description = "应用id")
private Long appId;
@Schema(description = "应用名称")
private String name;
@Schema(description = "应用描述")
private String description;
@Schema(description = "画布")
private GraphBO graph;
@Schema(description = "是否默认使用")
private YesOrNoEnum defaultUse;
}

View File

@@ -1,6 +1,7 @@
package com.metis.domain.bo;
import com.metis.domain.entity.base.Graph;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@@ -11,24 +12,32 @@ import lombok.Data;
@Builder
public final class BuildApp {
/**
* 应用id
*/
@Schema(description = "应用id")
private Long appId;
/**
* 用户id
*/
@Schema(description = "用户id")
private Long userId;
/**
* 名字
*/
@NotBlank(message = "流程名称不能为空")
@Schema(description = "流程名称")
private String name;
@NotNull(message = "流程模型不能为空")
@Valid
@NotNull(message = "流程模型不能为空")
@Schema(description = "流程模型")
private Graph graph;
/**
* 描述
*/
@Schema(description = "流程描述")
private String description;
}

View File

@@ -1,6 +1,7 @@
package com.metis.domain.bo;
import com.metis.domain.entity.base.Graph;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@@ -13,20 +14,24 @@ public class CreateApp {
/**
* 用户id
*/
@Schema(description = "用户id")
private Long userId;
/**
* 名字
*/
@NotBlank(message = "流程名称不能为空")
@Schema(description = "流程名称")
private String name;
@NotNull(message = "流程模型不能为空")
@Valid
@Schema(description = "流程模型")
private Graph graph;
/**
* 描述
*/
@Schema(description = "流程描述")
private String description;
}

View File

@@ -1,6 +1,8 @@
package com.metis.domain.bo;
import com.metis.enums.EdgeType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -10,7 +12,7 @@ public class EdgeBO {
/**
* 唯一标识符
*/
@NotNull(message = "唯一标识符不能为空")
@NotBlank(message = "唯一标识符不能为空")
private String id;
/**
@@ -28,39 +30,78 @@ public class EdgeBO {
* 源节点ID,对应节点id
*/
@NotNull(message = "源节点ID不能为空")
private Long source;
private String source;
/**
* 目标节点ID,对应节点id
*/
@NotNull(message = "目标节点ID不能为空")
private Long target;
private String target;
/**
* 源句柄id
*/
@NotNull(message = "源句柄ID不能为空")
private Long sourceHandle;
private String sourceHandle;
/**
* 目标句柄id
*/
@NotNull(message = "目标句柄ID不能为空")
private Long targetHandle;
private String targetHandle;
/**
* 边是否动画true/false
*/
@Schema(description = "边是否动画true/false")
private Boolean animated;
/**
* 开始标志
*/
@Schema(description = "开始标志")
private String markerStart;
/**
* 结束标记
*/
@Schema(description = "结束标记")
private String markerEnd;
/**
* 边是否可选中true,false
*/
@Schema(description = "边是否可选中true,false")
private Boolean selectable;
/**
* 边是否可更新true,false(更改它们的源/目标位置。)
*/
@Schema(description = "边是否可更新true,false(更改它们的源/目标位置。)")
private Boolean updatable;
/**
* 边是否可以删除true,false
*/
@Schema(description = "边是否可以删除true,false")
private Boolean deletable;
/**
* 源位置x坐标
*/
@Schema(description = "源位置x坐标")
private Float sourceX;
/**
* 源位置y坐标
*/
@Schema(description = "源位置y坐标")
private Float sourceY;
/**
* 目标位置x坐标
*/
@Schema(description = "目标位置x坐标")
private Float targetX;
/**
* 目标位置y坐标
*/
@Schema(description = "目标位置y坐标")
private Float targetY;
}

View File

@@ -1,5 +1,6 @@
package com.metis.domain.bo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
@@ -7,6 +8,7 @@ import lombok.Data;
import java.util.List;
@Data
@Schema(description = "画布")
public class GraphBO {
/**
@@ -14,6 +16,7 @@ public class GraphBO {
*/
@Valid
@NotEmpty(message = "连线不能为空")
@Schema(description = "连线")
private List<EdgeBO> edges;
/**
@@ -21,22 +24,26 @@ public class GraphBO {
*/
@Valid
@NotEmpty(message = "节点不能为空")
@Schema(description = "节点")
private List<NodeBO> nodes;
/**
* 位置
*/
@Schema(description = "位置")
private List<Double> position;
/**
* 变焦
*/
@Schema(description = "变焦")
private Double zoom;
/**
* 视窗
*/
@Schema(description = "视窗")
private ViewportBo viewport;

View File

@@ -2,6 +2,8 @@ package com.metis.domain.bo;
import com.metis.enums.HandleType;
import com.metis.enums.PositionType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -13,24 +15,28 @@ public class HandleBO {
/**
* 句柄id
*/
@NotNull(message = "句柄id不能为空")
private Long id;
@NotBlank(message = "句柄id不能为空")
@Schema(description = "句柄id")
private String id;
/**
* 句柄类型
*/
@NotNull(message = "句柄类型不能为空")
@Schema(description = "句柄类型")
private HandleType type;
/**
* 句柄位置
*/
@NotNull(message = "句柄位置不能为空")
@Schema(description = "句柄位置")
private PositionType position;
/**
* 是否可以连接
*/
@NotNull(message = "是否可以连接不能为空")
@Schema(description = "是否可以连接")
private Boolean connectable;
}

View File

@@ -1,7 +1,9 @@
package com.metis.domain.bo;
import com.metis.enums.NodeType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -11,18 +13,21 @@ public class NodeBO {
/**
* id
*/
@NotNull(message = "节点id不能为空")
private Long id;
@NotBlank(message = "节点id不能为空")
@Schema(description = "节点id")
private String id;
/**
* 类型
*/
@NotNull(message = "节点类型不能为空")
@Schema(description = "节点类型")
private NodeType type;
/**
* 自定义类型
*/
@Schema(description = "自定义类型")
private String customType;
/**
@@ -30,6 +35,7 @@ public class NodeBO {
*/
@Valid
@NotNull(message = "节点位置不能为空")
@Schema(description = "节点位置")
private PositionBO position;
/**
@@ -37,23 +43,25 @@ public class NodeBO {
*/
@Valid
@NotNull(message = "节点业务数据不能为空")
@Schema(description = "节点业务数据")
private NodeDataBO data;
/**
* 宽度
*/
// @NotNull(message = "节点宽度不能为空")
@Schema(description = "节点宽度")
private Integer width;
/**
* 高度
*/
// @NotNull(message = "节点高度不能为空")
@Schema(description = "节点高度")
private Integer height;
/**
* 节点是否选中
*/
@Schema(description = "节点是否选中")
private Boolean selected;

View File

@@ -23,6 +23,11 @@ public class NodeDataBO {
*/
private String icon;
/**
* 描述
*/
private String description;
/**
* 工具栏位置
*/

View File

@@ -1,19 +1,23 @@
package com.metis.domain.bo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
@Schema(description = "节点位置")
public class PositionBO {
/**
* x坐标
*/
@NotNull(message = "x坐标不能为空")
@Schema(description = "x坐标")
private Double x;
/**
* y坐标
*/
@Schema(description = "y坐标")
@NotNull(message = "y坐标不能为空")
private Double y;

View File

@@ -2,6 +2,7 @@ package com.metis.domain.bo;
import com.metis.enums.YesOrNoEnum;
import com.metis.domain.entity.base.Graph;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
@@ -15,22 +16,30 @@ public class UpdateApp {
/**
* id
*/
@Schema(description = "id")
private Long appId;
/**
* 名字
*/
@NotBlank(message = "流程名称不能为空")
@Schema(description = "名字")
private String name;
@NotNull(message = "流程模型不能为空")
@Valid
@Schema(description = "流程模型")
private Graph graph;
/**
* 描述
*/
@Schema(description = "描述")
private String description;
/**
* 是否默认使用
*/
@Schema(description = "是否默认使用")
private YesOrNoEnum defaultUse;
}

View File

@@ -1,10 +1,18 @@
package com.metis.domain.bo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "视窗")
public class ViewportBo {
@Schema(description = "x坐标")
private Double x;
@Schema(description = "y坐标")
private Double y;
@Schema(description = "变焦")
private Double zoom;
}

View File

@@ -1,18 +1,28 @@
package com.metis.domain.context;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import com.metis.runner.FlowRunningContext;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@Slf4j
@Getter
@Builder
public class RunningContext {
private final static String SYS_PREFIX = "sys.";
/**
* 系统数据
*/
@@ -26,12 +36,12 @@ public class RunningContext {
/**
* 节点运行上下文, 需要数据进行传递
*/
private Map<Long, JSONObject> nodeRunningContext;
private Map<String, JSONObject> nodeRunningContext;
/**
* 下一个运行节点id集合, 可能是多个, 执行器每一次清空该节点
*/
private Set<Long> nextRunNodeId;
private Set<String> nextRunNodeId;
/**
@@ -40,15 +50,79 @@ public class RunningContext {
* @param nodeId 节点id
* @param nodeRunningContext 节点运行背景信息
*/
public void addNodeRunningContext(Long nodeId, JSONObject nodeRunningContext) {
public void addNodeRunningContext(String nodeId, JSONObject nodeRunningContext) {
this.nodeRunningContext.put(nodeId, nodeRunningContext);
}
public JSONObject getRunningContext(Long nodeId) {
/**
* 获取运行上下文
*
* @param nodeId 节点id
* @return {@link JSONObject }
*/
public JSONObject getRunningContext(String nodeId) {
return this.nodeRunningContext.get(nodeId);
}
/**
* 获得值
*
* @param key 关键
* @return {@link Object }
*/
public Object getValue(String key) {
Assert.isTrue(StrUtil.isNotBlank(key), "key is blank");
if (key.startsWith(SYS_PREFIX)) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext(this);
return parser.parseExpression(key).getValue(context);
}
// try {
// // 解析 key 中的数字部分并转换为 Long 类型
// String[] parts = key.split("\\.");
// if (parts.length == 2 && StrUtil.isNumeric(parts[0])) {
// String nodeId = Long.valueOf(parts[0]);
// JSONObject runningContext = getRunningContext(nodeId);
// if (runningContext != null) {
// return runningContext.get(parts[1]);
// }
// }
// } catch (Exception e) {
// log.error("数字类型获取动态参数失败: {}", key);
// }
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
Map<String, Object> variables = new HashMap<>(this.nodeRunningContext);
context.setVariables(variables);
return parser.parseExpression(convertDotToSquareBrackets(key)).getValue(context);
}
/**
* 获得价值 不满足条件, 则返回null, 需要业务自行判断
*
* @param nodeId 节点id
* @param key 关键
* @return {@link Object }
*/
public Object getValue(String nodeId, String key) {
if (ObjectUtil.isNull(nodeId)) {
return null;
}
if (StrUtil.isBlank(key)) {
return null;
}
JSONObject runningContext = getRunningContext(nodeId);
if (ObjectUtil.isNull(runningContext)) {
return null;
}
return runningContext.get(key);
}
/**
* 构建上下文
*
@@ -63,6 +137,8 @@ public class RunningContext {
.build();
}
public String convertDotToSquareBrackets(String key) {
return key.replaceAll("(\\w+)\\.(\\w+)", "#$1['$2']");
}
}

View File

@@ -18,7 +18,7 @@ public class RunningResult {
/**
* 下一个运行节点id, 一些特殊节点需要, 必须条件节点满足后, 才会运行下一个节点
*/
private Set<Long> nextRunNodeId;
private Set<String> nextRunNodeId;
/**
@@ -28,7 +28,7 @@ public class RunningResult {
* @param nextRunNodeId 下一个运行节点id
* @return {@link RunningResult }
*/
public static RunningResult buildResult(JSONObject nodeContext, Set<Long> nextRunNodeId) {
public static RunningResult buildResult(JSONObject nodeContext, Set<String> nextRunNodeId) {
return RunningResult.builder()
.nodeContext(nodeContext)
.nextRunNodeId(nextRunNodeId)
@@ -41,7 +41,7 @@ public class RunningResult {
* @param nextRunNodeId 下一个运行节点id
* @return {@link RunningResult }
*/
public static RunningResult buildResult(Set<Long> nextRunNodeId) {
public static RunningResult buildResult(Set<String> nextRunNodeId) {
return RunningResult.builder()
.nextRunNodeId(nextRunNodeId)
.build();

View File

@@ -48,4 +48,5 @@ public class SysContext {
* 实例id
*/
private Long instanceId;
}

View File

@@ -12,13 +12,13 @@ import java.util.stream.Collectors;
public class GraphDto {
private final Map<Long, Node> nodeMap;
private final Map<String, Node> nodeMap;
private final Map<Long, Boolean> nodeReadyMap;
private final Map<String, Boolean> nodeReadyMap;
private final Map<Long, List<Edge>> edgeMap;
private final Map<String, List<Edge>> edgeMap;
private final Map<Long, List<Long>> adjacencyList = new HashMap<>();
private final Map<String, List<String>> adjacencyList = new HashMap<>();
private final List<Node> sortedNodes = new ArrayList<>();
@@ -27,7 +27,7 @@ public class GraphDto {
return new ArrayList<>(sortedNodes);
}
public List<Edge> getEdgeNodeId(Long nodeId) {
public List<Edge> getEdgeNodeId(String nodeId) {
return edgeMap.getOrDefault(nodeId, new ArrayList<>());
}
@@ -38,15 +38,15 @@ public class GraphDto {
.orElse(null);
}
public Node getNode(Long nodeId) {
public Node getNode(String nodeId) {
return nodeMap.get(nodeId);
}
public void updateNodeReadyMap(Long nodeId, Boolean ready) {
public void updateNodeReadyMap(String nodeId, Boolean ready) {
nodeReadyMap.put(nodeId, ready);
}
public Boolean isNodeReady(Long nodeId) {
public Boolean isNodeReady(String nodeId) {
return nodeReadyMap.get(nodeId);
}
@@ -69,7 +69,7 @@ public class GraphDto {
private void initAdjacencyList(List<Edge> edges) {
for (Edge edge : edges) {
List<Long> targetList = adjacencyList.getOrDefault(edge.getSource(), new ArrayList<>());
List<String> targetList = adjacencyList.getOrDefault(edge.getSource(), new ArrayList<>());
targetList.add(edge.getTarget());
adjacencyList.put(edge.getSource(), targetList);
}
@@ -83,9 +83,9 @@ public class GraphDto {
*/
private List<Node> topologicalSort() {
List<Node> sortedNodes = new ArrayList<>();
Set<Long> visited = new HashSet<>();
Set<Long> visiting = new HashSet<>();
for (Long nodeId : nodeMap.keySet()) {
Set<String> visited = new HashSet<>();
Set<String> visiting = new HashSet<>();
for (String nodeId : nodeMap.keySet()) {
if (!visited.contains(nodeId)) {
dfs(nodeId, visited, visiting, sortedNodes);
}
@@ -102,13 +102,13 @@ public class GraphDto {
* @param visiting 参观
* @param sortedNodes 排序节点
*/
private void dfs(Long nodeId, Set<Long> visited, Set<Long> visiting, List<Node> sortedNodes) {
private void dfs(String nodeId, Set<String> visited, Set<String> visiting, List<Node> sortedNodes) {
if (visiting.contains(nodeId)) {
throw new IllegalStateException("Cycle detected in the graph");
}
if (!visited.contains(nodeId)) {
visiting.add(nodeId);
for (Long neighbor : adjacencyList.getOrDefault(nodeId, new ArrayList<>())) {
for (String neighbor : adjacencyList.getOrDefault(nodeId, new ArrayList<>())) {
dfs(neighbor, visited, visiting, sortedNodes);
}
visiting.remove(nodeId);

View File

@@ -0,0 +1,69 @@
package com.metis.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.metis.domain.SimpleBaseEntity;
import com.metis.enums.ModelTypeEnum;
import com.metis.enums.YesOrNoEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* llm model平台
*
* @author clay
* @date 2025/04/26
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("model_platform")
public class ModelPlatform extends SimpleBaseEntity {
/**
* 主键
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 名字
*/
private String name;
/**
* 类型
*/
private ModelTypeEnum type;
/**
* 自定义类型
*/
private String customType;
/**
* 请求地址
*/
private String url;
/**
* api密匙
*/
private String apiKey;
/**
* 配置
*/
private String configJson;
/**
* 状态
*/
private YesOrNoEnum state;
/**
* 描述
*/
private String description;
}

View File

@@ -0,0 +1,28 @@
package com.metis.domain.entity;
import com.metis.enums.ModelTypeEnum;
import lombok.Data;
@Data
public class ModelPlatformInfo {
/**
* 名字
*/
private String name;
/**
* 类型
*/
private ModelTypeEnum type;
/**
* 请求地址
*/
private String url;
/**
* api密匙
*/
private String apiKey;
}

View File

@@ -1,6 +1,7 @@
package com.metis.domain.entity.base;
import com.metis.enums.EdgeType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -10,42 +11,42 @@ public class Edge {
/**
* 唯一标识符
*/
@NotNull(message = "唯一标识符不能为空")
@NotBlank(message = "唯一标识符不能为空")
private String id;
/**
* 标签
*/
private String label;
/**
* 节点类型
*/
@NotNull(message = "线类型不能为空")
private EdgeType type;
/**
* 标签
*/
private String label;
/**
* 源节点ID,对应节点id
*/
@NotNull(message = "源节点ID不能为空")
private Long source;
private String source;
/**
* 目标节点ID,对应节点id
*/
@NotNull(message = "目标节点ID不能为空")
private Long target;
private String target;
/**
* 源句柄id
*/
@NotNull(message = "源句柄ID不能为空")
private Long sourceHandle;
private String sourceHandle;
/**
* 目标句柄id
*/
@NotNull(message = "目标句柄ID不能为空")
private Long targetHandle;
private String targetHandle;
/**
* 边是否动画true/false
@@ -62,5 +63,36 @@ public class Edge {
*/
private String markerEnd;
/**
* 边是否可选中true,false
*/
private Boolean selectable;
/**
* 边是否可更新true,false(更改它们的源/目标位置。)
*/
private Boolean updatable;
/**
* 边是否可以删除true,false
*/
private Boolean deletable;
/**
* 源位置x坐标
*/
private Float sourceX;
/**
* 源位置y坐标
*/
private Float sourceY;
/**
* 目标位置x坐标
*/
private Float targetX;
/**
* 目标位置y坐标
*/
private Float targetY;
}

View File

@@ -2,6 +2,7 @@ package com.metis.domain.entity.base;
import com.metis.enums.HandleType;
import com.metis.enums.PositionType;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@@ -13,8 +14,8 @@ public class Handle {
/**
* 句柄id
*/
@NotNull(message = "句柄id不能为空")
private Long id;
@NotBlank(message = "句柄id不能为空")
private String id;
/**
* 句柄类型

View File

@@ -0,0 +1,84 @@
package com.metis.domain.entity.base;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSONObject;
import com.metis.enums.ModelTypeEnum;
import com.metis.enums.YesOrNoEnum;
import lombok.Data;
@Data
public class Model {
/**
* 主键
*/
private Long id;
/**
* 名字
*/
private String name;
/**
* 类型
*/
private ModelTypeEnum type;
/**
* 自定义类型
*/
private String customType;
/**
* 请求地址
*/
private String url;
/**
* api密匙
*/
private String apiKey;
/**
* 配置
*/
private JSONObject config;
/**
* 状态
*/
private YesOrNoEnum state;
/**
* 描述
*/
private String description;
private transient Class<?> configClass;
/**
* 获取配置
*
* @return {@link T }
*/
public <T> T getConfig() {
if (ObjectUtil.isNull(config)) {
return null;
}
return (T) config.to(configClass);
}
/**
* 设置配置类
*
* @param configClass 配置类
*/
public <T> void setConfigClass(Class<T> configClass) {
if (ObjectUtil.isNotNull(this.configClass)) {
return;
}
this.configClass = configClass;
}
}

View File

@@ -13,7 +13,7 @@ public class Node {
* id
*/
@NotNull(message = "节点id不能为空")
private Long id;
private String id;
/**
* 类型
@@ -40,13 +40,11 @@ public class Node {
/**
* 宽度
*/
// @NotNull(message = "节点宽度不能为空")
private Integer width;
/**
* 高度
*/
// @NotNull(message = "节点高度不能为空")
private Integer height;
/**

View File

@@ -23,6 +23,11 @@ public class NodeData {
*/
private String icon;
/**
* 描述
*/
private String description;
/**
* 工具栏位置
*/

View File

@@ -36,7 +36,7 @@ public class NodeVariable {
* 类型
*/
@NotNull(message = "类型不能为空")
private String type;
private NodeVariableType type;
/**
* 是否必填
@@ -74,7 +74,7 @@ public class NodeVariable {
}
private Object getSerializable(JSONObject custom) {
switch (getVariableType()) {
switch (this.type) {
case TEXT_INPUT, PARAGRAPH, SELECT, FILE -> {
return custom.getString(variable);
}
@@ -89,10 +89,5 @@ public class NodeVariable {
}
@JsonIgnore
public NodeVariableType getVariableType() {
return NodeVariableType.get(type);
}
}

View File

@@ -1,7 +1,7 @@
package com.metis.domain.entity.base;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@@ -15,7 +15,7 @@ public class VariableOption {
/**
* 值
*/
@NotNull(message = "值不能为空")
@NotBlank(message = "值不能为空")
private String value;

View File

@@ -1,10 +1,28 @@
package com.metis.domain.entity.config.node;
import com.metis.domain.entity.base.NodeConfig;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
public class EndNodeConfig extends NodeConfig {
@Valid
private List<Variable> variables;
@Data
public static class Variable {
@NotBlank(message = "参数字段不能为空")
private String variable;
@NotBlank(message = "参数key不能为空")
private String variableKey;
}
}

View File

@@ -1,10 +0,0 @@
package com.metis.domain.entity.config.node;
import com.metis.domain.entity.base.NodeConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class LLMNodeConfig extends NodeConfig {
}

View File

@@ -0,0 +1,30 @@
package com.metis.domain.entity.config.node.common;
import lombok.Data;
/**
* 重试配置
*
* @author clay
* @date 2025/05/04
*/
@Data
public class RetryConfig {
/**
* 启用
*/
private Boolean enable;
/**
* 最大重试次数
*/
private Integer maxRetries;
/**
* 重试时间间隔
*/
private Integer retryInterval;
}

View File

@@ -0,0 +1,31 @@
package com.metis.domain.entity.config.node.llm;
import com.metis.domain.entity.base.NodeConfig;
import com.metis.domain.entity.config.node.common.RetryConfig;
import com.metis.llm.domain.LLMChatModeConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
public class LLMNodeConfig extends NodeConfig {
private String context;
/**
* 重试配置
*/
private RetryConfig retryConfig;
/**
* 提示模板
*/
private List<PromptTemplate> promptTemplate;
/**
* 模型
*/
private LLMChatModeConfig model;
}

View File

@@ -0,0 +1,15 @@
package com.metis.domain.entity.config.node.llm;
import com.metis.enums.ChatRoleType;
import lombok.Data;
@Data
public class PromptTemplate {
private ChatRoleType role;
private String text;
private String id;
}

View File

@@ -67,4 +67,41 @@ public class EdgeVo {
@Schema(description = "结束标记")
private String markerEnd;
/**
* 边是否可选中true,false
*/
@Schema(description = "边是否可选中true,false")
private Boolean selectable;
/**
* 边是否可更新true,false(更改它们的源/目标位置。)
*/
@Schema(description = "边是否可更新true,false(更改它们的源/目标位置。)")
private Boolean updatable;
/**
* 边是否可以删除true,false
*/
@Schema(description = "边是否可以删除true,false")
private Boolean deletable;
/**
* 源位置x坐标
*/
@Schema(description = "源位置x坐标")
private Float sourceX;
/**
* 源位置y坐标
*/
@Schema(description = "源位置y坐标")
private Float sourceY;
/**
* 目标位置x坐标
*/
@Schema(description = "目标位置x坐标")
private Float targetX;
/**
* 目标位置y坐标
*/
@Schema(description = "目标位置y坐标")
private Float targetY;
}

View File

@@ -8,6 +8,7 @@ import java.util.List;
@Data
@Schema(description = "画布")
public class GraphVo {
/**

View File

@@ -23,6 +23,12 @@ public class NodeDataVo {
@Schema(description = "图标")
private String icon;
/**
* 描述
*/
@Schema(description = "描述")
private String description;
/**
* 工具栏位置
*/

View File

@@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "节点")
public class NodeVo {
/**

View File

@@ -59,7 +59,7 @@ public class AppFlowEngineRunnerServiceImpl implements AppFlowEngineRunnerServic
// 开始节点为空,则表示数据存在异常
Assert.isTrue(ObjectUtil.isNotNull(readyRunningNode), "流程图不存在开始节点");
for (Node node : graph.getSortedNodes()) {
Long nodeId = node.getId();
String nodeId = node.getId();
if (!graph.isNodeReady(nodeId)) {
continue;
}
@@ -79,7 +79,7 @@ public class AppFlowEngineRunnerServiceImpl implements AppFlowEngineRunnerServic
}
// 下一个需要运行的节点id加入到可以运行的节点中
if (CollUtil.isNotEmpty(result.getNextRunNodeId())) {
for (Long nextNodeId : result.getNextRunNodeId()) {
for (String nextNodeId : result.getNextRunNodeId()) {
graph.updateNodeReadyMap(nextNodeId, true);
}
} else {
@@ -95,7 +95,7 @@ public class AppFlowEngineRunnerServiceImpl implements AppFlowEngineRunnerServic
return RunnerResult.builder()
.result(endRunningContext)
.context(sysContext)
// .context(sysContext)
.build();
}

View File

@@ -0,0 +1,23 @@
package com.metis.enums;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ChatRoleType {
SYSTEM("system", "系统角色"),
USER("user", "用户角色"),
AI("ai", "ai返回"),
TOOL_EXECUTION_RESULT("toolExecutionResult", "工具(函数调用)返回");
@JsonValue
private final String value;
private final String desc;
}

View File

@@ -0,0 +1,51 @@
package com.metis.enums;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Optional;
/**
* llm 类型 枚举
*
* @author clay
* @date 2025/04/26
*/
@Getter
@AllArgsConstructor
public enum ModelTypeEnum implements BaseEnum<ModelTypeEnum> {
CUSTOM(0, "自定义"),
OPEN_AI(1, "OpenAI"),
OLLAMA(2, "Ollama"),
;
@JsonValue
private final Integer code;
private final String name;
/**
* 根据 code 转换枚举
*
* @param code 编码
* @return 枚举
*/
public static Optional<ModelTypeEnum> of(Integer code) {
return Optional.ofNullable(BaseEnum.parseByCode(ModelTypeEnum.class, code));
}
/**
* 枚举序列化器(前端传code时自动转换为对应枚举)
*
* @param code 编码
* @return 枚举
*/
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public static ModelTypeEnum get(Integer code) {
return BaseEnum.parseByCode(ModelTypeEnum.class, code);
}
}

View File

@@ -28,11 +28,11 @@ public class ProcessDefinitionFacade {
/**
* 通过部署id获取
*
* @param deploymentId 部署id
* @param workflowId 部署id
* @return {@link App }
*/
public AppVo getByDeploymentId(Long deploymentId) {
App app = appEngineService.getByWorkflowId(deploymentId);
public AppVo getByWorkflowId(Long workflowId) {
App app = appEngineService.getByWorkflowId(workflowId);
return AppConvert.INSTANCE.toVo(app);
}

View File

@@ -0,0 +1,42 @@
package com.metis.llm.domain;
import lombok.Data;
@Data
public class CompletionParams {
/**
* 温度
*/
private Double temperature;
/**
* 最大token数
*/
private Integer maxTokens;
/**
* Top P
*/
private Double topP;
/**
* Top K
*/
private Double topK;
/**
* 随机种子
*/
private Integer seed;
/**
* 重复处罚
*/
private Double presencePenalty;
/**
* 响应格式
*/
private String responseFormat;
}

View File

@@ -0,0 +1,24 @@
package com.metis.llm.domain;
import lombok.Data;
@Data
public class LLMChatModeConfig {
/**
* 模型id
*/
private Long modelId;
/**
* 模型名称
*/
private String modelName;
/**
* 完成参数
*/
private CompletionParams completionParams;
}

View File

@@ -0,0 +1,17 @@
package com.metis.llm.domain;
import lombok.Data;
@Data
public class LLMEmbeddingModelConfig {
/**
* 模型id
*/
private Long modelId;
/**
* 模型名称
*/
private String modelName;
}

View File

@@ -0,0 +1,49 @@
package com.metis.llm.domain;
import cn.hutool.core.util.ObjectUtil;
import dev.langchain4j.model.openai.OpenAiTokenUsage;
import dev.langchain4j.model.output.TokenUsage;
import lombok.Data;
@Data
public class Usage {
private Integer inputTokenCount;
private Integer outputTokenCount;
private Integer totalTokenCount;
private InputTokensDetails inputTokensDetails;
private OutputTokensDetails outputTokensDetails;
@Data
private static class InputTokensDetails {
private Integer cachedTokens;
}
@Data
private static class OutputTokensDetails {
private Integer reasoningTokens;
}
public static Usage buildTokenUsage(TokenUsage tokenUsage) {
Usage usage = new Usage();
usage.setInputTokenCount(tokenUsage.inputTokenCount());
usage.setOutputTokenCount(tokenUsage.outputTokenCount());
usage.setTotalTokenCount(tokenUsage.totalTokenCount());
if (tokenUsage instanceof OpenAiTokenUsage openAiTokenUsage) {
if (ObjectUtil.isNotNull(openAiTokenUsage.inputTokensDetails())) {
InputTokensDetails inputTokensDetails = new InputTokensDetails();
Integer cachedTokens = openAiTokenUsage.inputTokensDetails().cachedTokens();
inputTokensDetails.setCachedTokens(cachedTokens);
usage.setInputTokensDetails(inputTokensDetails);
}
if (ObjectUtil.isNotNull(openAiTokenUsage.outputTokensDetails())) {
OutputTokensDetails outputTokensDetails = new OutputTokensDetails();
Integer reasoningTokens = openAiTokenUsage.outputTokensDetails().reasoningTokens();
outputTokensDetails.setReasoningTokens(reasoningTokens);
usage.setOutputTokensDetails(outputTokensDetails);
}
}
return usage;
}
}

View File

@@ -0,0 +1,4 @@
package com.metis.llm.domain.config;
public abstract class BaseModelConfig {
}

View File

@@ -0,0 +1,17 @@
package com.metis.llm.domain.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Ollama模型配置
*
* @author clay
* @date 2025/05/04
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class OllamaModelConfig extends BaseModelConfig{
}

View File

@@ -0,0 +1,9 @@
package com.metis.llm.domain.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class OpenApiConfig extends BaseModelConfig {
}

View File

@@ -0,0 +1,55 @@
package com.metis.llm.engine;
import cn.hutool.core.util.StrUtil;
import com.metis.domain.entity.base.Model;
import com.metis.enums.ModelTypeEnum;
import com.metis.llm.domain.CompletionParams;
import com.metis.llm.domain.LLMChatModeConfig;
import com.metis.llm.domain.LLMEmbeddingModelConfig;
import com.metis.llm.domain.config.OllamaModelConfig;
import com.metis.llm.service.ModelEngine;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.request.ResponseFormat;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import dev.langchain4j.model.ollama.OllamaEmbeddingModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class OllamaModelEngine implements ModelEngine<OllamaModelConfig> {
@Override
public ModelTypeEnum getType() {
return ModelTypeEnum.OLLAMA;
}
@Override
public ChatModel getChatLanguageModel(Model model, LLMChatModeConfig modelConfig) {
CompletionParams completionParams = modelConfig.getCompletionParams();
ResponseFormat responseFormat = ResponseFormat.JSON;
if (StrUtil.isNotBlank(completionParams.getResponseFormat()) && "text".equals(completionParams.getResponseFormat())) {
responseFormat = ResponseFormat.TEXT;
}
return OllamaChatModel.builder()
.baseUrl(model.getUrl())
.modelName(modelConfig.getModelName())
.modelName(modelConfig.getModelName())
.temperature(completionParams.getTemperature())
.maxRetries(completionParams.getMaxTokens())
.topP(completionParams.getTopP())
.seed(completionParams.getSeed())
.responseFormat(responseFormat)
.build();
}
@Override
public EmbeddingModel getEmbeddingModel(Model model, LLMEmbeddingModelConfig modeConfig) {
return OllamaEmbeddingModel.builder()
.baseUrl(model.getUrl())
.modelName(modeConfig.getModelName())
.build();
}
}

View File

@@ -0,0 +1,51 @@
package com.metis.llm.engine;
import com.metis.domain.entity.base.Model;
import com.metis.enums.ModelTypeEnum;
import com.metis.llm.service.ModelEngine;
import com.metis.llm.domain.CompletionParams;
import com.metis.llm.domain.LLMChatModeConfig;
import com.metis.llm.domain.LLMEmbeddingModelConfig;
import com.metis.llm.domain.config.OpenApiConfig;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiEmbeddingModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class OpenApiModelEngine implements ModelEngine<OpenApiConfig> {
@Override
public ModelTypeEnum getType() {
return ModelTypeEnum.OPEN_AI;
}
@Override
public ChatModel getChatLanguageModel(Model model, LLMChatModeConfig modelConfig) {
CompletionParams completionParams = modelConfig.getCompletionParams();
return OpenAiChatModel.builder()
.apiKey(model.getApiKey())
.baseUrl(model.getUrl())
.modelName(modelConfig.getModelName())
.temperature(completionParams.getTemperature())
.maxTokens(completionParams.getMaxTokens())
.topP(completionParams.getTopP())
.seed(completionParams.getSeed())
.presencePenalty(completionParams.getPresencePenalty())
.responseFormat(completionParams.getResponseFormat())
.build();
}
@Override
public EmbeddingModel getEmbeddingModel(Model model, LLMEmbeddingModelConfig modelConfig) {
return OpenAiEmbeddingModel.builder()
.apiKey(model.getApiKey())
.baseUrl(model.getUrl())
.modelName(modelConfig.getModelName())
.build();
}
}

View File

@@ -0,0 +1,105 @@
package com.metis.llm.factory;
import cn.hutool.core.lang.Assert;
import com.metis.enums.ModelTypeEnum;
import com.metis.llm.service.CustomModelEngine;
import com.metis.llm.service.ModelEngine;
import com.metis.llm.domain.config.BaseModelConfig;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class ModelEngineFactory {
/**
* 模型图
*/
private static final Map<ModelTypeEnum, ModelEngine<? extends BaseModelConfig>> MODEL_MAP = new ConcurrentHashMap<>();
/**
* 自定义模型图
*/
private static final Map<String, ModelEngine<? extends BaseModelConfig>> CUSTOM_MODEL_MAP = new ConcurrentHashMap<>();
/**
* 型号表
*
* @return {@link Set }<{@link ModelTypeEnum }>
*/
public static Set<ModelTypeEnum> modelTypeList() {
return MODEL_MAP.keySet();
}
/**
* 自定义模型类型列表
*
* @return {@link Set }<{@link String }>
*/
public static Set<String> customModelTypeList() {
return CUSTOM_MODEL_MAP.keySet();
}
/**
* 发动机型号清单
*
* @return {@link List }<{@link ModelEngine }<{@link ? } {@link extends } {@link BaseModelConfig }>>
*/
public static List<ModelEngine<? extends BaseModelConfig>> modelEngineList() {
return new ArrayList<>(MODEL_MAP.values());
}
/**
* 自定义型号引擎列表
*
* @return {@link List }<{@link ModelEngine }<{@link ? } {@link extends } {@link BaseModelConfig }>>
*/
public static List<ModelEngine<? extends BaseModelConfig>> customModelEngineList() {
return new ArrayList<>(CUSTOM_MODEL_MAP.values());
}
/**
* 注册
*
* @param modelEngine 模型引擎
*/
static void register(ModelEngine<? extends BaseModelConfig> modelEngine) {
MODEL_MAP.put(modelEngine.getType(), modelEngine);
}
/**
* 得到
*
* @param type 类型
* @return {@link ModelEngine }<{@link ? } {@link extends } {@link BaseModelConfig }>
*/
public static ModelEngine<? extends BaseModelConfig> get(ModelTypeEnum type) {
return MODEL_MAP.get(type);
}
/**
* 注册自定义
*
* @param modelEngine 模型引擎
*/
static void registerCustom(CustomModelEngine<? extends BaseModelConfig> modelEngine) {
Assert.isTrue(!CUSTOM_MODEL_MAP.containsKey(modelEngine.getCustomType()), "已存在类型:{}, class:{}的模型", modelEngine.getCustomType(), modelEngine.getClass());
CUSTOM_MODEL_MAP.put(modelEngine.getCustomType(), modelEngine);
}
/**
* 得到自定义
*
* @param type 类型
* @return {@link ModelEngine }<{@link ? } {@link extends } {@link BaseModelConfig }>
*/
public static ModelEngine<? extends BaseModelConfig> getCustom(String type) {
return CUSTOM_MODEL_MAP.get(type);
}
}

View File

@@ -0,0 +1,32 @@
package com.metis.llm.factory;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import com.metis.enums.ModelTypeEnum;
import com.metis.llm.service.CustomModelEngine;
import com.metis.llm.service.ModelEngine;
import com.metis.llm.domain.config.BaseModelConfig;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class ModelEngineInitiate implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, ModelEngine> modelMap = applicationContext.getBeansOfType(ModelEngine.class);
modelMap.forEach((modelBeanName, model) -> {
if (ObjectUtil.isNull(model.getType())) {
return;
}
if (ModelTypeEnum.CUSTOM.equals(model.getType())) {
Assert.isTrue(model instanceof CustomModelEngine<? extends BaseModelConfig>, "自定义模型必须实现CustomModelEngine接口");
ModelEngineFactory.registerCustom((CustomModelEngine<? extends BaseModelConfig>) model);
}
ModelEngineFactory.register((ModelEngine<? extends BaseModelConfig>) model);
});
}
}

View File

@@ -0,0 +1,12 @@
package com.metis.llm.service;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.service.Result;
import java.util.List;
public interface ChatBoot {
Result<String> chat(List<ChatMessage> chatMessageList);
}

View File

@@ -0,0 +1,16 @@
package com.metis.llm.service;
import com.metis.enums.ModelTypeEnum;
import com.metis.llm.domain.config.BaseModelConfig;
public interface CustomModelEngine<T extends BaseModelConfig> extends ModelEngine<T> {
String getCustomType();
default ModelTypeEnum getType() {
return ModelTypeEnum.CUSTOM;
}
}

View File

@@ -0,0 +1,528 @@
package com.metis.llm.service;
import dev.langchain4j.Internal;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.request.ChatRequestParameters;
import dev.langchain4j.model.chat.request.ResponseFormat;
import dev.langchain4j.model.chat.request.json.JsonSchema;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.model.input.structured.StructuredPrompt;
import dev.langchain4j.model.input.structured.StructuredPromptProcessor;
import dev.langchain4j.model.moderation.Moderation;
import dev.langchain4j.rag.AugmentationRequest;
import dev.langchain4j.rag.AugmentationResult;
import dev.langchain4j.rag.query.Metadata;
import dev.langchain4j.service.*;
import dev.langchain4j.service.memory.ChatMemoryAccess;
import dev.langchain4j.service.memory.ChatMemoryService;
import dev.langchain4j.service.output.ServiceOutputParser;
import dev.langchain4j.service.tool.ToolServiceContext;
import dev.langchain4j.service.tool.ToolServiceResult;
import dev.langchain4j.spi.services.AiServicesFactory;
import dev.langchain4j.spi.services.TokenStreamAdapter;
import java.io.InputStream;
import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import static dev.langchain4j.internal.Exceptions.illegalArgument;
import static dev.langchain4j.internal.Utils.isNotNullOrBlank;
import static dev.langchain4j.model.chat.Capability.RESPONSE_FORMAT_JSON_SCHEMA;
import static dev.langchain4j.model.chat.request.ResponseFormatType.JSON;
import static dev.langchain4j.service.IllegalConfigurationException.illegalConfiguration;
import static dev.langchain4j.service.TypeUtils.typeHasRawClass;
import static dev.langchain4j.spi.ServiceHelper.loadFactories;
@Internal
public class FlowAiServices<T> extends AiServices<T> {
private final ServiceOutputParser serviceOutputParser = new ServiceOutputParser();
private final Collection<TokenStreamAdapter> tokenStreamAdapters = loadFactories(TokenStreamAdapter.class);
FlowAiServices(AiServiceContext context) {
super(context);
}
/**
* Begins the construction of an AI Service.
*
* @param aiService The class of the interface to be implemented.
* @return builder
*/
public static <T> AiServices<T> builder(Class<T> aiService) {
AiServiceContext context = new AiServiceContext(aiService);
for (AiServicesFactory factory : loadFactories(AiServicesFactory.class)) {
return factory.create(context);
}
return new FlowAiServices<>(context);
}
static void validateParameters(Method method) {
Parameter[] parameters = method.getParameters();
if (parameters == null || parameters.length < 2) {
return;
}
for (Parameter parameter : parameters) {
V v = parameter.getAnnotation(V.class);
dev.langchain4j.service.UserMessage userMessage =
parameter.getAnnotation(dev.langchain4j.service.UserMessage.class);
MemoryId memoryId = parameter.getAnnotation(MemoryId.class);
UserName userName = parameter.getAnnotation(UserName.class);
if (v == null && userMessage == null && memoryId == null && userName == null) {
throw illegalConfiguration(
"Parameter '%s' of method '%s' should be annotated with @V or @UserMessage "
+ "or @UserName or @MemoryId",
parameter.getName(), method.getName());
}
}
}
public T build() {
performBasicValidation();
if (!context.hasChatMemory() && ChatMemoryAccess.class.isAssignableFrom(context.aiServiceClass)) {
throw illegalConfiguration(
"In order to have a service implementing ChatMemoryAccess, please configure the ChatMemoryProvider on the '%s'.",
context.aiServiceClass.getName());
}
for (Method method : context.aiServiceClass.getMethods()) {
if (method.isAnnotationPresent(Moderate.class) && context.moderationModel == null) {
throw illegalConfiguration(
"The @Moderate annotation is present, but the moderationModel is not set up. "
+ "Please ensure a valid moderationModel is configured before using the @Moderate annotation.");
}
Class<?> returnType = method.getReturnType();
if (returnType == void.class) {
throw illegalConfiguration("'%s' is not a supported return type of an AI Service method", returnType.getName());
}
if (returnType == Result.class || returnType == List.class || returnType == Set.class) {
TypeUtils.validateReturnTypesAreProperlyParametrized(method.getName(), method.getGenericReturnType());
}
if (!context.hasChatMemory()) {
for (Parameter parameter : method.getParameters()) {
if (parameter.isAnnotationPresent(MemoryId.class)) {
throw illegalConfiguration(
"In order to use @MemoryId, please configure the ChatMemoryProvider on the '%s'.",
context.aiServiceClass.getName());
}
}
}
}
Object proxyInstance = Proxy.newProxyInstance(
context.aiServiceClass.getClassLoader(),
new Class<?>[]{context.aiServiceClass},
new InvocationHandler() {
private final ExecutorService executor = Executors.newCachedThreadPool();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
if (method.getDeclaringClass() == Object.class) {
// methods like equals(), hashCode() and toString() should not be handled by this proxy
return method.invoke(this, args);
}
if (method.getDeclaringClass() == ChatMemoryAccess.class) {
return switch (method.getName()) {
case "getChatMemory" -> context.chatMemoryService.getChatMemory(args[0]);
case "evictChatMemory" -> context.chatMemoryService.evictChatMemory(args[0]) != null;
default -> throw new UnsupportedOperationException(
"Unknown method on ChatMemoryAccess class : " + method.getName());
};
}
validateParameters(method);
final Object memoryId = findMemoryId(method, args).orElse(ChatMemoryService.DEFAULT);
final ChatMemory chatMemory = context.hasChatMemory()
? context.chatMemoryService.getOrCreateChatMemory(memoryId)
: null;
Optional<SystemMessage> systemMessage = prepareSystemMessage(memoryId, method, args);
UserMessage userMessage = prepareUserMessage(method, args);
AugmentationResult augmentationResult = null;
if (context.retrievalAugmentor != null) {
List<ChatMessage> chatMemoryMessages = chatMemory != null ? chatMemory.messages() : null;
Metadata metadata = Metadata.from(userMessage, memoryId, chatMemoryMessages);
AugmentationRequest augmentationRequest = new AugmentationRequest(userMessage, metadata);
augmentationResult = context.retrievalAugmentor.augment(augmentationRequest);
userMessage = (UserMessage) augmentationResult.chatMessage();
}
Type returnType = method.getGenericReturnType();
boolean streaming = returnType == TokenStream.class || canAdaptTokenStreamTo(returnType);
boolean supportsJsonSchema = supportsJsonSchema();
Optional<JsonSchema> jsonSchema = Optional.empty();
if (supportsJsonSchema && !streaming) {
jsonSchema = serviceOutputParser.jsonSchema(returnType);
}
if ((!supportsJsonSchema || jsonSchema.isEmpty()) && !streaming) {
userMessage = appendOutputFormatInstructions(returnType, userMessage);
}
List<ChatMessage> messages;
if (chatMemory != null) {
systemMessage.ifPresent(chatMemory::add);
chatMemory.add(userMessage);
messages = chatMemory.messages();
} else {
messages = new ArrayList<>();
systemMessage.ifPresent(messages::add);
messages.add(userMessage);
}
Future<Moderation> moderationFuture = triggerModerationIfNeeded(method, messages);
ToolServiceContext toolServiceContext =
context.toolService.createContext(memoryId, userMessage);
if (streaming) {
TokenStream tokenStream = new AiServiceTokenStream(AiServiceTokenStreamParameters.builder()
.messages(messages)
.toolSpecifications(toolServiceContext.toolSpecifications())
.toolExecutors(toolServiceContext.toolExecutors())
.retrievedContents(
augmentationResult != null ? augmentationResult.contents() : null)
.context(context)
.memoryId(memoryId)
.build());
// TODO moderation
if (returnType == TokenStream.class) {
return tokenStream;
} else {
return adapt(tokenStream, returnType);
}
}
ResponseFormat responseFormat = null;
if (supportsJsonSchema && jsonSchema.isPresent()) {
responseFormat = ResponseFormat.builder()
.type(JSON)
.jsonSchema(jsonSchema.get())
.build();
}
ChatRequestParameters parameters = ChatRequestParameters.builder()
.toolSpecifications(toolServiceContext.toolSpecifications())
.responseFormat(responseFormat)
.build();
ChatRequest chatRequest = ChatRequest.builder()
.messages(messages)
.parameters(parameters)
.build();
ChatResponse chatResponse = context.chatModel.chat(chatRequest);
verifyModerationIfNeeded(moderationFuture);
ToolServiceResult toolServiceResult = context.toolService.executeInferenceAndToolsLoop(
chatResponse,
parameters,
messages,
context.chatModel,
chatMemory,
memoryId,
toolServiceContext.toolExecutors());
chatResponse = toolServiceResult.chatResponse();
Object parsedResponse = serviceOutputParser.parse(chatResponse, returnType);
if (typeHasRawClass(returnType, Result.class)) {
return Result.builder()
.content(parsedResponse)
.tokenUsage(chatResponse.tokenUsage())
.sources(augmentationResult == null ? null : augmentationResult.contents())
.finishReason(chatResponse.finishReason())
.toolExecutions(toolServiceResult.toolExecutions())
.build();
} else {
return parsedResponse;
}
}
private boolean canAdaptTokenStreamTo(Type returnType) {
for (TokenStreamAdapter tokenStreamAdapter : tokenStreamAdapters) {
if (tokenStreamAdapter.canAdaptTokenStreamTo(returnType)) {
return true;
}
}
return false;
}
private Object adapt(TokenStream tokenStream, Type returnType) {
for (TokenStreamAdapter tokenStreamAdapter : tokenStreamAdapters) {
if (tokenStreamAdapter.canAdaptTokenStreamTo(returnType)) {
return tokenStreamAdapter.adapt(tokenStream);
}
}
throw new IllegalStateException("Can't find suitable TokenStreamAdapter");
}
private boolean supportsJsonSchema() {
return context.chatModel != null
&& context.chatModel.supportedCapabilities().contains(RESPONSE_FORMAT_JSON_SCHEMA);
}
private UserMessage appendOutputFormatInstructions(Type returnType, UserMessage userMessage) {
String outputFormatInstructions = serviceOutputParser.outputFormatInstructions(returnType);
String text = userMessage.singleText() + outputFormatInstructions;
if (isNotNullOrBlank(userMessage.name())) {
userMessage = UserMessage.from(userMessage.name(), text);
} else {
userMessage = UserMessage.from(text);
}
return userMessage;
}
private Future<Moderation> triggerModerationIfNeeded(Method method, List<ChatMessage> messages) {
if (method.isAnnotationPresent(Moderate.class)) {
return executor.submit(() -> {
List<ChatMessage> messagesToModerate = removeToolMessages(messages);
return context.moderationModel
.moderate(messagesToModerate)
.content();
});
}
return null;
}
});
return (T) proxyInstance;
}
private Optional<SystemMessage> prepareSystemMessage(Object memoryId, Method method, Object[] args) {
return findSystemMessageTemplate(memoryId, method)
.map(systemMessageTemplate -> PromptTemplate.from(systemMessageTemplate)
.apply(findTemplateVariables(systemMessageTemplate, method, args))
.toSystemMessage());
}
private Optional<String> findSystemMessageTemplate(Object memoryId, Method method) {
dev.langchain4j.service.SystemMessage annotation =
method.getAnnotation(dev.langchain4j.service.SystemMessage.class);
if (annotation != null) {
return Optional.of(getTemplate(
method, "System", annotation.fromResource(), annotation.value(), annotation.delimiter()));
}
return context.systemMessageProvider.apply(memoryId);
}
private static Map<String, Object> findTemplateVariables(String template, Method method, Object[] args) {
Parameter[] parameters = method.getParameters();
Map<String, Object> variables = new HashMap<>();
for (int i = 0; i < parameters.length; i++) {
String variableName = getVariableName(parameters[i]);
Object variableValue = args[i];
variables.put(variableName, variableValue);
}
if (template.contains("{{it}}") && !variables.containsKey("it")) {
String itValue = getValueOfVariableIt(parameters, args);
variables.put("it", itValue);
}
return variables;
}
private static String getVariableName(Parameter parameter) {
V annotation = parameter.getAnnotation(V.class);
if (annotation != null) {
return annotation.value();
} else {
return parameter.getName();
}
}
private static String getValueOfVariableIt(Parameter[] parameters, Object[] args) {
if (parameters.length == 1) {
Parameter parameter = parameters[0];
if (!parameter.isAnnotationPresent(MemoryId.class)
&& !parameter.isAnnotationPresent(dev.langchain4j.service.UserMessage.class)
&& !parameter.isAnnotationPresent(UserName.class)
&& (!parameter.isAnnotationPresent(V.class) || isAnnotatedWithIt(parameter))) {
return toString(args[0]);
}
}
for (int i = 0; i < parameters.length; i++) {
if (isAnnotatedWithIt(parameters[i])) {
return toString(args[i]);
}
}
throw illegalConfiguration("Error: cannot find the value of the prompt template variable \"{{it}}\".");
}
private static boolean isAnnotatedWithIt(Parameter parameter) {
V annotation = parameter.getAnnotation(V.class);
return annotation != null && "it".equals(annotation.value());
}
private static UserMessage prepareUserMessage(Method method, Object[] args) {
String template = getUserMessageTemplate(method, args);
Map<String, Object> variables = findTemplateVariables(template, method, args);
Prompt prompt = PromptTemplate.from(template).apply(variables);
Optional<String> maybeUserName = findUserName(method.getParameters(), args);
return maybeUserName
.map(userName -> UserMessage.from(userName, prompt.text()))
.orElseGet(prompt::toUserMessage);
}
private static String getUserMessageTemplate(Method method, Object[] args) {
Optional<String> templateFromMethodAnnotation = findUserMessageTemplateFromMethodAnnotation(method);
Optional<String> templateFromParameterAnnotation =
findUserMessageTemplateFromAnnotatedParameter(method.getParameters(), args);
if (templateFromMethodAnnotation.isPresent() && templateFromParameterAnnotation.isPresent()) {
throw illegalConfiguration(
"Error: The method '%s' has multiple @UserMessage annotations. Please use only one.",
method.getName());
}
if (templateFromMethodAnnotation.isPresent()) {
return templateFromMethodAnnotation.get();
}
if (templateFromParameterAnnotation.isPresent()) {
return templateFromParameterAnnotation.get();
}
Optional<String> templateFromTheOnlyArgument =
findUserMessageTemplateFromTheOnlyArgument(method.getParameters(), args);
if (templateFromTheOnlyArgument.isPresent()) {
return templateFromTheOnlyArgument.get();
}
throw illegalConfiguration("Error: The method '%s' does not have a user message defined.", method.getName());
}
private static Optional<String> findUserMessageTemplateFromMethodAnnotation(Method method) {
return Optional.ofNullable(method.getAnnotation(dev.langchain4j.service.UserMessage.class))
.map(a -> getTemplate(method, "User", a.fromResource(), a.value(), a.delimiter()));
}
private static Optional<String> findUserMessageTemplateFromAnnotatedParameter(
Parameter[] parameters, Object[] args) {
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].isAnnotationPresent(dev.langchain4j.service.UserMessage.class)) {
return Optional.of(toString(args[i]));
}
}
return Optional.empty();
}
private static Optional<String> findUserMessageTemplateFromTheOnlyArgument(Parameter[] parameters, Object[] args) {
if (parameters != null && parameters.length == 1 && parameters[0].getAnnotations().length == 0) {
return Optional.of(toString(args[0]));
}
return Optional.empty();
}
private static Optional<String> findUserName(Parameter[] parameters, Object[] args) {
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].isAnnotationPresent(UserName.class)) {
return Optional.of(args[i].toString());
}
}
return Optional.empty();
}
private static String getTemplate(Method method, String type, String resource, String[] value, String delimiter) {
String messageTemplate;
if (!resource.trim().isEmpty()) {
messageTemplate = getResourceText(method.getDeclaringClass(), resource);
if (messageTemplate == null) {
throw illegalConfiguration("@%sMessage's resource '%s' not found", type, resource);
}
} else {
messageTemplate = String.join(delimiter, value);
}
if (messageTemplate.trim().isEmpty()) {
throw illegalConfiguration("@%sMessage's template cannot be empty", type);
}
return messageTemplate;
}
private static String getResourceText(Class<?> clazz, String resource) {
InputStream inputStream = clazz.getResourceAsStream(resource);
if (inputStream == null) {
inputStream = clazz.getResourceAsStream("/" + resource);
}
return getText(inputStream);
}
private static String getText(InputStream inputStream) {
if (inputStream == null) {
return null;
}
try (Scanner scanner = new Scanner(inputStream);
Scanner s = scanner.useDelimiter("\\A")) {
return s.hasNext() ? s.next() : "";
}
}
private static Optional<Object> findMemoryId(Method method, Object[] args) {
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
if (parameters[i].isAnnotationPresent(MemoryId.class)) {
Object memoryId = args[i];
if (memoryId == null) {
throw illegalArgument(
"The value of parameter '%s' annotated with @MemoryId in method '%s' must not be null",
parameters[i].getName(), method.getName());
}
return Optional.of(memoryId);
}
}
return Optional.empty();
}
private static String toString(Object arg) {
if (arg.getClass().isArray()) {
return arrayToString(arg);
} else if (arg.getClass().isAnnotationPresent(StructuredPrompt.class)) {
return StructuredPromptProcessor.toPrompt(arg).text();
} else {
return arg.toString();
}
}
private static String arrayToString(Object arg) {
StringBuilder sb = new StringBuilder("[");
int length = Array.getLength(arg);
for (int i = 0; i < length; i++) {
sb.append(toString(Array.get(arg, i)));
if (i < length - 1) {
sb.append(", ");
}
}
sb.append("]");
return sb.toString();
}
}

View File

@@ -0,0 +1,41 @@
package com.metis.llm.service;
import com.metis.domain.entity.base.Model;
import com.metis.enums.ModelTypeEnum;
import com.metis.llm.domain.LLMChatModeConfig;
import com.metis.llm.domain.LLMEmbeddingModelConfig;
import com.metis.llm.domain.config.BaseModelConfig;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
public interface ModelEngine<T extends BaseModelConfig> {
/**
* 获取模型类型
*
* @return {@link ModelTypeEnum }
*/
ModelTypeEnum getType();
/**
* 获取聊天语言模型
*
* @param model 模型
* @param modelConfig
* @return {@link ChatModel }
*/
ChatModel getChatLanguageModel(Model model, LLMChatModeConfig modelConfig);
/**
* 获取嵌入模型
*
* @param model 模型
* @param modeConfig
* @return {@link EmbeddingModel }
*/
EmbeddingModel getEmbeddingModel(Model model, LLMEmbeddingModelConfig modeConfig);
}

View File

@@ -0,0 +1,27 @@
package com.metis.llm.service;
import com.metis.llm.domain.LLMChatModeConfig;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
public interface ModelEngineService {
/**
* 获取聊天语言模型
*
* @param modeConfig 模型id
* @return {@link ChatModel }
*/
ChatModel getChatLanguageModel(LLMChatModeConfig modeConfig);
/**
* 获取嵌入模型
*
* @param modeConfig@return {@link EmbeddingModel }
*/
EmbeddingModel getEmbeddingModel(LLMChatModeConfig modeConfig);
}

View File

@@ -0,0 +1,19 @@
package com.metis.llm.service;
import com.metis.domain.entity.base.Model;
import java.util.List;
public interface ModelService {
/**
* 列表
*
* @param modelId llm id
* @return {@link List }<{@link Model }>
*/
Model getByModelId(Long modelId);
}

View File

@@ -0,0 +1,81 @@
package com.metis.llm.service.impl;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import com.metis.domain.entity.base.Model;
import com.metis.enums.ModelTypeEnum;
import com.metis.llm.service.ModelEngine;
import com.metis.llm.service.ModelEngineService;
import com.metis.llm.service.ModelService;
import com.metis.llm.domain.LLMChatModeConfig;
import com.metis.llm.domain.LLMEmbeddingModelConfig;
import com.metis.llm.factory.ModelEngineFactory;
import com.metis.utils.GenericInterfacesUtils;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class ModelEngineServiceImpl implements ModelEngineService {
private final ModelService modelService;
@Override
public ChatModel getChatLanguageModel(LLMChatModeConfig modeConfig) {
ModelWrapper modelWrapper = getModelWrapper(modeConfig);
return modelWrapper.getChatLanguageModel();
}
@Override
public EmbeddingModel getEmbeddingModel(LLMChatModeConfig modeConfig) {
ModelWrapper modelWrapper = getModelWrapper(modeConfig);
return modelWrapper.getEmbeddingModel();
}
private ModelWrapper getModelWrapper(LLMChatModeConfig modeConfig) {
Model model = getModel(modeConfig.getModelId());
ModelEngine modelEngine = null;
if (model.getType().equals(ModelTypeEnum.CUSTOM)) {
modelEngine = ModelEngineFactory.getCustom(model.getCustomType());
} else {
modelEngine = ModelEngineFactory.get(model.getType());
}
model.setConfigClass(GenericInterfacesUtils.getClass(modelEngine));
return ModelWrapper.builder()
.modelEngine(modelEngine)
.modeConfig(modeConfig)
.model(model)
.build();
}
private Model getModel(Long modelId) {
Assert.isTrue(ObjectUtil.isNotNull(modelId), "模型ID不能为空");
Model model = modelService.getByModelId(modelId);
Assert.isTrue(ObjectUtil.isNotNull(model), "模型不存在");
return model;
}
@Builder
private record ModelWrapper(Model model,
ModelEngine modelEngine,
LLMChatModeConfig modeConfig,
LLMEmbeddingModelConfig embeddingModelConfig) {
public ChatModel getChatLanguageModel() {
return modelEngine.getChatLanguageModel(model, modeConfig);
}
public EmbeddingModel getEmbeddingModel() {
return modelEngine.getEmbeddingModel(model, embeddingModelConfig);
}
}
}

View File

@@ -0,0 +1,29 @@
package com.metis.llm.service.impl;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import com.metis.convert.ModelPlatformConvert;
import com.metis.domain.entity.ModelPlatform;
import com.metis.domain.entity.base.Model;
import com.metis.llm.service.ModelService;
import com.metis.service.ModelPlatformService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class ModelServiceImpl implements ModelService {
private final ModelPlatformService modelPlatformService;
@Override
public Model getByModelId(Long modelId) {
Assert.isTrue(ObjectUtil.isNotNull(modelId), "模型平台ID不能为空");
ModelPlatform modelPlatform = modelPlatformService.getById(modelId);
Assert.isTrue(ObjectUtil.isNotNull(modelPlatform), "模型平台不存在");
return ModelPlatformConvert.INSTANCE.toModel(modelPlatform);
}
}

View File

@@ -0,0 +1,18 @@
package com.metis.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.metis.domain.entity.ModelPlatform;
/**
* LLM模型平台映射器
*
* @author clay
* @date 2025/04/26
*/
public interface ModelPlatformMapper extends BaseMapper<ModelPlatform> {
}

View File

@@ -46,7 +46,7 @@ public interface NodeRunner<T extends NodeConfig> {
* @param edges 边缘
* @return {@link Set }<{@link Long }>
*/
default Set<Long> getNextNodeIds(List<Edge> edges) {
default Set<String> getNextNodeIds(List<Edge> edges) {
if (CollUtil.isEmpty(edges)) {
return Set.of();
}

View File

@@ -2,12 +2,9 @@ package com.metis.runner;
import com.alibaba.fastjson2.JSONObject;
import com.metis.domain.context.SysContext;
import lombok.Builder;
import lombok.Data;
import java.util.Map;
/**
* 运行结果
*
@@ -26,7 +23,7 @@ public class RunnerResult {
/**
* 上下文
*/
private SysContext context;
// private SysContext context;
}

View File

@@ -1,6 +1,7 @@
package com.metis.runner.factory;
import cn.hutool.core.lang.Assert;
import com.metis.domain.entity.base.NodeConfig;
import com.metis.enums.NodeType;
import com.metis.runner.CustomNodeRunner;
import com.metis.runner.NodeRunner;
@@ -13,18 +14,18 @@ public final class NodeRunnerFactory {
/**
* 内置节点运行器
*/
private static final Map<NodeType, NodeRunner> NODE_MAP = new ConcurrentHashMap<>(8);
private static final Map<NodeType, NodeRunner<? extends NodeConfig>> NODE_MAP = new ConcurrentHashMap<>(8);
/**
* 自定义节点映射
*/
private static final Map<String, NodeRunner> CUSTOM_NODE_MAP = new ConcurrentHashMap<>(8);
private static final Map<String, NodeRunner<? extends NodeConfig>> CUSTOM_NODE_MAP = new ConcurrentHashMap<>(8);
/**
* 注册
*
* @param runner 跑步者
*/
static void register(NodeRunner runner) {
static void register(NodeRunner<? extends NodeConfig> runner) {
NODE_MAP.put(runner.getType(), runner);
}
@@ -35,7 +36,7 @@ public final class NodeRunnerFactory {
* @param type 类型
* @return {@link NodeRunner }
*/
public static NodeRunner get(NodeType type) {
public static NodeRunner<? extends NodeConfig> get(NodeType type) {
return NODE_MAP.get(type);
}
@@ -45,7 +46,7 @@ public final class NodeRunnerFactory {
*
* @param runner 跑步者
*/
static void registerCustom(CustomNodeRunner runner) {
static void registerCustom(CustomNodeRunner<? extends NodeConfig> runner) {
Assert.isTrue(!CUSTOM_NODE_MAP.containsKey(runner.getCustomNodeType()), "已存在类型:{}, class:{}的运行器", runner.getCustomNodeType(), runner.getClass());
CUSTOM_NODE_MAP.put(runner.getCustomNodeType(), runner);
}
@@ -56,7 +57,7 @@ public final class NodeRunnerFactory {
* @param type 类型
* @return {@link NodeRunner }
*/
public static NodeRunner getCustom(String type) {
public static NodeRunner<? extends NodeConfig> getCustom(String type) {
return CUSTOM_NODE_MAP.get(type);
}

View File

@@ -1,6 +1,8 @@
package com.metis.runner.factory;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import com.metis.domain.entity.base.NodeConfig;
import com.metis.enums.NodeType;
import com.metis.runner.CustomNodeRunner;
import com.metis.runner.NodeRunner;
@@ -26,11 +28,14 @@ public class RunnerInitialize implements ApplicationContextAware {
Map<String, NodeRunner> runnerMap = applicationContext.getBeansOfType(NodeRunner.class);
runnerMap.forEach((runnerBeanName, runner) -> {
if (ObjectUtil.isNull(runner.getType())) {
return;
}
if (NodeType.CUSTOM.equals(runner.getType())) {
Assert.isTrue(runner instanceof CustomNodeRunner, "自定义节点必须实现CustomNodeRunner接口");
NodeRunnerFactory.registerCustom((CustomNodeRunner) runner);
NodeRunnerFactory.registerCustom((CustomNodeRunner<? extends NodeConfig>) runner);
} else {
NodeRunnerFactory.register(runner);
NodeRunnerFactory.register((NodeRunner<? extends NodeConfig>) runner);
}
});
}

View File

@@ -1,6 +1,7 @@
package com.metis.runner.impl;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson2.JSONObject;
import com.metis.domain.context.RunningContext;
import com.metis.domain.context.RunningResult;
@@ -21,7 +22,15 @@ public class EndNodeRunner implements NodeRunner<EndNodeConfig> {
@Override
public RunningResult run(RunningContext context, Node node, List<Edge> edges) {
JSONObject contextNodeValue = new JSONObject();
contextNodeValue.put("userId", context.getSys().getAppId());
EndNodeConfig config = node.getConfig();
List<EndNodeConfig.Variable> variables = config.getVariables();
if (CollUtil.isEmpty(variables)){
return RunningResult.buildResult();
}
for (EndNodeConfig.Variable variable : variables) {
Object value = context.getValue(variable.getVariableKey());
contextNodeValue.put(variable.getVariable(), value);
}
return RunningResult.buildResult(contextNodeValue);
}

View File

@@ -1,27 +1,107 @@
package com.metis.runner.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.fastjson2.JSONObject;
import com.metis.constant.BaseConstant;
import com.metis.domain.context.RunningContext;
import com.metis.domain.context.RunningResult;
import com.metis.domain.entity.base.Edge;
import com.metis.domain.entity.base.Node;
import com.metis.domain.entity.config.node.LLMNodeConfig;
import com.metis.domain.entity.config.node.llm.LLMNodeConfig;
import com.metis.domain.entity.config.node.llm.PromptTemplate;
import com.metis.enums.NodeType;
import com.metis.llm.domain.LLMChatModeConfig;
import com.metis.llm.domain.Usage;
import com.metis.llm.service.ChatBoot;
import com.metis.llm.service.FlowAiServices;
import com.metis.llm.service.ModelEngineService;
import com.metis.runner.NodeRunner;
import com.metis.template.domain.RenderContext;
import com.metis.template.utils.VelocityUtil;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class LLMNodeRunner implements NodeRunner<LLMNodeConfig> {
private final ModelEngineService modelEngineService;
@Override
public RunningResult run(RunningContext context, Node node, List<Edge> edges) {
return RunningResult.buildResult();
LLMNodeConfig config = node.getConfig();
LLMChatModeConfig model = config.getModel();
ChatModel chatModel = modelEngineService.getChatLanguageModel(model);
List<ChatMessage> chatMessageList = buildChatMessage(context, config);
// todo 需要使用FlowAiServices单独扩展build方法中返回数据格式的操作, 可以用在二期or三期进行扩展开发
ChatBoot chatBoot = FlowAiServices.builder(ChatBoot.class)
.chatModel(chatModel)
.build();
Result<String> chatResult = chatBoot.chat(chatMessageList);
String text = chatResult.content();
log.info("LLM 输出结果: {}", text);
JSONObject result = new JSONObject();
result.put(BaseConstant.TEXT, text);
result.put(BaseConstant.USAGE, Usage.buildTokenUsage(chatResult.tokenUsage()));
result.put(BaseConstant.FINISH_REASON, chatResult.finishReason());
return RunningResult.buildResult(result);
}
@Override
public NodeType getType() {
return NodeType.LLM;
}
private List<ChatMessage> buildChatMessage(RunningContext context,
LLMNodeConfig config) {
// 获取模板
List<PromptTemplate> promptTemplate = config.getPromptTemplate();
// 如果没有模板, 则直接返回
if (CollUtil.isEmpty(promptTemplate)) {
return List.of();
}
// 获取模板参数
Map<String, String> templateMap = promptTemplate.stream().collect(Collectors.toMap(PromptTemplate::getId, PromptTemplate::getText));
// 上下文参数
String contextValue = (String) context.getValue(config.getContext());
RenderContext renderContext = RenderContext.builder()
.context(contextValue)
.sys(context.getSys())
.templateMap(templateMap)
.nodeRunningContext(context.getNodeRunningContext())
.build();
// 渲染结果
Map<String, String> rendered = VelocityUtil.renderBatch(renderContext);
// 构建消息返回
return promptTemplate.stream()
.map(template -> switch (template.getRole()) {
case SYSTEM -> SystemMessage.from(rendered.get(template.getId()));
case USER -> UserMessage.from(rendered.get(template.getId()));
case AI -> AiMessage.from(rendered.get(template.getId()));
default -> null;
}).filter(ObjectUtil::isNotNull)
.toList();
}
}

View File

@@ -21,11 +21,11 @@ public class QuestionClassifierRunner implements NodeRunner<QuestionClassifierCo
@Override
public RunningResult run(RunningContext context, Node node, List<Edge> edges) {
Set<Long> nextNodeIds = getNextNodeIds(edges);
Set<String> nextNodeIds = getNextNodeIds(edges);
// 生成随机索引
Random random = new Random();
int randomIndex = random.nextInt(nextNodeIds.size());
List<Long> nodeIds = new ArrayList<>(nextNodeIds);
List<String> nodeIds = new ArrayList<>(nextNodeIds);
return RunningResult.buildResult(Set.of(nodeIds.get(randomIndex)));
}

View File

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import com.alibaba.fastjson2.JSONObject;
import com.metis.domain.context.RunningContext;
import com.metis.domain.context.RunningResult;
import com.metis.domain.context.SysContext;
import com.metis.domain.entity.base.Edge;
import com.metis.domain.entity.base.Node;
import com.metis.domain.entity.base.NodeVariable;
@@ -37,6 +38,10 @@ public class StartNodeRunner implements NodeRunner<StartNodeConfig> {
// 获取用户自定义参数
JSONObject custom = context.getCustom();
JSONObject contextNodeValue = new JSONObject();
// 获取到系统上下文, 并将系统上下文放入到start的运行结果中, 用于后续调用
// JSONObject sysContext = getSysContext(context);
// contextNodeValue.putAll(sysContext);
for (NodeVariable variable : variables) {
Object value = variable.getValue(custom);
contextNodeValue.put(variable.getVariable(), value);
@@ -44,6 +49,27 @@ public class StartNodeRunner implements NodeRunner<StartNodeConfig> {
return RunningResult.buildResult(contextNodeValue);
}
/**
* 获取系统上下文
*
* @param context 上下文
* @return {@link JSONObject }
*/
private JSONObject getSysContext(RunningContext context) {
JSONObject sys = new JSONObject();
// 系统参数全部以sys.开头
SysContext sysContext = context.getSys();
sys.put("sys.userId", sysContext.getUserId());
sys.put("sys.appId", sysContext.getAppId());
sys.put("sys.workflowId", sysContext.getWorkflowId());
sys.put("sys.instanceId", sysContext.getInstanceId());
sys.put("sys.conversationId", sysContext.getConversationId());
sys.put("sys.dialogueCount", sysContext.getDialogueCount());
sys.put("sys.files", sysContext.getFiles());
sys.put("sys.query", sysContext.getQuery());
return sys;
}
@Override
public NodeType getType() {

View File

@@ -0,0 +1,14 @@
package com.metis.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.metis.domain.entity.ModelPlatform;
/**
* LLM模型平台服务
*
* @author clay
* @date 2025/04/26
*/
public interface ModelPlatformService extends IService<ModelPlatform> {
}

View File

@@ -0,0 +1,25 @@
package com.metis.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.metis.domain.entity.ModelPlatform;
import com.metis.mapper.ModelPlatformMapper;
import com.metis.service.ModelPlatformService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* LLM模型平台服务实现
*
* @author clay
* @date 2025/04/26
*/
@Slf4j
@Service
public class ModelPlatformServiceImpl extends ServiceImpl<ModelPlatformMapper, ModelPlatform>
implements ModelPlatformService {
}

View File

@@ -0,0 +1,52 @@
package com.metis.template.domain;
import com.alibaba.fastjson2.JSONObject;
import com.metis.domain.context.SysContext;
import lombok.Builder;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
@Builder
public class RenderContext {
/**
* 模板
*/
private String template;
/**
* 模板映射
*/
private Map<String, String> templateMap;
/**
* 上下文
*/
private String context;
/**
* 系统数据
*/
private SysContext sys;
/**
* 节点运行上下文, 需要数据进行传递
*/
private Map<String, JSONObject> nodeRunningContext;
public Map<String, Object> getContext() {
Map<String, Object> context = new HashMap<>();
context.put("context", this.context);
context.put("sys", this.sys);
for (Map.Entry<String, JSONObject> entry : nodeRunningContext.entrySet()) {
context.put(String.valueOf(entry.getKey()), entry.getValue());
}
return context;
}
}

View File

@@ -0,0 +1 @@
package com.metis.template;

View File

@@ -0,0 +1,66 @@
package com.metis.template.utils;
import com.metis.template.domain.RenderContext;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.StringResourceLoader;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Map;
public class VelocityUtil {
// 初始化Velocity引擎
private static final VelocityEngine engine = new VelocityEngine();
static {
engine.setProperty(RuntimeConstants.RESOURCE_LOADER, "string");
engine.setProperty("string.resource.loader.class",
StringResourceLoader.class.getName());
engine.init();
}
public static String parseContent(String templateContent, Map<String, ?> context) {
VelocityContext velocityContext = new VelocityContext(context);
// 执行模板渲染
StringWriter writer = new StringWriter();
engine.evaluate(velocityContext, writer, "StringTemplate", templateContent);
return writer.toString();
}
/**
* 渲染
*
* @param renderContext 渲染上下文
* @return {@link String }
*/
public static String render(RenderContext renderContext) {
// 获取上下文
Map<String, Object> context = renderContext.getContext();
// 渲染
return VelocityUtil.parseContent(renderContext.getTemplate(), context);
}
/**
* 渲染
*
* @param renderContext 渲染上下文
* @return {@link Map }<{@link String:标识 }, {@link String:渲染结果 }>
*/
public static Map<String, String> renderBatch(RenderContext renderContext) {
Map<String, String> result = new HashMap<>();
for (Map.Entry<String, String> entry : renderContext.getTemplateMap().entrySet()) {
String key = entry.getKey();
String template = entry.getValue();
String render = VelocityUtil.parseContent(template, renderContext.getContext());
result.put(key, render);
}
return result;
}
}

View File

@@ -137,10 +137,10 @@ public class ValidatorServiceImpl implements ValidatorService {
* @param edges 边缘
*/
private void validateEdgeConnections(List<Node> nodes, List<Edge> edges) {
Map<Long, Node> nodeMap = nodes.stream().collect(Collectors.toMap(Node::getId, Function.identity()));
Map<String, Node> nodeMap = nodes.stream().collect(Collectors.toMap(Node::getId, Function.identity()));
for (Edge edge : edges) {
Long source = edge.getSource();
Long target = edge.getTarget();
String source = edge.getSource();
String target = edge.getTarget();
Assert.isTrue(nodeMap.containsKey(source), "边 {} 的源节点 {} 不存在", edge.getLabel(), source);
Assert.isTrue(nodeMap.containsKey(target), "边 {} 的目标节点 {} 不存在", edge.getLabel(), target);
}
@@ -153,7 +153,7 @@ public class ValidatorServiceImpl implements ValidatorService {
* @param edges 边缘
*/
private void validateCycle(List<Node> nodes, List<Edge> edges) {
Map<Long, List<Long>> adjacencyList = buildAdjacencyList(edges);
Map<String, List<String>> adjacencyList = buildAdjacencyList(edges);
for (Node node : nodes) {
if (hasCycle(node.getId(), adjacencyList, new HashSet<>())) {
throw new IllegalArgumentException("图中存在环结构,起始节点: " + node.getData().getLabel());
@@ -168,7 +168,7 @@ public class ValidatorServiceImpl implements ValidatorService {
* @param edges 边缘
*/
private void validateIsolatedNodes(List<Node> nodes, List<Edge> edges) {
Set<Long> connectedNodes = new HashSet<>();
Set<String> connectedNodes = new HashSet<>();
for (Edge edge : edges) {
connectedNodes.add(edge.getSource());
connectedNodes.add(edge.getTarget());
@@ -184,8 +184,8 @@ public class ValidatorServiceImpl implements ValidatorService {
* @param edges 边缘
* @return {@link Map }<{@link Long }, {@link List }<{@link Long }>>
*/
private Map<Long, List<Long>> buildAdjacencyList(List<Edge> edges) {
Map<Long, List<Long>> adjacencyList = new HashMap<>();
private Map<String, List<String>> buildAdjacencyList(List<Edge> edges) {
Map<String, List<String>> adjacencyList = new HashMap<>();
for (Edge edge : edges) {
adjacencyList.computeIfAbsent(edge.getSource(), k -> new ArrayList<>()).add(edge.getTarget());
}
@@ -200,12 +200,12 @@ public class ValidatorServiceImpl implements ValidatorService {
* @param visited 是否已经访问过了
* @return boolean
*/
private boolean hasCycle(Long nodeId, Map<Long, List<Long>> adjacencyList, Set<Long> visited) {
private boolean hasCycle(String nodeId, Map<String, List<String>> adjacencyList, Set<String> visited) {
if (visited.contains(nodeId)) {
return true; // 发现环
}
visited.add(nodeId);
for (Long neighbor : adjacencyList.getOrDefault(nodeId, new ArrayList<>())) {
for (String neighbor : adjacencyList.getOrDefault(nodeId, new ArrayList<>())) {
if (hasCycle(neighbor, adjacencyList, visited)) {
return true;
}
@@ -236,8 +236,8 @@ public class ValidatorServiceImpl implements ValidatorService {
*/
private void validateNodeRelations(List<Node> nodes, List<Edge> edges) {
// 构建 source 和 target 的映射
Map<Long, List<Edge>> sourceMap = edges.stream().collect(Collectors.groupingBy(Edge::getSource));
Map<Long, List<Edge>> targetMap = edges.stream().collect(Collectors.groupingBy(Edge::getTarget));
Map<String, List<Edge>> sourceMap = edges.stream().collect(Collectors.groupingBy(Edge::getSource));
Map<String, List<Edge>> targetMap = edges.stream().collect(Collectors.groupingBy(Edge::getTarget));
for (Node node : nodes) {
NodeValidator validator = getNodeValidator(node);

View File

@@ -2,7 +2,7 @@ package com.metis.validator.impl.node;
import com.metis.domain.entity.base.Edge;
import com.metis.domain.entity.base.Node;
import com.metis.domain.entity.config.node.LLMNodeConfig;
import com.metis.domain.entity.config.node.llm.LLMNodeConfig;
import com.metis.enums.NodeType;
import com.metis.validator.NodeValidator;
import com.metis.validator.ValidatorResult;

View File

@@ -36,7 +36,7 @@ public class StartNodeValidator implements NodeValidator<StartNodeConfig> {
}
for (NodeVariable variable : variables) {
switch (variable.getVariableType()) {
switch (variable.getType()) {
// 文本类型校验
case TEXT_INPUT, PARAGRAPH -> textVariableValidator(variable);
// 下拉框变量校验

View File

@@ -0,0 +1,37 @@
package aa;
import dev.langchain4j.http.client.*;
import java.util.HashMap;
import java.util.Map;
import static java.time.Duration.ofSeconds;
public class ModelClient {
public static void main(String[] args) {
String baseUrl = "https://api.siliconflow.cn";
HttpClientBuilder httpClientBuilder = HttpClientBuilderLoader.loadHttpClientBuilder();
HttpClient httpClient = httpClientBuilder
.connectTimeout(ofSeconds(15))
.readTimeout(ofSeconds(60))
.build();
Map<String, String> defaultHeaders = new HashMap<>();
defaultHeaders.put("Authorization", "Bearer sk-nnaoladfdjcybfelzkyhmihhnbbazycemiosghvhxfqujfjl");
HttpRequest httpRequest = HttpRequest.builder()
.method(HttpMethod.GET)
.url(baseUrl, "/v1/models/'Qwen/Qwen2.5-Coder-7B-Instruct'")
// .url(baseUrl, "/v1/models/Qwen2.5-Coder-32B-Instruct")
.addHeader("Content-Type", "application/json")
.addHeaders(defaultHeaders)
.build();
SuccessfulHttpResponse httpResponse = httpClient.execute(httpRequest);
String body = httpResponse.body();
System.out.println(body);
}
}

View File

@@ -0,0 +1,253 @@
{
"name": "llm运行测试",
"description": "llm运行测试",
"graph": {
"nodes": [
{
"id": "node_5",
"type": "start",
"initialized": false,
"position": {
"x": -221.9030111576117,
"y": 59.99186709389075
},
"data": {
"label": "开始",
"icon": "SuitcaseLine",
"toolbarPosition": "right",
"config": {
// 输入变量
"variables": [
{
// 输入变量字段名称
"variable": "query",
"label": "查询条件",
// 类型 TEXT_INPUT(1, "text-input", "文本"),
// PARAGRAPH(2, "paragraph", "段落"),
// SELECT(3, "select", "下拉框"),
// NUMBER(4, "number", "数字"),
// FILE(5, "file", "文件"),
// FILE_LIST(6, "file-list", "文件列表")
"type": "text-input",
// 最大长度
"maxLength": 60,
// 是否必填
"required": true,
"options": {
"label": "描述",
// 描述为空的时候, label显示值
"value": "值"
// 值不能为空
},
"allowedFileUploadMethods": [
"localFile",
"remoteUrl"
],
// 允许上传方式
"allowedFileTypes": [
""
],
// 允许文件类型 自定义
"allowedFileExtensions": [
""
]
// 允许文件扩展名 可以自定义
},
{
"variable": "background",
"label": "背景",
"type": "text-input",
"maxLength": 60,
"required": true
}
]
},
"handles": [
{
"id": "51",
"type": "source",
"position": "right",
"connectable": true
}
]
},
"customType": null,
"width": 200,
"height": 40
},
{
"id": "node_700",
"type": "llm",
"initialized": false,
"position": {
"x": 6.81248018754539,
"y": 68.80431452712736
},
"data": {
"label": "llm",
"icon": "",
"toolbarPosition": "right",
"config": {
// 上下文字段 node_5 为节点id, background为阶段运行之后的内容
"context": "node_5.background",
"retryConfig": {
"enable": true,
"maxRetries": 3,
"retryInterval": 1000
},
// 提示模板
"promptTemplate": [
{
// 角色
// SYSTEM("system", "系统角色"),
// USER("user", "用户角色"),
// AI("ai", "ai返回"),
// TOOL_EXECUTION_RESULT("toolExecutionResult", "工具(函数调用)返回");
"role": "system",
"text": "你的背景是${context}",
"id": "1"
},
{
"role": "user",
"text": "请你解释一下上述问题${node_5.query}",
"id": "2"
}
],
// 模型参数
"model": {
// 模型id, 需要在模型管理中创建, 调用接口返回
"modelId": 1,
// 模型名称, 接口中获取
"modelName": "Qwen/Qwen2.5-Coder-32B-Instruct",
// 模型参数. 与dify保持一致, 默认值也使用dify
"completionParams": {
"temperature": 0.7,
"topP": 0.9,
"topK": 1.1,
"maxTokens": 1024,
"seed": 1234,
"presencePenalty": 0,
"responseFormat": "text"
}
}
},
"handles": [
{
"id": "57",
"type": "source",
"position": "left",
"connectable": true
},
{
"id": "35",
"type": "source",
"position": "right",
"connectable": true
}
]
},
"customType": null,
"width": 200,
"height": 40
},
{
"id": "node_802",
"type": "end",
"initialized": false,
"position": {
"x": 221.7192701843481,
"y": 67.57111673995398
},
"data": {
"label": "结束",
"icon": "",
"toolbarPosition": "right",
"config": {
// 结束节点的返回变量
"variables": [
{
// 返回变量字段名称
"variable": "query",
// 返回变量字段值, 动态模板获取
"variableKey": "node_5.query"
},
{
"variable": "background",
"variableKey": "node_5.background"
},
{
"variable": "usage",
"variableKey": "node_700.usage"
},
{
"variable": "finishReason",
"variableKey": "node_700.finishReason"
},
{
"variable": "text",
"variableKey": "node_700.text"
}
]
},
"handles": [
{
"id": "13",
"type": "target",
"position": "left",
"connectable": true
}
]
},
"customType": null,
"width": 200,
"height": 40
}
],
"edges": [
{
"id": "vueflow__edge-551-70057",
"type": "default",
"source": "node_5",
"target": "node_700",
"sourceHandle": "51",
"targetHandle": "57",
"data": {},
"label": "",
"animated": true,
"markerStart": "none",
"markerEnd": "none",
"sourceX": -15.903013850339164,
"sourceY": 79.9918688890424,
"targetX": 4.312479626560497,
"targetY": 88.80432081015815
},
{
"id": "802",
"type": "default",
"source": "node_700",
"target": "node_802",
"sourceHandle": "35",
"targetHandle": "13",
"data": {},
"label": "",
"animated": true,
"markerStart": "none",
"markerEnd": "none",
"sourceX": 212.81248478762151,
"sourceY": 88.80432081015815,
"targetX": 219.2192701843481,
"targetY": 87.5711275108639
}
],
"position": [
987.9498689166815,
102.12938290987324
],
"zoom": 3.5988075528449266,
"viewport": {
"x": 987.9498689166815,
"y": 102.12938290987324,
"zoom": 3.5988075528449266
}
}
}

40
pom.xml
View File

@@ -1,5 +1,6 @@
<?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">
<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>
<groupId>com.metis</groupId>
@@ -19,7 +20,11 @@
<lombok.version>1.18.34</lombok.version>
<org.mapstruct.version>1.6.2</org.mapstruct.version>
<sanitizer.version>1.2.3</sanitizer.version>
<velocity.version>1.7</velocity.version>
<langchain4j.version>1.0.0-beta2</langchain4j.version>
<open.api.version>1.0.0-rc1</open.api.version>
<ollama.version>1.0.0-beta4</ollama.version>
<mcp.version>1.0.0-beta4</mcp.version>
<mybatis-plus.version>3.5.8</mybatis-plus.version>
<versions-maven-plugin.version>2.18.0</versions-maven-plugin.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
@@ -45,16 +50,6 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>1.0.0-beta2</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
@@ -78,7 +73,7 @@
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.24</version>
<version>5.8.37</version>
</dependency>
<!-- 类转换 -->
<dependency>
@@ -101,6 +96,27 @@
<artifactId>swagger-annotations-jakarta</artifactId>
<version>2.2.15</version>
</dependency>
<!--velocity模板渲染-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>${velocity.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${open.api.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>${ollama.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>${mcp.version}</version>
</dependency>
</dependencies>
</dependencyManagement>