From 049e0af2bac0aaebb1298201db7aa6d4140d5e3a Mon Sep 17 00:00:00 2001 From: suifeng <369202865@qq.com> Date: Wed, 13 Aug 2025 23:42:45 +0800 Subject: [PATCH 1/3] =?UTF-8?q?[dev]=20=E6=97=A5=E5=BF=97=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=90=8E=E7=AB=AF=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AICallLogController.java | 71 ++++++ .../sfchain/controller/AIModelController.java | 4 +- .../controller/AIOperationController.java | 71 +++++- .../sfchain/core/BaseAIOperation.java | 69 ++++- .../sfchain/core/logging/AICallLog.java | 71 ++++++ .../sfchain/core/logging/AICallLogAspect.java | 76 ++++++ .../core/logging/AICallLogManager.java | 235 ++++++++++++++++++ .../operations/JSONRepairOperation.java | 3 +- .../TextClassificationOperation.java | 3 +- .../persistence/PersistenceManager.java | 77 ++++-- .../PostgreSQLPersistenceService.java | 21 +- 11 files changed, 655 insertions(+), 46 deletions(-) create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AICallLogController.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/logging/AICallLog.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/logging/AICallLogAspect.java create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/logging/AICallLogManager.java diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AICallLogController.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AICallLogController.java new file mode 100644 index 0000000..23ae93d --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/controller/AICallLogController.java @@ -0,0 +1,71 @@ +package io.github.timemachinelab.sfchain.controller; + +import io.github.timemachinelab.sfchain.core.logging.AICallLog; +import io.github.timemachinelab.sfchain.core.logging.AICallLogManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +/** + * AI调用日志查询控制器 + */ +@RestController +@RequestMapping("/sf/api/ai-logs") +public class AICallLogController { + + @Autowired + private AICallLogManager logManager; + + /** + * 获取所有日志 + */ + @GetMapping + public ResponseEntity> getAllLogs() { + return ResponseEntity.ok(logManager.getAllLogs()); + } + + /** + * 根据调用ID获取日志 + */ + @GetMapping("/{callId}") + public ResponseEntity getLog(@PathVariable String callId) { + AICallLog log = logManager.getLog(callId); + return log != null ? ResponseEntity.ok(log) : ResponseEntity.notFound().build(); + } + + /** + * 根据操作类型获取日志 + */ + @GetMapping("/operation/{operationType}") + public ResponseEntity> getLogsByOperation(@PathVariable String operationType) { + return ResponseEntity.ok(logManager.getLogsByOperation(operationType)); + } + + /** + * 根据模型名称获取日志 + */ + @GetMapping("/model/{modelName}") + public ResponseEntity> getLogsByModel(@PathVariable String modelName) { + return ResponseEntity.ok(logManager.getLogsByModel(modelName)); + } + + /** + * 获取统计信息 + */ + @GetMapping("/statistics") + public ResponseEntity getStatistics() { + return ResponseEntity.ok(logManager.getStatistics()); + } + + /** + * 清空所有日志 + */ + @DeleteMapping + public ResponseEntity> clearLogs() { + logManager.clearLogs(); + return ResponseEntity.ok(Map.of("message", "所有日志已清空")); + } +} \ 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 index c008ade..7dc264d 100644 --- 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 @@ -87,11 +87,11 @@ public ResponseEntity getModel(@PathVariable String modelName) { /** * 创建或更新模型配置 */ - @PostMapping("/{modelName}") + @PostMapping("/save") public ResponseEntity> saveModel( - @PathVariable String modelName, @Valid @RequestBody ModelConfigData config) { Map result = new HashMap<>(); + String modelName = config.getModelName(); try { // 验证模型配置 boolean validationResult = validateModelConfig(modelName, config); 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 index c4b04c4..b5ac64e 100644 --- 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 @@ -94,14 +94,22 @@ public ResponseEntity getOperation(@PathVariable String operationType) { } /** - * 保存操作配置 + * 保存操作配置 - 改为统一的save接口,从请求体获取operationType */ - @PostMapping("/{operationType}") + @PostMapping("/save") public ResponseEntity> saveOperationConfig( - @PathVariable String operationType, @Valid @RequestBody OperationConfigData config) { Map result = new HashMap<>(); try { + // 从config对象中获取operationType + String operationType = config.getOperationType(); + + if (operationType == null || operationType.trim().isEmpty()) { + result.put("success", false); + result.put("message", "操作类型不能为空"); + return ResponseEntity.badRequest().body(result); + } + persistenceManager.saveOperationConfig(operationType, config); result.put("success", true); @@ -110,12 +118,52 @@ public ResponseEntity> saveOperationConfig( return ResponseEntity.ok(result); } catch (Exception e) { - log.error("保存操作配置失败: {} - {}", operationType, e.getMessage()); + log.error("保存操作配置失败: {}", e.getMessage()); result.put("success", false); result.put("message", "保存失败: " + e.getMessage()); return ResponseEntity.badRequest().body(result); } } + + /** + * 获取单个操作配置 - 改为POST请求体参数 + */ + @PostMapping("/get") + public ResponseEntity getOperation(@RequestBody Map request) { + try { + String operationType = request.get("operationType"); + + if (operationType == null || operationType.trim().isEmpty()) { + return ResponseEntity.badRequest() + .body(Map.of("error", "操作类型不能为空")); + } + + 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("获取操作配置失败: {}", e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "获取操作配置失败: " + e.getMessage())); + } + } /** * 批量设置操作模型映射 @@ -179,15 +227,22 @@ public ResponseEntity> setOperationMappings( } /** - * 设置单个操作模型映射 + * 设置单个操作模型映射 - 改为POST请求体参数 */ - @PostMapping("/{operationType}/mapping") + @PostMapping("/mapping") public ResponseEntity> setOperationMapping( - @PathVariable String operationType, @RequestBody Map request) { Map result = new HashMap<>(); try { + String operationType = request.get("operationType"); String modelName = request.get("modelName"); + + if (operationType == null || operationType.trim().isEmpty()) { + result.put("success", false); + result.put("message", "操作类型不能为空"); + return ResponseEntity.badRequest().body(result); + } + if (modelName == null || modelName.trim().isEmpty()) { result.put("success", false); result.put("message", "模型名称不能为空"); @@ -218,7 +273,7 @@ public ResponseEntity> setOperationMapping( return ResponseEntity.ok(result); } catch (Exception e) { - log.error("设置操作映射失败: {} - {}", operationType, e.getMessage()); + log.error("设置操作映射失败: {}", e.getMessage()); result.put("success", false); result.put("message", "设置失败: " + e.getMessage()); return ResponseEntity.badRequest().body(result); 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 index 6c07c6e..499ac2f 100644 --- 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 @@ -2,6 +2,8 @@ import com.alibaba.fastjson.JSONObject; import io.github.timemachinelab.sfchain.annotation.AIOp; +import io.github.timemachinelab.sfchain.core.logging.AICallLog; +import io.github.timemachinelab.sfchain.core.logging.AICallLogManager; import io.github.timemachinelab.sfchain.core.openai.OpenAICompatibleModel; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -12,6 +14,8 @@ import javax.annotation.PostConstruct; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.util.UUID; import static io.github.timemachinelab.sfchain.constants.AIOperationConstant.JSON_REPAIR_OP; @@ -130,42 +134,93 @@ public OUTPUT execute(INPUT input) { * @param modelName 指定的模型名称,为null时使用默认模型 * @return 输出结果 */ + // 在BaseAIOperation类中添加以下字段和方法 + + @Autowired + private AICallLogManager logManager; + + // 在execute方法中添加详细日志记录 public OUTPUT execute(INPUT input, String modelName) { + String callId = UUID.randomUUID().toString(); + LocalDateTime startTime = LocalDateTime.now(); + long startMillis = System.currentTimeMillis(); + + AICallLog.AICallLogBuilder logBuilder = AICallLog.builder() + .callId(callId) + .operationType(annotation.value()) + .callTime(startTime) + .input(input) + .modelName(modelName) + .frequency(1) + .lastAccessTime(startTime); + try { // 获取模型 AIModel model = getModel(modelName); + logBuilder.modelName(model.getName()); // 构建提示词 String prompt = buildPrompt(input); + logBuilder.prompt(prompt); // 获取操作配置 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模型 - 根据模型类型选择合适的方法 + // 记录请求参数 + AICallLog.AIRequestParams requestParams = AICallLog.AIRequestParams.builder() + .maxTokens(finalMaxTokens) + .temperature(finalTemperature) + .jsonOutput(finalJsonOutput) + .thinking(finalThinking) + .build(); + logBuilder.requestParams(requestParams); + + // 调用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); } + logBuilder.rawResponse(response); + // 解析响应 - return parseResponse(response, input); + OUTPUT result = parseResponse(response, input); + + // 记录成功日志 + long duration = System.currentTimeMillis() - startMillis; + AICallLog log = logBuilder + .status(AICallLog.CallStatus.SUCCESS) + .duration(duration) + .output(result) + .build(); + + logManager.addLog(log); + + return result; + } catch (Exception e) { + // 记录失败日志 + long duration = System.currentTimeMillis() - startMillis; + AICallLog callLog = logBuilder + .status(AICallLog.CallStatus.FAILED) + .duration(duration) + .errorMessage(e.getMessage()) + .build(); + + logManager.addLog(callLog); + log.error("执行AI操作失败: {} - {}", annotation.value(), e.getMessage(), e); throw new RuntimeException("AI操作执行失败: " + e.getMessage(), e); } diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/logging/AICallLog.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/logging/AICallLog.java new file mode 100644 index 0000000..f956bad --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/logging/AICallLog.java @@ -0,0 +1,71 @@ +package io.github.timemachinelab.sfchain.core.logging; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * AI调用日志实体 + */ +@Data +@Builder +public class AICallLog { + + /** 调用ID */ + private String callId; + + /** 操作类型 */ + private String operationType; + + /** 模型名称 */ + private String modelName; + + /** 调用时间 */ + private LocalDateTime callTime; + + /** 执行耗时(毫秒) */ + private long duration; + + /** 调用状态 */ + private CallStatus status; + + /** 原始输入参数 */ + private Object input; + + /** 构建的提示词 */ + private String prompt; + + /** AI请求参数 */ + private AIRequestParams requestParams; + + /** 模型原始返回结果 */ + private String rawResponse; + + /** 最终输出结果 */ + private Object output; + + /** 错误信息(如果有) */ + private String errorMessage; + + /** 调用频次(用于LFU) */ + private int frequency; + + /** 最后访问时间(用于LFU) */ + private LocalDateTime lastAccessTime; + + public enum CallStatus { + SUCCESS, FAILED, TIMEOUT + } + + @Data + @Builder + public static class AIRequestParams { + private Integer maxTokens; + private Double temperature; + private Boolean jsonOutput; + private Boolean thinking; + private Map additionalParams; + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/logging/AICallLogAspect.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/logging/AICallLogAspect.java new file mode 100644 index 0000000..b5928fe --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/logging/AICallLogAspect.java @@ -0,0 +1,76 @@ +package io.github.timemachinelab.sfchain.core.logging; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * AI调用日志记录切面 + */ +@Slf4j +@Aspect +@Component +public class AICallLogAspect { + + @Autowired + private AICallLogManager logManager; + + /** + * 拦截BaseAIOperation的execute方法 + */ + @Around("execution(* io.github.timemachinelab.sfchain.core.BaseAIOperation.execute(..))") + public Object logAIOperation(ProceedingJoinPoint joinPoint) throws Throwable { + String callId = UUID.randomUUID().toString(); + LocalDateTime startTime = LocalDateTime.now(); + long startMillis = System.currentTimeMillis(); + + AICallLog.AICallLogBuilder logBuilder = AICallLog.builder() + .callId(callId) + .callTime(startTime) + .frequency(1) + .lastAccessTime(startTime); + + try { + // 获取输入参数 + Object[] args = joinPoint.getArgs(); + Object input = args.length > 0 ? args[0] : null; + String modelName = args.length > 1 ? (String) args[1] : null; + + logBuilder.input(input).modelName(modelName); + + // 执行原方法 + Object result = joinPoint.proceed(); + + // 记录成功日志 + long duration = System.currentTimeMillis() - startMillis; + AICallLog log = logBuilder + .status(AICallLog.CallStatus.SUCCESS) + .duration(duration) + .output(result) + .build(); + + logManager.addLog(log); + + return result; + + } catch (Exception e) { + // 记录失败日志 + long duration = System.currentTimeMillis() - startMillis; + AICallLog log = logBuilder + .status(AICallLog.CallStatus.FAILED) + .duration(duration) + .errorMessage(e.getMessage()) + .build(); + + logManager.addLog(log); + + throw e; + } + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/logging/AICallLogManager.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/logging/AICallLogManager.java new file mode 100644 index 0000000..6f66eb4 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/sfchain/core/logging/AICallLogManager.java @@ -0,0 +1,235 @@ +package io.github.timemachinelab.sfchain.core.logging; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; + +/** + * AI调用日志管理器 - 基于LFU算法 + */ +@Slf4j +@Component +public class AICallLogManager { + + private static final int MAX_CAPACITY = 100; + + /** 日志存储 */ + private final Map logStorage = new ConcurrentHashMap<>(); + + /** 频次计数器 */ + private final Map frequencyMap = new ConcurrentHashMap<>(); + + /** 频次分组 - 频次 -> 调用ID集合 */ + private final Map> frequencyGroups = new ConcurrentHashMap<>(); + + /** 最小频次 */ + private volatile int minFrequency = 1; + + /** 读写锁 */ + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + /** + * 添加调用日志 + */ + public void addLog(AICallLog callLog) { + lock.writeLock().lock(); + try { + String callId = callLog.getCallId(); + + // 如果已达到容量上限,移除最少使用的日志 + if (logStorage.size() >= MAX_CAPACITY && !logStorage.containsKey(callId)) { + evictLFU(); + } + + // 添加或更新日志 + logStorage.put(callId, callLog); + updateFrequency(callId); + + log.debug("添加AI调用日志: {}", callId); + + } finally { + lock.writeLock().unlock(); + } + } + + /** + * 获取调用日志 + */ + public AICallLog getLog(String callId) { + lock.readLock().lock(); + try { + AICallLog log = logStorage.get(callId); + if (log != null) { + // 更新访问时间和频次 + log.setLastAccessTime(LocalDateTime.now()); + updateFrequency(callId); + } + return log; + } finally { + lock.readLock().unlock(); + } + } + + /** + * 获取所有日志(按时间倒序) + */ + public List getAllLogs() { + lock.readLock().lock(); + try { + return logStorage.values().stream() + .sorted((a, b) -> b.getCallTime().compareTo(a.getCallTime())) + .collect(Collectors.toList()); + } finally { + lock.readLock().unlock(); + } + } + + /** + * 根据操作类型获取日志 + */ + public List getLogsByOperation(String operationType) { + lock.readLock().lock(); + try { + return logStorage.values().stream() + .filter(log -> operationType.equals(log.getOperationType())) + .sorted((a, b) -> b.getCallTime().compareTo(a.getCallTime())) + .collect(Collectors.toList()); + } finally { + lock.readLock().unlock(); + } + } + + /** + * 根据模型名称获取日志 + */ + public List getLogsByModel(String modelName) { + lock.readLock().lock(); + try { + return logStorage.values().stream() + .filter(log -> modelName.equals(log.getModelName())) + .sorted((a, b) -> b.getCallTime().compareTo(a.getCallTime())) + .collect(Collectors.toList()); + } finally { + lock.readLock().unlock(); + } + } + + /** + * 获取统计信息 + */ + public LogStatistics getStatistics() { + lock.readLock().lock(); + try { + long totalCalls = logStorage.size(); + long successCalls = logStorage.values().stream() + .mapToLong(log -> log.getStatus() == AICallLog.CallStatus.SUCCESS ? 1 : 0) + .sum(); + + double avgDuration = logStorage.values().stream() + .mapToLong(AICallLog::getDuration) + .average() + .orElse(0.0); + + Map operationCounts = logStorage.values().stream() + .collect(Collectors.groupingBy( + AICallLog::getOperationType, + Collectors.counting() + )); + + Map modelCounts = logStorage.values().stream() + .collect(Collectors.groupingBy( + AICallLog::getModelName, + Collectors.counting() + )); + + return LogStatistics.builder() + .totalCalls(totalCalls) + .successCalls(successCalls) + .successRate(totalCalls > 0 ? (double) successCalls / totalCalls : 0.0) + .averageDuration(avgDuration) + .operationCounts(operationCounts) + .modelCounts(modelCounts) + .build(); + } finally { + lock.readLock().unlock(); + } + } + + /** + * 清空所有日志 + */ + public void clearLogs() { + lock.writeLock().lock(); + try { + logStorage.clear(); + frequencyMap.clear(); + frequencyGroups.clear(); + minFrequency = 1; + log.info("已清空所有AI调用日志"); + } finally { + lock.writeLock().unlock(); + } + } + + /** + * 更新频次 + */ + private void updateFrequency(String callId) { + int oldFreq = frequencyMap.getOrDefault(callId, 0); + int newFreq = oldFreq + 1; + + frequencyMap.put(callId, newFreq); + + // 从旧频次组中移除 + if (oldFreq > 0) { + frequencyGroups.get(oldFreq).remove(callId); + if (frequencyGroups.get(oldFreq).isEmpty() && oldFreq == minFrequency) { + minFrequency++; + } + } + + // 添加到新频次组 + frequencyGroups.computeIfAbsent(newFreq, k -> new LinkedHashSet<>()).add(callId); + + // 更新最小频次 + if (newFreq < minFrequency) { + minFrequency = newFreq; + } + } + + /** + * 淘汰最少使用的日志 + */ + private void evictLFU() { + // 找到最小频次组中最早的元素 + LinkedHashSet minFreqGroup = frequencyGroups.get(minFrequency); + if (minFreqGroup != null && !minFreqGroup.isEmpty()) { + String evictCallId = minFreqGroup.iterator().next(); + + // 移除日志 + logStorage.remove(evictCallId); + frequencyMap.remove(evictCallId); + minFreqGroup.remove(evictCallId); + + log.debug("淘汰AI调用日志: {}", evictCallId); + } + } + + @lombok.Data + @lombok.Builder + public static class LogStatistics { + private long totalCalls; + private long successCalls; + private double successRate; + private double averageDuration; + private Map operationCounts; + private Map modelCounts; + } +} \ 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 index bdde4a7..a4c5941 100644 --- 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 @@ -20,8 +20,7 @@ */ @AIOp(value = JSON_REPAIR_OP, description = "修复格式错误的JSON字符串", - defaultModel = "deepseek-chat", - supportedModels = {"deepseek-chat", "gpt-4o-mini", "qwen-turbo"}, + defaultModel = "deepseek-ai/DeepSeek-V3", requireJsonOutput = false, autoRepairJson = false) @Component 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 index b25f47f..80730f3 100644 --- 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 @@ -19,8 +19,7 @@ @AIOp( value = TEXT_CLASSIFICATION_OP, description = "对输入文本进行分类,支持情感分析、主题分类等", - defaultModel = "deepseek-chat", - supportedModels = {"deepseek-chat", "gpt-4o", "siliconflow-qwen"} + defaultModel = "deepseek-ai/DeepSeek-V3" ) public class TextClassificationOperation extends BaseAIOperation { 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 index bc519ed..87a278f 100644 --- 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 @@ -167,6 +167,7 @@ public Map getAllModelConfigs() { /** * 保存操作配置 + * 同时更新数据库和内存容器 * * @param operationType 操作类型 * @param config 操作配置 @@ -189,15 +190,20 @@ public void saveOperationConfig(String operationType, OperationConfigData config !modelRegistry.isModelRegistered(config.getModelName())) { throw new IllegalArgumentException("指定的模型不存在: " + config.getModelName()); } - - // 同步更新操作注册中心的模型映射 - operationRegistry.setModelForOperation(operationType, config.getModelName()); } - // 保存到持久化存储 + // 1. 先保存到数据库 persistenceService.saveOperationConfig(operationType, config); - log.info("成功保存操作配置: {}", operationType); + // 2. 再同步更新操作注册中心的模型映射 + if (config.getModelName() != null && !config.getModelName().isEmpty()) { + operationRegistry.setModelForOperation(operationType, config.getModelName()); + } else { + // 如果模型名称为空,移除映射 + operationRegistry.getModelMapping().remove(operationType); + } + + log.info("成功保存操作配置并同步到内存容器: {} -> 模型: {}", operationType, config.getModelName()); } catch (Exception e) { log.error("保存操作配置失败: {} - {}", operationType, e.getMessage()); throw new RuntimeException("保存操作配置失败: " + operationType, e); @@ -206,40 +212,44 @@ public void saveOperationConfig(String operationType, OperationConfigData config /** * 获取操作配置 - * 优先从@AIOp注解获取动态配置,如果不存在则从持久化存储获取 + * 优先从数据库获取,如果不存在则从@AIOp注解获取 * * @param operationType 操作类型 * @return 操作配置 */ public Optional getOperationConfig(String operationType) { - // 优先从注解获取动态配置 + // 优先从数据库获取 + Optional persistedConfig = persistenceService.getOperationConfig(operationType); + if (persistedConfig.isPresent()) { + log.debug("从数据库获取操作配置: {}", operationType); + return persistedConfig; + } + + // 如果数据库配置不存在,则从注解获取 Optional dynamicConfig = dynamicOperationConfigService.getOperationConfig(operationType); if (dynamicConfig.isPresent()) { log.debug("从注解获取操作配置: {}", operationType); - return dynamicConfig; } - // 如果注解配置不存在,则从持久化存储获取 - log.debug("从持久化存储获取操作配置: {}", operationType); - return persistenceService.getOperationConfig(operationType); + return dynamicConfig; } /** * 获取所有操作配置 - * 合并动态配置(从注解)和持久化配置,动态配置优先 + * 优先使用数据库配置,注解配置作为补充 * * @return 所有操作配置 */ public Map getAllOperationConfigs() { - // 获取持久化配置 + // 获取持久化配置(数据库) Map persistedConfigs = persistenceService.getAllOperationConfigs(); // 获取动态配置(从注解) Map dynamicConfigs = dynamicOperationConfigService.getAllOperationConfigs(); - // 合并配置,动态配置优先 - Map allConfigs = new HashMap<>(persistedConfigs); - allConfigs.putAll(dynamicConfigs); + // 合并配置,数据库配置优先 + Map allConfigs = new HashMap<>(dynamicConfigs); + allConfigs.putAll(persistedConfigs); // 数据库配置覆盖注解配置 log.debug("获取所有操作配置: 持久化配置{}个, 动态配置{}个, 总计{}个", persistedConfigs.size(), dynamicConfigs.size(), allConfigs.size()); @@ -400,7 +410,7 @@ private void loadPersistedConfigurations() { /** * 初始化操作配置 - * 从@AIOp注解获取配置并保存到数据库(仅当数据库中不存在时) + * 优先使用数据库中的配置,如果不存在则使用注解配置并保存到数据库 */ private void initializeOperationConfigs() { try { @@ -408,22 +418,41 @@ private void initializeOperationConfigs() { for (String operationType : operationRegistry.getAllOperations()) { // 检查数据库中是否已存在该操作的配置 Optional existingConfig = persistenceService.getOperationConfig(operationType); - if (existingConfig.isEmpty()) { - // 从注解获取动态配置 + + if (existingConfig.isPresent()) { + // 数据库中已存在配置,使用数据库配置更新内存容器 + OperationConfigData dbConfig = existingConfig.get(); + + // 同步模型映射到操作注册中心 + if (dbConfig.getModelName() != null && !dbConfig.getModelName().isEmpty()) { + operationRegistry.setModelForOperation(operationType, dbConfig.getModelName()); + log.info("使用数据库配置初始化操作: {} -> 模型: {}", operationType, dbConfig.getModelName()); + } else { + log.info("使用数据库配置初始化操作: {} (无关联模型)", operationType); + } + } else { + // 数据库中不存在配置,从注解获取并保存到数据库 Optional dynamicConfig = dynamicOperationConfigService.getOperationConfig(operationType); if (dynamicConfig.isPresent()) { + OperationConfigData annotationConfig = dynamicConfig.get(); + // 保存到数据库 - persistenceService.saveOperationConfig(operationType, dynamicConfig.get()); - log.info("初始化操作配置到数据库: {}", operationType); + persistenceService.saveOperationConfig(operationType, annotationConfig); + + // 同步模型映射到操作注册中心 + if (annotationConfig.getModelName() != null && !annotationConfig.getModelName().isEmpty()) { + operationRegistry.setModelForOperation(operationType, annotationConfig.getModelName()); + log.info("从注解初始化操作配置到数据库: {} -> 模型: {}", operationType, annotationConfig.getModelName()); + } else { + log.info("从注解初始化操作配置到数据库: {} (无关联模型)", operationType); + } } else { log.debug("操作 {} 没有@AIOp注解配置,跳过初始化", operationType); } - } else { - log.debug("操作配置已存在于数据库中,跳过初始化: {}", operationType); } } } catch (Exception e) { - log.warn("初始化操作配置时出现警告: {}", e.getMessage()); + log.error("初始化操作配置时发生错误: {}", e.getMessage(), e); } } 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 index 6d6cdd3..13f973f 100644 --- 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 @@ -105,7 +105,26 @@ public List getAllModelNames() { public void saveOperationConfig(String operationType, OperationConfigData config) { log.debug("保存操作配置: {}", operationType); config.setOperationType(operationType); - OperationConfigEntity entity = convertToEntity(config); + + // 查找现有实体并更新,或创建新实体 + OperationConfigEntity entity = operationConfigRepository.findByOperationType(operationType) + .map(existing -> { + // 更新现有实体,保留 id 和 createdAt + Long id = existing.getId(); + LocalDateTime createdAt = existing.getCreatedAt(); + + // 转换新的配置数据 + OperationConfigEntity updatedEntity = convertToEntity(config); + updatedEntity.setId(id); + updatedEntity.setCreatedAt(createdAt); + + return updatedEntity; + }) + .orElseGet(() -> { + // 创建新实体 + return convertToEntity(config); + }); + operationConfigRepository.save(entity); log.info("操作配置已保存: {}", operationType); } From d6c8ce8cd007024a9ec048ea19ffe00e9bff5016 Mon Sep 17 00:00:00 2001 From: suifeng <369202865@qq.com> Date: Thu, 14 Aug 2025 00:12:05 +0800 Subject: [PATCH 2/3] =?UTF-8?q?[dev]=20=E5=89=8D=E7=AB=AF=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E5=8A=9F=E8=83=BD=E5=AF=B9=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ainode/AIChatOperation.java | 105 + .../src/components/AiNodeConfig.vue | 1809 ++++++++++++++--- .../src/components/ApiInfoConfig.vue | 1 - prompto-lab-ui/src/services/aiModelApi.ts | 21 +- prompto-lab-ui/src/services/aiOperationApi.ts | 55 + prompto-lab-ui/src/views/ApiConfigView.vue | 41 +- 6 files changed, 1764 insertions(+), 268 deletions(-) create mode 100644 prompto-lab-app/src/main/java/io/github/timemachinelab/ainode/AIChatOperation.java create mode 100644 prompto-lab-ui/src/services/aiOperationApi.ts diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/ainode/AIChatOperation.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/ainode/AIChatOperation.java new file mode 100644 index 0000000..93f67b0 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/ainode/AIChatOperation.java @@ -0,0 +1,105 @@ +package io.github.timemachinelab.ainode; + +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; + + + +/** + * 描述: AI对话Demo节点 + * 专门用于演示一个简单的AI对话功能 + * + * @author suifeng + * 日期: 2025/8/13 + */ +@AIOp(value = "AI_CHAT_OP", + description = "实现一个简单的AI对话功能示例", + defaultModel = "gpt-5-chat") +@Component +@Slf4j +public class AIChatOperation extends BaseAIOperation { + + /** + * 构建对话提示 + */ + @Override + public String buildPrompt(ChatRequest input) { + return String.format(""" + 你是一名智能AI助手,需要根据用户的提问进行回答。 + + ## 用户提问 + %s + + ## 输出要求 + 1. 请准确回答用户的问题 + 2. 保持回答简洁、直接 + 3. 避免任何多余或无关的内容 + 4. 返回的答案必须是以下JSON格式: + ```json + { + "answer": "回答内容", + "userInput": "用户输入内容" + } + ``` + + ## 注意事项 + - 确保返回的JSON有效且格式正确 + - 如果问题无法回答,请在回答中说明原因 + """, input.getQuestion()); + } + + /** + * 解析AI返回的JSON结果 + */ + @Override + protected ChatResponse parseResult(String jsonContent, ChatRequest input) { + try { + // 使用通用的JSON解析工具转换为对象 + return objectMapper.readValue(jsonContent, ChatResponse.class); + } catch (Exception e) { + log.warn("解析AI对话结果失败,使用默认响应: {}", e.getMessage()); + ChatResponse response = new ChatResponse(); + response.setAnswer("抱歉,我暂时无法回答这个问题。"); + response.setUserInput(input.getQuestion()); + return response; + } + } + + /** + * 对话请求数据结构 + */ + @Data + public static class ChatRequest { + /** + * 用户提问内容 + */ + private String question; + + public ChatRequest() { + this.question = "你好,AI!"; + } + + public ChatRequest(String question) { + this.question = question; + } + } + + /** + * 对话响应数据结构 + */ + @Data + public static class ChatResponse { + /** + * AI的回答内容 + */ + private String answer; + + /** + * 用户的输入内容 + */ + private String userInput; + } +} \ No newline at end of file diff --git a/prompto-lab-ui/src/components/AiNodeConfig.vue b/prompto-lab-ui/src/components/AiNodeConfig.vue index e5049a9..89e57a0 100644 --- a/prompto-lab-ui/src/components/AiNodeConfig.vue +++ b/prompto-lab-ui/src/components/AiNodeConfig.vue @@ -1,410 +1,1711 @@ \ No newline at end of file + diff --git a/prompto-lab-ui/src/components/ApiInfoConfig.vue b/prompto-lab-ui/src/components/ApiInfoConfig.vue index eef8df0..751ab8b 100644 --- a/prompto-lab-ui/src/components/ApiInfoConfig.vue +++ b/prompto-lab-ui/src/components/ApiInfoConfig.vue @@ -35,7 +35,6 @@
{{ modelsData.total }}
全部模型
-
🤖
{ - return apiJsonRequest(`${API_CONFIG.BASE_URL}/sf/api/models/${encodeURIComponent(modelName)}`, { - method: 'GET', + return apiJsonRequest(`${API_CONFIG.BASE_URL}/sf/api/models/get`, { + method: 'POST', + body: JSON.stringify({ modelName }), requireAuth: true }) }, // 保存模型配置(创建或更新) async saveModel(modelName: string, config: ModelConfigData): Promise { - return apiJsonRequest(`${API_CONFIG.BASE_URL}/sf/api/models/${encodeURIComponent(modelName)}`, { + return apiJsonRequest(`${API_CONFIG.BASE_URL}/sf/api/models/save`, { method: 'POST', - body: JSON.stringify(config), + body: JSON.stringify({ ...config, modelName }), requireAuth: true }) }, - // 删除模型配置 + // 删除模型配置 - 改为POST请求 async deleteModel(modelName: string): Promise { - return apiJsonRequest(`${API_CONFIG.BASE_URL}/sf/api/models/${encodeURIComponent(modelName)}`, { - method: 'DELETE', + return apiJsonRequest(`${API_CONFIG.BASE_URL}/sf/api/models/delete`, { + method: 'POST', + body: JSON.stringify({ modelName }), requireAuth: true }) }, - // 测试模型连接 + // 测试模型连接 - 改为POST请求 + // 测试模型连接 - 修改为路径参数方式 async testModel(modelName: string): Promise { return apiJsonRequest(`${API_CONFIG.BASE_URL}/sf/api/models/${encodeURIComponent(modelName)}/test`, { method: 'POST', diff --git a/prompto-lab-ui/src/services/aiOperationApi.ts b/prompto-lab-ui/src/services/aiOperationApi.ts new file mode 100644 index 0000000..0b8172b --- /dev/null +++ b/prompto-lab-ui/src/services/aiOperationApi.ts @@ -0,0 +1,55 @@ +import { API_CONFIG } from './apiConfig' +import { apiJsonRequest } from './apiUtils' +import type { OperationConfigData, OperationsResponse, OperationDetailResponse, ApiResponse } from '@/types/system' + +export const aiOperationApi = { + // 获取所有操作配置 + async getAllOperations(): Promise { + return apiJsonRequest(`${API_CONFIG.BASE_URL}/sf/api/operations`, { + method: 'GET', + requireAuth: true + }) + }, + + // 获取单个操作配置 - 改为POST请求 + async getOperation(operationType: string): Promise { + return apiJsonRequest(`${API_CONFIG.BASE_URL}/sf/api/operations/get`, { + method: 'POST', + body: JSON.stringify({ operationType }), + requireAuth: true + }) + }, + + // 保存操作配置 - 修正数据结构 + async saveOperationConfig(operationType: string, config: OperationConfigData): Promise { + // 确保operationType设置在config对象中 + const configWithType = { + ...config, + operationType: operationType + } + + return apiJsonRequest(`${API_CONFIG.BASE_URL}/sf/api/operations/save`, { + method: 'POST', + body: JSON.stringify(configWithType), + requireAuth: true + }) + }, + + // 设置单个操作模型映射 - 改为请求体参数 + async setOperationMapping(operationType: string, modelName: string): Promise { + return apiJsonRequest(`${API_CONFIG.BASE_URL}/sf/api/operations/mapping`, { + method: 'POST', + body: JSON.stringify({ operationType, modelName }), + requireAuth: true + }) + }, + + // 批量设置操作模型映射 + async setOperationMappings(mappings: Record): Promise { + return apiJsonRequest(`${API_CONFIG.BASE_URL}/sf/api/operations/mappings`, { + method: 'POST', + body: JSON.stringify({ mappings }), + requireAuth: true + }) + } +} diff --git a/prompto-lab-ui/src/views/ApiConfigView.vue b/prompto-lab-ui/src/views/ApiConfigView.vue index 04f4ada..51c9ba4 100644 --- a/prompto-lab-ui/src/views/ApiConfigView.vue +++ b/prompto-lab-ui/src/views/ApiConfigView.vue @@ -41,7 +41,7 @@ @click="switchTab(tab.key)" >
- {{ tab.icon }} +
{{ tab.title }}
@@ -144,19 +144,52 @@ const tabs = [ key: 'api', title: 'AI模型', description: '配置AI模型API', - icon: '🔗' + icon: ` + + + + + + + ` }, { key: 'operations', title: 'AI节点', description: '管理AI节点映射', - icon: '🤖' + icon: ` + + + + + + + + + + + + + + + ` }, { key: 'system', title: '系统管理', description: '系统维护操作', - icon: '🛠️' + icon: ` + + + + + + + + + + + ` } ] From f786909685b533a42876dcc321bb4b6c27f9ee59 Mon Sep 17 00:00:00 2001 From: suifeng <369202865@qq.com> Date: Thu, 14 Aug 2025 00:27:13 +0800 Subject: [PATCH 3/3] =?UTF-8?q?[dev]=20ai=E8=8A=82=E7=82=B9=E6=B5=8B?= =?UTF-8?q?=E8=AF=95demo=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../testnode}/ainode/AIChatOperation.java | 13 +--- .../testnode/controller/AIDemoController.java | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+), 11 deletions(-) rename prompto-lab-app/src/{main/java/io/github/timemachinelab => test/java/io/github/timemachinelab/testnode}/ainode/AIChatOperation.java (90%) create mode 100644 prompto-lab-app/src/test/java/io/github/timemachinelab/testnode/controller/AIDemoController.java diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/ainode/AIChatOperation.java b/prompto-lab-app/src/test/java/io/github/timemachinelab/testnode/ainode/AIChatOperation.java similarity index 90% rename from prompto-lab-app/src/main/java/io/github/timemachinelab/ainode/AIChatOperation.java rename to prompto-lab-app/src/test/java/io/github/timemachinelab/testnode/ainode/AIChatOperation.java index 93f67b0..0d99b4e 100644 --- a/prompto-lab-app/src/main/java/io/github/timemachinelab/ainode/AIChatOperation.java +++ b/prompto-lab-app/src/test/java/io/github/timemachinelab/testnode/ainode/AIChatOperation.java @@ -1,4 +1,4 @@ -package io.github.timemachinelab.ainode; +package io.github.timemachinelab.testnode.ainode; import io.github.timemachinelab.sfchain.annotation.AIOp; import io.github.timemachinelab.sfchain.core.BaseAIOperation; @@ -6,8 +6,6 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; - - /** * 描述: AI对话Demo节点 * 专门用于演示一个简单的AI对话功能 @@ -40,8 +38,7 @@ public String buildPrompt(ChatRequest input) { 4. 返回的答案必须是以下JSON格式: ```json { - "answer": "回答内容", - "userInput": "用户输入内容" + "answer": "回答内容" } ``` @@ -63,7 +60,6 @@ protected ChatResponse parseResult(String jsonContent, ChatRequest input) { log.warn("解析AI对话结果失败,使用默认响应: {}", e.getMessage()); ChatResponse response = new ChatResponse(); response.setAnswer("抱歉,我暂时无法回答这个问题。"); - response.setUserInput(input.getQuestion()); return response; } } @@ -96,10 +92,5 @@ public static class ChatResponse { * AI的回答内容 */ private String answer; - - /** - * 用户的输入内容 - */ - private String userInput; } } \ No newline at end of file diff --git a/prompto-lab-app/src/test/java/io/github/timemachinelab/testnode/controller/AIDemoController.java b/prompto-lab-app/src/test/java/io/github/timemachinelab/testnode/controller/AIDemoController.java new file mode 100644 index 0000000..8d3348a --- /dev/null +++ b/prompto-lab-app/src/test/java/io/github/timemachinelab/testnode/controller/AIDemoController.java @@ -0,0 +1,60 @@ +package io.github.timemachinelab.testnode.controller; + +import io.github.timemachinelab.testnode.ainode.AIChatOperation; +import io.github.timemachinelab.sfchain.core.AIService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +/** + * 描述: AI服务Demo控制器 + * 演示如何使用AIService调用AI节点 + * + * @author suifeng + * 日期: 2025/1/20 + */ +@RestController +@RequestMapping("/api/ai") +@Slf4j +public class AIDemoController { + + @Resource + private AIService aiService; + + /** + * 简单AI对话接口 + * + * @param question 用户问题 + * @return AI回答 + */ + @PostMapping("/chat") + public Map chat(@RequestParam String question) { + try { + // 创建请求对象 + AIChatOperation.ChatRequest request = new AIChatOperation.ChatRequest(question); + + // 调用AI服务 + AIChatOperation.ChatResponse response = aiService.execute("AI_CHAT_OP", request); + + // 返回结果 + Map result = new HashMap<>(); + result.put("success", true); + result.put("data", response); + result.put("message", "AI对话成功"); + + return result; + + } catch (Exception e) { + log.error("AI对话失败: {}", e.getMessage(), e); + + Map result = new HashMap<>(); + result.put("success", false); + result.put("message", "AI对话失败: " + e.getMessage()); + + return result; + } + } +} \ No newline at end of file