diff --git a/ai/viewer-ai-mcp-client/src/main/java/xyz/thoughtset/viewer/ai/mcp/client/entity/McpBotInfo.java b/ai/viewer-ai-mcp-client/src/main/java/xyz/thoughtset/viewer/ai/mcp/client/entity/McpBotInfo.java index 333032f4849aa59e81f7f54e82bb91e74aa12c37..8e9a474413f32f360afa108907c76606c0d857de 100644 --- a/ai/viewer-ai-mcp-client/src/main/java/xyz/thoughtset/viewer/ai/mcp/client/entity/McpBotInfo.java +++ b/ai/viewer-ai-mcp-client/src/main/java/xyz/thoughtset/viewer/ai/mcp/client/entity/McpBotInfo.java @@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; +import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.spec.McpSchema; import lombok.AllArgsConstructor; import lombok.Data; @@ -51,8 +52,7 @@ public class McpBotInfo extends BaseMeta { .sseEndpoint(sseEndpoint) .connectTimeout(Duration.ofSeconds(this.connectTimeout!= null ? this.connectTimeout : 10)) .clientBuilder(HttpClient.newBuilder()) - .objectMapper(objectMapper) -// .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) + .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) ; HttpClientSseClientTransport transport = transportBuilder.build(); McpSchema.Implementation clientInfo = new McpSchema.Implementation( diff --git a/ai/viewer-ai-mcp-server/src/main/java/xyz/thoughtset/viewer/ai/mcp/server/factory/McpServerFactory.java b/ai/viewer-ai-mcp-server/src/main/java/xyz/thoughtset/viewer/ai/mcp/server/factory/McpServerFactory.java index 46d3bcbb0c6aa5dfcd2cf021fdf674a4032c85fe..4bd7af2d908800da9dc1721ffdd7b2f609202839 100644 --- a/ai/viewer-ai-mcp-server/src/main/java/xyz/thoughtset/viewer/ai/mcp/server/factory/McpServerFactory.java +++ b/ai/viewer-ai-mcp-server/src/main/java/xyz/thoughtset/viewer/ai/mcp/server/factory/McpServerFactory.java @@ -1,6 +1,7 @@ package xyz.thoughtset.viewer.ai.mcp.server.factory; import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.json.jackson.JacksonMcpJsonMapper; import io.modelcontextprotocol.server.McpServer; import io.modelcontextprotocol.server.McpSyncServer; import io.modelcontextprotocol.server.transport.WebMvcSseServerTransportProvider; @@ -69,8 +70,7 @@ public class McpServerFactory implements DisposableBean { throw UrlConflictException.build(); } WebMvcSseServerTransportProvider.Builder transportProviderBuilder = WebMvcSseServerTransportProvider.builder() -// .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) - .objectMapper(objectMapper) + .jsonMapper(new JacksonMcpJsonMapper(objectMapper)) .baseUrl(baseUrl) .sseEndpoint(sseEndpoint); if (StringUtils.hasText(messageEndpoint)){ diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/option/ExtraParamChatOptions.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/option/ExtraParamChatOptions.java new file mode 100644 index 0000000000000000000000000000000000000000..c4591d358ffa6dad6b89688699985b7fd795f1de --- /dev/null +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/option/ExtraParamChatOptions.java @@ -0,0 +1,19 @@ +package xyz.thoughtset.viewer.common.ai.model.entity.option; + +import lombok.Data; +import org.springframework.ai.model.ModelOptions; + +@Data +public class ExtraParamChatOptions implements ModelOptions { + private String responseFormatType; + + + public void respByJsonObject(){ + this.responseFormatType = "json_object"; + } + + public boolean responseUsedJsonObject(){ + return "json_object".equals(this.responseFormatType); + } + +} diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ChatModelBuilder.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ChatModelBuilder.java index e729e62263a12e4c7e6ea8a380ad2fe2da1bb6f9..8bfdda8b487f8cdb1ae6948b68cc9a42f0617f1d 100644 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ChatModelBuilder.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ChatModelBuilder.java @@ -3,18 +3,21 @@ package xyz.thoughtset.viewer.common.ai.model.factory; import lombok.Getter; import lombok.NonNull; +import org.springframework.ai.model.ModelOptions; +import xyz.thoughtset.viewer.common.ai.model.entity.purpose.ModelPurposeEnum; + +import java.util.List; @Getter public abstract class ChatModelBuilder extends ModelBuilder{ protected ChatModelBuilder(@NonNull String provider) { - super(provider); + super(provider, ModelPurposeEnum.CHAT); } - protected ChatModelBuilder(@NonNull String provider, String aiModel) { - super(provider, aiModel); + protected ChatModelBuilder(@NonNull String provider,List limitModels) { + super(provider, ModelPurposeEnum.CHAT, limitModels); } - } diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/DefaultModel.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/DefaultModel.java new file mode 100644 index 0000000000000000000000000000000000000000..af206602e2b3041192089d669f55ff54e6fea6cd --- /dev/null +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/DefaultModel.java @@ -0,0 +1,10 @@ +package xyz.thoughtset.viewer.common.ai.model.factory; + + +import java.lang.annotation.*; + + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.ANNOTATION_TYPE}) +public @interface DefaultModel { +} diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/DefaultModelBuilder.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/DefaultModelBuilder.java deleted file mode 100644 index be6ed16bc9876bc3002aa07198d7cc6c5501787b..0000000000000000000000000000000000000000 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/DefaultModelBuilder.java +++ /dev/null @@ -1,29 +0,0 @@ -package xyz.thoughtset.viewer.common.ai.model.factory; - - -import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.Getter; -import lombok.NonNull; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.util.StringUtils; -import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; -import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; - -@Getter -public abstract class DefaultModelBuilder extends ModelBuilder{ - - - protected DefaultModelBuilder(@NonNull String provider) { - super(provider); - } - - protected DefaultModelBuilder(@NonNull String provider, String aiModel) { - super(provider, aiModel); - } - - @Override - public boolean wasDefault() { - return true; - } -} diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ImageModelBuilder.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ImageModelBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..daaed762696e7cd1bd721d91b8910265c39b10e7 --- /dev/null +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ImageModelBuilder.java @@ -0,0 +1,23 @@ +package xyz.thoughtset.viewer.common.ai.model.factory; + + +import lombok.Getter; +import lombok.NonNull; +import xyz.thoughtset.viewer.common.ai.model.entity.purpose.ModelPurposeEnum; + +import java.util.List; + +@Getter +public abstract class ImageModelBuilder extends ModelBuilder{ + + + protected ImageModelBuilder(@NonNull String provider) { + super(provider, ModelPurposeEnum.IMAGE); + } + + protected ImageModelBuilder(@NonNull String provider, List limitModels) { + super(provider, ModelPurposeEnum.IMAGE, limitModels); + } + + +} diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelBuilder.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelBuilder.java index a8e2bac3bf401862402c3f7066672602d177d1fd..9de05a98096a9d1ef67d416e784d906d827dd075 100644 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelBuilder.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelBuilder.java @@ -4,49 +4,54 @@ package xyz.thoughtset.viewer.common.ai.model.factory; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Getter; import lombok.NonNull; -import org.springframework.ai.chat.model.ChatModel; +import lombok.SneakyThrows; import org.springframework.ai.model.Model; +import org.springframework.ai.model.ModelOptions; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; import xyz.thoughtset.viewer.common.ai.model.entity.purpose.ModelPurposeEnum; +import xyz.thoughtset.viewer.common.core.util.MapMergeUtil; import java.util.List; +import java.util.Map; import java.util.Optional; @Getter public abstract class ModelBuilder { protected String provider; - protected List supportModels; -// -// protected ModelPurposeEnum purposeEnum; + protected ModelPurposeEnum purpose; + protected List limitModels; @Autowired protected ObjectMapper objectMapper; - protected ModelBuilder(@NonNull String provider) { - this(provider,""); + protected ModelBuilder(@NonNull String provider,@NonNull ModelPurposeEnum purpose) { + this(provider,purpose, (List) null); } - protected ModelBuilder(@NonNull String provider, String aiModel) { - this(provider, List.of(aiModel)); + protected ModelBuilder(@NonNull String provider,@NonNull ModelPurposeEnum purpose, String aiModel) { + this(provider,purpose, List.of(aiModel)); } - protected ModelBuilder(String provider, List supportModels) { + protected ModelBuilder(String provider,@NonNull ModelPurposeEnum purpose, List limitModels) { this.provider = provider; - this.supportModels = supportModels; + this.purpose = purpose; + this.limitModels = limitModels; ModelsRegistry.registerModel(this); } - public boolean wasDefault(){return false;} public boolean checkExecModel(AiNode node, ModelParam modelParam){ - boolean checkFlag = ObjectUtils.isEmpty(this.supportModels); + boolean modelPurpose = this.purpose.equals(modelParam.getPurpose()); + if (!modelPurpose) return false; + boolean checkFlag = ObjectUtils.isEmpty(this.limitModels); if (!checkFlag){ - for (String model : this.supportModels) { + for (String model : this.limitModels) { //todo: 模型模糊匹配 - if (model.equals(modelParam.getModel())){ + if (model.equalsIgnoreCase(modelParam.getModel())){ checkFlag = true; break; } @@ -55,11 +60,32 @@ public abstract class ModelBuilder { return this.provider.equals(node.getProvider()) && checkFlag; } - public abstract Model buildMode(AiNode node, ModelParam modelParam); + public Model buildMode(AiNode node, ModelParam modelParam){ + return buildMode(node,modelParam,null); + } + public abstract Model buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions); public void setOptionalValue(T value, java.util.function.Consumer setter) { Optional.ofNullable(value).ifPresent(setter); } + public void mergeOptions(ModelOptions source,ModelOptions extraOptions) {} + + @SneakyThrows + protected T loadAndMergeModelOptions(ModelParam modelParam, Class valueType,ModelOptions extraOptions) { + Map argMap = null; + if (StringUtils.hasText(modelParam.getSetting())){ + argMap = objectMapper.readValue(modelParam.getSetting(), Map.class); + } + ModelOptions targetOptions = (ModelOptions) objectMapper.convertValue(MapMergeUtil.merge(argMap,modelParam.getParamMap()), valueType); + if (extraOptions != null){ + if (valueType.equals(extraOptions.getClass())){ + BeanUtils.copyProperties(extraOptions,targetOptions); + }else { + mergeOptions(targetOptions,extraOptions); + } + } + return (T) targetOptions; + } } diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelFactory.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelFactory.java index 966a82e5794319ed4deea49dcd42395b541bf412..a640b5ef3677371a107aad2e00fd114589617bfe 100644 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelFactory.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelFactory.java @@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.model.Model; +import org.springframework.ai.model.ModelOptions; import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -33,15 +34,21 @@ public class ModelFactory { return ToolCallingManager.builder().build(); } - public Model buildModel(ModelParam modelParam){ + return buildModel(modelParam, null); + } + + public Model buildModel(ModelParam modelParam, ModelOptions modelOptions){ AiNode aiNode = aiNodeService.selectDetail(modelParam.getPid()); ModelBuilder builder = ModelsRegistry.loadBuilder(aiNode, modelParam); return builder.buildMode(aiNode, modelParam); } public ChatClient.Builder clientBuilder(@NonNull ModelParam modelParam){ - ChatModel chatModel = (ChatModel) buildModel(modelParam); + return clientBuilder(modelParam,null); + } + public ChatClient.Builder clientBuilder(@NonNull ModelParam modelParam, ModelOptions modelOptions){ + ChatModel chatModel = (ChatModel) buildModel(modelParam,modelOptions); return ChatClient.builder(chatModel); } diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelsRegistry.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelsRegistry.java index ae2b5bcb977fce4038ebb8c75808b2d76be6c73f..3f732f0a2469d874d081e9bbdb6e80d29c03ed1d 100644 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelsRegistry.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelsRegistry.java @@ -3,35 +3,52 @@ package xyz.thoughtset.viewer.common.ai.model.factory; import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMapAdapter; +import org.springframework.util.ObjectUtils; import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; +import xyz.thoughtset.viewer.common.ai.model.entity.purpose.ModelPurposeEnum; +import xyz.thoughtset.viewer.common.crud.core.annotation.ApiCRUDPower; import java.util.*; public class ModelsRegistry { - private static final Map DEFAULT_MODEL_BUILDER_MAP = new HashMap(); - private static final MultiValueMap MODEL_BUILDER_MAP = new MultiValueMapAdapter<>(new HashMap<>()); + private static final Map > MODEL_BUILDER_MAP = new HashMap(); + private static final MultiValueMap DEFAULT_MODEL_BUILDER_MAP = new MultiValueMapAdapter<>(new HashMap<>()); public static void registerModel(ModelBuilder modelBuilder){ String modelProvider = modelBuilder.getProvider(); - if (modelBuilder.wasDefault()){ - DEFAULT_MODEL_BUILDER_MAP.put(modelProvider,modelBuilder); + if (modelBuilder.getClass().getAnnotation(DefaultModel.class) != null){ + DEFAULT_MODEL_BUILDER_MAP.add(modelProvider,modelBuilder); } - MODEL_BUILDER_MAP.add(modelProvider,modelBuilder); + MultiValueMap map = MODEL_BUILDER_MAP.get(modelProvider); + if (ObjectUtils.isEmpty(map)){ + map = new MultiValueMapAdapter<>(new HashMap<>()); + MODEL_BUILDER_MAP.put(modelProvider,map); + } + map.add(modelBuilder.getPurpose(),modelBuilder); } - public static ModelBuilder loadBuilder(AiNode aiNode, ModelParam params){ - List list = MODEL_BUILDER_MAP.get(aiNode.getProvider()); + public static ModelBuilder loadBuilder(AiNode aiNode, ModelParam param){ + ModelPurposeEnum purposeEnum = param.getPurpose(); + List list = MODEL_BUILDER_MAP.get(aiNode.getProvider()).get(purposeEnum); ModelBuilder targetBuilder = null; for (ModelBuilder ele : list){ - if (ele.checkExecModel(aiNode,params)){ + if (ele.checkExecModel(aiNode,param)){ targetBuilder = ele; break; } } if (targetBuilder == null){ - targetBuilder = DEFAULT_MODEL_BUILDER_MAP.get(aiNode.getProvider()); + List defaultList = DEFAULT_MODEL_BUILDER_MAP.get(aiNode.getProvider()); + if (ObjectUtils.isEmpty(defaultList)) return null; +// targetBuilder = defaultList.get(0); + for (ModelBuilder builder:defaultList){ + if (purposeEnum.equals(param.getPurpose())){ + targetBuilder = builder; + break; + } + } } return targetBuilder; } diff --git a/commons/viewer-common-core/src/main/java/xyz/thoughtset/viewer/common/core/util/MapMergeUtil.java b/commons/viewer-common-core/src/main/java/xyz/thoughtset/viewer/common/core/util/MapMergeUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..2765f976643a5a46d71207f6d0fa5572a2f55803 --- /dev/null +++ b/commons/viewer-common-core/src/main/java/xyz/thoughtset/viewer/common/core/util/MapMergeUtil.java @@ -0,0 +1,306 @@ +package xyz.thoughtset.viewer.common.core.util; + +import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; + +import java.util.*; +import java.util.function.BiFunction; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Map合并工具类 + * 提供多种Map合并策略 + * + * @author Claude Code + * @since 1.0.0 + */ +public class MapMergeUtil { + + private MapMergeUtil() { + throw new UnsupportedOperationException("Utility class cannot be instantiated"); + } + + /** + * 简单合并两个Map(后者覆盖前者) + * + * @param map1 第一个Map + * @param map2 第二个Map + * @param 键类型 + * @param 值类型 + * @return 合并后的新Map + */ + public static Map merge(@Nullable Map map1, @Nullable Map map2) { + if (CollectionUtils.isEmpty(map1)) { + return map2 == null ? new HashMap<>() : new HashMap<>(map2); + } + if (CollectionUtils.isEmpty(map2)) { + return new HashMap<>(map1); + } + + Map result = new HashMap<>(map1); + result.putAll(map2); + return result; + } + + /** + * 合并多个Map(后面的覆盖前面的) + * + * @param maps 要合并的Map数组 + * @param 键类型 + * @param 值类型 + * @return 合并后的新Map + */ + @SafeVarargs + public static Map mergeMultiple(Map... maps) { + if (maps == null || maps.length == 0) { + return new HashMap<>(); + } + + return Stream.of(maps) + .filter(Objects::nonNull) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (v1, v2) -> v2, + HashMap::new + )); + } + + /** + * 使用自定义合并函数合并两个Map + * + * @param map1 第一个Map + * @param map2 第二个Map + * @param mergeFunction 值冲突时的合并函数 + * @param 键类型 + * @param 值类型 + * @return 合并后的新Map + */ + public static Map mergeWithFunction( + @Nullable Map map1, + @Nullable Map map2, + BiFunction mergeFunction) { + + if (CollectionUtils.isEmpty(map1)) { + return map2 == null ? new HashMap<>() : new HashMap<>(map2); + } + if (CollectionUtils.isEmpty(map2)) { + return new HashMap<>(map1); + } + + Map result = new HashMap<>(map1); + map2.forEach((key, value) -> + result.merge(key, value, mergeFunction) + ); + return result; + } + + /** + * 保留第一个Map的值(忽略冲突) + * + * @param map1 第一个Map + * @param map2 第二个Map + * @param 键类型 + * @param 值类型 + * @return 合并后的新Map + */ + public static Map mergeKeepFirst(@Nullable Map map1, @Nullable Map map2) { + return mergeWithFunction(map1, map2, (v1, v2) -> v1); + } + + /** + * 合并数值类型的Map(相同key的值相加) + * + * @param map1 第一个Map + * @param map2 第二个Map + * @param 键类型 + * @return 合并后的新Map + */ + public static Map mergeIntegerMaps( + @Nullable Map map1, + @Nullable Map map2) { + + return mergeWithFunction(map1, map2, Integer::sum); + } + + /** + * 合并Long类型的Map(相同key的值相加) + * + * @param map1 第一个Map + * @param map2 第二个Map + * @param 键类型 + * @return 合并后的新Map + */ + public static Map mergeLongMaps( + @Nullable Map map1, + @Nullable Map map2) { + + return mergeWithFunction(map1, map2, Long::sum); + } + + /** + * 合并Double类型的Map(相同key的值相加) + * + * @param map1 第一个Map + * @param map2 第二个Map + * @param 键类型 + * @return 合并后的新Map + */ + public static Map mergeDoubleMaps( + @Nullable Map map1, + @Nullable Map map2) { + + return mergeWithFunction(map1, map2, Double::sum); + } + + /** + * 合并List类型的Map(相同key的List合并) + * + * @param map1 第一个Map + * @param map2 第二个Map + * @param 键类型 + * @param List中元素的类型 + * @return 合并后的新Map + */ + public static Map> mergeListMaps( + @Nullable Map> map1, + @Nullable Map> map2) { + + return mergeWithFunction(map1, map2, (list1, list2) -> { + List merged = new ArrayList<>(list1); + merged.addAll(list2); + return merged; + }); + } + + /** + * 合并Set类型的Map(相同key的Set合并,自动去重) + * + * @param map1 第一个Map + * @param map2 第二个Map + * @param 键类型 + * @param Set中元素的类型 + * @return 合并后的新Map + */ + public static Map> mergeSetMaps( + @Nullable Map> map1, + @Nullable Map> map2) { + + return mergeWithFunction(map1, map2, (set1, set2) -> { + Set merged = new HashSet<>(set1); + merged.addAll(set2); + return merged; + }); + } + + /** + * 深度合并嵌套的Map(递归合并) + * + * @param map1 第一个Map + * @param map2 第二个Map + * @return 合并后的新Map + */ + @SuppressWarnings("unchecked") + public static Map deepMerge( + @Nullable Map map1, + @Nullable Map map2) { + + if (CollectionUtils.isEmpty(map1)) { + return map2 == null ? new HashMap<>() : new HashMap<>(map2); + } + if (CollectionUtils.isEmpty(map2)) { + return new HashMap<>(map1); + } + + Map result = new HashMap<>(map1); + + map2.forEach((key, value) -> { + if (value instanceof Map && result.get(key) instanceof Map) { + // 递归合并嵌套的Map + result.put(key, deepMerge( + (Map) result.get(key), + (Map) value + )); + } else { + result.put(key, value); + } + }); + + return result; + } + + /** + * 合并并返回不可修改的Map + * + * @param map1 第一个Map + * @param map2 第二个Map + * @param 键类型 + * @param 值类型 + * @return 不可修改的合并后Map + */ + public static Map mergeImmutable( + @Nullable Map map1, + @Nullable Map map2) { + + return Collections.unmodifiableMap(merge(map1, map2)); + } + + /** + * 使用Stream API合并多个Map + * + * @param maps Map集合 + * @param 键类型 + * @param 值类型 + * @return 合并后的新Map + */ + public static Map mergeFromCollection(Collection> maps) { + if (CollectionUtils.isEmpty(maps)) { + return new HashMap<>(); + } + + return maps.stream() + .filter(Objects::nonNull) + .flatMap(map -> map.entrySet().stream()) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (v1, v2) -> v2, + HashMap::new + )); + } + + /** + * 条件合并:只合并满足条件的键值对 + * + * @param map1 第一个Map + * @param map2 第二个Map + * @param predicate 键值对过滤条件 + * @param 键类型 + * @param 值类型 + * @return 合并后的新Map + */ + public static Map mergeWithFilter( + @Nullable Map map1, + @Nullable Map map2, + java.util.function.Predicate> predicate) { + + Map result = new HashMap<>(); + + if (!CollectionUtils.isEmpty(map1)) { + map1.entrySet().stream() + .filter(predicate) + .forEach(entry -> result.put(entry.getKey(), entry.getValue())); + } + + if (!CollectionUtils.isEmpty(map2)) { + map2.entrySet().stream() + .filter(predicate) + .forEach(entry -> result.put(entry.getKey(), entry.getValue())); + } + + return result; + } +} + diff --git a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AIChatExecutor.java b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AIChatExecutor.java index 37e7f3bcb5a4b32175e3dce5ce23a422b5a10ef4..cca2d126ef95cd93c0a196a194920865e07e7772 100644 --- a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AIChatExecutor.java +++ b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AIChatExecutor.java @@ -17,6 +17,7 @@ import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.StringUtils; import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; +import xyz.thoughtset.viewer.common.ai.model.entity.option.ExtraParamChatOptions; import xyz.thoughtset.viewer.common.exc.exceptions.ExecException; import xyz.thoughtset.viewer.modules.step.entity.AISupportBody; import xyz.thoughtset.viewer.modules.step.entity.BlockTypeEnum; @@ -46,11 +47,10 @@ public abstract class AIChatExecutor extends AbstractAI HashMap filterMap = filterDataAsMapForPrompt(body, params, parser, context); ModelParam modelParam = loadModelParam(body); - ChatClient.Builder builder = modelFactory.clientBuilder(modelParam); MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(modelParam.getMaxMemory()) .build(); - + ExtraParamChatOptions extraParam = new ExtraParamChatOptions(); ToolCallingChatOptions.Builder toolCallingBuilder = ToolCallingChatOptions.builder() .internalToolExecutionEnabled(false); String limitSystemPrompt = @@ -65,12 +65,13 @@ public abstract class AIChatExecutor extends AbstractAI """+ System.lineSeparator(); String systemText = modelParam.chatSystemPrompt(); if (body.getJsonType()!=null && body.getJsonType().booleanValue()){ - + extraParam.setResponseFormatType("json_object"); systemText = """ 最终结果必须按照JSON数组格式返回,不要有其余任何内容,不要有任何拼写错误,不要有任何语法错误,当后续要求与当前冲突时,以当前要求为准! """ + (StringUtils.hasText(systemText) ? systemText : ""); } + ChatClient.Builder builder = modelFactory.clientBuilder(modelParam,extraParam); String finalSystemText = limitSystemPrompt + System.lineSeparator() + systemText; builder.defaultSystem(fillAndRenderPromptToStr(finalSystemText, parser, context, filterMap)); loadToolCallbacks(block,body,builder); diff --git a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AbstractAISupportBlockExecutor.java b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AbstractAISupportBlockExecutor.java index b556e910d482da3220f44a09a021b2f95cf0be2c..08c41ef542c77eeb2f775ae73377190550c63415 100644 --- a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AbstractAISupportBlockExecutor.java +++ b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/AbstractAISupportBlockExecutor.java @@ -108,18 +108,23 @@ public abstract class AbstractAISupportBlockExecutor ex return modelParam; } - protected static String fillPrompt(String template, ExpressionParser parser,StandardEvaluationContext context) { + protected String fillPrompt(String template, ExpressionParser parser,StandardEvaluationContext context) { if (!StringUtils.hasText(template)){ return template; } try { - return parser.parseExpression(template).getValue(context, String.class); + Object result = parser.parseExpression(template).getValue(context); + if (result instanceof String){ + return (String) result; + } + return objectMapper.writeValueAsString(result); }catch (Exception e){ + e.printStackTrace(); return template; } } - protected static String fillAndRenderPromptToStr(String template, ExpressionParser parser,StandardEvaluationContext context,Map resultMaps) { + protected String fillAndRenderPromptToStr(String template, ExpressionParser parser,StandardEvaluationContext context,Map resultMaps) { return renderPrompt( fillPrompt(template, parser, context),resultMaps ); diff --git a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/ExecAIBlockExecutor.java b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/ExecAIBlockExecutor.java index 55347c389a56beda66037d4d51d2969260b71ba3..a8299759559351b8b365bdd7b0ebba6ecb5d07ec 100644 --- a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/ExecAIBlockExecutor.java +++ b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/ExecAIBlockExecutor.java @@ -1,10 +1,7 @@ package xyz.thoughtset.viewer.executor.blocks.executor; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.image.Image; -import org.springframework.ai.image.ImageModel; -import org.springframework.ai.image.ImagePrompt; -import org.springframework.ai.image.ImageResponse; +import org.springframework.ai.image.*; import org.springframework.ai.model.Model; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; @@ -30,40 +27,19 @@ public class ExecAIBlockExecutor extends AbstractAISupportBlockExecutor params, ExpressionParser parser, StandardEvaluationContext context) throws ExecException { -// HashMap resultMaps = new HashMap<>(params); -// List eleParams = body.getDataParams(); -// if (Objects.nonNull(eleParams)){ -// for (EleParam blockParam : eleParams){ -// Object value = ""; -// if(StringUtils.hasLength(blockParam.getDataExp())){ -// String exp =blockParam.getDataExp(); -// exp = blockParam.getDataExp().startsWith("#")?exp:"#"+exp; -// value = parser.parseExpression(exp).getValue(context); -// } -// String key = blockParam.getParamId(); -// if (StringUtils.hasText(key) && !ObjectUtils.isEmpty(value)){ -// if (!(value instanceof String)){ -// try { -// value = objectMapper.writeValueAsString(value); -// } catch (Exception e) { -// log.error("objectMapper error", value); -// e.printStackTrace(); -// value = ""; -// } -// } -// resultMaps.put(key, value); -// } -// } -// } HashMap resultMaps = filterDataAsMapForPrompt(body, params, parser, context); ModelParam modelParam = loadModelParam(body); String userPrompt = fillAndRenderPromptToStr(body.getUserMsg(), parser, context, resultMaps); -// String systemPrompt = fillPrompt(modelParam.getSystemPrompt(), parser, context); Model aiModel = modelFactory.buildModel(modelParam); Object result = null; +// var options = ImageOptionsBuilder.builder().model("cogview-3-flash").height(1024).width(1024).build(); +// var instructions = """ +// A light cream colored mini golden doodle with a sign that contains the message "I'm on my way to BARCADE!"."""; +// ImagePrompt imagePrompt = new ImagePrompt(instructions,options); +// ImageResponse imageResponse = ((ImageModel) aiModel).call(imagePrompt); if (aiModel instanceof ImageModel){ result = ((ImageModel) aiModel).call( - new ImagePrompt(userPrompt) + new ImagePrompt(userPrompt,null) ).getResult().getOutput(); } // aiModel. @@ -71,6 +47,7 @@ public class ExecAIBlockExecutor extends AbstractAISupportBlockExecutor params, ExpressionParser parser, StandardEvaluationContext context) throws ExecException { ModelParam modelParam = loadModelParam(body); - ChatClient.Builder builder = modelFactory.clientBuilder(modelParam); + ChatClient.Builder builder = modelFactory.clientBuilder(modelParam,null); MessageWindowChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(modelParam.getMaxMemory()) .build(); diff --git a/models/viewer-models-deepseek/src/main/java/xyz/thoughtset/viewer/models/deepseek/DeepSeekBuilder.java b/models/viewer-models-deepseek/src/main/java/xyz/thoughtset/viewer/models/deepseek/DeepSeekBuilder.java index d61948147552a223590e54c592b4a9de669813b5..314cfca11035aad301368b024f7f047c50354d0c 100644 --- a/models/viewer-models-deepseek/src/main/java/xyz/thoughtset/viewer/models/deepseek/DeepSeekBuilder.java +++ b/models/viewer-models-deepseek/src/main/java/xyz/thoughtset/viewer/models/deepseek/DeepSeekBuilder.java @@ -1,47 +1,40 @@ package xyz.thoughtset.viewer.models.deepseek; -import lombok.NonNull; +import lombok.SneakyThrows; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.deepseek.DeepSeekChatModel; import org.springframework.ai.deepseek.DeepSeekChatOptions; import org.springframework.ai.deepseek.api.DeepSeekApi; +import org.springframework.ai.deepseek.api.ResponseFormat; +import org.springframework.ai.model.ModelOptions; import org.springframework.stereotype.Component; import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; -import xyz.thoughtset.viewer.common.ai.model.entity.purpose.ChatModelSetting; -import xyz.thoughtset.viewer.common.ai.model.factory.ModelBuilder; +import xyz.thoughtset.viewer.common.ai.model.entity.option.ExtraParamChatOptions; +import xyz.thoughtset.viewer.common.ai.model.factory.ChatModelBuilder; import java.util.Optional; @Component -public class DeepSeekBuilder extends ModelBuilder { +public class DeepSeekBuilder extends ChatModelBuilder { public DeepSeekBuilder() { super("DeepSeek"); } - @Override - public boolean wasDefault() { - return true; - } + @SneakyThrows @Override - public ChatModel buildMode(AiNode node, ModelParam modelParam) { - DeepSeekChatOptions.Builder builder = DeepSeekChatOptions.builder() - .model(modelParam.getModel()); - ChatModelSetting settingObj = (ChatModelSetting) modelParam.getModelArgs(); -// if(settingObj!=null) { -// if (settingObj.getMaxTokens() != null) { -// builder.maxTokens(settingObj.getMaxTokens().intValue()); -// } -// if (settingObj.getTemperature() != null) { -// builder.temperature(settingObj.getTemperature()); -// } -// } - Optional.ofNullable(settingObj).ifPresent(setting -> { - setOptionalValue(setting.getMaxTokens(), value -> builder.maxTokens(value.intValue())); - setOptionalValue(setting.getTemperature(), builder::temperature); - }); - DeepSeekChatOptions options = builder.build(); + public ChatModel buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) { +// DeepSeekChatOptions.Builder builder = DeepSeekChatOptions.builder() +// .model(modelParam.getModel()); +// ChatModelSetting settingObj = (ChatModelSetting) modelParam.getModelArgs(); +// Optional.ofNullable(settingObj).ifPresent(setting -> { +// setOptionalValue(setting.getMaxTokens(), value -> builder.maxTokens(value.intValue())); +// setOptionalValue(setting.getTemperature(), builder::temperature); +// }); +// DeepSeekChatOptions options = builder.build(); + DeepSeekChatOptions options = loadAndMergeModelOptions(modelParam, DeepSeekChatOptions.class, modelOptions); + options.setModel(modelParam.getModel()); DeepSeekApi api = DeepSeekApi.builder() .baseUrl(node.getBaseUrl()) .apiKey(node.getApiKey()).build(); @@ -51,4 +44,15 @@ public class DeepSeekBuilder extends ModelBuilder { .build(); return chatModel; } + + @Override + public void mergeOptions(ModelOptions source, ModelOptions extraOptions) { + if (!(extraOptions instanceof ExtraParamChatOptions)) return ; + DeepSeekChatOptions options = (DeepSeekChatOptions) source; + ExtraParamChatOptions extraParamChatOptions = (ExtraParamChatOptions) extraOptions; + if (extraParamChatOptions.responseUsedJsonObject()){ + options.setResponseFormat(ResponseFormat.builder().type(ResponseFormat.Type.JSON_OBJECT).build()); + } + } + } diff --git a/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAIChatBuilder.java b/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAIChatBuilder.java index 6d698e90d1cee5a2e9c42ebf3f4a0c0467956a84..6aba663a11901ae3971489d18156a255e65d224a 100644 --- a/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAIChatBuilder.java +++ b/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAIChatBuilder.java @@ -1,41 +1,37 @@ package xyz.thoughtset.viewer.models.openai; import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.model.ModelOptions; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.openai.api.ResponseFormat; import org.springframework.stereotype.Component; import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; -import xyz.thoughtset.viewer.common.ai.model.entity.purpose.ChatModelSetting; -import xyz.thoughtset.viewer.common.ai.model.factory.DefaultModelBuilder; -import xyz.thoughtset.viewer.common.ai.model.factory.ModelBuilder; - -import java.util.Optional; +import xyz.thoughtset.viewer.common.ai.model.entity.option.ExtraParamChatOptions; +import xyz.thoughtset.viewer.common.ai.model.factory.ChatModelBuilder; @Component -public class OpenAIChatBuilder extends DefaultModelBuilder { +public class OpenAIChatBuilder extends ChatModelBuilder { public OpenAIChatBuilder() { super("OpenAI"); } @Override - public ChatModel buildMode(AiNode node, ModelParam modelParam) { - OpenAiChatOptions.Builder builder = OpenAiChatOptions.builder() - .model(modelParam.getModel()); - ChatModelSetting settingObj = (ChatModelSetting) modelParam.getModelArgs(); -// if (settingObj.getMaxTokens() != null) { -// builder.maxTokens(settingObj.getMaxTokens().intValue()); -// } -// if (settingObj.getTemperature() != null) { -// builder.temperature(settingObj.getTemperature()); -// } - Optional.ofNullable(settingObj).ifPresent(setting -> { - setOptionalValue(setting.getMaxTokens(), value -> builder.maxTokens(value.intValue())); - setOptionalValue(setting.getTemperature(), builder::temperature); - }); - OpenAiChatOptions options = builder.build(); + public ChatModel buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) { +// OpenAiChatOptions.Builder builder = OpenAiChatOptions.builder() +// .model(modelParam.getModel()); +// ChatModelSetting settingObj = (ChatModelSetting) modelParam.getModelArgs(); + +// Optional.ofNullable(settingObj).ifPresent(setting -> { +// setOptionalValue(setting.getMaxTokens(), value -> builder.maxTokens(value.intValue())); +// setOptionalValue(setting.getTemperature(), builder::temperature); +// }); +// OpenAiChatOptions options = builder.build(); + OpenAiChatOptions options = loadAndMergeModelOptions(modelParam, OpenAiChatOptions.class, modelOptions); + options.setModel(modelParam.getModel()); OpenAiApi api = OpenAiApi.builder() .baseUrl(node.getBaseUrl()) .apiKey(node.getApiKey()).build(); @@ -45,4 +41,14 @@ public class OpenAIChatBuilder extends DefaultModelBuilder { .build(); return chatModel; } + + @Override + public void mergeOptions(ModelOptions source, ModelOptions extraOptions) { + if (!(extraOptions instanceof ExtraParamChatOptions)) return ; + OpenAiChatOptions options = (OpenAiChatOptions) source; + ExtraParamChatOptions extraParamChatOptions = (ExtraParamChatOptions) extraOptions; + if (extraParamChatOptions.responseUsedJsonObject()){ + options.setResponseFormat(ResponseFormat.builder().type(ResponseFormat.Type.JSON_OBJECT).build()); + } + } } diff --git a/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAIImageBuilder.java b/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAIImageBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..907d2b10b57b98091423a261aef425504814de11 --- /dev/null +++ b/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAIImageBuilder.java @@ -0,0 +1,88 @@ +package xyz.thoughtset.viewer.models.openai; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.model.Model; +import org.springframework.ai.model.ModelOptions; +import org.springframework.ai.openai.OpenAiImageModel; +import org.springframework.ai.openai.OpenAiImageOptions; +import org.springframework.ai.openai.api.OpenAiImageApi; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.ai.retry.TransientAiException; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryListener; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; +import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; +import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; +import xyz.thoughtset.viewer.common.ai.model.factory.ImageModelBuilder; +import xyz.thoughtset.viewer.common.ai.model.factory.ModelBuilder; + +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class OpenAIImageBuilder extends ImageModelBuilder { + public OpenAIImageBuilder() { + super("OpenAI"); + } + + + @Override + public Model buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) { + OpenAiImageOptions.Builder builder = OpenAiImageOptions.builder() + .model(modelParam.getModel()); + OpenAiImageOptions options = builder.build(); + OpenAiImageApi api = OpenAiImageApi.builder() + .baseUrl(node.getBaseUrl()) + .apiKey(node.getApiKey()) + .build(); + OpenAiImageModel model = new OpenAiImageModel(api, options, + RetryTemplate.builder().maxAttempts(3) + .retryOn(TransientAiException.class) + .retryOn(ResourceAccessException.class) + .exponentialBackoff(Duration.ofMillis(11000L), 5.0, Duration.ofMillis(180000L)) + .withListener(new RetryListener() { + public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { + log.warn("Retry error. Retry count:{}", context.getRetryCount(), throwable); + } + }).build() + ); + return model; + } + + private RestClient.Builder createLoggingRestClient() { + // 创建支持缓冲的请求工厂(允许多次读取请求体和响应体) + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(30000); + requestFactory.setReadTimeout(120000); + + ClientHttpRequestFactory bufferingFactory = + new BufferingClientHttpRequestFactory(requestFactory); + + // 创建RestClient并添加拦截器 + return RestClient.builder() + .defaultHeaders(h -> h.setContentType(MediaType.APPLICATION_JSON)) + .requestFactory(bufferingFactory) + // 使用美化版拦截器 +// .requestInterceptor(new PrettyRestClientLoggingInterceptor()) + // 或使用简单版拦截器 + // .requestInterceptor(new RestClientLoggingInterceptor()) + ; + } + + +} diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/openai/ZhipuImageBuilder.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/openai/ZhipuImageBuilder.java deleted file mode 100644 index 30980663faf801dd45f48f30c2b51a7a8948bab5..0000000000000000000000000000000000000000 --- a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/openai/ZhipuImageBuilder.java +++ /dev/null @@ -1,41 +0,0 @@ -package xyz.thoughtset.viewer.models.openai; - -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.model.Model; -import org.springframework.ai.retry.RetryUtils; -import org.springframework.ai.zhipuai.ZhiPuAiChatModel; -import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; -import org.springframework.ai.zhipuai.ZhiPuAiImageModel; -import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; -import org.springframework.ai.zhipuai.api.ZhiPuAiApi; -import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; -import org.springframework.stereotype.Component; -import org.springframework.web.client.RestClient; -import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; -import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; -import xyz.thoughtset.viewer.common.ai.model.factory.DefaultModelBuilder; -import xyz.thoughtset.viewer.common.ai.model.factory.ModelBuilder; - -import java.util.List; - -@Component -public class ZhipuImageBuilder extends ModelBuilder { - public ZhipuImageBuilder() { - super("ZhipuAI", List.of("CogView-4","Cogview-3-Flash" )); - } - - - @Override - public Model buildMode(AiNode node, ModelParam modelParam) { - ZhiPuAiImageOptions.Builder builder = ZhiPuAiImageOptions.builder() - .model(modelParam.getModel()); - ZhiPuAiImageOptions options = builder.build(); - ZhiPuAiImageApi api = new ZhiPuAiImageApi(node.getBaseUrl(), node.getApiKey(), RestClient.builder()); - ZhiPuAiImageModel model = new ZhiPuAiImageModel(api, options, RetryUtils.DEFAULT_RETRY_TEMPLATE); -// ChatModel chatModel = ZhiPuAiChatModel.builder() -// .defaultOptions(options) -// .openAiApi(api) -// .build(); - return model; - } -} diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/openai/ModelZhipuAIAutoConfiguration.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ModelZhipuAIAutoConfiguration.java similarity index 87% rename from models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/openai/ModelZhipuAIAutoConfiguration.java rename to models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ModelZhipuAIAutoConfiguration.java index 47d02f6703281c196e220fe312dcba1efd981fa5..834499807b439140b8053059a78c2fa59b815f60 100644 --- a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/openai/ModelZhipuAIAutoConfiguration.java +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ModelZhipuAIAutoConfiguration.java @@ -1,4 +1,4 @@ -package xyz.thoughtset.viewer.models.openai; +package xyz.thoughtset.viewer.models.zhipuai; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.ComponentScan; diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/PrettyRestClientLoggingInterceptor.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/PrettyRestClientLoggingInterceptor.java new file mode 100644 index 0000000000000000000000000000000000000000..d0a81f4be8d732a1fa2fd18f64dc95263eb3add0 --- /dev/null +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/PrettyRestClientLoggingInterceptor.java @@ -0,0 +1,137 @@ +package xyz.thoughtset.viewer.models.zhipuai; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.StreamUtils; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +@Slf4j +public class PrettyRestClientLoggingInterceptor implements ClientHttpRequestInterceptor { + + private final ObjectMapper objectMapper; + + public PrettyRestClientLoggingInterceptor() { + this.objectMapper = new ObjectMapper(); + this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + } + + @Override + public ClientHttpResponse intercept( + HttpRequest request, + byte[] body, + ClientHttpRequestExecution execution) throws IOException { + + long startTime = System.currentTimeMillis(); + + // 打印请求 + printRequest(request, body); + + // 执行请求并包装响应(确保可以多次读取) + ClientHttpResponse response = execution.execute(request, body); + ClientHttpResponse bufferedResponse = new BufferedClientHttpResponse(response); + + // 打印响应 + long duration = System.currentTimeMillis() - startTime; + printResponse(bufferedResponse, duration); + + return bufferedResponse; + } + + private void printRequest(HttpRequest request, byte[] body) { + System.out.println("\n" + "=".repeat(100)); + System.out.println("🚀 HTTP REQUEST"); + System.out.println("=".repeat(100)); + System.out.println("📍 URI : " + request.getURI()); + System.out.println("📮 Method : " + request.getMethod()); + System.out.println("📋 Headers :"); + request.getHeaders().forEach((name, values) -> + values.forEach(value -> System.out.println(" " + name + ": " + value)) + ); + + if (body.length > 0) { + String bodyString = new String(body, StandardCharsets.UTF_8); + System.out.println("📦 Request Body:"); + System.out.println(prettyPrintJson(bodyString)); + } else { + System.out.println("📦 Request Body: (empty)"); + } + System.out.println("=".repeat(100) + "\n"); + } + + private void printResponse(ClientHttpResponse response, long duration) throws IOException { + byte[] bodyBytes = StreamUtils.copyToByteArray(response.getBody()); + String bodyString = new String(bodyBytes, StandardCharsets.UTF_8); + + System.out.println("\n" + "=".repeat(100)); + System.out.println("📥 HTTP RESPONSE (⏱️ " + duration + "ms)"); + System.out.println("=".repeat(100)); + System.out.println("📊 Status : " + response.getStatusCode().value() + " " + response.getStatusText()); + System.out.println("📋 Headers :"); + response.getHeaders().forEach((name, values) -> + values.forEach(value -> System.out.println(" " + name + ": " + value)) + ); + System.out.println("📦 Response Body:"); + System.out.println(prettyPrintJson(bodyString)); + System.out.println("=".repeat(100) + "\n"); + } + + private String prettyPrintJson(String json) { + try { + Object jsonObject = objectMapper.readValue(json, Object.class); + return objectMapper.writeValueAsString(jsonObject); + } catch (Exception e) { + // 如果不是JSON,直接返回 + if (json.length() > 2000) { + return json.substring(0, 2000) + "\n... (truncated, total length: " + json.length() + ")"; + } + return json; + } + } + + /** + * 可缓冲的响应包装器,允许多次读取响应体 + */ + private static class BufferedClientHttpResponse implements ClientHttpResponse { + private final ClientHttpResponse response; + private byte[] body; + + public BufferedClientHttpResponse(ClientHttpResponse response) throws IOException { + this.response = response; + this.body = StreamUtils.copyToByteArray(response.getBody()); + } + + @Override + public org.springframework.http.HttpStatusCode getStatusCode() throws IOException { + return response.getStatusCode(); + } + + @Override + public String getStatusText() throws IOException { + return response.getStatusText(); + } + + @Override + public void close() { + response.close(); + } + + @Override + public InputStream getBody() throws IOException { + return new ByteArrayInputStream(body); + } + + @Override + public org.springframework.http.HttpHeaders getHeaders() { + return response.getHeaders(); + } + } +} \ No newline at end of file diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/openai/ZhipuChatBuilder.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuChatBuilder.java similarity index 42% rename from models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/openai/ZhipuChatBuilder.java rename to models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuChatBuilder.java index ea2fd0e15efd45109f8a2bf89a57dcc804ecf8ca..49e9ed7e93b2bc4935b946d714c6cc566f55e0fb 100644 --- a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/openai/ZhipuChatBuilder.java +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuChatBuilder.java @@ -1,36 +1,33 @@ -package xyz.thoughtset.viewer.models.openai; +package xyz.thoughtset.viewer.models.zhipuai; import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.model.ModelOptions; import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.stereotype.Component; import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; -import xyz.thoughtset.viewer.common.ai.model.entity.purpose.ChatModelSetting; -import xyz.thoughtset.viewer.common.ai.model.factory.DefaultModelBuilder; - -import java.util.Optional; +import xyz.thoughtset.viewer.common.ai.model.factory.ChatModelBuilder; @Component -public class ZhipuChatBuilder extends DefaultModelBuilder { +public class ZhipuChatBuilder extends ChatModelBuilder { public ZhipuChatBuilder() { super("ZhipuAI"); } @Override - public ChatModel buildMode(AiNode node, ModelParam modelParam) { - ZhiPuAiChatOptions.Builder builder = ZhiPuAiChatOptions.builder() - .model(modelParam.getModel()); - ChatModelSetting settingObj = (ChatModelSetting) modelParam.getModelArgs(); - Optional.ofNullable(settingObj).ifPresent(setting -> { - setOptionalValue(setting.getMaxTokens(), value -> builder.maxTokens(value.intValue())); - setOptionalValue(setting.getTemperature(), builder::temperature); - }); - ZhiPuAiChatOptions options = builder.build(); - ZhiPuAiApi api = new ZhiPuAiApi(node.getBaseUrl(), node.getApiKey()); + public ChatModel buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) { + ZhiPuAiChatOptions options = loadAndMergeModelOptions(modelParam, ZhiPuAiChatOptions.class, modelOptions); + options.setModel(modelParam.getModel()); + ZhiPuAiApi api = ZhiPuAiApi.builder() + .baseUrl(node.getBaseUrl()).apiKey(node.getApiKey()) + .build(); ChatModel chatModel = new ZhiPuAiChatModel(api, options); return chatModel; } + + + } diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuImageBuilder.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuImageBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..273ddd13ceb09dabe235e681907886c7f7874152 --- /dev/null +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuImageBuilder.java @@ -0,0 +1,79 @@ +package xyz.thoughtset.viewer.models.zhipuai; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.model.Model; +import org.springframework.ai.model.ModelOptions; +import org.springframework.ai.retry.TransientAiException; +import org.springframework.ai.zhipuai.ZhiPuAiImageModel; +import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; +import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.*; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryListener; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.stereotype.Component; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; +import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; +import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; +import xyz.thoughtset.viewer.common.ai.model.factory.ImageModelBuilder; +import xyz.thoughtset.viewer.common.ai.model.factory.ModelBuilder; + +import java.time.Duration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Slf4j +@Component +public class ZhipuImageBuilder extends ImageModelBuilder { + public ZhipuImageBuilder() { + super("ZhipuAI"); + } + + + @Override + public Model buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) { + ZhiPuAiImageOptions.Builder builder = ZhiPuAiImageOptions.builder() + .model(modelParam.getModel()); + ZhiPuAiImageOptions options = builder.build(); + ZhiPuAiImageApi api = new ZhiPuAiImageApi(node.getBaseUrl(), node.getApiKey(), createLoggingRestClient()); + ZhiPuAiImageModel model = new ZhiPuAiImageModel(api, options, + RetryTemplate.builder().maxAttempts(3).retryOn(TransientAiException.class).retryOn(ResourceAccessException.class).exponentialBackoff(Duration.ofMillis(9000L), 5.0, Duration.ofMillis(180000L)).withListener(new RetryListener() { + public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { + log.warn("Retry error. Retry count:{}", context.getRetryCount(), throwable); + } + }).build() + ); + return model; + } + + + private RestClient.Builder createLoggingRestClient() { + // 创建支持缓冲的请求工厂(允许多次读取请求体和响应体) + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(30000); + requestFactory.setReadTimeout(120000); + + ClientHttpRequestFactory bufferingFactory = + new BufferingClientHttpRequestFactory(requestFactory); + + // 创建RestClient并添加拦截器 + return RestClient.builder() + .defaultHeaders(h -> h.setContentType(MediaType.APPLICATION_JSON)) + .requestFactory(bufferingFactory) + // 使用美化版拦截器 + .requestInterceptor(new PrettyRestClientLoggingInterceptor()) + // 或使用简单版拦截器 + // .requestInterceptor(new RestClientLoggingInterceptor()) + ; + } + + +} diff --git a/models/viewer-models-zhipuai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/models/viewer-models-zhipuai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index f15a472e456f50e23803ecc61c5f205684db872b..b00b752df8c161087a4e8b7c3bddf4ef3988287c 100644 --- a/models/viewer-models-zhipuai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/models/viewer-models-zhipuai/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1 @@ -xyz.thoughtset.viewer.models.openai.ModelOpenAIAutoConfiguration \ No newline at end of file +xyz.thoughtset.viewer.models.zhipuai.ModelZhipuAIAutoConfiguration \ No newline at end of file diff --git a/pom.xml b/pom.xml index 157b31406454871705b99295ce2918df044f654b..84b89dbef1c688ecc213c035afd14e4c80e8b8f7 100644 --- a/pom.xml +++ b/pom.xml @@ -53,9 +53,10 @@ 4.11.0 0.2.25 3.2.0 - 1.0.3 - 0.12.1 + 1.1.0 + 0.16.0 + org.springframework.boot