diff --git a/metis-starter/pom.xml b/metis-starter/pom.xml index 6f9d18a..14a8e03 100644 --- a/metis-starter/pom.xml +++ b/metis-starter/pom.xml @@ -31,14 +31,6 @@ org.springframework.boot spring-boot-starter-validation - - dev.langchain4j - langchain4j-open-ai - - - dev.langchain4j - langchain4j-mcp - com.alibaba.fastjson2 fastjson2 @@ -77,6 +69,24 @@ io.swagger.core.v3 swagger-annotations-jakarta + + + org.apache.velocity + velocity + + + + dev.langchain4j + langchain4j-open-ai + + + dev.langchain4j + langchain4j-ollama + + + dev.langchain4j + langchain4j-mcp + diff --git a/metis-starter/src/main/java/com/metis/constant/BaseConstant.java b/metis-starter/src/main/java/com/metis/constant/BaseConstant.java index 9eb934b..a6781f3 100644 --- a/metis-starter/src/main/java/com/metis/constant/BaseConstant.java +++ b/metis-starter/src/main/java/com/metis/constant/BaseConstant.java @@ -3,4 +3,6 @@ package com.metis.constant; public interface BaseConstant { Integer DEFAULT_VERSION = 1; + + String TEXT = "text"; } diff --git a/metis-starter/src/main/java/com/metis/convert/ModelPlatformConvert.java b/metis-starter/src/main/java/com/metis/convert/ModelPlatformConvert.java index e2f1480..43c2393 100644 --- a/metis-starter/src/main/java/com/metis/convert/ModelPlatformConvert.java +++ b/metis-starter/src/main/java/com/metis/convert/ModelPlatformConvert.java @@ -2,6 +2,7 @@ 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; @@ -13,4 +14,6 @@ public interface ModelPlatformConvert { ModelPlatformInfo toInfo(ModelPlatform modelPlatform); + Model toModel(ModelPlatform modelPlatform); + } diff --git a/metis-starter/src/main/java/com/metis/domain/context/RunningContext.java b/metis-starter/src/main/java/com/metis/domain/context/RunningContext.java index 2b61a3a..09ecef0 100644 --- a/metis-starter/src/main/java/com/metis/domain/context/RunningContext.java +++ b/metis-starter/src/main/java/com/metis/domain/context/RunningContext.java @@ -1,16 +1,22 @@ 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 { @@ -59,6 +65,39 @@ public class RunningContext { } + /** + * 获得值 + * + * @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])) { + Long 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(this); + return parser.parseExpression(key).getValue(context); + } + + /** * 获得价值 不满足条件, 则返回null, 需要业务自行判断 * diff --git a/metis-starter/src/main/java/com/metis/domain/entity/ModelPlatform.java b/metis-starter/src/main/java/com/metis/domain/entity/ModelPlatform.java index 9a86841..f5288ef 100644 --- a/metis-starter/src/main/java/com/metis/domain/entity/ModelPlatform.java +++ b/metis-starter/src/main/java/com/metis/domain/entity/ModelPlatform.java @@ -51,6 +51,11 @@ public class ModelPlatform extends SimpleBaseEntity { */ private String apiKey; + /** + * 配置 + */ + private String configJson; + /** * 状态 */ diff --git a/metis-starter/src/main/java/com/metis/domain/entity/base/Model.java b/metis-starter/src/main/java/com/metis/domain/entity/base/Model.java index ee0f8cd..7ecfd74 100644 --- a/metis-starter/src/main/java/com/metis/domain/entity/base/Model.java +++ b/metis-starter/src/main/java/com/metis/domain/entity/base/Model.java @@ -1,12 +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 getConfig() { + if (ObjectUtil.isNull(config)) { + return null; + } + return (T) config.to(configClass); + } + + /** + * 设置配置类 + * + * @param configClass 配置类 + */ + public void setConfigClass(Class configClass) { + if (ObjectUtil.isNotNull(this.configClass)) { + return; + } + this.configClass = configClass; + } + } diff --git a/metis-starter/src/main/java/com/metis/domain/entity/base/Node.java b/metis-starter/src/main/java/com/metis/domain/entity/base/Node.java index c5d976b..3148a0f 100644 --- a/metis-starter/src/main/java/com/metis/domain/entity/base/Node.java +++ b/metis-starter/src/main/java/com/metis/domain/entity/base/Node.java @@ -40,13 +40,11 @@ public class Node { /** * 宽度 */ -// @NotNull(message = "节点宽度不能为空") private Integer width; /** * 高度 */ -// @NotNull(message = "节点高度不能为空") private Integer height; /** diff --git a/metis-starter/src/main/java/com/metis/domain/entity/config/node/LLMNodeConfig.java b/metis-starter/src/main/java/com/metis/domain/entity/config/node/LLMNodeConfig.java deleted file mode 100644 index 6a2df06..0000000 --- a/metis-starter/src/main/java/com/metis/domain/entity/config/node/LLMNodeConfig.java +++ /dev/null @@ -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 { -} diff --git a/metis-starter/src/main/java/com/metis/domain/entity/config/node/common/RetryConfig.java b/metis-starter/src/main/java/com/metis/domain/entity/config/node/common/RetryConfig.java new file mode 100644 index 0000000..510289f --- /dev/null +++ b/metis-starter/src/main/java/com/metis/domain/entity/config/node/common/RetryConfig.java @@ -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; + + +} diff --git a/metis-starter/src/main/java/com/metis/domain/entity/config/node/llm/LLMNodeConfig.java b/metis-starter/src/main/java/com/metis/domain/entity/config/node/llm/LLMNodeConfig.java new file mode 100644 index 0000000..bf9e87b --- /dev/null +++ b/metis-starter/src/main/java/com/metis/domain/entity/config/node/llm/LLMNodeConfig.java @@ -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; + + /** + * 模型 + */ + private LLMChatModeConfig model; +} diff --git a/metis-starter/src/main/java/com/metis/domain/entity/config/node/llm/PromptTemplate.java b/metis-starter/src/main/java/com/metis/domain/entity/config/node/llm/PromptTemplate.java new file mode 100644 index 0000000..b3cc0cf --- /dev/null +++ b/metis-starter/src/main/java/com/metis/domain/entity/config/node/llm/PromptTemplate.java @@ -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; + +} diff --git a/metis-starter/src/main/java/com/metis/enums/ChatRoleType.java b/metis-starter/src/main/java/com/metis/enums/ChatRoleType.java new file mode 100644 index 0000000..d78382c --- /dev/null +++ b/metis-starter/src/main/java/com/metis/enums/ChatRoleType.java @@ -0,0 +1,13 @@ +package com.metis.enums; + +import lombok.Getter; + +@Getter +public enum ChatRoleType { + SYSTEM, + USER, + AI, + TOOL_EXECUTION_RESULT; + + +} diff --git a/metis-starter/src/main/java/com/metis/llm/CustomModelEngine.java b/metis-starter/src/main/java/com/metis/llm/CustomModelEngine.java new file mode 100644 index 0000000..173bcd4 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/CustomModelEngine.java @@ -0,0 +1,16 @@ +package com.metis.llm; + +import com.metis.enums.ModelTypeEnum; +import com.metis.llm.domain.config.BaseModelConfig; + +public interface CustomModelEngine extends ModelEngine { + + + String getCustomType(); + + + default ModelTypeEnum getType() { + return ModelTypeEnum.CUSTOM; + } + +} diff --git a/metis-starter/src/main/java/com/metis/llm/ModelEngine.java b/metis-starter/src/main/java/com/metis/llm/ModelEngine.java new file mode 100644 index 0000000..6fafd26 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/ModelEngine.java @@ -0,0 +1,41 @@ +package com.metis.llm; + + +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 { + + + /** + * 获取模型类型 + * + * @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); + + +} diff --git a/metis-starter/src/main/java/com/metis/llm/ModelEngineService.java b/metis-starter/src/main/java/com/metis/llm/ModelEngineService.java new file mode 100644 index 0000000..2463934 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/ModelEngineService.java @@ -0,0 +1,27 @@ +package com.metis.llm; + +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); + + +} diff --git a/metis-starter/src/main/java/com/metis/llm/ModelService.java b/metis-starter/src/main/java/com/metis/llm/ModelService.java index 879d943..0afb5c6 100644 --- a/metis-starter/src/main/java/com/metis/llm/ModelService.java +++ b/metis-starter/src/main/java/com/metis/llm/ModelService.java @@ -13,7 +13,7 @@ public interface ModelService { * @param modelId llm id * @return {@link List }<{@link Model }> */ - List listModel(Long modelId); + Model getByModelId(Long modelId); } diff --git a/metis-starter/src/main/java/com/metis/llm/PlatformModeList.java b/metis-starter/src/main/java/com/metis/llm/PlatformModeList.java deleted file mode 100644 index e7dbf54..0000000 --- a/metis-starter/src/main/java/com/metis/llm/PlatformModeList.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.metis.llm; - -import com.metis.domain.entity.ModelPlatform; -import com.metis.domain.entity.base.Model; -import com.metis.enums.ModelTypeEnum; - -import java.util.List; - -public interface PlatformModeList { - - - List modelList(ModelPlatform modelPlatform); - - - /** - * 得到类型 - * - * @return {@link ModelTypeEnum } - */ - ModelTypeEnum getType(); - - -} diff --git a/metis-starter/src/main/java/com/metis/llm/domain/CompletionParams.java b/metis-starter/src/main/java/com/metis/llm/domain/CompletionParams.java new file mode 100644 index 0000000..6b27282 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/domain/CompletionParams.java @@ -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; +} diff --git a/metis-starter/src/main/java/com/metis/llm/domain/LLMChatModeConfig.java b/metis-starter/src/main/java/com/metis/llm/domain/LLMChatModeConfig.java new file mode 100644 index 0000000..a758e07 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/domain/LLMChatModeConfig.java @@ -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; + + +} diff --git a/metis-starter/src/main/java/com/metis/llm/domain/LLMEmbeddingModelConfig.java b/metis-starter/src/main/java/com/metis/llm/domain/LLMEmbeddingModelConfig.java new file mode 100644 index 0000000..d03b558 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/domain/LLMEmbeddingModelConfig.java @@ -0,0 +1,17 @@ +package com.metis.llm.domain; + +import lombok.Data; + +@Data +public class LLMEmbeddingModelConfig { + + /** + * 模型id + */ + private Long modelId; + + /** + * 模型名称 + */ + private String modelName; +} diff --git a/metis-starter/src/main/java/com/metis/llm/domain/config/BaseModelConfig.java b/metis-starter/src/main/java/com/metis/llm/domain/config/BaseModelConfig.java new file mode 100644 index 0000000..9c41cb1 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/domain/config/BaseModelConfig.java @@ -0,0 +1,4 @@ +package com.metis.llm.domain.config; + +public abstract class BaseModelConfig { +} diff --git a/metis-starter/src/main/java/com/metis/llm/domain/config/OllamaModelConfig.java b/metis-starter/src/main/java/com/metis/llm/domain/config/OllamaModelConfig.java new file mode 100644 index 0000000..46f6d93 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/domain/config/OllamaModelConfig.java @@ -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{ + + +} diff --git a/metis-starter/src/main/java/com/metis/llm/domain/config/OpenApiConfig.java b/metis-starter/src/main/java/com/metis/llm/domain/config/OpenApiConfig.java new file mode 100644 index 0000000..28b82ab --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/domain/config/OpenApiConfig.java @@ -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 { +} diff --git a/metis-starter/src/main/java/com/metis/llm/engine/OllamaModelEngine.java b/metis-starter/src/main/java/com/metis/llm/engine/OllamaModelEngine.java new file mode 100644 index 0000000..fe2c36e --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/engine/OllamaModelEngine.java @@ -0,0 +1,9 @@ +package com.metis.llm.engine; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class OllamaModelEngine { +} diff --git a/metis-starter/src/main/java/com/metis/llm/engine/OpenApiModelEngine.java b/metis-starter/src/main/java/com/metis/llm/engine/OpenApiModelEngine.java new file mode 100644 index 0000000..ad2b494 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/engine/OpenApiModelEngine.java @@ -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.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 { + + @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(modelConfig.getModelName()) + .modelName(modelConfig.getModelName()) + .build(); + } + +} diff --git a/metis-starter/src/main/java/com/metis/llm/factory/ModelEngineFactory.java b/metis-starter/src/main/java/com/metis/llm/factory/ModelEngineFactory.java new file mode 100644 index 0000000..e97dc26 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/factory/ModelEngineFactory.java @@ -0,0 +1,105 @@ +package com.metis.llm.factory; + +import cn.hutool.core.lang.Assert; +import com.metis.enums.ModelTypeEnum; +import com.metis.llm.CustomModelEngine; +import com.metis.llm.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> MODEL_MAP = new ConcurrentHashMap<>(); + + /** + * 自定义模型图 + */ + private static final Map> CUSTOM_MODEL_MAP = new ConcurrentHashMap<>(); + + /** + * 型号表 + * + * @return {@link Set }<{@link ModelTypeEnum }> + */ + public static Set modelTypeList() { + return MODEL_MAP.keySet(); + } + + /** + * 自定义模型类型列表 + * + * @return {@link Set }<{@link String }> + */ + public static Set customModelTypeList() { + return CUSTOM_MODEL_MAP.keySet(); + } + + /** + * 发动机型号清单 + * + * @return {@link List }<{@link ModelEngine }<{@link ? } {@link extends } {@link BaseModelConfig }>> + */ + public static List> modelEngineList() { + return new ArrayList<>(MODEL_MAP.values()); + } + + /** + * 自定义型号引擎列表 + * + * @return {@link List }<{@link ModelEngine }<{@link ? } {@link extends } {@link BaseModelConfig }>> + */ + public static List> customModelEngineList() { + return new ArrayList<>(CUSTOM_MODEL_MAP.values()); + } + + /** + * 注册 + * + * @param modelEngine 模型引擎 + */ + static void register(ModelEngine modelEngine) { + MODEL_MAP.put(modelEngine.getType(), modelEngine); + } + + + /** + * 得到 + * + * @param type 类型 + * @return {@link ModelEngine }<{@link ? } {@link extends } {@link BaseModelConfig }> + */ + public static ModelEngine get(ModelTypeEnum type) { + return MODEL_MAP.get(type); + } + + /** + * 注册自定义 + * + * @param modelEngine 模型引擎 + */ + static void registerCustom(CustomModelEngine 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 getCustom(String type) { + return CUSTOM_MODEL_MAP.get(type); + } + + +} diff --git a/metis-starter/src/main/java/com/metis/llm/factory/ModelEngineInitiate.java b/metis-starter/src/main/java/com/metis/llm/factory/ModelEngineInitiate.java new file mode 100644 index 0000000..47b44ba --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/factory/ModelEngineInitiate.java @@ -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.CustomModelEngine; +import com.metis.llm.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 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, "自定义模型必须实现CustomModelEngine接口"); + ModelEngineFactory.registerCustom((CustomModelEngine) model); + } + ModelEngineFactory.register((ModelEngine) model); + }); + } +} diff --git a/metis-starter/src/main/java/com/metis/llm/impl/ModelEngineServiceImpl.java b/metis-starter/src/main/java/com/metis/llm/impl/ModelEngineServiceImpl.java new file mode 100644 index 0000000..7bf0815 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/llm/impl/ModelEngineServiceImpl.java @@ -0,0 +1,81 @@ +package com.metis.llm.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.ModelEngine; +import com.metis.llm.ModelEngineService; +import com.metis.llm.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); + } + } + + +} diff --git a/metis-starter/src/main/java/com/metis/llm/impl/ModelServiceImpl.java b/metis-starter/src/main/java/com/metis/llm/impl/ModelServiceImpl.java index 9224ace..56f0591 100644 --- a/metis-starter/src/main/java/com/metis/llm/impl/ModelServiceImpl.java +++ b/metis-starter/src/main/java/com/metis/llm/impl/ModelServiceImpl.java @@ -2,6 +2,7 @@ package com.metis.llm.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.ModelService; @@ -10,8 +11,6 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; -import java.util.List; - @Slf4j @Service @RequiredArgsConstructor @@ -21,11 +20,10 @@ public class ModelServiceImpl implements ModelService { @Override - public List listModel(Long modelId) { + public Model getByModelId(Long modelId) { Assert.isTrue(ObjectUtil.isNotNull(modelId), "模型平台ID不能为空"); ModelPlatform modelPlatform = modelPlatformService.getById(modelId); Assert.isTrue(ObjectUtil.isNotNull(modelPlatform), "模型平台不存在"); - - return List.of(); + return ModelPlatformConvert.INSTANCE.toModel(modelPlatform); } } diff --git a/metis-starter/src/main/java/com/metis/runner/factory/NodeRunnerFactory.java b/metis-starter/src/main/java/com/metis/runner/factory/NodeRunnerFactory.java index a2204fa..41e927a 100644 --- a/metis-starter/src/main/java/com/metis/runner/factory/NodeRunnerFactory.java +++ b/metis-starter/src/main/java/com/metis/runner/factory/NodeRunnerFactory.java @@ -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 NODE_MAP = new ConcurrentHashMap<>(8); + private static final Map> NODE_MAP = new ConcurrentHashMap<>(8); /** * 自定义节点映射 */ - private static final Map CUSTOM_NODE_MAP = new ConcurrentHashMap<>(8); + private static final Map> CUSTOM_NODE_MAP = new ConcurrentHashMap<>(8); /** * 注册 * * @param runner 跑步者 */ - static void register(NodeRunner runner) { + static void register(NodeRunner 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 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 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 getCustom(String type) { return CUSTOM_NODE_MAP.get(type); } diff --git a/metis-starter/src/main/java/com/metis/runner/factory/RunnerInitialize.java b/metis-starter/src/main/java/com/metis/runner/factory/RunnerInitialize.java index b02873a..e0848a3 100644 --- a/metis-starter/src/main/java/com/metis/runner/factory/RunnerInitialize.java +++ b/metis-starter/src/main/java/com/metis/runner/factory/RunnerInitialize.java @@ -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 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) runner); } else { - NodeRunnerFactory.register(runner); + NodeRunnerFactory.register((NodeRunner) runner); } }); } diff --git a/metis-starter/src/main/java/com/metis/runner/impl/LLMNodeRunner.java b/metis-starter/src/main/java/com/metis/runner/impl/LLMNodeRunner.java index 1779a34..f91bd65 100644 --- a/metis-starter/src/main/java/com/metis/runner/impl/LLMNodeRunner.java +++ b/metis-starter/src/main/java/com/metis/runner/impl/LLMNodeRunner.java @@ -1,27 +1,96 @@ 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.ModelEngineService; +import com.metis.llm.domain.LLMChatModeConfig; 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.model.chat.response.ChatResponse; +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 { + + private final ModelEngineService modelEngineService; + + @Override public RunningResult run(RunningContext context, Node node, List edges) { - return RunningResult.buildResult(); + LLMNodeConfig config = node.getConfig(); + LLMChatModeConfig model = config.getModel(); + + ChatModel chatModel = modelEngineService.getChatLanguageModel(model); + List chatMessageList = buildChatMessage(context, config); + ChatResponse chat = chatModel.chat(chatMessageList); + String string = chat.toString(); + + log.info("LLM 输出结果: {}", string); + JSONObject result = new JSONObject(); + result.put(BaseConstant.TEXT, string); + return RunningResult.buildResult(result); } @Override public NodeType getType() { return NodeType.LLM; } + + + private List buildChatMessage(RunningContext context, + LLMNodeConfig config) { + // 获取模板 + List promptTemplate = config.getPromptTemplate(); + // 如果没有模板, 则直接返回 + if (CollUtil.isEmpty(promptTemplate)) { + return List.of(); + } + // 获取模板参数 + Map 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 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(); + } + + } diff --git a/metis-starter/src/main/java/com/metis/template/domain/RenderContext.java b/metis-starter/src/main/java/com/metis/template/domain/RenderContext.java new file mode 100644 index 0000000..0ef2378 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/template/domain/RenderContext.java @@ -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 templateMap; + + /** + * 上下文 + */ + private String context; + + /** + * 系统数据 + */ + private SysContext sys; + + /** + * 节点运行上下文, 需要数据进行传递 + */ + private Map nodeRunningContext; + + + public Map getContext() { + Map context = new HashMap<>(); + context.put("context", this.context); + context.put("sys", this.sys); + for (Map.Entry entry : nodeRunningContext.entrySet()) { + context.put(String.valueOf(entry.getKey()), entry.getValue()); + } + return context; + } + + +} diff --git a/metis-starter/src/main/java/com/metis/template/package-info.java b/metis-starter/src/main/java/com/metis/template/package-info.java new file mode 100644 index 0000000..6b4fb33 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/template/package-info.java @@ -0,0 +1 @@ +package com.metis.template; \ No newline at end of file diff --git a/metis-starter/src/main/java/com/metis/template/utils/VelocityUtil.java b/metis-starter/src/main/java/com/metis/template/utils/VelocityUtil.java new file mode 100644 index 0000000..358c374 --- /dev/null +++ b/metis-starter/src/main/java/com/metis/template/utils/VelocityUtil.java @@ -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 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 context = renderContext.getContext(); + // 渲染 + return VelocityUtil.parseContent(renderContext.getTemplate(), context); + } + + + /** + * 渲染 + * + * @param renderContext 渲染上下文 + * @return {@link Map }<{@link String:标识 }, {@link String:渲染结果 }> + */ + public static Map renderBatch(RenderContext renderContext) { + Map result = new HashMap<>(); + for (Map.Entry 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; + } + + +} diff --git a/metis-starter/src/main/java/com/metis/validator/impl/node/LLMNodeValidator.java b/metis-starter/src/main/java/com/metis/validator/impl/node/LLMNodeValidator.java index 2dd04a6..3b4002b 100644 --- a/metis-starter/src/main/java/com/metis/validator/impl/node/LLMNodeValidator.java +++ b/metis-starter/src/main/java/com/metis/validator/impl/node/LLMNodeValidator.java @@ -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; diff --git a/metis-starter/src/test/resources/flow.json b/metis-starter/src/test/resources/flow.json new file mode 100644 index 0000000..526b43a --- /dev/null +++ b/metis-starter/src/test/resources/flow.json @@ -0,0 +1,189 @@ + +{ + "appId": 1919041086810968064, + "name": "llm运行测试", + "description": "llm运行测试", + "graph": { + "nodes": [ + { + "id": "5", + "type": "start", + "initialized": false, + "position": { + "x": -221.9030111576117, + "y": 59.99186709389075 + }, + "data": { + "label": "开始", + "icon": "SuitcaseLine", + "toolbarPosition": "right", + "config": { + "variables": [ + { + "variable": "query", + "label": "查询条件", + "type": "text-input", + "maxLength": 60, + "required": true + }, + { + "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": "700", + "type": "llm", + "initialized": false, + "position": { + "x": 6.81248018754539, + "y": 68.80431452712736 + }, + "data": { + "label": "llm", + "icon": "", + "toolbarPosition": "right", + "config": { + "context": "5.background", + "retryConfig": { + "enable": true, + "maxRetries": 3, + "retryInterval": 1000 + }, + "promptTemplate": [ + { + "role": "system", + "text": "你的背景是${context}", + "id": "1" + }, + { + "role": "user", + "text": "请你解释一下上述问题${5.query}", + "id": "2" + } + ], + "model": { + "modelId": 1, + "modelName": "Qwen/Qwen2.5-Coder-32B-Instruct", + "completionParams": { + "temperature": 0.7, + "topP": 0.9, + "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": "802", + "type": "end", + "initialized": false, + "position": { + "x": 221.7192701843481, + "y": 67.57111673995398 + }, + "data": { + "label": "结束", + "icon": "", + "toolbarPosition": "right", + "config": {}, + "handles": [ + { + "id": "13", + "type": "target", + "position": "left", + "connectable": true + } + ] + }, + "customType": null, + "width": 200, + "height": 40 + } + ], + "edges": [ + { + "id": "vueflow__edge-551-70057", + "type": "default", + "source": "5", + "target": "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": "700", + "target": "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 + } + } + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 12a1df8..0f6c817 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 com.metis @@ -19,7 +20,11 @@ 1.18.34 1.6.2 1.2.3 + 1.7 1.0.0-beta2 + 1.0.0-rc1 + 1.0.0-beta4 + 1.0.0-beta4 3.5.8 2.18.0 3.8.1 @@ -45,16 +50,6 @@ pom import - - dev.langchain4j - langchain4j-open-ai - ${langchain4j.version} - - - dev.langchain4j - langchain4j-mcp - 1.0.0-beta2 - com.alibaba.fastjson2 fastjson2 @@ -78,7 +73,7 @@ cn.hutool hutool-all - 5.8.24 + 5.8.37 @@ -101,6 +96,27 @@ swagger-annotations-jakarta 2.2.15 + + + org.apache.velocity + velocity + ${velocity.version} + + + dev.langchain4j + langchain4j-open-ai + ${open.api.version} + + + dev.langchain4j + langchain4j-ollama + ${ollama.version} + + + dev.langchain4j + langchain4j-mcp + ${mcp.version} +