From 7791db29d1ebc8e64187691e6a682d0d7e785ce3 Mon Sep 17 00:00:00 2001 From: suifeng <369202865@qq.com> Date: Tue, 12 Aug 2025 22:50:57 +0800 Subject: [PATCH] =?UTF-8?q?[dev]=20sfchian=E5=90=8E=E7=AB=AF=E4=BB=A3?= =?UTF-8?q?=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- prompto-lab-app/pom.xml | 8 + .../sfchain/annotation/AIOp.java | 71 +++ .../config/OpenAIAutoConfiguration.java | 74 +++ .../sfchain/config/OpenAIModelsConfig.java | 133 +++++ .../constants/AIOperationConstant.java | 33 ++ .../sfchain/controller/AIModelController.java | 245 +++++++++ .../controller/AIOperationController.java | 227 +++++++++ .../controller/AISystemController.java | 132 +++++ .../timemachinelab/sfchain/core/AIModel.java | 40 ++ .../sfchain/core/AIOperationRegistry.java | 158 ++++++ .../sfchain/core/AIService.java | 364 ++++++++++++++ .../sfchain/core/BaseAIOperation.java | 400 +++++++++++++++ .../sfchain/core/ModelRegistry.java | 71 +++ .../core/openai/OpenAICompatibleModel.java | 155 ++++++ .../sfchain/core/openai/OpenAIHttpClient.java | 143 ++++++ .../core/openai/OpenAIModelConfig.java | 120 +++++ .../core/openai/OpenAIModelFactory.java | 79 +++ .../sfchain/core/openai/OpenAIRequest.java | 102 ++++ .../sfchain/core/openai/OpenAIResponse.java | 108 ++++ .../operations/JSONRepairOperation.java | 141 ++++++ .../operations/ModelValidationOperation.java | 83 ++++ .../TextClassificationOperation.java | 185 +++++++ .../DynamicOperationConfigService.java | 82 +++ .../sfchain/persistence/ModelConfigData.java | 125 +++++ .../persistence/OperationConfigData.java | 143 ++++++ .../persistence/PersistenceManager.java | 470 ++++++++++++++++++ .../persistence/PersistenceService.java | 127 +++++ .../PostgreSQLPersistenceService.java | 301 +++++++++++ .../persistence/entity/ModelConfigEntity.java | 73 +++ .../entity/OperationConfigEntity.java | 82 +++ .../repository/ModelConfigRepository.java | 68 +++ .../repository/OperationConfigRepository.java | 75 +++ .../db/migration/V1__Create_AI_Tables.sql | 75 +++ 33 files changed, 4693 insertions(+) create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/annotation/AIOp.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/config/OpenAIAutoConfiguration.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/config/OpenAIModelsConfig.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/constants/AIOperationConstant.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AIModelController.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AIOperationController.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AISystemController.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/AIModel.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/AIOperationRegistry.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/AIService.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/BaseAIOperation.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/ModelRegistry.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAICompatibleModel.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIHttpClient.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIModelConfig.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIModelFactory.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIRequest.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIResponse.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/operations/JSONRepairOperation.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/operations/ModelValidationOperation.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/operations/TextClassificationOperation.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/DynamicOperationConfigService.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/ModelConfigData.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/OperationConfigData.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/PersistenceManager.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/PersistenceService.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/PostgreSQLPersistenceService.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/entity/ModelConfigEntity.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/entity/OperationConfigEntity.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/repository/ModelConfigRepository.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/repository/OperationConfigRepository.java create mode 100644 prompto-lab-app/src/main/resources/db/migration/V1__Create_AI_Tables.sql diff --git a/prompto-lab-app/pom.xml b/prompto-lab-app/pom.xml index 43abca9..acd8845 100644 --- a/prompto-lab-app/pom.xml +++ b/prompto-lab-app/pom.xml @@ -131,6 +131,14 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-compiler-plugin + + 16 + 16 + + \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/annotation/AIOp.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/annotation/AIOp.java new file mode 100644 index 0000000..71f7b27 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/annotation/AIOp.java @@ -0,0 +1,71 @@ +package io.github.timemachinelab.sfchain.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 描述: AI操作注解 + * 用于标识AI操作类,并指定操作类型和默认模型 + * @author suifeng + * 日期: 2025/8/11 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface AIOp { + + /** + * 操作类型标识 + * 例如: "POSITION_BASIC_INFO_PARSE_OP" + */ + String value(); + + /** + * 默认使用的模型名称 + * 如果不指定,将使用操作映射配置中的模型 + */ + String defaultModel() default ""; + + /** + * 操作描述 + */ + String description() default ""; + + /** + * 是否启用该操作 + */ + boolean enabled() default true; + + /** + * 支持的模型列表(可选) + * 如果指定,将限制该操作只能使用这些模型 + */ + String[] supportedModels() default {}; + + /** + * 是否需要JSON输出 + */ + boolean requireJsonOutput() default true; + + /** + * 是否自动修复JSON格式错误 + * 当requireJsonOutput为true且AI返回的JSON格式有误时,自动调用JSON修复操作 + */ + boolean autoRepairJson() default true; + + /** + * 是否支持思考模式 + */ + boolean supportThinking() default false; + + /** + * 默认最大token数 + */ + int defaultMaxTokens() default 4096; + + /** + * 默认温度参数 + */ + double defaultTemperature() default 0.7; +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/config/OpenAIAutoConfiguration.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/config/OpenAIAutoConfiguration.java new file mode 100644 index 0000000..6ee7303 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/config/OpenAIAutoConfiguration.java @@ -0,0 +1,74 @@ +package io.github.timemachinelab.sfchain.config; + +import io.github.timemachinelab.sfchain.core.AIModel; +import io.github.timemachinelab.sfchain.core.openai.OpenAIModelConfig; +import io.github.timemachinelab.sfchain.core.openai.OpenAIModelFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 描述: OpenAI兼容模型自动配 + * @author suifeng + * 日期: 2025/8/11 + */ +@Slf4j +@Configuration +@EnableConfigurationProperties(OpenAIModelsConfig.class) +@RequiredArgsConstructor +@ConditionalOnProperty(prefix = "ai.openai-models", name = "enabled", havingValue = "true", matchIfMissing = true) +public class OpenAIAutoConfiguration { + + private final OpenAIModelsConfig openAIModelsConfig; + + /** + * 创建OpenAI模型工厂 + */ + @Bean + @Primary + public OpenAIModelFactory openAIModelFactory() { + OpenAIModelFactory factory = new OpenAIModelFactory(); + + // 注册配置文件中的模型 + Map modelConfigs = openAIModelsConfig.getValidModelConfigs(); + modelConfigs.forEach((name, config) -> { + try { + factory.registerModel(config); + log.info("成功注册模型: {} ({})", config.getModelName(), config.getProvider()); + } catch (Exception e) { + log.error("注册模型失败: {} - {}", config.getModelName(), e.getMessage()); + } + }); + + return factory; + } + + /** + * 创建AI模型列表,供ModelRegistry使用 + */ + @Bean + public List aiModels(OpenAIModelFactory factory) { + List models = new ArrayList<>(); + + // 为每个注册的模型创建实例 + factory.getRegisteredModelNames().forEach(modelName -> { + try { + AIModel model = factory.createModel(modelName); + models.add(model); + log.info("创建AI模型实例: {}", modelName); + } catch (Exception e) { + log.error("创建模型实例失败: {} - {}", modelName, e.getMessage()); + } + }); + + return models; + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/config/OpenAIModelsConfig.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/config/OpenAIModelsConfig.java new file mode 100644 index 0000000..24784cb --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/config/OpenAIModelsConfig.java @@ -0,0 +1,133 @@ +package io.github.timemachinelab.sfchain.config; + +import io.github.timemachinelab.sfchain.core.openai.OpenAIModelConfig; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +/** + * 描述: OpenAI兼容模型配置 + * @author suifeng + * 日期: 2025/8/11 + */ +@Data +@Component +@ConfigurationProperties(prefix = "ai.openai-models") +public class OpenAIModelsConfig { + + /** + * 模型配置映射 + * key: 模型名称 + * value: 模型配置 + */ + private Map models = new HashMap<>(); + + /** + * 模型配置属性 + */ + @Data + public static class ModelConfigProperties { + /** + * 模型名称 + */ + private String modelName; + + /** + * API基础URL + */ + private String baseUrl; + + /** + * API密钥 + */ + private String apiKey; + + /** + * 默认最大token数 + */ + private Integer defaultMaxTokens = 4096; + + /** + * 默认温度参数 + */ + private Double defaultTemperature = 0.7; + + /** + * 是否支持流式输出 + */ + private Boolean supportStream = false; + + /** + * 是否支持JSON格式输出 + */ + private Boolean supportJsonOutput = false; + + /** + * 是否支持思考模式 + */ + private Boolean supportThinking = false; + + /** + * 额外的HTTP请求头 + */ + private Map additionalHeaders = new HashMap<>(); + + /** + * 模型描述 + */ + private String description; + + /** + * 模型提供商 + */ + private String provider; + + /** + * 是否启用 + */ + private Boolean enabled = true; + + /** + * 转换为OpenAIModelConfig + */ + public OpenAIModelConfig toOpenAIModelConfig() { + return OpenAIModelConfig.builder() + .modelName(modelName) + .baseUrl(baseUrl) + .apiKey(apiKey) + .defaultMaxTokens(defaultMaxTokens) + .defaultTemperature(defaultTemperature) + .supportStream(supportStream) + .supportJsonOutput(supportJsonOutput) + .supportThinking(supportThinking) + .additionalHeaders(additionalHeaders) + .description(description) + .provider(provider) + .enabled(enabled) + .build(); + } + } + + /** + * 获取所有有效的模型配置 + */ + public Map getValidModelConfigs() { + Map validConfigs = new HashMap<>(); + + models.forEach((key, properties) -> { + if (properties.getModelName() == null) { + properties.setModelName(key); + } + + OpenAIModelConfig config = properties.toOpenAIModelConfig(); + if (config.isValid() && Boolean.TRUE.equals(config.getEnabled())) { + validConfigs.put(key, config); + } + }); + + return validConfigs; + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/constants/AIOperationConstant.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/constants/AIOperationConstant.java new file mode 100644 index 0000000..eb4a795 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/constants/AIOperationConstant.java @@ -0,0 +1,33 @@ +package io.github.timemachinelab.sfchain.constants; + +/** + * 描述: AI操作常量定义 + * 定义所有AI操作的标识符 + * + * @author suifeng + * 日期: 2025/8/11 + */ +public class AIOperationConstant { + + /** + * JSON修复操作 + */ + public static final String JSON_REPAIR_OP = "JSON_REPAIR_OP"; + + /** + * 文本分类操作 + */ + public static final String TEXT_CLASSIFICATION_OP = "TEXT_CLASSIFICATION"; + + /** + * 模型验证操作 + */ + public static final String MODEL_VALIDATION_OP = "MODEL_VALIDATION_OP"; + + /** + * 私有构造函数,防止实例化 + */ + private AIOperationConstant() { + throw new UnsupportedOperationException("常量类不允许实例化"); + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AIModelController.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AIModelController.java new file mode 100644 index 0000000..c008ade --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AIModelController.java @@ -0,0 +1,245 @@ +package io.github.timemachinelab.sfchain.controller; + +import io.github.timemachinelab.sfchain.core.AIOperationRegistry; +import io.github.timemachinelab.sfchain.core.openai.OpenAIModelConfig; +import io.github.timemachinelab.sfchain.core.openai.OpenAIModelFactory; +import io.github.timemachinelab.sfchain.operations.ModelValidationOperation; +import io.github.timemachinelab.sfchain.persistence.ModelConfigData; +import io.github.timemachinelab.sfchain.persistence.PersistenceManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static io.github.timemachinelab.sfchain.constants.AIOperationConstant.MODEL_VALIDATION_OP; + +/** + * 描述: AI模型配置管理控制器 + * 提供AI模型的增删改查、测试验证等功能 + * + * @author suifeng + * 日期: 2025/8/11 + */ +@Slf4j +@RestController +@RequestMapping("/sf/api/models") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class AIModelController { + + private final PersistenceManager persistenceManager; + private final AIOperationRegistry operationRegistry; + private final OpenAIModelFactory modelFactory; + + /** + * 获取所有模型配置(包含状态信息) + */ + @GetMapping("/list") + public ResponseEntity> getAllModels() { + try { + Map models = persistenceManager.getAllModelConfigs(); + Map result = new HashMap<>(); + + // 按提供商分组 + Map> groupedByProvider = models.values().stream() + .collect(Collectors.groupingBy(m -> m.getProvider() != null ? m.getProvider() : "未知")); + + result.put("models", models); + result.put("groupedByProvider", groupedByProvider); + result.put("total", models.size()); + + return ResponseEntity.ok(result); + } catch (Exception e) { + log.error("获取模型列表失败: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "获取模型列表失败: " + e.getMessage())); + } + } + + /** + * 获取单个模型配置 + */ + @GetMapping("/{modelName}") + public ResponseEntity getModel(@PathVariable String modelName) { + try { + Map models = persistenceManager.getAllModelConfigs(); + ModelConfigData model = models.get(modelName); + + if (model == null) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok(model); + } catch (Exception e) { + log.error("获取模型配置失败: {} - {}", modelName, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "获取模型配置失败: " + e.getMessage())); + } + } + + /** + * 创建或更新模型配置 + */ + @PostMapping("/{modelName}") + public ResponseEntity> saveModel( + @PathVariable String modelName, + @Valid @RequestBody ModelConfigData config) { + Map result = new HashMap<>(); + try { + // 验证模型配置 + boolean validationResult = validateModelConfig(modelName, config); + + if (!validationResult) { + result.put("success", false); + result.put("message", "模型验证失败,请检查配置参数"); + return ResponseEntity.badRequest().body(result); + } + + // 检查模型是否已存在 + Map existingModels = persistenceManager.getAllModelConfigs(); + boolean modelExists = existingModels.containsKey(modelName); + + // 根据模型是否存在选择添加或更新 + if (modelExists) { + persistenceManager.updateModelConfig(modelName, config); + result.put("operation", "updated"); + result.put("message", "模型配置更新成功"); + log.info("模型配置已更新: {}", modelName); + } else { + persistenceManager.addModelConfig(modelName, config); + result.put("operation", "created"); + result.put("message", "模型配置创建成功"); + log.info("模型配置已创建: {}", modelName); + } + + result.put("success", true); + result.put("modelName", modelName); + result.put("validated", true); + + return ResponseEntity.ok(result); + } catch (Exception e) { + log.error("保存模型配置失败: {} - {}", modelName, e.getMessage()); + result.put("success", false); + result.put("message", "保存失败: " + e.getMessage()); + return ResponseEntity.badRequest().body(result); + } + } + + /** + * 删除模型配置 + */ + @DeleteMapping("/{modelName}") + public ResponseEntity> deleteModel(@PathVariable String modelName) { + Map result = new HashMap<>(); + try { + persistenceManager.deleteModelConfig(modelName); + result.put("success", true); + result.put("message", "模型配置删除成功"); + result.put("modelName", modelName); + return ResponseEntity.ok(result); + } catch (Exception e) { + log.error("删除模型配置失败: {} - {}", modelName, e.getMessage()); + result.put("success", false); + result.put("message", "删除失败: " + e.getMessage()); + return ResponseEntity.badRequest().body(result); + } + } + + /** + * 测试模型连接 + */ + @PostMapping("/{modelName}/test") + public ResponseEntity> testModel(@PathVariable String modelName) { + Map result = new HashMap<>(); + try { + Map models = persistenceManager.getAllModelConfigs(); + ModelConfigData config = models.get(modelName); + + if (config == null) { + result.put("success", false); + result.put("message", "模型配置不存在"); + return ResponseEntity.notFound().build(); + } + + boolean testResult = validateModelConfig(modelName, config); + result.put("success", testResult); + result.put("message", testResult ? "模型连接测试成功" : "模型连接测试失败"); + result.put("modelName", modelName); + + return ResponseEntity.ok(result); + } catch (Exception e) { + log.error("测试模型连接失败: {} - {}", modelName, e.getMessage()); + result.put("success", false); + result.put("message", "测试失败: " + e.getMessage()); + return ResponseEntity.badRequest().body(result); + } + } + + // ==================== 私有方法 ==================== + + /** + * 验证模型配置是否可用 + */ + private boolean validateModelConfig(String modelName, ModelConfigData config) { + boolean tempRegistered = false; + try { + log.info("开始验证模型配置: {}", modelName); + + ModelValidationOperation validationOp = (ModelValidationOperation) operationRegistry.getOperation(MODEL_VALIDATION_OP); + if (validationOp == null) { + log.warn("未找到模型验证操作,跳过验证"); + return true; + } + + OpenAIModelConfig tempConfig = convertToOpenAIConfig(config); + modelFactory.registerModel(tempConfig); + tempRegistered = true; + + ModelValidationOperation.ValidationRequest request = + new ModelValidationOperation.ValidationRequest("请回答:2+3等于几?"); + + ModelValidationOperation.ValidationResult result = validationOp.execute(request, modelName); + + return result != null && result.getAnswer() != null && !result.getAnswer().trim().isEmpty(); + + } catch (Exception e) { + log.error("模型验证失败: {} - {}", modelName, e.getMessage()); + return false; + } finally { + if (tempRegistered) { + try { + modelFactory.removeModel(modelName); + } catch (Exception e) { + log.warn("清理临时模型配置失败: {}", e.getMessage()); + } + } + } + } + + /** + * 转换配置格式 + */ + private OpenAIModelConfig convertToOpenAIConfig(ModelConfigData config) { + return OpenAIModelConfig.builder() + .modelName(config.getModelName()) + .baseUrl(config.getBaseUrl()) + .apiKey(config.getApiKey()) + .defaultMaxTokens(config.getDefaultMaxTokens()) + .defaultTemperature(config.getDefaultTemperature()) + .supportStream(config.getSupportStream()) + .supportJsonOutput(config.getSupportJsonOutput()) + .supportThinking(config.getSupportThinking()) + .additionalHeaders(config.getAdditionalHeaders()) + .description(config.getDescription()) + .provider(config.getProvider()) + .enabled(config.getEnabled()) + .build(); + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AIOperationController.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AIOperationController.java new file mode 100644 index 0000000..c4b04c4 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AIOperationController.java @@ -0,0 +1,227 @@ +package io.github.timemachinelab.sfchain.controller; + +import io.github.timemachinelab.sfchain.persistence.ModelConfigData; +import io.github.timemachinelab.sfchain.persistence.OperationConfigData; +import io.github.timemachinelab.sfchain.persistence.PersistenceManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * 描述: AI操作配置管理控制器 + * 提供AI操作的配置管理、模型映射等功能 + * + * @author suifeng + * 日期: 2025/8/11 + */ +@Slf4j +@RestController +@RequestMapping("/sf/api/operations") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class AIOperationController { + + private final PersistenceManager persistenceManager; + + /** + * 获取所有AI操作及其配置状态 + */ + @GetMapping + public ResponseEntity> getAllOperations() { + try { + Map configs = persistenceManager.getAllOperationConfigs(); + + // 构建操作模型映射信息(从操作配置中提取) + Map mappings = new HashMap<>(); + configs.forEach((operationType, config) -> { + if (config.getModelName() != null && !config.getModelName().isEmpty()) { + mappings.put(operationType, config.getModelName()); + } + }); + + Map result = new HashMap<>(); + result.put("mappings", mappings); + result.put("configs", configs); + result.put("totalOperations", configs.size()); + result.put("configuredOperations", mappings.size()); + + return ResponseEntity.ok(result); + } catch (Exception e) { + log.error("获取操作列表失败: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "获取操作列表失败: " + e.getMessage())); + } + } + + /** + * 获取操作配置(包含关联的模型信息) + */ + @GetMapping("/{operationType}") + public ResponseEntity getOperation(@PathVariable String operationType) { + try { + Optional operationOpt = persistenceManager.getOperationConfig(operationType); + + if (operationOpt.isEmpty()) { + return ResponseEntity.notFound().build(); + } + + OperationConfigData operation = operationOpt.get(); + + // 如果有关联模型,获取模型信息 + if (operation.getModelName() != null) { + Map models = persistenceManager.getAllModelConfigs(); + ModelConfigData model = models.get(operation.getModelName()); + + Map result = new HashMap<>(); + result.put("operation", operation); + result.put("associatedModel", model); + return ResponseEntity.ok(result); + } + + return ResponseEntity.ok(operation); + } catch (Exception e) { + log.error("获取操作配置失败: {} - {}", operationType, e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "获取操作配置失败: " + e.getMessage())); + } + } + + /** + * 保存操作配置 + */ + @PostMapping("/{operationType}") + public ResponseEntity> saveOperationConfig( + @PathVariable String operationType, + @Valid @RequestBody OperationConfigData config) { + Map result = new HashMap<>(); + try { + persistenceManager.saveOperationConfig(operationType, config); + + result.put("success", true); + result.put("message", "操作配置保存成功"); + result.put("operationType", operationType); + + return ResponseEntity.ok(result); + } catch (Exception e) { + log.error("保存操作配置失败: {} - {}", operationType, e.getMessage()); + result.put("success", false); + result.put("message", "保存失败: " + e.getMessage()); + return ResponseEntity.badRequest().body(result); + } + } + + /** + * 批量设置操作模型映射 + */ + @PostMapping("/mappings") + public ResponseEntity> setOperationMappings( + @RequestBody Map mappings) { + Map result = new HashMap<>(); + int successCount = 0; + int failCount = 0; + Map errors = new HashMap<>(); + + try { + for (Map.Entry entry : mappings.entrySet()) { + try { + String operationType = entry.getKey(); + String modelName = entry.getValue(); + + // 获取现有操作配置 + Optional configOpt = persistenceManager.getOperationConfig(operationType); + OperationConfigData config; + + if (configOpt.isPresent()) { + config = configOpt.get(); + config.setModelName(modelName); + } else { + // 创建新的操作配置 + config = new OperationConfigData(); + config.setModelName(modelName); + config.setEnabled(true); + config.setDescription("通过映射设置创建的配置"); + } + + persistenceManager.saveOperationConfig(operationType, config); + successCount++; + } catch (Exception e) { + failCount++; + errors.put(entry.getKey(), e.getMessage()); + } + } + + result.put("success", failCount == 0); + result.put("successCount", successCount); + result.put("failCount", failCount); + result.put("total", mappings.size()); + + if (failCount > 0) { + result.put("errors", errors); + result.put("message", "部分操作映射设置失败"); + } else { + result.put("message", "所有操作映射设置成功"); + } + + return failCount > 0 ? ResponseEntity.badRequest().body(result) : ResponseEntity.ok(result); + } catch (Exception e) { + log.error("设置操作映射失败: {}", e.getMessage()); + result.put("success", false); + result.put("message", "设置失败: " + e.getMessage()); + return ResponseEntity.badRequest().body(result); + } + } + + /** + * 设置单个操作模型映射 + */ + @PostMapping("/{operationType}/mapping") + public ResponseEntity> setOperationMapping( + @PathVariable String operationType, + @RequestBody Map request) { + Map result = new HashMap<>(); + try { + String modelName = request.get("modelName"); + if (modelName == null || modelName.trim().isEmpty()) { + result.put("success", false); + result.put("message", "模型名称不能为空"); + return ResponseEntity.badRequest().body(result); + } + + // 获取现有操作配置 + Optional configOpt = persistenceManager.getOperationConfig(operationType); + OperationConfigData config; + + if (configOpt.isPresent()) { + config = configOpt.get(); + config.setModelName(modelName); + } else { + // 创建新的操作配置 + config = new OperationConfigData(); + config.setModelName(modelName); + config.setEnabled(true); + config.setDescription("通过映射设置创建的配置"); + } + + persistenceManager.saveOperationConfig(operationType, config); + + result.put("success", true); + result.put("message", "操作映射设置成功"); + result.put("operationType", operationType); + result.put("modelName", modelName); + + return ResponseEntity.ok(result); + } catch (Exception e) { + log.error("设置操作映射失败: {} - {}", operationType, e.getMessage()); + result.put("success", false); + result.put("message", "设置失败: " + e.getMessage()); + return ResponseEntity.badRequest().body(result); + } + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AISystemController.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AISystemController.java new file mode 100644 index 0000000..5144be6 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AISystemController.java @@ -0,0 +1,132 @@ +package io.github.timemachinelab.sfchain.controller; + +import io.github.timemachinelab.sfchain.persistence.ModelConfigData; +import io.github.timemachinelab.sfchain.persistence.OperationConfigData; +import io.github.timemachinelab.sfchain.persistence.PersistenceManager; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * 描述: AI系统管理控制器 + * 提供系统概览、备份、刷新、重置等系统级功能 + * + * @author suifeng + * 日期: 2025/8/11 + */ +@Slf4j +@RestController +@RequestMapping("/sf/api/system") +@RequiredArgsConstructor +@CrossOrigin(origins = "*") +public class AISystemController { + + private final PersistenceManager persistenceManager; + + /** + * 获取AI系统概览信息 + */ + @GetMapping("/overview") + public ResponseEntity> getSystemOverview() { + Map overview = new HashMap<>(); + try { + // 模型统计 + Map models = persistenceManager.getAllModelConfigs(); + overview.put("totalModels", models.size()); + overview.put("enabledModels", models.values().stream() + .mapToInt(m -> Boolean.TRUE.equals(m.getEnabled()) ? 1 : 0).sum()); + + // 操作统计 + Map configs = persistenceManager.getAllOperationConfigs(); + overview.put("totalOperations", configs.size()); + overview.put("configuredOperations", configs.values().stream() + .mapToInt(config -> config.getModelName() != null && !config.getModelName().isEmpty() ? 1 : 0).sum()); + + // 配置统计 + overview.put("totalConfigs", configs.size()); + + // 系统状态 + overview.put("systemStatus", "running"); + overview.put("lastUpdate", System.currentTimeMillis()); + + return ResponseEntity.ok(overview); + } catch (Exception e) { + log.error("获取系统概览失败: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "获取系统概览失败: " + e.getMessage())); + } + } + + /** + * 系统配置备份 + */ + @PostMapping("/backup") + public ResponseEntity> createBackup() { + Map result = new HashMap<>(); + try { + String backupName = "backup_" + System.currentTimeMillis(); + persistenceManager.createBackup(backupName); + + result.put("success", true); + result.put("message", "配置备份创建成功"); + result.put("backupName", backupName); + + return ResponseEntity.ok(result); + } catch (Exception e) { + log.error("创建备份失败: {}", e.getMessage()); + result.put("success", false); + result.put("message", "备份失败: " + e.getMessage()); + return ResponseEntity.badRequest().body(result); + } + } + + /** + * 刷新系统配置 + */ + @PostMapping("/refresh") + public ResponseEntity> refreshSystem() { + Map result = new HashMap<>(); + try { + persistenceManager.flushConfigurations(); + + result.put("success", true); + result.put("message", "系统配置刷新成功"); + result.put("timestamp", System.currentTimeMillis()); + + return ResponseEntity.ok(result); + } catch (Exception e) { + log.error("刷新系统配置失败: {}", e.getMessage()); + result.put("success", false); + result.put("message", "刷新失败: " + e.getMessage()); + return ResponseEntity.badRequest().body(result); + } + } + + /** + * 重置系统配置 + */ + @PostMapping("/reset") + public ResponseEntity> resetSystem() { + Map result = new HashMap<>(); + try { + // 重新加载配置(替代原来的resetOperationMappingsToDefault方法) + persistenceManager.reloadConfigurations(); + + result.put("success", true); + result.put("message", "系统配置重置成功"); + result.put("timestamp", System.currentTimeMillis()); + + return ResponseEntity.ok(result); + } catch (Exception e) { + log.error("重置系统配置失败: {}", e.getMessage()); + result.put("success", false); + result.put("message", "重置失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result); + } + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/AIModel.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/AIModel.java new file mode 100644 index 0000000..32cd7ba --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/AIModel.java @@ -0,0 +1,40 @@ +package io.github.timemachinelab.sfchain.core; + +/** + * 描述: AI模型接口 + * @author suifeng + * 日期: 2025/8/11 + */ +public interface AIModel { + + /** + * 获取模型名称 + */ + String getName(); + + /** + * 获取模型描述 + */ + String description(); + + /** + * 生成文本响应 + * @param prompt 提示词 + * @return 生成的文本 + */ + String generate(String prompt); + + /** + * 生成指定类型的响应 + * @param prompt 提示词 + * @param responseType 响应类型 + * @return 生成的响应对象 + */ + T generate(String prompt, Class responseType); + + /** + * 检查模型是否可用 + * @return 是否可用 + */ + boolean isAvailable(); +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/AIOperationRegistry.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/AIOperationRegistry.java new file mode 100644 index 0000000..d91f765 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/AIOperationRegistry.java @@ -0,0 +1,158 @@ +package io.github.timemachinelab.sfchain.core; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 描述: AI操作注册中心 - 新框架版本 + * 管理AI操作和模型的映射关系 + * + * @author suifeng + * 日期: 2025/8/11 + */ +@Slf4j +@Component +@ConfigurationProperties(prefix = "ai.operations") +public class AIOperationRegistry { + + /** + * 操作到实例的映射 + */ + private final Map> operationMap = new ConcurrentHashMap<>(); + + /** + * 操作到模型的映射配置 + * -- GETTER -- + * 获取模型映射配置(用于配置文件绑定) + * -- SETTER -- + * 设置模型映射配置(用于配置文件绑定) + */ + @Setter + @Getter + private Map modelMapping = new ConcurrentHashMap<>(); + + /** + * 操作的默认配置 + * -- GETTER -- + * 获取操作配置(用于配置文件绑定) + * -- SETTER -- + * 设置操作配置(用于配置文件绑定) + */ + @Setter + @Getter + private Map configs = new ConcurrentHashMap<>(); + + @Resource + private ModelRegistry modelRegistry; + + /** + * 注册操作 + * + * @param operationType 操作类型 + * @param operation 操作实例 + */ + public void registerOperation(String operationType, BaseAIOperation operation) { + operationMap.put(operationType, operation); + log.info("注册AI操作: {} -> {}", operationType, operation.getClass().getSimpleName()); + } + + /** + * 获取操作实例 + * + * @param operationType 操作类型 + * @return 操作实例 + */ + public BaseAIOperation getOperation(String operationType) { + BaseAIOperation operation = operationMap.get(operationType); + if (operation == null) { + throw new IllegalArgumentException("未找到操作: " + operationType); + } + return operation; + } + + /** + * 获取操作对应的模型 + * + * @param operationType 操作类型 + * @return 模型名称 + */ + public String getModelForOperation(String operationType) { + // 优先从内存缓存获取 + String cachedModel = modelMapping.get(operationType); + if (cachedModel != null) { + return cachedModel; + } + + // 如果缓存中没有,可以从 PersistenceManager 获取操作配置中的模型名称 + return null; // 或者注入 PersistenceManager 来获取 + } + + /** + * 设置操作的模型映射 + * + * @param operationType 操作类型 + * @param modelName 模型名称 + */ + public void setModelForOperation(String operationType, String modelName) { + // 验证模型是否存在 + AIModel model = modelRegistry.getModel(modelName); + if (model == null) { + throw new IllegalArgumentException("模型不存在: " + modelName); + } + + modelMapping.put(operationType, modelName); + log.info("设置操作模型映射: {} -> {}", operationType, modelName); + } + + /** + * 获取操作配置 + * + * @param operationType 操作类型 + * @return 操作配置 + */ + public OperationConfig getOperationConfig(String operationType) { + return configs.getOrDefault(operationType, new OperationConfig()); + } + + /** + * 获取所有已注册的操作 + * + * @return 操作类型列表 + */ + public List getAllOperations() { + return List.copyOf(operationMap.keySet()); + } + + /** + * 检查操作是否已注册 + * + * @param operationType 操作类型 + * @return 是否已注册 + */ + public boolean isOperationRegistered(String operationType) { + return operationMap.containsKey(operationType); + } + + /** + * 操作配置类 + */ + @Setter + @Getter + public static class OperationConfig { + private boolean enabled = true; + private int maxTokens = 4096; + private double temperature = 0.7; + private boolean requireJsonOutput = true; + private boolean supportThinking = false; + private int timeoutSeconds = 30; + private int retryCount = 2; + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/AIService.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/AIService.java new file mode 100644 index 0000000..ca5aa26 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/AIService.java @@ -0,0 +1,364 @@ +package io.github.timemachinelab.sfchain.core; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 描述: AI服务类 - 新框架版本 + * 统一管理AI操作的执行 + * + * @author suifeng + * 日期: 2025/8/11 + */ +@Slf4j +@Service +public class AIService { + + @Resource + private AIOperationRegistry operationRegistry; + + /** + * 操作执行统计 + */ + private final Map executionStats = new ConcurrentHashMap<>(); + + /** + * 执行AI操作 + * + * @param operationType 操作类型 + * @param input 输入参数 + * @param 输入类型 + * @param 输出类型 + * @return 执行结果 + */ + @SuppressWarnings("unchecked") + public OUTPUT execute(String operationType, INPUT input) { + return execute(operationType, input, null); + } + + /** + * 执行AI操作(指定模型) + * + * @param operationType 操作类型 + * @param input 输入参数 + * @param modelName 指定的模型名称 + * @param 输入类型 + * @param 输出类型 + * @return 执行结果 + */ + @SuppressWarnings("unchecked") + public OUTPUT execute(String operationType, INPUT input, String modelName) { + long startTime = System.currentTimeMillis(); + + try { + // 获取操作实例 + BaseAIOperation operation = (BaseAIOperation) operationRegistry.getOperation(operationType); + + // 检查操作是否启用 + if (!operation.isEnabled()) { + throw new IllegalStateException("操作已禁用: " + operationType); + } + + // 执行操作 + OUTPUT result = operation.execute(input, modelName); + + // 记录执行统计 + recordExecution(operationType, true, System.currentTimeMillis() - startTime); + + log.debug("AI操作执行成功: {} - 耗时: {}ms", operationType, System.currentTimeMillis() - startTime); + + return result; + + } catch (Exception e) { + // 记录执行统计 + recordExecution(operationType, false, System.currentTimeMillis() - startTime); + + log.error("AI操作执行失败: {} - {}", operationType, e.getMessage(), e); + throw new RuntimeException("AI操作执行失败: " + e.getMessage(), e); + } + } + + /** + * 异步执行AI操作 + * + * @param operationType 操作类型 + * @param input 输入参数 + * @param 输入类型 + * @param 输出类型 + * @return 异步执行结果 + */ + @SuppressWarnings("unchecked") + public CompletableFuture executeAsync(String operationType, INPUT input) { + return executeAsync(operationType, input, null); + } + + /** + * 异步执行AI操作(指定模型) + * + * @param operationType 操作类型 + * @param input 输入参数 + * @param modelName 指定的模型名称 + * @param 输入类型 + * @param 输出类型 + * @return 异步执行结果 + */ + @SuppressWarnings("unchecked") + public CompletableFuture executeAsync(String operationType, INPUT input, String modelName) { + return CompletableFuture.supplyAsync(() -> execute(operationType, input, modelName)); + } + + /** + * 批量执行AI操作 + * + * @param operationType 操作类型 + * @param inputs 输入参数列表 + * @param 输入类型 + * @param 输出类型 + * @return 执行结果列表 + */ + public List executeBatch(String operationType, List inputs) { + return executeBatch(operationType, inputs, null); + } + + /** + * 批量执行AI操作(指定模型) + * + * @param operationType 操作类型 + * @param inputs 输入参数列表 + * @param modelName 指定的模型名称 + * @param 输入类型 + * @param 输出类型 + * @return 执行结果列表 + */ + public List executeBatch(String operationType, List inputs, String modelName) { + return inputs.parallelStream() + .map(input -> this.execute(operationType, input, modelName)) + .toList(); + } + + /** + * 异步批量执行AI操作 + * + * @param operationType 操作类型 + * @param inputs 输入参数列表 + * @param 输入类型 + * @param 输出类型 + * @return 异步执行结果列表 + */ + public CompletableFuture> executeBatchAsync(String operationType, List inputs) { + return executeBatchAsync(operationType, inputs, null); + } + + /** + * 异步批量执行AI操作(指定模型) + * + * @param operationType 操作类型 + * @param inputs 输入参数列表 + * @param modelName 指定的模型名称 + * @param 输入类型 + * @param 输出类型 + * @return 异步执行结果列表 + */ + public CompletableFuture> executeBatchAsync(String operationType, List inputs, String modelName) { + List> futures = inputs.stream() + .map(input -> this.executeAsync(operationType, input, modelName)) + .toList(); + + return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) + .thenApply(v -> futures.stream() + .map(CompletableFuture::join) + .toList()); + } + + /** + * 获取所有可用的操作 + * + * @return 操作类型列表 + */ + public List getAvailableOperations() { + return operationRegistry.getAllOperations(); + } + + /** + * 检查操作是否可用 + * + * @param operationType 操作类型 + * @return 是否可用 + */ + public boolean isOperationAvailable(String operationType) { + try { + BaseAIOperation operation = operationRegistry.getOperation(operationType); + return operation != null && operation.isEnabled(); + } catch (Exception e) { + return false; + } + } + + /** + * 获取操作信息 + * + * @param operationType 操作类型 + * @return 操作信息 + */ + public OperationInfo getOperationInfo(String operationType) { + BaseAIOperation operation = operationRegistry.getOperation(operationType); + if (operation == null) { + return null; + } + + return OperationInfo.builder() + .operationType(operationType) + .description(operation.getDescription()) + .inputType(operation.getInputType()) + .outputType(operation.getOutputType()) + .enabled(operation.isEnabled()) + .supportedModels(operation.getSupportedModels()) + .defaultModel(operation.getAnnotation().defaultModel()) + .build(); + } + + /** + * 获取操作执行统计 + * + * @param operationType 操作类型 + * @return 执行统计 + */ + public ExecutionStats getExecutionStats(String operationType) { + return executionStats.getOrDefault(operationType, new ExecutionStats()); + } + + /** + * 获取所有操作的执行统计 + * + * @return 执行统计映射 + */ + public Map getAllExecutionStats() { + return Map.copyOf(executionStats); + } + + /** + * 清空执行统计 + */ + public void clearExecutionStats() { + executionStats.clear(); + } + + /** + * 记录执行统计 + * + * @param operationType 操作类型 + * @param success 是否成功 + * @param duration 执行时长 + */ + private void recordExecution(String operationType, boolean success, long duration) { + executionStats.computeIfAbsent(operationType, k -> new ExecutionStats()) + .record(success, duration); + } + + /** + * 操作信息类 + */ + @Getter + public static class OperationInfo { + // Getters + private String operationType; + private String description; + private Class inputType; + private Class outputType; + private boolean enabled; + private String[] supportedModels; + private String defaultModel; + + public static OperationInfoBuilder builder() { + return new OperationInfoBuilder(); + } + + public static class OperationInfoBuilder { + private OperationInfo info = new OperationInfo(); + + public OperationInfoBuilder operationType(String operationType) { + info.operationType = operationType; + return this; + } + + public OperationInfoBuilder description(String description) { + info.description = description; + return this; + } + + public OperationInfoBuilder inputType(Class inputType) { + info.inputType = inputType; + return this; + } + + public OperationInfoBuilder outputType(Class outputType) { + info.outputType = outputType; + return this; + } + + public OperationInfoBuilder enabled(boolean enabled) { + info.enabled = enabled; + return this; + } + + public OperationInfoBuilder supportedModels(String[] supportedModels) { + info.supportedModels = supportedModels; + return this; + } + + public OperationInfoBuilder defaultModel(String defaultModel) { + info.defaultModel = defaultModel; + return this; + } + + public OperationInfo build() { + return info; + } + } + } + + /** + * 执行统计类 + */ + public static class ExecutionStats { + private long totalExecutions = 0; + @Getter + private long successfulExecutions = 0; + @Getter + private long failedExecutions = 0; + @Getter + private long totalDuration = 0; + private long minDuration = Long.MAX_VALUE; + private long maxDuration = 0; + + public synchronized void record(boolean success, long duration) { + totalExecutions++; + if (success) { + successfulExecutions++; + } else { + failedExecutions++; + } + + totalDuration += duration; + minDuration = Math.min(minDuration, duration); + maxDuration = Math.max(maxDuration, duration); + } + + public double getSuccessRate() { + return totalExecutions > 0 ? (double) successfulExecutions / totalExecutions : 0.0; + } + + public double getAverageDuration() { + return totalExecutions > 0 ? (double) totalDuration / totalExecutions : 0.0; + } + + public long getMinDuration() { return minDuration == Long.MAX_VALUE ? 0 : minDuration; } + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/BaseAIOperation.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/BaseAIOperation.java new file mode 100644 index 0000000..6c07c6e --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/BaseAIOperation.java @@ -0,0 +1,400 @@ +package io.github.timemachinelab.sfchain.core; + +import com.alibaba.fastjson.JSONObject; +import io.github.timemachinelab.sfchain.annotation.AIOp; +import io.github.timemachinelab.sfchain.core.openai.OpenAICompatibleModel; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.annotation.PostConstruct; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import static io.github.timemachinelab.sfchain.constants.AIOperationConstant.JSON_REPAIR_OP; + +/** + * 描述: AI操作抽象基类 - 新框架版本 + * 提供统一的AI操作接口和实现 + * + * @author suifeng + * 日期: 2025/8/11 + */ +@Slf4j +public abstract class BaseAIOperation { + + @Autowired + protected AIOperationRegistry operationRegistry; + + @Autowired + protected ModelRegistry modelRegistry; + + @Autowired + protected ObjectMapper objectMapper; + + /** + * 操作的注解信息 + * -- GETTER -- + * 获取注解信息 + * + * @return 注解信息 + */ + @Getter + private AIOp annotation; + + /** + * 输入类型 + * -- GETTER -- + * 获取输入类型 + * + * @return 输入类型 + + */ + @Getter + private Class inputType; + + /** + * 输出类型 + * -- GETTER -- + * 获取输出类型 + * + * @return 输出类型 + + */ + @Getter + private Class outputType; + + /** + * 初始化方法 + */ + @PostConstruct + @SuppressWarnings("unchecked") + public void init() { + // 获取注解信息 + this.annotation = this.getClass().getAnnotation(AIOp.class); + if (annotation == null) { + throw new IllegalStateException("AI操作类必须使用@AIOp注解: " + this.getClass().getSimpleName()); + } + + // 获取泛型类型 + Type superClass = this.getClass().getGenericSuperclass(); + if (superClass instanceof ParameterizedType parameterizedType) { + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + if (typeArguments.length >= 2) { + this.inputType = (Class) typeArguments[0]; + this.outputType = (Class) typeArguments[1]; + } + } + + // 注册到操作注册中心 + operationRegistry.registerOperation(annotation.value(), this); + + // 如果注解中有默认模型且当前没有设置模型映射,则自动设置 + if (!annotation.defaultModel().isEmpty()) { + String currentModel = operationRegistry.getModelForOperation(annotation.value()); + if (currentModel == null) { + try { + // 验证模型是否存在 + if (modelRegistry.getModel(annotation.defaultModel()) != null) { + operationRegistry.setModelForOperation(annotation.value(), annotation.defaultModel()); + log.info("自动设置操作默认模型映射: {} -> {}", annotation.value(), annotation.defaultModel()); + } + } catch (Exception e) { + log.warn("无法设置默认模型映射 {} -> {}: {}", annotation.value(), annotation.defaultModel(), e.getMessage()); + } + } + } + + log.info("初始化AI操作: {} [{}] -> 输入类型: {}, 输出类型: {}", + annotation.value(), this.getClass().getSimpleName(), + inputType != null ? inputType.getSimpleName() : "Unknown", + outputType != null ? outputType.getSimpleName() : "Unknown"); + } + + /** + * 执行AI操作 + * + * @param input 输入参数 + * @return 输出结果 + */ + public OUTPUT execute(INPUT input) { + return execute(input, null); + } + + /** + * 执行AI操作(指定模型) + * + * @param input 输入参数 + * @param modelName 指定的模型名称,为null时使用默认模型 + * @return 输出结果 + */ + public OUTPUT execute(INPUT input, String modelName) { + try { + // 获取模型 + AIModel model = getModel(modelName); + + // 构建提示词 + String prompt = buildPrompt(input); + + // 获取操作配置 + AIOperationRegistry.OperationConfig config = operationRegistry.getOperationConfig(annotation.value()); + + // 合并注解配置和运行时配置 + Integer finalMaxTokens = config.getMaxTokens() > 0 ? Integer.valueOf(config.getMaxTokens()) : (annotation.defaultMaxTokens() > 0 ? annotation.defaultMaxTokens() : null); + Double finalTemperature = config.getTemperature() >= 0 ? Double.valueOf(config.getTemperature()) : (annotation.defaultTemperature() >= 0 ? annotation.defaultTemperature() : null); + Boolean finalJsonOutput = config.isRequireJsonOutput() || annotation.requireJsonOutput(); + boolean finalThinking = config.isSupportThinking() || annotation.supportThinking(); + + // 调用AI模型 - 根据模型类型选择合适的方法 + String response; + if (model instanceof OpenAICompatibleModel openAIModel) { + + if (finalThinking) { + // 使用思考模式 + response = openAIModel.generateWithThinking(prompt, finalMaxTokens, finalTemperature); + } else { + // 使用普通模式 + response = openAIModel.generate(prompt, finalMaxTokens, finalTemperature, finalJsonOutput); + } + } else { + // 对于其他类型的模型,使用基础接口 + response = model.generate(prompt); + } + + // 解析响应 + return parseResponse(response, input); + } catch (Exception e) { + log.error("执行AI操作失败: {} - {}", annotation.value(), e.getMessage(), e); + throw new RuntimeException("AI操作执行失败: " + e.getMessage(), e); + } + } + + /** + * 构建提示词(子类实现) + * + * @param input 输入参数 + * @return 提示词 + */ + protected abstract String buildPrompt(INPUT input); + + /** + * 解析AI响应(最终方法,子类不应重写) + * + * @param response AI响应 + * @param input 输入参数 + * @return 解析后的结果 + */ + protected final OUTPUT parseResponse(String response, INPUT input) { + if (outputType == String.class) { + return (OUTPUT) response; + } + + try { + // 1. 预处理响应(子类可自定义) + String processedResponse = preprocessResponse(response, input); + + // 2. 提取JSON内容 + String jsonContent = extractJsonFromResponse(processedResponse); + + // 3. 预处理JSON内容(子类可自定义) + String processedJson = preprocessJson(jsonContent, input); + + // 4. 解析为对象(子类可自定义解析逻辑) + return parseJsonToResult(processedJson, input, response); + + } catch (JsonProcessingException e) { + // 如果启用了自动JSON修复且需要JSON输出,尝试修复JSON + if (annotation.requireJsonOutput() && annotation.autoRepairJson()) { + log.warn("JSON解析失败,尝试自动修复: {}", e.getMessage()); + try { + // 通过操作注册中心获取JSON修复操作,避免循环依赖 + BaseAIOperation jsonRepairOp = operationRegistry.getOperation(JSON_REPAIR_OP); + if (jsonRepairOp != null) { + String jsonContent = extractJsonFromResponse(response); + @SuppressWarnings("unchecked") + BaseAIOperation repairOperation = (BaseAIOperation) jsonRepairOp; + JSONObject repairedJson = repairOperation.execute(jsonContent); + String repairedJsonStr = repairedJson.toJSONString(); + return parseJsonToResult(repairedJsonStr, input, response); + } + } catch (Exception repairException) { + log.error("JSON修复也失败: {}", repairException.getMessage(), repairException); + throw new RuntimeException("JSON解析和修复都失败: 原始错误=" + e.getMessage() + ", 修复错误=" + repairException.getMessage(), e); + } + } + + log.error("解析AI响应失败: {}", e.getMessage(), e); + throw new RuntimeException("解析AI响应失败: " + e.getMessage(), e); + } + } + + /** + * 预处理AI响应(子类可重写) + * 在提取JSON之前对原始响应进行处理 + * + * @param response 原始AI响应 + * @param input 输入参数 + * @return 处理后的响应 + */ + protected String preprocessResponse(String response, INPUT input) { + return response; + } + + /** + * 预处理JSON内容(子类可重写) + * 在JSON解析之前对提取的JSON字符串进行处理 + * + * @param jsonContent 提取的JSON字符串 + * @param input 输入参数 + * @return 处理后的JSON字符串 + */ + protected String preprocessJson(String jsonContent, INPUT input) { + return jsonContent; + } + + /** + * 将JSON字符串解析为结果对象(高级用法,一般用户无需重写) + * + * @param jsonContent JSON内容 + * @param input 输入参数 + * @param originalResponse 原始响应 + * @return 解析后的结果对象 + * @throws JsonProcessingException JSON解析异常 + */ + protected OUTPUT parseJsonToResult(String jsonContent, INPUT input, String originalResponse) throws JsonProcessingException { + // 先尝试用户自定义的解析方法 + OUTPUT customResult = parseResult(jsonContent, input); + if (customResult != null) { + return customResult; + } + + // 如果用户没有自定义解析,使用默认的JSON解析 + return objectMapper.readValue(jsonContent, outputType); + } + + /** + * 解析AI返回的JSON为最终结果(推荐用户重写此方法) + * 用户可以在此方法中处理AI返回的原始JSON,并转换为最终的结果对象 + * + * @param jsonContent AI返回的JSON字符串 + * @param input 输入参数 + * @return 最终结果对象,如果返回null则使用默认的JSON解析 + */ + protected OUTPUT parseResult(String jsonContent, INPUT input) { + return null; // 默认返回null,表示使用框架的默认JSON解析 + } + + /** + * 工具方法:将JSON字符串解析为指定类型的对象 + * + * @param jsonContent JSON字符串 + * @param clazz 目标类型 + * @param 泛型类型 + * @return 解析后的对象 + * @throws JsonProcessingException JSON解析异常 + */ + protected T parseJsonToObject(String jsonContent, Class clazz) throws JsonProcessingException { + return objectMapper.readValue(jsonContent, clazz); + } + + + + /** + * 从响应中提取JSON内容 + * + * @param response 原始响应 + * @return JSON字符串 + */ + protected String extractJsonFromResponse(String response) { + // 查找JSON代码块 + String jsonStart = "```json"; + String jsonEnd = "```"; + + int startIndex = response.indexOf(jsonStart); + if (startIndex != -1) { + startIndex += jsonStart.length(); + int endIndex = response.indexOf(jsonEnd, startIndex); + if (endIndex != -1) { + return response.substring(startIndex, endIndex).trim(); + } + } + + // 查找花括号包围的JSON + int braceStart = response.indexOf('{'); + int braceEnd = response.lastIndexOf('}'); + if (braceStart != -1 && braceEnd != -1 && braceEnd > braceStart) { + return response.substring(braceStart, braceEnd + 1); + } + + // 如果都找不到,返回原始响应 + return response; + } + + /** + * 获取模型实例 + * + * @param modelName 模型名称,为null时使用默认模型 + * @return 模型实例 + */ + private AIModel getModel(String modelName) { + if (modelName == null) { + // 使用注册中心配置的模型 + modelName = operationRegistry.getModelForOperation(annotation.value()); + } + + if (modelName == null) { + // 使用注解中的默认模型 + modelName = annotation.defaultModel(); + } + + if (modelName == null || modelName.isEmpty()) { + throw new IllegalStateException("未配置模型: " + annotation.value()); + } + + AIModel model = modelRegistry.getModel(modelName); + if (model == null) { + throw new IllegalArgumentException("模型不存在: " + modelName); + } + + return model; + } + + /** + * 获取操作类型 + * + * @return 操作类型 + */ + public String getOperationType() { + return annotation != null ? annotation.value() : null; + } + + /** + * 检查操作是否启用 + * + * @return 是否启用 + */ + public boolean isEnabled() { + AIOperationRegistry.OperationConfig config = operationRegistry.getOperationConfig(annotation.value()); + return config.isEnabled() && annotation.enabled(); + } + + /** + * 获取操作描述 + * + * @return 操作描述 + */ + public String getDescription() { + return annotation.description(); + } + + /** + * 获取支持的模型列表 + * + * @return 支持的模型列表 + */ + public String[] getSupportedModels() { + return annotation.supportedModels(); + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/ModelRegistry.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/ModelRegistry.java new file mode 100644 index 0000000..cc7d3b7 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/ModelRegistry.java @@ -0,0 +1,71 @@ +package io.github.timemachinelab.sfchain.core; + +import io.github.timemachinelab.sfchain.core.openai.OpenAIModelFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; + +/** + * 描述: AI模型注册中心 + * @author suifeng + * 日期: 2025/8/11 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ModelRegistry { + + private final OpenAIModelFactory modelFactory; + + /** + * 获取模型实例 + * @param modelName 模型名称 + * @return AI模型实例 + */ + public AIModel getModel(String modelName) { + try { + return modelFactory.createModel(modelName); + } catch (Exception e) { + log.error("获取模型失败: {} - {}", modelName, e.getMessage()); + throw new RuntimeException("无法获取模型: " + modelName, e); + } + } + + /** + * 检查模型是否已注册 + * @param modelName 模型名称 + * @return 是否已注册 + */ + public boolean isModelRegistered(String modelName) { + return modelFactory.isModelRegistered(modelName); + } + + /** + * 获取所有已注册的模型名称 + * @return 模型名称集合 + */ + public Set getRegisteredModelNames() { + return modelFactory.getRegisteredModelNames(); + } + + /** + * 获取可用的模型列表 + * @return 可用模型列表 + */ + public List getAvailableModels() { + return getRegisteredModelNames().stream() + .filter(modelName -> { + try { + AIModel model = getModel(modelName); + return model.isAvailable(); + } catch (Exception e) { + log.warn("检查模型可用性失败: {} - {}", modelName, e.getMessage()); + return false; + } + }) + .toList(); + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAICompatibleModel.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAICompatibleModel.java new file mode 100644 index 0000000..3337b02 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAICompatibleModel.java @@ -0,0 +1,155 @@ +package io.github.timemachinelab.sfchain.core.openai; + +import com.alibaba.fastjson2.JSON; +import io.github.timemachinelab.sfchain.core.AIModel; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; +import java.util.Map; + +/** + * 描述: OpenAI兼容的通用模型实现 + * @author suifeng + * 日期: 2025/8/11 + */ +@Slf4j +public class OpenAICompatibleModel implements AIModel { + + /** + * -- GETTER -- + * 获取模型配置 + */ + @Getter + private final OpenAIModelConfig config; + private final OpenAIHttpClient httpClient; + + public OpenAICompatibleModel(OpenAIModelConfig config) { + if (!config.isValid()) { + throw new IllegalArgumentException("模型配置无效: " + config); + } + + this.config = config; + this.httpClient = new OpenAIHttpClient( + config.getBaseUrl(), + config.getApiKey(), + config.getAdditionalHeaders() + ); + + log.info("初始化OpenAI兼容模型: {} ({})", config.getModelName(), config.getProvider()); + } + + @Override + public String getName() { + return config.getModelName(); + } + + @Override + public String description() { + return config.getDescription() != null ? config.getDescription() : + String.format("%s模型 (提供商: %s)", config.getModelName(), config.getProvider()); + } + + @Override + public String generate(String prompt) { + return generate(prompt, null, null, null); + } + + @Override + public T generate(String prompt, Class responseType) { + String result = generate(prompt); + if (responseType == String.class) { + return responseType.cast(result); + } + + try { + return JSON.parseObject(result, responseType); + } catch (Exception e) { + log.error("解析响应为{}类型失败: {}", responseType.getSimpleName(), e.getMessage()); + throw new RuntimeException("响应解析失败: " + e.getMessage(), e); + } + } + + /** + * 生成响应 - 支持自定义参数 + */ + public String generate(String prompt, Integer maxTokens, Double temperature, Boolean jsonOutput) { + try { + OpenAIRequest request = buildRequest(prompt, maxTokens, temperature, jsonOutput); + OpenAIResponse response = httpClient.chatCompletion(request); + return httpClient.extractContent(response); + } catch (Exception e) { + log.error("模型{}生成失败", config.getModelName(), e); + throw new RuntimeException("模型生成失败: " + e.getMessage(), e); + } + } + + /** + * 生成响应 - 支持思考模式 + */ + public String generateWithThinking(String prompt, Integer maxTokens, Double temperature) { + if (!Boolean.TRUE.equals(config.getSupportThinking())) { + log.warn("模型{}不支持思考模式,使用普通模式", config.getModelName()); + return generate(prompt, maxTokens, temperature, null); + } + + try { + OpenAIRequest request = buildRequestWithThinking(prompt, maxTokens, temperature); + OpenAIResponse response = httpClient.chatCompletion(request); + return httpClient.extractContent(response); + } catch (Exception e) { + log.error("模型{}思考模式生成失败", config.getModelName(), e); + throw new RuntimeException("思考模式生成失败: " + e.getMessage(), e); + } + } + + /** + * 构建请求对象 + */ + private OpenAIRequest buildRequest(String prompt, Integer maxTokens, Double temperature, Boolean jsonOutput) { + var builder = OpenAIRequest.builder() + .model(config.getModelName()) + .messages(List.of( + OpenAIRequest.Message.builder() + .role("user") + .content(prompt) + .build() + )) + .max_tokens(maxTokens != null ? maxTokens : config.getDefaultMaxTokens()) + .temperature(temperature != null ? temperature : config.getDefaultTemperature()) + .stream(false); + + // 设置JSON输出格式 + if (Boolean.TRUE.equals(jsonOutput) && Boolean.TRUE.equals(config.getSupportJsonOutput())) { + builder.response_format(Map.of("type", "json_object")); + } + + return builder.build(); + } + + /** + * 构建带思考模式的请求对象 + */ + private OpenAIRequest buildRequestWithThinking(String prompt, Integer maxTokens, Double temperature) { + return OpenAIRequest.builder() + .model(config.getModelName()) + .messages(List.of( + OpenAIRequest.Message.builder() + .role("user") + .content(prompt) + .build() + )) + .max_tokens(maxTokens != null ? maxTokens : config.getDefaultMaxTokens()) + .temperature(temperature != null ? temperature : config.getDefaultTemperature()) + .stream(false) + .enable_thinking(true) + .build(); + } + + /** + * 检查模型是否可用 + */ + public boolean isAvailable() { + return Boolean.TRUE.equals(config.getEnabled()) && config.isValid(); + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIHttpClient.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIHttpClient.java new file mode 100644 index 0000000..be43113 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIHttpClient.java @@ -0,0 +1,143 @@ +package io.github.timemachinelab.sfchain.core.openai; + +import com.alibaba.fastjson2.JSON; +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * 描述: OpenAI兼容的HTTP客户端 + * @author suifeng + * 日期: 2025/8/11 + */ +@Slf4j +public class OpenAIHttpClient { + + private final String baseUrl; + private final String apiKey; + private final Map defaultHeaders; + + public OpenAIHttpClient(String baseUrl, String apiKey) { + this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + this.apiKey = apiKey; + this.defaultHeaders = Map.of( + "Content-Type", "application/json", + "Authorization", "Bearer " + apiKey + ); + } + + public OpenAIHttpClient(String baseUrl, String apiKey, Map additionalHeaders) { + this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + this.apiKey = apiKey; + this.defaultHeaders = new HashMap<>(); + this.defaultHeaders.put("Content-Type", "application/json"); + this.defaultHeaders.put("Authorization", "Bearer " + apiKey); + if (additionalHeaders != null) { + this.defaultHeaders.putAll(additionalHeaders); + } + } + + /** + * 发送聊天完成请求 + */ + public OpenAIResponse chatCompletion(OpenAIRequest request) { + try { + // 智能构建endpoint,避免重复的/v1路径 + String endpoint; + if (baseUrl.endsWith("/v1") || baseUrl.contains("/v1/")) { + // baseUrl已包含v1路径,直接添加chat/completions + endpoint = baseUrl + (baseUrl.endsWith("/") ? "" : "/") + "chat/completions"; + } else { + // baseUrl不包含v1路径,添加完整路径 + endpoint = baseUrl + "/v1/chat/completions"; + } + String requestBody = JSON.toJSONString(request); + + log.debug("发送请求到: {}", endpoint); + log.debug("请求体: {}", requestBody); + log.info("构建的API端点: {}", endpoint); + + HttpURLConnection connection = createConnection(endpoint); + + // 发送请求体 + try (OutputStream os = connection.getOutputStream()) { + byte[] input = requestBody.getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } + + // 读取响应 + StringBuilder response = new StringBuilder(); + int responseCode = connection.getResponseCode(); + + if (responseCode == HttpURLConnection.HTTP_OK) { + try (BufferedReader br = new BufferedReader( + new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + response.append(line); + } + } + } else { + // 读取错误响应 + try (BufferedReader br = new BufferedReader( + new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + response.append(line); + } + } + throw new RuntimeException("HTTP请求失败,状态码: " + responseCode + ", 响应: " + response.toString()); + } + + String responseBody = response.toString(); + log.debug("响应体: {}", responseBody); + + return JSON.parseObject(responseBody, OpenAIResponse.class); + + } catch (Exception e) { + log.error("OpenAI API调用失败", e); + throw new RuntimeException("OpenAI API调用失败: " + e.getMessage(), e); + } + } + + /** + * 创建HTTP连接 + */ + private HttpURLConnection createConnection(String endpoint) throws Exception { + URL url = new URL(endpoint); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setConnectTimeout(30000); // 30秒连接超时 + connection.setReadTimeout(120000); // 120秒读取超时 + + // 设置请求头 + defaultHeaders.forEach(connection::setRequestProperty); + + return connection; + } + + /** + * 提取响应内容 + */ + public String extractContent(OpenAIResponse response) { + if (response == null || response.getChoices() == null || response.getChoices().isEmpty()) { + return ""; + } + + OpenAIResponse.Choice choice = response.getChoices().get(0); + if (choice.getMessage() != null && choice.getMessage().getContent() != null) { + return choice.getMessage().getContent(); + } + + return ""; + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIModelConfig.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIModelConfig.java new file mode 100644 index 0000000..f7b2a42 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIModelConfig.java @@ -0,0 +1,120 @@ +package io.github.timemachinelab.sfchain.core.openai; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +/** + * 描述: OpenAI兼容的模型配置 + * @author suifeng + * 日期: 2025/8/11 + */ +@Data +@Builder(toBuilder = true) +@AllArgsConstructor +@NoArgsConstructor +public class OpenAIModelConfig { + + /** + * 模型名称 + */ + private String modelName; + + /** + * API基础URL + */ + private String baseUrl; + + /** + * API密钥 + */ + private String apiKey; + + /** + * 默认最大token数 + */ + private Integer defaultMaxTokens; + + /** + * 默认温度参数 + */ + private Double defaultTemperature; + + /** + * 是否支持流式输出 + */ + private Boolean supportStream; + + /** + * 是否支持JSON格式输出 + */ + private Boolean supportJsonOutput; + + /** + * 是否支持思考模式 + */ + private Boolean supportThinking; + + /** + * 额外的HTTP请求头 + */ + private Map additionalHeaders; + + /** + * 模型描述 + */ + private String description; + + /** + * 模型提供商 + */ + private String provider; + + /** + * 是否启用 + */ + private Boolean enabled; + + /** + * 获取额外请求头,如果为null则返回空Map + */ + public Map getAdditionalHeaders() { + return additionalHeaders != null ? additionalHeaders : new HashMap<>(); + } + + /** + * 添加额外请求头 + */ + public void addHeader(String key, String value) { + if (additionalHeaders == null) { + additionalHeaders = new HashMap<>(); + } + additionalHeaders.put(key, value); + } + + /** + * 检查配置是否有效 + */ + public boolean isValid() { + return modelName != null && !modelName.trim().isEmpty() && + baseUrl != null && !baseUrl.trim().isEmpty() && + apiKey != null && !apiKey.trim().isEmpty(); + } + + /** + * 获取默认配置的构建器 + */ + public static OpenAIModelConfigBuilder defaultConfig() { + return OpenAIModelConfig.builder() + .defaultMaxTokens(4096) + .defaultTemperature(0.7) + .supportStream(false) + .supportJsonOutput(false) + .supportThinking(false) + .enabled(true); + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIModelFactory.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIModelFactory.java new file mode 100644 index 0000000..1f9c3ad --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIModelFactory.java @@ -0,0 +1,79 @@ +package io.github.timemachinelab.sfchain.core.openai; + +import io.github.timemachinelab.sfchain.core.AIModel; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 描述: OpenAI兼容模型工厂 + * @author suifeng + * 日期: 2025/8/11 + */ +@Slf4j +public class OpenAIModelFactory { + + private final Map modelConfigs = new ConcurrentHashMap<>(); + private final Map modelInstances = new ConcurrentHashMap<>(); + + /** + * 注册模型配置 + */ + public void registerModel(OpenAIModelConfig config) { + if (!config.isValid()) { + throw new IllegalArgumentException("无效的模型配置: " + config.getModelName()); + } + + modelConfigs.put(config.getModelName(), config); + log.info("注册模型配置: {} ({})", config.getModelName(), config.getProvider()); + } + + /** + * 创建模型实例 + */ + public AIModel createModel(String modelName) { + return modelInstances.computeIfAbsent(modelName, name -> { + OpenAIModelConfig config = modelConfigs.get(name); + if (config == null) { + throw new IllegalArgumentException("未找到模型配置: " + name); + } + + if (!Boolean.TRUE.equals(config.getEnabled())) { + throw new IllegalStateException("模型已禁用: " + name); + } + + return new OpenAICompatibleModel(config); + }); + } + + /** + * 获取所有已注册的模型名称 + */ + public java.util.Set getRegisteredModelNames() { + return modelConfigs.keySet(); + } + + /** + * 获取模型配置 + */ + public OpenAIModelConfig getModelConfig(String modelName) { + return modelConfigs.get(modelName); + } + + /** + * 检查模型是否已注册 + */ + public boolean isModelRegistered(String modelName) { + return modelConfigs.containsKey(modelName); + } + + /** + * 移除模型 + */ + public void removeModel(String modelName) { + modelConfigs.remove(modelName); + modelInstances.remove(modelName); + log.info("移除模型: {}", modelName); + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIRequest.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIRequest.java new file mode 100644 index 0000000..181cdcf --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIRequest.java @@ -0,0 +1,102 @@ +package io.github.timemachinelab.sfchain.core.openai; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +/** + * 描述: OpenAI兼容的请求体 + * @author suifeng + * 日期: 2025/8/11 + */ +@Data +@Builder(toBuilder = true) +@AllArgsConstructor +@NoArgsConstructor +public class OpenAIRequest { + + /** + * 模型名称 + */ + private String model; + + /** + * 消息列表 + */ + private List messages; + + /** + * 最大token数 + */ + private Integer max_tokens; + + /** + * 温度参数 (0.0-2.0) + */ + private Double temperature; + + /** + * 是否流式输出 + */ + private Boolean stream; + + /** + * 响应格式 + */ + private Map response_format; + + /** + * 是否启用思考模式 (部分模型支持) + */ + private Boolean enable_thinking; + + /** + * top_p参数 + */ + private Double top_p; + + /** + * 频率惩罚 + */ + private Double frequency_penalty; + + /** + * 存在惩罚 + */ + private Double presence_penalty; + + /** + * 停止词 + */ + private List stop; + + /** + * 用户标识 + */ + private String user; + + @Data + @Builder(toBuilder = true) + @AllArgsConstructor + @NoArgsConstructor + public static class Message { + /** + * 角色: system, user, assistant + */ + private String role; + + /** + * 消息内容 + */ + private String content; + + /** + * 消息名称 (可选) + */ + private String name; + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIResponse.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIResponse.java new file mode 100644 index 0000000..83a80da --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/openai/OpenAIResponse.java @@ -0,0 +1,108 @@ +package io.github.timemachinelab.sfchain.core.openai; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 描述: OpenAI兼容的响应体 + * @author suifeng + * 日期: 2025/8/11 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class OpenAIResponse { + + /** + * 响应ID + */ + private String id; + + /** + * 对象类型 + */ + private String object; + + /** + * 创建时间戳 + */ + private Long created; + + /** + * 模型名称 + */ + private String model; + + /** + * 选择列表 + */ + private List choices; + + /** + * 使用情况 + */ + private Usage usage; + + /** + * 系统指纹 + */ + private String system_fingerprint; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Choice { + /** + * 选择索引 + */ + private Integer index; + + /** + * 消息内容 + */ + private OpenAIRequest.Message message; + + /** + * 完成原因 + */ + private String finish_reason; + + /** + * logprobs (可选) + */ + private Object logprobs; + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Usage { + /** + * 提示token数 + */ + private Integer prompt_tokens; + + /** + * 完成token数 + */ + private Integer completion_tokens; + + /** + * 总token数 + */ + private Integer total_tokens; + + /** + * 提示token详情 (可选) + */ + private Object prompt_tokens_details; + + /** + * 完成token详情 (可选) + */ + private Object completion_tokens_details; + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/operations/JSONRepairOperation.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/operations/JSONRepairOperation.java new file mode 100644 index 0000000..bdde4a7 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/operations/JSONRepairOperation.java @@ -0,0 +1,141 @@ +package io.github.timemachinelab.sfchain.operations; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import io.github.timemachinelab.sfchain.annotation.AIOp; +import io.github.timemachinelab.sfchain.core.BaseAIOperation; +import org.springframework.stereotype.Component; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static io.github.timemachinelab.sfchain.constants.AIOperationConstant.JSON_REPAIR_OP; + + +/** + * 描述: JSON修复操作 + * 专门用于修复AI返回的格式错误的JSON字符串 + * @author suifeng + * 日期: 2025/8/11 + */ +@AIOp(value = JSON_REPAIR_OP, + description = "修复格式错误的JSON字符串", + defaultModel = "deepseek-chat", + supportedModels = {"deepseek-chat", "gpt-4o-mini", "qwen-turbo"}, + requireJsonOutput = false, + autoRepairJson = false) +@Component +public class JSONRepairOperation extends BaseAIOperation { + + /** + * 当前输入的JSON字符串 + */ + private String currentInput; + + @Override + public String buildPrompt(String brokenJson) { + this.currentInput = brokenJson; // 保存当前输入 + + // 如果输入已经是有效JSON,尝试直接解析 + if (isValidJson(brokenJson)) { + return ""; // 返回空字符串表示不需要AI修复 + } + + return String.format(""" + 你是一位专业的JSON格式修复专家,需要将格式错误的JSON字符串修复为有效的JSON格式。 + + ## 任务描述 + 我将提供一个可能包含格式错误的JSON字符串。你的任务是: + 1. 识别并修复所有格式错误,包括但不限于: + - 缺失或多余的引号、逗号、括号 + - 非法的转义字符 + - 重复的键 + - 不符合JSON规范的值格式 + 2. 保留原始JSON的所有数据和结构 + 3. 返回修复后的有效JSON字符串 + + ## 需要修复的JSON + ``` + %s + ``` + + ## 输出要求 + 1. 只返回修复后的JSON字符串,不要有任何解释或额外文字 + 2. 确保输出是有效的JSON格式 + 3. 保持原始数据的完整性,除非格式错误导致数据冗余 + 4. 如果某部分无法修复,使用最合理的猜测进行修复 + + ## 注意事项 + - 不要给我任何多余的东西,只需要正确的JSON对象 + - 不要添加原始JSON中不存在的键或值 + - 如果原始字符串中包含多个JSON对象,只修复第一个完整的JSON对象 + - 确保所有字符串值使用双引号包围 + - 确保数字、布尔值和null值不使用引号 + - 移除任何注释或非JSON元素 + """, brokenJson); + } + + @Override + protected String preprocessResponse(String aiResponse, String brokenJson) { + // 如果提示为空(表示原始输入已是有效JSON),直接返回原始输入 + if (aiResponse.isEmpty()) { + return this.currentInput; + } + return aiResponse; + } + + @Override + protected String preprocessJson(String jsonContent, String brokenJson) { + // 如果提取失败,尝试本地修复 + if (!isValidJson(jsonContent)) { + return localJsonRepair(jsonContent); + } + return jsonContent; + } + + /** + * 检查字符串是否为有效的JSON + */ + private boolean isValidJson(String jsonStr) { + try { + JSON.parseObject(jsonStr); + return true; + } catch (Exception e) { + return false; + } + } + + /** + * 本地JSON修复逻辑,用于简单错误的修复 + */ + private String localJsonRepair(String brokenJson) { + // 1. 移除可能的代码块标记 + String json = brokenJson.replaceAll("```json|```", "").trim(); + + // 2. 确保对象以 { 开始,以 } 结束 + Pattern objectPattern = Pattern.compile("\\{.*\\}", Pattern.DOTALL); + Matcher objectMatcher = objectPattern.matcher(json); + if (objectMatcher.find()) { + json = objectMatcher.group(); + } + + // 3. 修复常见引号问题 + json = json.replaceAll("(? { + + @Override + protected String buildPrompt(ValidationRequest input) { + return String.format( + """ + 请回答一个简单的问题来验证模型是否正常工作。 + 问题: %s + 请严格作答,并以JSON格式返回结果: + ```json + { + "answer": "5" + } + ``` + 注意:请确保返回有效的JSON格式。""", + input.getQuestion() + ); + } + + @Override + protected ValidationResult parseResult(String jsonContent, ValidationRequest input) { + try { + return objectMapper.readValue(jsonContent, ValidationResult.class); + } catch (Exception e) { + log.warn("解析验证响应失败,使用默认结果: {}", e.getMessage()); + ValidationResult result = new ValidationResult(); + result.setAnswer("模型响应解析失败,但模型可以正常通信"); + return result; + } + } + + @Override + public String getDescription() { + return "模型验证操作 - 通过简单问答验证模型配置是否可用"; + } + + /** + * 验证请求 + */ + @Data + public static class ValidationRequest { + @JsonProperty("question") + private String question; + + public ValidationRequest() { + this.question = "1+1等于几?"; + } + + public ValidationRequest(String question) { + this.question = question; + } + } + + /** + * 验证结果 + */ + @Data + public static class ValidationResult { + @JsonProperty("answer") + private String answer; + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/operations/TextClassificationOperation.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/operations/TextClassificationOperation.java new file mode 100644 index 0000000..b25f47f --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/operations/TextClassificationOperation.java @@ -0,0 +1,185 @@ +package io.github.timemachinelab.sfchain.operations; + +import io.github.timemachinelab.sfchain.annotation.AIOp; +import io.github.timemachinelab.sfchain.core.BaseAIOperation; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import static io.github.timemachinelab.sfchain.constants.AIOperationConstant.TEXT_CLASSIFICATION_OP; + +/** + * 描述: 文本分类操作 - 对文本进行情感分析或主题分类 + * + * @author suifeng + * 日期: 2025/8/11 + */ +@Slf4j +@Component +@AIOp( + value = TEXT_CLASSIFICATION_OP, + description = "对输入文本进行分类,支持情感分析、主题分类等", + defaultModel = "deepseek-chat", + supportedModels = {"deepseek-chat", "gpt-4o", "siliconflow-qwen"} +) +public class TextClassificationOperation extends BaseAIOperation { + + @Override + protected String buildPrompt(ClassificationRequest request) { + return String.format(""" + 请对以下文本进行分类: + + 文本内容: + %s + + 分类类型:%s + + 请根据分类类型对文本进行分析,并以JSON格式返回结果: + + ```json + { + "category": "分类结果", + "confidence": 0.95, + "reason": "分类理由" + } + ``` + + 注意: + - category: 分类结果(如:正面/负面/中性,或具体的主题类别) + - confidence: 置信度(0-1之间的数值) + - reason: 简要说明分类的理由 + """, request.getText(), request.getClassificationType()); + } + + /** + * 解析AI返回的JSON为最终结果 + */ + @Override + protected ClassificationResult parseResult(String jsonContent, ClassificationRequest input) { + try { + // 解析AI返回的JSON + ClassificationResult result = parseJsonToObject(jsonContent, ClassificationResult.class); + + // 验证分类结果 + if (result.getCategory() == null || result.getCategory().trim().isEmpty()) { + throw new RuntimeException("分类结果为空"); + } + + // 确保置信度在合理范围内 + if (result.getConfidence() < 0.0 || result.getConfidence() > 1.0) { + result.setConfidence(Math.max(0.0, Math.min(1.0, result.getConfidence()))); + } + + // 根据请求类型进行额外的后处理 + if (input != null) { + // 例如:根据分类类型进行特殊处理 + if ("情感分析".contains(input.getClassificationType())) { + // 情感分析的特殊处理逻辑 + validateSentimentResult(result); + } else if ("垃圾邮件检测".contains(input.getClassificationType())) { + // 垃圾邮件检测的特殊处理逻辑 + validateSpamDetectionResult(result); + } + } + + return result; + } catch (Exception e) { + log.error("解析分类结果失败: {}", e.getMessage(), e); + throw new RuntimeException("解析分类结果失败: " + e.getMessage(), e); + } + } + + /** + * 验证情感分析结果 + */ + private void validateSentimentResult(ClassificationResult result) { + if (result.getCategory() != null) { + String category = result.getCategory().toLowerCase(); + if (!category.contains("正面") && !category.contains("负面") && !category.contains("中性")) { + log.warn("情感分析结果可能不准确: {}", result.getCategory()); + } + } + } + + /** + * 验证垃圾邮件检测结果 + */ + private void validateSpamDetectionResult(ClassificationResult result) { + if (result.getCategory() != null) { + String category = result.getCategory().toLowerCase(); + if (!category.contains("垃圾") && !category.contains("正常") && !category.contains("spam") && !category.contains("ham")) { + log.warn("垃圾邮件检测结果可能不准确: {}", result.getCategory()); + } + } + } + + /** + * 分类请求 + */ + @Data + public static class ClassificationRequest { + /** + * 待分类的文本 + */ + private String text; + + /** + * 分类类型(如:情感分析、主题分类、垃圾邮件检测等) + */ + private String classificationType; + + public ClassificationRequest() {} + + public ClassificationRequest(String text, String classificationType) { + this.text = text; + this.classificationType = classificationType; + } + } + + /** + * 分类结果 + */ + @Data + public static class ClassificationResult { + /** + * 分类结果 + */ + private String category; + + /** + * 置信度(0-1) + */ + private Double confidence; + + /** + * 分类理由 + */ + private String reason; + + /** + * 是否为高置信度结果 + */ + public boolean isHighConfidence() { + return confidence != null && confidence >= 0.8; + } + + /** + * 获取置信度等级 + */ + public String getConfidenceLevel() { + if (confidence == null) return "未知"; + if (confidence >= 0.9) return "很高"; + if (confidence >= 0.7) return "高"; + if (confidence >= 0.5) return "中等"; + return "低"; + } + + @Override + public String toString() { + return String.format( + "ClassificationResult{category='%s', confidence=%.2f (%s), reason='%s'}", + category, confidence, getConfidenceLevel(), reason + ); + } + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/DynamicOperationConfigService.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/DynamicOperationConfigService.java new file mode 100644 index 0000000..63f5e13 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/DynamicOperationConfigService.java @@ -0,0 +1,82 @@ +package io.github.timemachinelab.sfchain.persistence; + +import io.github.timemachinelab.sfchain.annotation.AIOp; +import io.github.timemachinelab.sfchain.core.AIOperationRegistry; +import io.github.timemachinelab.sfchain.core.BaseAIOperation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * 描述: 动态操作配置服务 + * 从@AIOp注解中获取操作配置信息 + * + * @author suifeng + * 日期: 2025/1/27 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class DynamicOperationConfigService { + + private final AIOperationRegistry operationRegistry; + + /** + * 从@AIOp注解获取操作配置 + * + * @param operationType 操作类型 + * @return 操作配置 + */ + public Optional getOperationConfig(String operationType) { + try { + if (!operationRegistry.isOperationRegistered(operationType)) { + return Optional.empty(); + } + + BaseAIOperation operation = operationRegistry.getOperation(operationType); + AIOp annotation = operation.getAnnotation(); + + if (annotation == null) { + return Optional.empty(); + } + + // 从注解构建配置 + OperationConfigData config = OperationConfigData.builder() + .operationType(operationType) + .description(annotation.description()) + .enabled(annotation.enabled()) + .maxTokens(annotation.defaultMaxTokens() > 0 ? annotation.defaultMaxTokens() : null) + .temperature(annotation.defaultTemperature() >= 0 ? annotation.defaultTemperature() : null) + .jsonOutput(annotation.requireJsonOutput()) + .thinkingMode(annotation.supportThinking()) + .modelName(annotation.defaultModel().isEmpty() ? null : annotation.defaultModel()) + .build(); + + return Optional.of(config); + } catch (Exception e) { + log.warn("获取操作 {} 的动态配置失败: {}", operationType, e.getMessage()); + return Optional.empty(); + } + } + + /** + * 获取所有操作的动态配置 + * + * @return 所有动态配置 + */ + public Map getAllOperationConfigs() { + Map configs = new HashMap<>(); + + for (String operationType : operationRegistry.getAllOperations()) { + getOperationConfig(operationType).ifPresent(config -> + configs.put(operationType, config) + ); + } + + return configs; + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/ModelConfigData.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/ModelConfigData.java new file mode 100644 index 0000000..c539f41 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/ModelConfigData.java @@ -0,0 +1,125 @@ +package io.github.timemachinelab.sfchain.persistence; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +/** + * 描述: 模型配置数据类 + * 用于持久化存储的模型配置信息 + * + * @author suifeng + * 日期: 2025/1/27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class ModelConfigData { + + /** + * 模型名称 + */ + private String modelName; + + /** + * API基础URL + */ + private String baseUrl; + + /** + * API密钥 + */ + private String apiKey; + + /** + * 默认最大token数 + */ + @Builder.Default + private Integer defaultMaxTokens = 4096; + + /** + * 默认温度参数 + */ + @Builder.Default + private Double defaultTemperature = 0.7; + + /** + * 是否支持流式输出 + */ + @Builder.Default + private Boolean supportStream = false; + + /** + * 是否支持JSON格式输出 + */ + @Builder.Default + private Boolean supportJsonOutput = false; + + /** + * 是否支持思考模式 + */ + @Builder.Default + private Boolean supportThinking = false; + + /** + * 额外的HTTP请求头 + */ + @Builder.Default + private Map additionalHeaders = new HashMap<>(); + + /** + * 模型描述 + */ + private String description; + + /** + * 模型提供商 + */ + private String provider; + + /** + * 是否启用 + */ + @Builder.Default + private Boolean enabled = true; + + /** + * 创建时间戳 + */ + private Long createdAt; + + /** + * 更新时间戳 + */ + private Long updatedAt; + + /** + * 验证配置是否有效 + * + * @return 是否有效 + */ + public boolean isValid() { + return modelName != null && !modelName.trim().isEmpty() && + baseUrl != null && !baseUrl.trim().isEmpty() && + apiKey != null && !apiKey.trim().isEmpty(); + } + + /** + * 更新时间戳 + */ + public void updateTimestamp() { + this.updatedAt = System.currentTimeMillis(); + if (this.createdAt == null) { + this.createdAt = this.updatedAt; + } + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/OperationConfigData.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/OperationConfigData.java new file mode 100644 index 0000000..220b446 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/OperationConfigData.java @@ -0,0 +1,143 @@ +package io.github.timemachinelab.sfchain.persistence; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.HashMap; +import java.util.Map; + +/** + * 描述: 操作配置数据类 + * 用于持久化存储的操作配置信息 + * + * @author suifeng + * 日期: 2025/1/27 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class OperationConfigData { + + /** + * 操作类型 + */ + private String operationType; + + /** + * 操作描述 + */ + private String description; + + /** + * 是否启用 + */ + @Builder.Default + private Boolean enabled = true; + + /** + * 最大token数 + */ + private Integer maxTokens; + + /** + * 温度参数 + */ + private Double temperature; + + /** + * 超时时间(毫秒) + */ + private Long timeout; + + /** + * 重试次数 + */ + @Builder.Default + private Integer retryCount = 3; + + /** + * 是否启用JSON输出 + */ + @Builder.Default + private Boolean jsonOutput = false; + + /** + * 是否启用流式输出 + */ + @Builder.Default + private Boolean streamOutput = false; + + /** + * 是否启用思考模式 + */ + @Builder.Default + private Boolean thinkingMode = false; + + /** + * 自定义提示词前缀 + */ + private String promptPrefix; + + /** + * 自定义提示词后缀 + */ + private String promptSuffix; + + /** + * 系统提示词 + */ + private String systemPrompt; + + /** + * 输出格式说明 + */ + private String outputFormat; + + /** + * 自定义参数 + */ + @Builder.Default + private Map customParams = new HashMap<>(); + + /** + * 关联的模型名称 + */ + private String modelName; + + /** + * 验证配置是否有效 + * @return 是否有效 + */ + public boolean isValid() { + // 基本验证:操作类型不能为空 + if (operationType == null || operationType.trim().isEmpty()) { + return false; + } + + // 验证数值范围 + if (maxTokens != null && maxTokens <= 0) { + return false; + } + + if (temperature != null && (temperature < 0.0 || temperature > 2.0)) { + return false; + } + + if (retryCount != null && retryCount < 0) { + return false; + } + + if (timeout != null && timeout <= 0) { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/PersistenceManager.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/PersistenceManager.java new file mode 100644 index 0000000..bc519ed --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/PersistenceManager.java @@ -0,0 +1,470 @@ +package io.github.timemachinelab.sfchain.persistence; + +import io.github.timemachinelab.sfchain.core.AIOperationRegistry; +import io.github.timemachinelab.sfchain.core.ModelRegistry; +import io.github.timemachinelab.sfchain.core.openai.OpenAIModelConfig; +import io.github.timemachinelab.sfchain.core.openai.OpenAIModelFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * 描述: 持久化管理器 + * 负责协调持久化服务与现有的模型注册和操作注册系统 + * + * @author suifeng + * 日期: 2025/1/27 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class PersistenceManager { + + private final PersistenceService persistenceService; + private final ModelRegistry modelRegistry; + private final AIOperationRegistry operationRegistry; + private final OpenAIModelFactory modelFactory; + private final DynamicOperationConfigService dynamicOperationConfigService; + + /** + * 应用启动完成后同步配置 + */ + @EventListener(ApplicationReadyEvent.class) + public void onApplicationReady() { + log.info("开始同步持久化配置..."); + + // 同步现有配置到持久化存储 + syncExistingConfigurations(); + + // 从持久化存储加载额外配置 + loadPersistedConfigurations(); + + log.info("持久化配置同步完成"); + } + + // ==================== 模型配置管理 ==================== + + /** + * 添加模型配置 + * + * @param modelName 模型名称 + * @param config 模型配置数据 + */ + public void addModelConfig(String modelName, ModelConfigData config) { + try { + // 验证配置 + if (!config.isValid()) { + throw new IllegalArgumentException("无效的模型配置: " + modelName); + } + + // 转换为OpenAI模型配置 + OpenAIModelConfig openAIConfig = convertToOpenAIConfig(config); + + // 注册到模型工厂 + modelFactory.registerModel(openAIConfig); + + // 保存到持久化存储 + persistenceService.saveModelConfig(modelName, config); + + log.info("成功添加模型配置: {}", modelName); + } catch (Exception e) { + log.error("添加模型配置失败: {} - {}", modelName, e.getMessage()); + throw new RuntimeException("添加模型配置失败: " + modelName, e); + } + } + + /** + * 更新模型配置 + * + * @param modelName 模型名称 + * @param config 模型配置数据 + */ + public void updateModelConfig(String modelName, ModelConfigData config) { + try { + // 检查模型是否存在 + if (!persistenceService.existsModelConfig(modelName)) { + throw new IllegalArgumentException("模型配置不存在: " + modelName); + } + + // 验证配置 + if (!config.isValid()) { + throw new IllegalArgumentException("无效的模型配置: " + modelName); + } + + // 移除旧配置 + modelFactory.removeModel(modelName); + + // 转换为OpenAI模型配置 + OpenAIModelConfig openAIConfig = convertToOpenAIConfig(config); + + // 重新注册到模型工厂 + modelFactory.registerModel(openAIConfig); + + // 更新持久化存储 + persistenceService.saveModelConfig(modelName, config); + + log.info("成功更新模型配置: {}", modelName); + } catch (Exception e) { + log.error("更新模型配置失败: {} - {}", modelName, e.getMessage()); + throw new RuntimeException("更新模型配置失败: " + modelName, e); + } + } + + /** + * 删除模型配置 + * + * @param modelName 模型名称 + */ + public void deleteModelConfig(String modelName) { + try { + // 检查是否有操作正在使用此模型(从操作配置中检查) + Map allConfigs = persistenceService.getAllOperationConfigs(); + boolean inUse = allConfigs.values().stream() + .anyMatch(config -> modelName.equals(config.getModelName())); + if (inUse) { + throw new IllegalStateException("模型正在被操作使用,无法删除: " + modelName); + } + + // 从模型工厂移除 + modelFactory.removeModel(modelName); + + // 从持久化存储删除 + persistenceService.deleteModelConfig(modelName); + + log.info("成功删除模型配置: {}", modelName); + } catch (Exception e) { + log.error("删除模型配置失败: {} - {}", modelName, e.getMessage()); + throw new RuntimeException("删除模型配置失败: " + modelName, e); + } + } + + /** + * 获取模型配置 + * + * @param modelName 模型名称 + * @return 模型配置 + */ + public Optional getModelConfig(String modelName) { + return persistenceService.getModelConfig(modelName); + } + + /** + * 获取所有模型配置 + * + * @return 所有模型配置 + */ + public Map getAllModelConfigs() { + return persistenceService.getAllModelConfigs(); + } + + // ==================== 操作配置管理 ==================== + + /** + * 保存操作配置 + * + * @param operationType 操作类型 + * @param config 操作配置 + */ + public void saveOperationConfig(String operationType, OperationConfigData config) { + try { + // 验证操作是否已注册 + if (!operationRegistry.isOperationRegistered(operationType)) { + throw new IllegalArgumentException("操作未注册: " + operationType); + } + + // 验证配置 + if (!config.isValid()) { + throw new IllegalArgumentException("无效的操作配置: " + operationType); + } + + // 如果配置中指定了模型,验证模型是否存在 + if (config.getModelName() != null && !config.getModelName().isEmpty()) { + if (!persistenceService.existsModelConfig(config.getModelName()) && + !modelRegistry.isModelRegistered(config.getModelName())) { + throw new IllegalArgumentException("指定的模型不存在: " + config.getModelName()); + } + + // 同步更新操作注册中心的模型映射 + operationRegistry.setModelForOperation(operationType, config.getModelName()); + } + + // 保存到持久化存储 + persistenceService.saveOperationConfig(operationType, config); + + log.info("成功保存操作配置: {}", operationType); + } catch (Exception e) { + log.error("保存操作配置失败: {} - {}", operationType, e.getMessage()); + throw new RuntimeException("保存操作配置失败: " + operationType, e); + } + } + + /** + * 获取操作配置 + * 优先从@AIOp注解获取动态配置,如果不存在则从持久化存储获取 + * + * @param operationType 操作类型 + * @return 操作配置 + */ + public Optional getOperationConfig(String operationType) { + // 优先从注解获取动态配置 + Optional dynamicConfig = dynamicOperationConfigService.getOperationConfig(operationType); + if (dynamicConfig.isPresent()) { + log.debug("从注解获取操作配置: {}", operationType); + return dynamicConfig; + } + + // 如果注解配置不存在,则从持久化存储获取 + log.debug("从持久化存储获取操作配置: {}", operationType); + return persistenceService.getOperationConfig(operationType); + } + + /** + * 获取所有操作配置 + * 合并动态配置(从注解)和持久化配置,动态配置优先 + * + * @return 所有操作配置 + */ + public Map getAllOperationConfigs() { + // 获取持久化配置 + Map persistedConfigs = persistenceService.getAllOperationConfigs(); + + // 获取动态配置(从注解) + Map dynamicConfigs = dynamicOperationConfigService.getAllOperationConfigs(); + + // 合并配置,动态配置优先 + Map allConfigs = new HashMap<>(persistedConfigs); + allConfigs.putAll(dynamicConfigs); + + log.debug("获取所有操作配置: 持久化配置{}个, 动态配置{}个, 总计{}个", + persistedConfigs.size(), dynamicConfigs.size(), allConfigs.size()); + + return allConfigs; + } + + /** + * 删除操作配置 + * + * @param operationType 操作类型 + */ + public void deleteOperationConfig(String operationType) { + try { + persistenceService.deleteOperationConfig(operationType); + + // 同时从操作注册中心移除模型映射 + operationRegistry.getModelMapping().remove(operationType); + + log.info("成功删除操作配置: {}", operationType); + } catch (Exception e) { + log.error("删除操作配置失败: {} - {}", operationType, e.getMessage()); + throw new RuntimeException("删除操作配置失败: " + operationType, e); + } + } + + // ==================== 备份和恢复 ==================== + + /** + * 创建配置备份 + * + * @param backupName 备份名称 + */ + public void createBackup(String backupName) { + persistenceService.backup(backupName); + } + + /** + * 从备份恢复配置 + * + * @param backupName 备份名称 + */ + public void restoreFromBackup(String backupName) { + try { + // 恢复配置 + persistenceService.restoreFromBackup(backupName); + + // 重新同步配置 + loadPersistedConfigurations(); + + log.info("成功从备份恢复配置: {}", backupName); + } catch (Exception e) { + log.error("从备份恢复配置失败: {} - {}", backupName, e.getMessage()); + throw new RuntimeException("从备份恢复配置失败: " + backupName, e); + } + } + + /** + * 获取所有备份名称 + * + * @return 备份名称列表 + */ + public java.util.List getAllBackupNames() { + return persistenceService.getAllBackupNames(); + } + + /** + * 刷新配置到持久化存储 + */ + public void flushConfigurations() { + syncExistingConfigurations(); + log.info("配置已刷新到持久化存储"); + } + + /** + * 重新加载配置 + */ + public void reloadConfigurations() { + try { + // 重新加载配置 + loadPersistedConfigurations(); + + log.info("配置重新加载完成"); + } catch (Exception e) { + log.error("重新加载配置失败: {}", e.getMessage(), e); + throw new RuntimeException("重新加载配置失败", e); + } + } + + // ==================== 私有方法 ==================== + + /** + * 同步现有配置到持久化存储 + */ + private void syncExistingConfigurations() { + try { + // 同步现有的模型配置 + for (String modelName : modelFactory.getRegisteredModelNames()) { + OpenAIModelConfig openAIConfig = modelFactory.getModelConfig(modelName); + if (openAIConfig != null && !persistenceService.existsModelConfig(modelName)) { + ModelConfigData configData = convertFromOpenAIConfig(openAIConfig); + persistenceService.saveModelConfig(modelName, configData); + } + } + + // 初始化操作配置:从@AIOp注解获取配置并保存到数据库 + initializeOperationConfigs(); + + } catch (Exception e) { + log.warn("同步现有配置时出现警告: {}", e.getMessage()); + } + } + + /** + * 从持久化存储加载配置 + */ + private void loadPersistedConfigurations() { + try { + // 加载模型配置 + Map modelConfigs = persistenceService.getAllModelConfigs(); + for (Map.Entry entry : modelConfigs.entrySet()) { + String modelName = entry.getKey(); + ModelConfigData config = entry.getValue(); + + if (!modelFactory.isModelRegistered(modelName)) { + try { + OpenAIModelConfig openAIConfig = convertToOpenAIConfig(config); + modelFactory.registerModel(openAIConfig); + log.info("从持久化存储加载模型配置: {}", modelName); + } catch (Exception e) { + log.warn("加载模型配置失败: {} - {}", modelName, e.getMessage()); + } + } + } + + // 加载操作配置并同步模型映射到操作注册中心 + Map operationConfigs = persistenceService.getAllOperationConfigs(); + for (Map.Entry entry : operationConfigs.entrySet()) { + String operationType = entry.getKey(); + OperationConfigData config = entry.getValue(); + + // 如果操作配置中指定了模型,同步到操作注册中心 + if (config.getModelName() != null && !config.getModelName().isEmpty() && + operationRegistry.isOperationRegistered(operationType)) { + try { + operationRegistry.setModelForOperation(operationType, config.getModelName()); + log.debug("从持久化存储同步操作模型映射: {} -> {}", operationType, config.getModelName()); + } catch (Exception e) { + log.warn("同步操作模型映射失败: {} -> {} - {}", operationType, config.getModelName(), e.getMessage()); + } + } + } + + } catch (Exception e) { + log.error("加载持久化配置失败: {}", e.getMessage(), e); + } + } + + /** + * 初始化操作配置 + * 从@AIOp注解获取配置并保存到数据库(仅当数据库中不存在时) + */ + private void initializeOperationConfigs() { + try { + // 获取所有已注册的操作类型 + for (String operationType : operationRegistry.getAllOperations()) { + // 检查数据库中是否已存在该操作的配置 + Optional existingConfig = persistenceService.getOperationConfig(operationType); + if (existingConfig.isEmpty()) { + // 从注解获取动态配置 + Optional dynamicConfig = dynamicOperationConfigService.getOperationConfig(operationType); + if (dynamicConfig.isPresent()) { + // 保存到数据库 + persistenceService.saveOperationConfig(operationType, dynamicConfig.get()); + log.info("初始化操作配置到数据库: {}", operationType); + } else { + log.debug("操作 {} 没有@AIOp注解配置,跳过初始化", operationType); + } + } else { + log.debug("操作配置已存在于数据库中,跳过初始化: {}", operationType); + } + } + } catch (Exception e) { + log.warn("初始化操作配置时出现警告: {}", e.getMessage()); + } + } + + /** + * 转换为OpenAI模型配置 + */ + private OpenAIModelConfig convertToOpenAIConfig(ModelConfigData config) { + return OpenAIModelConfig.builder() + .modelName(config.getModelName()) + .baseUrl(config.getBaseUrl()) + .apiKey(config.getApiKey()) + .defaultMaxTokens(config.getDefaultMaxTokens()) + .defaultTemperature(config.getDefaultTemperature()) + .supportStream(config.getSupportStream()) + .supportJsonOutput(config.getSupportJsonOutput()) + .supportThinking(config.getSupportThinking()) + .additionalHeaders(config.getAdditionalHeaders()) + .description(config.getDescription()) + .provider(config.getProvider()) + .enabled(config.getEnabled()) + .build(); + } + + /** + * 从OpenAI模型配置转换 + */ + private ModelConfigData convertFromOpenAIConfig(OpenAIModelConfig config) { + ModelConfigData data = new ModelConfigData(); + data.setModelName(config.getModelName()); + data.setBaseUrl(config.getBaseUrl()); + data.setApiKey(config.getApiKey()); + data.setDefaultMaxTokens(config.getDefaultMaxTokens()); + data.setDefaultTemperature(config.getDefaultTemperature()); + data.setSupportStream(config.getSupportStream()); + data.setSupportJsonOutput(config.getSupportJsonOutput()); + data.setSupportThinking(config.getSupportThinking()); + data.setAdditionalHeaders(config.getAdditionalHeaders()); + data.setDescription(config.getDescription()); + data.setProvider(config.getProvider()); + data.setEnabled(config.getEnabled()); + data.updateTimestamp(); + return data; + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/PersistenceService.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/PersistenceService.java new file mode 100644 index 0000000..3f9c7a5 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/PersistenceService.java @@ -0,0 +1,127 @@ +package io.github.timemachinelab.sfchain.persistence; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 描述: 持久化服务接口 + * 提供模型配置和操作映射的增删改查功能 + * + * @author suifeng + * 日期: 2025/1/27 + */ +public interface PersistenceService { + + // ==================== 模型配置管理 ==================== + /** + * 保存模型配置 + * + * @param modelName 模型名称 + * @param config 模型配置 + */ + void saveModelConfig(String modelName, ModelConfigData config); + + /** + * 获取模型配置 + * + * @param modelName 模型名称 + * @return 模型配置,如果不存在则返回空 + */ + Optional getModelConfig(String modelName); + + /** + * 获取所有模型配置 + * + * @return 所有模型配置的映射 + */ + Map getAllModelConfigs(); + + /** + * 删除模型配置 + * + * @param modelName 模型名称 + * @return 是否删除成功 + */ + boolean deleteModelConfig(String modelName); + + /** + * 检查模型配置是否存在 + * + * @param modelName 模型名称 + * @return 是否存在 + */ + boolean existsModelConfig(String modelName); + + /** + * 获取所有模型名称 + * + * @return 模型名称列表 + */ + List getAllModelNames(); + + // 移除整个 "操作模型映射管理" 部分 + + // ==================== 操作配置管理 ==================== + /** + * 保存操作配置 + * + * @param operationType 操作类型 + * @param config 操作配置 + */ + void saveOperationConfig(String operationType, OperationConfigData config); + + /** + * 获取操作配置 + * + * @param operationType 操作类型 + * @return 操作配置,如果不存在则返回空 + */ + Optional getOperationConfig(String operationType); + + /** + * 获取所有操作配置 + * + * @return 所有操作配置的映射 + */ + Map getAllOperationConfigs(); + + /** + * 删除操作配置 + * + * @param operationType 操作类型 + * @return 是否删除成功 + */ + boolean deleteOperationConfig(String operationType); + + // ==================== 数据同步和备份 ==================== + + /** + * 刷新数据到持久化存储 + */ + void flush(); + + /** + * 从持久化存储重新加载数据 + */ + void reload(); + + /** + * 备份当前配置 + * + * @param backupName 备份名称 + */ + void backup(String backupName); + + /** + * 从备份恢复配置 + * @param backupName 备份名称 + */ + void restoreFromBackup(String backupName); + + /** + * 获取所有备份名称 + * @return 备份名称列表 + */ + List getAllBackupNames(); +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/PostgreSQLPersistenceService.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/PostgreSQLPersistenceService.java new file mode 100644 index 0000000..6d6cdd3 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/PostgreSQLPersistenceService.java @@ -0,0 +1,301 @@ +package io.github.timemachinelab.sfchain.persistence; +import io.github.timemachinelab.sfchain.persistence.entity.ModelConfigEntity; +import io.github.timemachinelab.sfchain.persistence.entity.OperationConfigEntity; +import io.github.timemachinelab.sfchain.persistence.repository.ModelConfigRepository; +import io.github.timemachinelab.sfchain.persistence.repository.OperationConfigRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * 描述: 基于PostgreSQL的持久化服务实现 + * 替代原有的JSON文件持久化方案 + * + * @author suifeng + * 日期: 2025/1/27 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class PostgreSQLPersistenceService implements PersistenceService { + + private final ModelConfigRepository modelConfigRepository; + private final OperationConfigRepository operationConfigRepository; + // 移除 operationModelMappingRepository 依赖 + + @Override + public void saveModelConfig(String modelName, ModelConfigData config) { + log.debug("保存模型配置: {}", modelName); + config.setModelName(modelName); + + // 查找现有实体并更新,或创建新实体 + ModelConfigEntity entity = modelConfigRepository.findByModelName(modelName) + .map(existing -> { + // 更新现有实体,保留 id 和 createdAt + Long id = existing.getId(); + LocalDateTime createdAt = existing.getCreatedAt(); + + // 使用新的转换方法,而不是 BeanUtils.copyProperties + ModelConfigEntity updatedEntity = convertToEntity(config); + updatedEntity.setId(id); + updatedEntity.setCreatedAt(createdAt); + + return updatedEntity; + }) + .orElseGet(() -> { + // 创建新实体,使用新的转换方法 + return convertToEntity(config); + }); + + modelConfigRepository.save(entity); + log.info("模型配置已保存: {}", modelName); + } + + @Override + public Optional getModelConfig(String modelName) { + log.debug("加载模型配置: {}", modelName); + return modelConfigRepository.findByModelName(modelName) + .map(this::convertToData); + } + + @Override + public Map getAllModelConfigs() { + log.debug("加载所有模型配置"); + return modelConfigRepository.findAll().stream() + .collect(Collectors.toMap( + ModelConfigEntity::getModelName, + this::convertToData + )); + } + + @Override + public boolean deleteModelConfig(String modelName) { + log.debug("删除模型配置: {}", modelName); + Optional entity = modelConfigRepository.findByModelName(modelName); + if (entity.isPresent()) { + modelConfigRepository.deleteById(entity.get().getId()); + log.info("模型配置已删除: {}", modelName); + return true; + } else { + log.warn("模型配置不存在,无法删除: {}", modelName); + return false; + } + } + + @Override + public boolean existsModelConfig(String modelName) { + return modelConfigRepository.existsByModelName(modelName); + } + + @Override + public List getAllModelNames() { + log.debug("获取所有模型名称"); + return modelConfigRepository.findEnabledModelNames(); + } + + @Override + public void saveOperationConfig(String operationType, OperationConfigData config) { + log.debug("保存操作配置: {}", operationType); + config.setOperationType(operationType); + OperationConfigEntity entity = convertToEntity(config); + operationConfigRepository.save(entity); + log.info("操作配置已保存: {}", operationType); + } + + @Override + public Optional getOperationConfig(String operationType) { + log.debug("加载操作配置: {}", operationType); + return operationConfigRepository.findByOperationType(operationType) + .map(this::convertToData); + } + + @Override + public Map getAllOperationConfigs() { + log.debug("加载所有操作配置"); + return operationConfigRepository.findAll().stream() + .collect(Collectors.toMap( + OperationConfigEntity::getOperationType, + this::convertToData + )); + } + + @Override + public boolean deleteOperationConfig(String operationType) { + log.debug("删除操作配置: {}", operationType); + Optional entity = operationConfigRepository.findByOperationType(operationType); + if (entity.isPresent()) { + operationConfigRepository.deleteById(entity.get().getId()); + log.info("操作配置已删除: {}", operationType); + return true; + } else { + log.warn("操作配置不存在,无法删除: {}", operationType); + return false; + } + } + + @Override + public void flush() { + // PostgreSQL自动提交事务,无需手动flush + log.debug("PostgreSQL持久化服务flush操作(无需手动操作)"); + } + + @Override + public void reload() { + // PostgreSQL数据实时同步,无需手动reload + log.debug("PostgreSQL持久化服务reload操作(数据实时同步)"); + } + + @Override + @Transactional + public void backup(String backupName) { + // PostgreSQL备份通常使用pg_dump等工具,这里记录备份请求 + log.info("PostgreSQL备份请求,备份名称: {}。请使用pg_dump等工具进行数据库备份。", backupName); + } + + @Override + @Transactional + public void restoreFromBackup(String backupName) { + // PostgreSQL恢复通常使用pg_restore等工具,这里记录恢复请求 + log.info("PostgreSQL恢复请求,备份名称: {}。请使用pg_restore等工具进行数据库恢复。", backupName); + } + + @Override + public List getAllBackupNames() { + // PostgreSQL备份文件管理通常在文件系统层面,这里返回空列表 + log.debug("PostgreSQL备份文件列表查询(需要在文件系统层面管理)"); + return List.of(); + } + + /** + * 将ModelConfigData转换为ModelConfigEntity + */ + private ModelConfigEntity convertToEntity(ModelConfigData data) { + ModelConfigEntity entity = new ModelConfigEntity(); + + // 映射基本字段 + entity.setModelName(data.getModelName()); + entity.setProvider(data.getProvider()); + entity.setApiKey(data.getApiKey()); + entity.setBaseUrl(data.getBaseUrl()); + entity.setEnabled(data.getEnabled()); + entity.setDescription(data.getDescription()); + + // 将扩展字段映射到customParams + Map customParams = new HashMap<>(); + if (data.getDefaultMaxTokens() != null) { + customParams.put("defaultMaxTokens", data.getDefaultMaxTokens()); + } + if (data.getDefaultTemperature() != null) { + customParams.put("defaultTemperature", data.getDefaultTemperature()); + } + if (data.getSupportStream() != null) { + customParams.put("supportStream", data.getSupportStream()); + } + if (data.getSupportJsonOutput() != null) { + customParams.put("supportJsonOutput", data.getSupportJsonOutput()); + } + if (data.getSupportThinking() != null) { + customParams.put("supportThinking", data.getSupportThinking()); + } + if (data.getAdditionalHeaders() != null && !data.getAdditionalHeaders().isEmpty()) { + customParams.put("additionalHeaders", data.getAdditionalHeaders()); + } + if (data.getCreatedAt() != null) { + customParams.put("createdAt", data.getCreatedAt()); + } + if (data.getUpdatedAt() != null) { + customParams.put("updatedAt", data.getUpdatedAt()); + } + + entity.setCustomParams(customParams); + return entity; + } + + /** + * 将ModelConfigEntity转换为ModelConfigData + */ + private ModelConfigData convertToData(ModelConfigEntity entity) { + ModelConfigData.ModelConfigDataBuilder builder = ModelConfigData.builder() + .modelName(entity.getModelName()) + .provider(entity.getProvider()) + .apiKey(entity.getApiKey()) + .baseUrl(entity.getBaseUrl()) + .enabled(entity.getEnabled()) + .description(entity.getDescription()); + + // 从customParams中提取扩展字段 + Map customParams = entity.getCustomParams(); + if (customParams != null) { + if (customParams.containsKey("defaultMaxTokens")) { + builder.defaultMaxTokens((Integer) customParams.get("defaultMaxTokens")); + } + if (customParams.containsKey("defaultTemperature")) { + builder.defaultTemperature((Double) customParams.get("defaultTemperature")); + } + if (customParams.containsKey("supportStream")) { + builder.supportStream((Boolean) customParams.get("supportStream")); + } + if (customParams.containsKey("supportJsonOutput")) { + builder.supportJsonOutput((Boolean) customParams.get("supportJsonOutput")); + } + if (customParams.containsKey("supportThinking")) { + builder.supportThinking((Boolean) customParams.get("supportThinking")); + } + if (customParams.containsKey("additionalHeaders")) { + @SuppressWarnings("unchecked") + Map headers = (Map) customParams.get("additionalHeaders"); + builder.additionalHeaders(headers != null ? headers : new HashMap<>()); + } + if (customParams.containsKey("createdAt")) { + builder.createdAt((Long) customParams.get("createdAt")); + } + if (customParams.containsKey("updatedAt")) { + builder.updatedAt((Long) customParams.get("updatedAt")); + } + } + + return builder.build(); + } + + /** + * 将OperationConfigData转换为OperationConfigEntity + */ + private OperationConfigEntity convertToEntity(OperationConfigData data) { + OperationConfigEntity entity = new OperationConfigEntity(); + entity.setOperationType(data.getOperationType()); + entity.setDescription(data.getDescription()); + entity.setEnabled(data.getEnabled()); + entity.setMaxTokens(data.getMaxTokens()); + entity.setTemperature(data.getTemperature()); + entity.setJsonOutput(data.getJsonOutput()); + entity.setThinkingMode(data.getThinkingMode()); + entity.setCustomParams(data.getCustomParams()); + entity.setModelName(data.getModelName()); + return entity; + } + + /** + * 将OperationConfigEntity转换为OperationConfigData + */ + private OperationConfigData convertToData(OperationConfigEntity entity) { + return OperationConfigData.builder() + .operationType(entity.getOperationType()) + .description(entity.getDescription()) + .enabled(entity.getEnabled()) + .maxTokens(entity.getMaxTokens()) + .temperature(entity.getTemperature()) + .jsonOutput(entity.getJsonOutput()) + .thinkingMode(entity.getThinkingMode()) + .customParams(entity.getCustomParams()) + .modelName(entity.getModelName()) + .build(); + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/entity/ModelConfigEntity.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/entity/ModelConfigEntity.java new file mode 100644 index 0000000..1a9f024 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/entity/ModelConfigEntity.java @@ -0,0 +1,73 @@ +package io.github.timemachinelab.sfchain.persistence.entity; + +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 描述: 模型配置实体类 + * 用于存储AI模型的配置信息 + * + * @author suifeng + * 日期: 2025/1/27 + */ +@Entity +@Table(name = "ai_model_configs") +@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ModelConfigEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "model_name", length = 100, nullable = false, unique = true) + private String modelName; + + @Column(name = "provider", length = 50, nullable = false) + private String provider; + + @Column(name = "api_key", length = 500) + private String apiKey; + + @Column(name = "base_url", length = 500) + private String baseUrl; + + @Column(name = "enabled", nullable = false) + private Boolean enabled = true; + + @Column(name = "description", length = 1000) + private String description; + + @Type(type = "jsonb") + @Column(name = "custom_params", columnDefinition = "jsonb") + private Map customParams; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + LocalDateTime now = LocalDateTime.now(); + createdAt = now; + updatedAt = now; + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/entity/OperationConfigEntity.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/entity/OperationConfigEntity.java new file mode 100644 index 0000000..3edfe73 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/entity/OperationConfigEntity.java @@ -0,0 +1,82 @@ +package io.github.timemachinelab.sfchain.persistence.entity; + +import com.vladmihalcea.hibernate.type.json.JsonBinaryType; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 描述: 操作配置实体类 + * 用于存储AI操作的配置信息 + * + * @author suifeng + * 日期: 2025/1/27 + */ +@Entity +@Table(name = "ai_operation_configs") +@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class) +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OperationConfigEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "operation_type", length = 100, nullable = false, unique = true) + private String operationType; + + @Column(name = "description", length = 1000) + private String description; + + @Column(name = "enabled", nullable = false) + private Boolean enabled = true; + + @Column(name = "max_tokens") + private Integer maxTokens; + + @Column(name = "temperature") + private Double temperature; + + @Column(name = "json_output", nullable = false) + private Boolean jsonOutput = false; + + @Column(name = "thinking_mode", nullable = false) + private Boolean thinkingMode = false; + + @Type(type = "jsonb") + @Column(name = "custom_params", columnDefinition = "jsonb") + private Map customParams; + + /** + * 关联的模型名称 + */ + @Column(name = "model_name", length = 100) + private String modelName; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @PrePersist + protected void onCreate() { + LocalDateTime now = LocalDateTime.now(); + createdAt = now; + updatedAt = now; + } + + @PreUpdate + protected void onUpdate() { + updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/repository/ModelConfigRepository.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/repository/ModelConfigRepository.java new file mode 100644 index 0000000..0f71087 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/repository/ModelConfigRepository.java @@ -0,0 +1,68 @@ +package io.github.timemachinelab.sfchain.persistence.repository; + +import io.github.timemachinelab.sfchain.persistence.entity.ModelConfigEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * 描述: 模型配置Repository接口 + * 用于模型配置的数据库操作 + * + * @author suifeng + * 日期: 2025/1/27 + */ +@Repository +public interface ModelConfigRepository extends JpaRepository { + + /** + * 根据模型名称查找配置 + * @param modelName 模型名称 + * @return 模型配置 + */ + Optional findByModelName(String modelName); + + /** + * 根据提供商查找配置 + * @param provider 提供商 + * @return 模型配置列表 + */ + List findByProvider(String provider); + + /** + * 根据启用状态查找配置 + * @param enabled 启用状态 + * @return 模型配置列表 + */ + List findByEnabled(Boolean enabled); + + /** + * 检查模型名称是否存在配置 + * @param modelName 模型名称 + * @return 是否存在 + */ + boolean existsByModelName(String modelName); + + /** + * 根据模型名称删除配置 + * @param modelName 模型名称 + */ + void deleteByModelName(String modelName); + + /** + * 获取所有提供商 + * @return 提供商列表 + */ + @Query("SELECT DISTINCT m.provider FROM ModelConfigEntity m") + List findAllProviders(); + + /** + * 获取所有启用的模型名称 + * @return 启用的模型名称列表 + */ + @Query("SELECT m.modelName FROM ModelConfigEntity m WHERE m.enabled = true") + List findEnabledModelNames(); +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/repository/OperationConfigRepository.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/repository/OperationConfigRepository.java new file mode 100644 index 0000000..aa911bc --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/persistence/repository/OperationConfigRepository.java @@ -0,0 +1,75 @@ +package io.github.timemachinelab.sfchain.persistence.repository; + +import io.github.timemachinelab.sfchain.persistence.entity.OperationConfigEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +/** + * 描述: 操作配置Repository接口 + * 用于操作配置的数据库操作 + * + * @author suifeng + * 日期: 2025/1/27 + */ +@Repository +public interface OperationConfigRepository extends JpaRepository { + + /** + * 根据操作类型查找配置 + * @param operationType 操作类型 + * @return 操作配置 + */ + Optional findByOperationType(String operationType); + + /** + * 根据启用状态查找配置 + * @param enabled 启用状态 + * @return 操作配置列表 + */ + List findByEnabled(Boolean enabled); + + /** + * 检查操作类型是否存在配置 + * @param operationType 操作类型 + * @return 是否存在 + */ + boolean existsByOperationType(String operationType); + + /** + * 根据操作类型删除配置 + * @param operationType 操作类型 + */ + void deleteByOperationType(String operationType); + + /** + * 根据JSON输出模式查找配置 + * @param jsonOutput JSON输出模式 + * @return 操作配置列表 + */ + List findByJsonOutput(Boolean jsonOutput); + + /** + * 根据思考模式查找配置 + * @param thinkingMode 思考模式 + * @return 操作配置列表 + */ + List findByThinkingMode(Boolean thinkingMode); + + /** + * 获取所有操作类型 + * @return 操作类型列表 + */ + @Query("SELECT c.operationType FROM OperationConfigEntity c") + List findAllOperationTypes(); + + /** + * 获取所有启用的操作类型 + * @return 启用的操作类型列表 + */ + @Query("SELECT c.operationType FROM OperationConfigEntity c WHERE c.enabled = true") + List findEnabledOperationTypes(); +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/resources/db/migration/V1__Create_AI_Tables.sql b/prompto-lab-app/src/main/resources/db/migration/V1__Create_AI_Tables.sql new file mode 100644 index 0000000..bc8fb9b --- /dev/null +++ b/prompto-lab-app/src/main/resources/db/migration/V1__Create_AI_Tables.sql @@ -0,0 +1,75 @@ +-- 创建AI相关数据表 +-- 作者: suifeng +-- 日期: 2025/1/27 + +-- 创建模型配置表 +CREATE TABLE IF NOT EXISTS ai_model_configs ( + id BIGSERIAL PRIMARY KEY, + model_name VARCHAR(100) NOT NULL UNIQUE, + provider VARCHAR(50) NOT NULL, + api_key VARCHAR(500), + base_url VARCHAR(500), + enabled BOOLEAN NOT NULL DEFAULT true, + description VARCHAR(1000), + custom_params JSONB, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- 创建操作配置表(直接包含模型名称字段) +CREATE TABLE IF NOT EXISTS ai_operation_configs ( + id BIGSERIAL PRIMARY KEY, + operation_type VARCHAR(100) NOT NULL UNIQUE, + description VARCHAR(1000), + enabled BOOLEAN NOT NULL DEFAULT true, + max_tokens INTEGER, + temperature DOUBLE PRECISION, + json_output BOOLEAN NOT NULL DEFAULT false, + thinking_mode BOOLEAN NOT NULL DEFAULT false, + custom_params JSONB, + model_name VARCHAR(100), -- 直接在操作配置表中存储关联的模型名称 + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- 创建索引 +CREATE INDEX IF NOT EXISTS idx_model_configs_model_name ON ai_model_configs(model_name); +CREATE INDEX IF NOT EXISTS idx_model_configs_provider ON ai_model_configs(provider); +CREATE INDEX IF NOT EXISTS idx_model_configs_enabled ON ai_model_configs(enabled); +CREATE INDEX IF NOT EXISTS idx_operation_configs_operation_type ON ai_operation_configs(operation_type); +CREATE INDEX IF NOT EXISTS idx_operation_configs_enabled ON ai_operation_configs(enabled); +CREATE INDEX IF NOT EXISTS idx_operation_configs_model ON ai_operation_configs(model_name); + +-- 添加外键约束 +ALTER TABLE ai_operation_configs + ADD CONSTRAINT fk_operation_model + FOREIGN KEY (model_name) REFERENCES ai_model_configs(model_name) + ON DELETE SET NULL; + +-- 创建更新时间戳的触发器函数 +CREATE OR REPLACE FUNCTION update_updated_at_column() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ language 'plpgsql'; + +-- 为每个表创建更新时间戳触发器 +CREATE TRIGGER update_ai_model_configs_updated_at + BEFORE UPDATE ON ai_model_configs + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +CREATE TRIGGER update_ai_operation_configs_updated_at + BEFORE UPDATE ON ai_operation_configs + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- 插入一些示例数据 +INSERT INTO ai_model_configs (model_name, provider, enabled, description) VALUES +('deepseek-chat', 'deepseek', true, 'DeepSeek Chat模型'), +('gpt-4o', 'openai', true, 'OpenAI GPT-4o模型'), +('siliconflow-qwen', 'siliconflow', true, 'SiliconFlow Qwen模型') +ON CONFLICT (model_name) DO NOTHING; + +-- 提交事务 +COMMIT; \ No newline at end of file