feat: llm节点进行调试

This commit is contained in:
2025-05-04 22:56:58 +08:00
parent f783658227
commit 7a72358fc7
38 changed files with 1129 additions and 72 deletions

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

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

View File

@@ -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);
}

View File

@@ -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, 需要业务自行判断
*

View File

@@ -51,6 +51,11 @@ public class ModelPlatform extends SimpleBaseEntity {
*/
private String apiKey;
/**
* 配置
*/
private String configJson;
/**
* 状态
*/

View File

@@ -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> 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

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

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

@@ -0,0 +1,13 @@
package com.metis.enums;
import lombok.Getter;
@Getter
public enum ChatRoleType {
SYSTEM,
USER,
AI,
TOOL_EXECUTION_RESULT;
}

View File

@@ -0,0 +1,16 @@
package com.metis.llm;
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,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<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;
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

@@ -13,7 +13,7 @@ public interface ModelService {
* @param modelId llm id
* @return {@link List }<{@link Model }>
*/
List<Model> listModel(Long modelId);
Model getByModelId(Long modelId);
}

View File

@@ -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<Model> modelList(ModelPlatform modelPlatform);
/**
* 得到类型
*
* @return {@link ModelTypeEnum }
*/
ModelTypeEnum getType();
}

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,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,9 @@
package com.metis.llm.engine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class OllamaModelEngine {
}

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.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(modelConfig.getModelName())
.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.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<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.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<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,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);
}
}
}

View File

@@ -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<Model> 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);
}
}

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,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<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);
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<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

@@ -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<Long, 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<Long, 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

@@ -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

@@ -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
}
}
}

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>