diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/core/constant/QATreePrompt.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/constant/QATreePrompt.java index 8b4a25b..1f156c4 100644 --- a/prompto-lab-app/src/main/java/io/github/timemachinelab/core/constant/QATreePrompt.java +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/constant/QATreePrompt.java @@ -91,7 +91,7 @@ public class QATreePrompt { #### 多选类型 { "question": "选择问题描述", - "type": "muti", + "type": "multi", "parentId": "对话ID", "options": [ { @@ -112,13 +112,13 @@ public class QATreePrompt { { "id": "字段标识", "question": "字段问题描述", - "type": "input|single|muti", + "type": "input|single|multi", "options": [ { "id": "选项标识", "label": "选项显示文本" } - ], // 仅single/muti类型需要 + ], // 仅single/multi类型需要 "desc": "字段的详细说明或引导", // 可选 "weight": "权重分数" } diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/core/constant/QFormPrompt.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/constant/QFormPrompt.java index 16b894e..be5f851 100644 --- a/prompto-lab-app/src/main/java/io/github/timemachinelab/core/constant/QFormPrompt.java +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/constant/QFormPrompt.java @@ -33,13 +33,13 @@ public class QFormPrompt { { "id": "字段标识", "question": "字段问题描述", - "type": "input|single|muti", + "type": "input|single|multi", "options": [ { "id": "选项标识", "label": "选项显示文本" } - ], // 仅single/muti类型需要 + ], // 仅single/multi类型需要 "desc": "字段的详细说明或引导", // 可选 "weight": "权重分数" } @@ -69,21 +69,21 @@ public class QFormPrompt { + **question**:AI生成的简洁明确问题,不超过20字 + **type**:优先级顺序选择 - single:是否类型或单选题 - - muti:多选题 + - multi:多选题 - **input**:预期一句话或简短描述 - + **options**:仅single/muti类型需要的选项数组 + + **options**:仅single/multi类型需要的选项数组 + **desc**:可选字段,当字段需要补充说明时使用 + **weight**:权重分数1-10,最重要的信息为10分,依次递减 **4. options数组生成规则** - + **适用类型**:仅当type为single或muti时需要 - + **选项数量**:single类型2-4个选项,muti类型3-6个选项 + + **适用类型**:仅当type为single或multi时需要 + + **选项数量**:single类型2-4个选项,multi类型3-6个选项 + **选项内容**: - **id**:英文标识,用于程序处理 - **label**:中文显示文本,简洁明确 + **是否类型**:single类型常用于是/否、有/无等二元选择 - + **多选类型**:muti类型用于可选择多个答案的情况 + + **多选类型**:multi类型用于可选择多个答案的情况 **5. desc字段规则** diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/core/constant/QSelectPrompt.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/constant/QSelectPrompt.java index b87b278..dd2cb6f 100644 --- a/prompto-lab-app/src/main/java/io/github/timemachinelab/core/constant/QSelectPrompt.java +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/constant/QSelectPrompt.java @@ -46,7 +46,7 @@ public class QSelectPrompt { ```json { "question": "选择问题描述", - "type": "muti", + "type": "multi", "parentId": "对话ID", "options": [ { @@ -69,8 +69,8 @@ public class QSelectPrompt { **2. type字段规则** + single:单选模式,用户只能选择一个选项 - + muti:多选模式,用户可以选择多个选项 - + **判断标准**:问题中包含"哪些"、"都有什么"等多选指向词汇时使用muti + + multi:多选模式,用户可以选择多个选项 + + **判断标准**:问题中包含"哪些"、"都有什么"等多选指向词汇时使用multi **3. options数组生成规则** diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/core/question/BaseQuestion.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/question/BaseQuestion.java index 44c666c..3c244c0 100644 --- a/prompto-lab-app/src/main/java/io/github/timemachinelab/core/question/BaseQuestion.java +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/question/BaseQuestion.java @@ -35,7 +35,6 @@ public abstract class BaseQuestion { */ private String type; - /** * 问题的详细说明、引导提示或补充解释(可选) */ diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/core/question/QuestionParseException.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/question/QuestionParseException.java new file mode 100644 index 0000000..5b154a6 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/question/QuestionParseException.java @@ -0,0 +1,39 @@ +package io.github.timemachinelab.core.question; + +/** + * 问题解析异常 + * + * @author suifeng + * 日期: 2025/8/20 + */ +public class QuestionParseException extends Exception { + + private final String jsonContent; + private final String failureReason; + + public QuestionParseException(String message, String jsonContent, String failureReason) { + super(message); + this.jsonContent = jsonContent; + this.failureReason = failureReason; + } + + public QuestionParseException(String message, String jsonContent, String failureReason, Throwable cause) { + super(message, cause); + this.jsonContent = jsonContent; + this.failureReason = failureReason; + } + + public String getJsonContent() { + return jsonContent; + } + + public String getFailureReason() { + return failureReason; + } + + @Override + public String toString() { + return String.format("QuestionParseException: %s\n原因: %s\nJSON内容: %s", + getMessage(), failureReason, jsonContent); + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/core/question/QuestionParser.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/question/QuestionParser.java new file mode 100644 index 0000000..3afca5c --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/question/QuestionParser.java @@ -0,0 +1,213 @@ +package io.github.timemachinelab.core.question; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.JSONObject; +import lombok.extern.slf4j.Slf4j; + +import java.util.Arrays; +import java.util.List; +import java.util.ArrayList; + +/** + * 问题JSON解析工具类 + * 使用责任链模式依次尝试解析不同类型的问题 + * + * @author suifeng + * 日期: 2025/1/27 + */ +@Slf4j +public class QuestionParser { + + /** + * 问题类型解析链,按顺序尝试解析 + */ + private static final List> QUESTION_TYPES = Arrays.asList( + FormQuestion.class, + SingleChoiceQuestion.class, + MultipleChoiceQuestion.class, + InputQuestion.class + ); + + /** + * 解析JSON字符串为BaseQuestion对象 + * 依次尝试解析成不同类型,直到成功为止 + * + * @param jsonStr JSON字符串 + * @return BaseQuestion对象 + * @throws QuestionParseException 解析失败时抛出异常 + */ + public static BaseQuestion parseQuestion(String jsonStr) throws QuestionParseException { + if (jsonStr == null || jsonStr.trim().isEmpty()) { + throw new QuestionParseException("JSON字符串不能为空", jsonStr, "输入为空或null"); + } + + // 先解析为JSONObject以便验证字段 + JSONObject jsonObject; + try { + jsonObject = JSON.parseObject(jsonStr); + } catch (JSONException e) { + throw new QuestionParseException("JSON格式错误", jsonStr, "JSON语法不正确: " + e.getMessage(), e); + } + + // 收集所有解析失败的原因 + List failureReasons = new ArrayList<>(); + + // 依次尝试解析成不同类型 + for (Class questionType : QUESTION_TYPES) { + try { + BaseQuestion question = JSON.parseObject(jsonStr, questionType); + if (question != null) { + String validationResult = validateQuestion(question, jsonObject); + if (validationResult == null) { + log.info("成功解析为: {}", questionType.getSimpleName()); + return question; + } else { + failureReasons.add(questionType.getSimpleName() + ": " + validationResult); + } + } else { + failureReasons.add(questionType.getSimpleName() + ": 解析结果为null"); + } + } catch (JSONException e) { + failureReasons.add(questionType.getSimpleName() + ": JSON解析异常 - " + e.getMessage()); + } catch (Exception e) { + failureReasons.add(questionType.getSimpleName() + ": 未知异常 - " + e.getMessage()); + } + } + + // 所有类型都解析失败,抛出详细异常 + String allFailures = String.join("; ", failureReasons); + throw new QuestionParseException( + "无法解析为任何已知的问题类型", + jsonStr, + "所有类型解析失败: " + allFailures + ); + } + + /** + * 验证解析后的问题对象是否有效 + * + * @param question 解析后的问题对象 + * @param jsonObject 原始JSON对象 + * @return 验证失败原因,null表示验证通过 + */ + private static String validateQuestion(BaseQuestion question, JSONObject jsonObject) { + // 首先验证type字段是否存在 + if (!jsonObject.containsKey("type")) { + return "缺少必需的type字段"; + } + + String type = jsonObject.getString("type"); + if (type == null || type.trim().isEmpty()) { + return "type字段值为空"; + } + + if (question instanceof FormQuestion) { + // 表单问题必须type为"form"且有fields字段 + if (!"form".equals(type)) { + return "type字段值应为'form',实际为'" + type + "'"; + } + if (!jsonObject.containsKey("fields")) { + return "表单问题缺少fields字段"; + } + if (((FormQuestion) question).getFields() == null || ((FormQuestion) question).getFields().isEmpty()) { + return "表单问题的fields字段为空"; + } + } else if (question instanceof SingleChoiceQuestion) { + // 单选问题必须type为"single"且有options字段 + if (!"single".equals(type)) { + return "type字段值应为'single',实际为'" + type + "'"; + } + if (!jsonObject.containsKey("options")) { + return "单选问题缺少options字段"; + } + if (((SingleChoiceQuestion) question).getOptions() == null || ((SingleChoiceQuestion) question).getOptions().isEmpty()) { + return "单选问题的options字段为空"; + } + } else if (question instanceof MultipleChoiceQuestion) { + // 多选问题必须type为"multi"且有options字段 + if (!"multi".equals(type)) { + return "type字段值应为'multi',实际为'" + type + "'"; + } + if (!jsonObject.containsKey("options")) { + return "多选问题缺少options字段"; + } + if (((MultipleChoiceQuestion) question).getOptions() == null || ((MultipleChoiceQuestion) question).getOptions().isEmpty()) { + return "多选问题的options字段为空"; + } + } else if (question instanceof InputQuestion) { + // 输入问题必须type为"input"且有question字段 + if (!"input".equals(type)) { + return "type字段值应为'input',实际为'" + type + "'"; + } + if (!jsonObject.containsKey("question")) { + return "输入问题缺少question字段"; + } + if (question.getQuestion() == null || question.getQuestion().trim().isEmpty()) { + return "输入问题的question字段为空"; + } + } else { + return "未知的问题类型: " + question.getClass().getSimpleName(); + } + + return null; // 验证通过 + } + + /** + * 解析JSON字符串为指定类型的问题对象 + * + * @param jsonStr JSON字符串 + * @param clazz 目标类型 + * @param 问题类型 + * @return 指定类型的问题对象 + * @throws QuestionParseException 解析失败时抛出异常 + */ + public static T parseQuestion(String jsonStr, Class clazz) throws QuestionParseException { + if (jsonStr == null || jsonStr.trim().isEmpty()) { + throw new QuestionParseException("JSON字符串不能为空", jsonStr, "输入为空或null"); + } + + try { + T result = JSON.parseObject(jsonStr, clazz); + if (result == null) { + throw new QuestionParseException( + "解析为指定类型失败", + jsonStr, + "无法解析为" + clazz.getSimpleName() + "类型" + ); + } + return result; + } catch (JSONException e) { + throw new QuestionParseException( + "JSON解析失败", + jsonStr, + "解析为" + clazz.getSimpleName() + "时发生异常: " + e.getMessage(), + e + ); + } + } + + /** + * 将问题对象转换为JSON字符串 + * + * @param question 问题对象 + * @return JSON字符串 + * @throws QuestionParseException 转换失败时抛出异常 + */ + public static String toJson(BaseQuestion question) throws QuestionParseException { + if (question == null) { + throw new QuestionParseException("问题对象不能为空", null, "输入对象为null"); + } + + try { + return JSON.toJSONString(question); + } catch (Exception e) { + throw new QuestionParseException( + "对象转JSON失败", + question.toString(), + "序列化异常: " + e.getMessage(), + e + ); + } + } +} \ No newline at end of file diff --git a/prompto-lab-app/src/main/java/io/github/timemachinelab/core/session/infrastructure/ai/QuestionGenerationOperation.java b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/session/infrastructure/ai/QuestionGenerationOperation.java new file mode 100644 index 0000000..fb3c2b0 --- /dev/null +++ b/prompto-lab-app/src/main/java/io/github/timemachinelab/core/session/infrastructure/ai/QuestionGenerationOperation.java @@ -0,0 +1,93 @@ +package io.github.timemachinelab.core.session.infrastructure.ai; + +import io.github.timemachinelab.core.question.BaseQuestion; +import io.github.timemachinelab.core.question.QuestionParser; +import io.github.timemachinelab.core.question.QuestionParseException; +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; + +@AIOp(value = "QUESTION_GENERATION_OP", + description = "基于对话树和用户输入生成结构化问题的AI操作" +) +@Component +@Slf4j +public class QuestionGenerationOperation extends BaseAIOperation { + + @Override + public String buildPrompt(QuestionGenerationRequest input) { + StringBuilder promptBuilder = new StringBuilder(); + + // 添加全局提示词 + if (input.getGlobalPrompt() != null && !input.getGlobalPrompt().trim().isEmpty()) { + promptBuilder.append(input.getGlobalPrompt()); + promptBuilder.append("\n\n"); + } + + // 添加对话树信息 + if (input.getConversationTree() != null && !input.getConversationTree().trim().isEmpty()) { + promptBuilder.append("## 对话树结构\n"); + promptBuilder.append(input.getConversationTree()); + promptBuilder.append("\n\n"); + } + + // 添加用户输入 + promptBuilder.append("## 当前用户输入\n"); + promptBuilder.append(input.getUserInput()); + + return promptBuilder.toString(); + } + + @Override + protected BaseQuestion parseResult(String jsonContent, QuestionGenerationRequest input) { + try { + // 使用QuestionParser解析AI返回的JSON + BaseQuestion question = QuestionParser.parseQuestion(jsonContent); + log.info("成功生成问题,类型: {}", question.getClass().getSimpleName()); + return question; + + } catch (QuestionParseException e) { + log.error("解析AI生成的问题失败: {}", e.toString()); + throw new RuntimeException("问题解析失败: " + e.getFailureReason(), e); + } catch (Exception e) { + log.error("处理AI响应时发生未知错误: {}", e.getMessage(), e); + throw new RuntimeException("处理响应失败: " + e.getMessage(), e); + } + } + + @Data + public static class QuestionGenerationRequest { + /** + * 全局提示词 + */ + private String globalPrompt; + + /** + * 对话树结构 + */ + private String conversationTree; + + /** + * 用户输入 + */ + private String userInput; + + /** + * 额外的上下文信息 + */ + private java.util.Map context; + + public QuestionGenerationRequest() { + this.context = new java.util.HashMap<>(); + } + + public QuestionGenerationRequest(String globalPrompt, String conversationTree, String userInput) { + this(); + this.globalPrompt = globalPrompt; + this.conversationTree = conversationTree; + this.userInput = userInput; + } + } +} \ No newline at end of file