Skip to content

Commit 1c5ba79

Browse files
zxuexingzhijieAias00VLSMB
authored
feat(nl2sql): add a manual review function to support (alibaba#2392)
* feature(deepresearch):The access of the MCP tool for codernode and researchnode * format adjust * add space * Make adjustments by referring to the source code * Reconfigure the MCP configuration * format adjustment * format adjust * fix the toolback injection problem * delete unused tool * feat(deepresearch): update McpAssignNodeAutoConfiguration * delete unused package * Update the AgentsConfiguration class * fix(nl2sql): add methods for obtaining and setting optimization prompts to simplify the configuration management logic * fix format * feat(nl2sql): add a manual review function to support waiting for user decisions before implementing the plan * refactor(nl2sql): Rewrite the human feedback nodes and related logic to support the manual review process * refactor(nl2sql): 在中断前保存检查点并支持线程ID传递 * fix: frontend Co-authored-by: VLSMB <2047857654@qq.com> * fix: format frontend Co-authored-by: VLSMB <2047857654@qq.com> * fix: format code Co-authored-by: VLSMB <2047857654@qq.com> * feat: add highlight for json Co-authored-by: VLSMB <2047857654@qq.com> * feat: add scrollbar Co-authored-by: VLSMB <2047857654@qq.com> * refactor(nl2sql): 重构人类反馈处理逻辑,优化反馈节点与计划生成流程 * feat(nl2sql): 支持表的联合主键处理,优化主键设置逻辑 * fix format * Revert "Merge remote-tracking branch 'origin/main' into Makoto-humanfeedback" This reverts commit a2c931c, reversing changes made to 33d884b. * Reapply "Merge remote-tracking branch 'origin/main' into Makoto-humanfeedback" This reverts commit c16b7b6. * fix conflict * 更新流式处理逻辑 * fix format * fix format --------- Co-authored-by: aias00 <liuhongyu@apache.org> Co-authored-by: VLSMB <2047857654@qq.com>
1 parent d553960 commit 1c5ba79

File tree

17 files changed

+997
-248
lines changed

17 files changed

+997
-248
lines changed

spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/config/Nl2sqlConfiguration.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,8 @@ public StateGraph nl2sqlGraph(ChatClient.Builder chatClientBuilder) throws Graph
155155
// NL2SQL相关
156156
keyStrategyHashMap.put(IS_ONLY_NL2SQL, new ReplaceStrategy());
157157
keyStrategyHashMap.put(ONLY_NL2SQL_OUTPUT, new ReplaceStrategy());
158+
// Human Review keys
159+
keyStrategyHashMap.put(HUMAN_REVIEW_ENABLED, new ReplaceStrategy());
158160
// Final result
159161
keyStrategyHashMap.put(RESULT, new ReplaceStrategy());
160162
return keyStrategyHashMap;
@@ -176,7 +178,8 @@ public StateGraph nl2sqlGraph(ChatClient.Builder chatClientBuilder) throws Graph
176178
.addNode(PYTHON_EXECUTE_NODE, node_async(new PythonExecuteNode(codePoolExecutor)))
177179
.addNode(PYTHON_ANALYZE_NODE, node_async(new PythonAnalyzeNode(chatClientBuilder)))
178180
.addNode(REPORT_GENERATOR_NODE, node_async(new ReportGeneratorNode(chatClientBuilder, promptConfigService)))
179-
.addNode(SEMANTIC_CONSISTENCY_NODE, node_async(new SemanticConsistencyNode(nl2SqlService)));
181+
.addNode(SEMANTIC_CONSISTENCY_NODE, node_async(new SemanticConsistencyNode(nl2SqlService)))
182+
.addNode("human_feedback", node_async(new HumanFeedbackNode()));
180183

181184
stateGraph.addEdge(START, QUERY_REWRITE_NODE)
182185
.addConditionalEdges(QUERY_REWRITE_NODE, edge_async(new QueryRewriteDispatcher()),
@@ -203,6 +206,16 @@ public StateGraph nl2sqlGraph(ChatClient.Builder chatClientBuilder) throws Graph
203206
// If validation passes, proceed to the correct execution node
204207
SQL_EXECUTE_NODE, SQL_EXECUTE_NODE, PYTHON_GENERATE_NODE, PYTHON_GENERATE_NODE,
205208
REPORT_GENERATOR_NODE, REPORT_GENERATOR_NODE,
209+
// If human review is enabled, go to human_feedback node
210+
"human_feedback", "human_feedback",
211+
// If max repair attempts are reached, end the process
212+
END, END))
213+
// Human feedback node routing
214+
.addConditionalEdges("human_feedback", edge_async(new HumanFeedbackDispatcher()), Map.of(
215+
// If plan is rejected, go back to PlannerNode
216+
PLANNER_NODE, PLANNER_NODE,
217+
// If plan is approved, continue with execution
218+
PLAN_EXECUTOR_NODE, PLAN_EXECUTOR_NODE,
206219
// If max repair attempts are reached, end the process
207220
END, END))
208221
.addEdge(REPORT_GENERATOR_NODE, END)

spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/constant/Constant.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,7 @@ public class Constant {
139139

140140
public static final String ONLY_NL2SQL_OUTPUT = "ONLY_NL2SQL_OUTPUT";
141141

142+
// 人类复核相关
143+
public static final String HUMAN_REVIEW_ENABLED = "HUMAN_REVIEW_ENABLED";
144+
142145
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.alibaba.cloud.ai.dispatcher;
18+
19+
import com.alibaba.cloud.ai.graph.OverAllState;
20+
import com.alibaba.cloud.ai.graph.action.EdgeAction;
21+
22+
import static com.alibaba.cloud.ai.graph.StateGraph.END;
23+
24+
/**
25+
* Dispatcher for human feedback node routing.
26+
*
27+
* @author Makoto
28+
*/
29+
public class HumanFeedbackDispatcher implements EdgeAction {
30+
31+
@Override
32+
public String apply(OverAllState state) throws Exception {
33+
String nextNode = (String) state.value("human_next_node", END);
34+
35+
// 如果是等待反馈状态,返回END让图暂停
36+
if ("WAIT_FOR_FEEDBACK".equals(nextNode)) {
37+
return END;
38+
}
39+
40+
return nextNode;
41+
}
42+
43+
}

spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/PlanExecutorDispatcher.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,12 @@ public String apply(OverAllState state) {
4242

4343
if (validationPassed) {
4444
logger.info("Plan validation passed. Proceeding to next step.");
45-
return state.value(PLAN_NEXT_NODE, END);
45+
String nextNode = state.value(PLAN_NEXT_NODE, END);
46+
// 如果返回的是"END",直接返回END常量
47+
if ("END".equals(nextNode)) {
48+
return END;
49+
}
50+
return nextNode;
4651
}
4752
else {
4853
// Plan validation failed, check repair count and decide whether to retry or
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.alibaba.cloud.ai.node;
18+
19+
import com.alibaba.cloud.ai.graph.OverAllState;
20+
import com.alibaba.cloud.ai.graph.action.NodeAction;
21+
import com.alibaba.cloud.ai.util.StateUtils;
22+
import org.slf4j.Logger;
23+
import org.slf4j.LoggerFactory;
24+
import org.springframework.util.StringUtils;
25+
26+
import java.util.HashMap;
27+
import java.util.Map;
28+
29+
import static com.alibaba.cloud.ai.constant.Constant.*;
30+
31+
/**
32+
* Human feedback node for plan review and modification.
33+
*
34+
* @author Makoto
35+
*/
36+
public class HumanFeedbackNode implements NodeAction {
37+
38+
private static final Logger logger = LoggerFactory.getLogger(HumanFeedbackNode.class);
39+
40+
@Override
41+
public Map<String, Object> apply(OverAllState state) throws Exception {
42+
logger.info("Processing human feedback");
43+
Map<String, Object> updated = new HashMap<>();
44+
45+
// 检查最大修复次数
46+
int repairCount = StateUtils.getObjectValue(state, PLAN_REPAIR_COUNT, Integer.class, 0);
47+
if (repairCount >= 3) {
48+
logger.warn("Max repair attempts (3) exceeded, ending process");
49+
updated.put("human_next_node", "END");
50+
return updated;
51+
}
52+
53+
// 等待用户反馈
54+
OverAllState.HumanFeedback humanFeedback = state.humanFeedback();
55+
if (humanFeedback == null) {
56+
updated.put("human_next_node", "WAIT_FOR_FEEDBACK");
57+
return updated;
58+
}
59+
60+
// 处理反馈结果
61+
Map<String, Object> feedbackData = humanFeedback.data();
62+
boolean approved = (boolean) feedbackData.getOrDefault("feed_back", true);
63+
64+
if (approved) {
65+
logger.info("Plan approved → execution");
66+
updated.put("human_next_node", PLAN_EXECUTOR_NODE);
67+
updated.put(HUMAN_REVIEW_ENABLED, false);
68+
}
69+
else {
70+
logger.info("Plan rejected → regeneration (attempt {})", repairCount + 1);
71+
updated.put("human_next_node", PLANNER_NODE);
72+
updated.put(PLAN_REPAIR_COUNT, repairCount + 1);
73+
updated.put(PLAN_CURRENT_STEP, 1);
74+
updated.put(HUMAN_REVIEW_ENABLED, true);
75+
76+
// 保存用户反馈内容
77+
String feedbackContent = feedbackData.getOrDefault("feed_back_content", "").toString();
78+
updated.put(PLAN_VALIDATION_ERROR,
79+
StringUtils.hasLength(feedbackContent) ? feedbackContent : "Plan rejected by user");
80+
state.withoutResume();
81+
}
82+
83+
return updated;
84+
}
85+
86+
}

spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/node/PlanExecutorNode.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.util.Set;
3232

3333
import static com.alibaba.cloud.ai.constant.Constant.IS_ONLY_NL2SQL;
34+
import static com.alibaba.cloud.ai.constant.Constant.HUMAN_REVIEW_ENABLED;
3435
import static com.alibaba.cloud.ai.constant.Constant.ONLY_NL2SQL_OUTPUT;
3536
import static com.alibaba.cloud.ai.constant.Constant.PLANNER_NODE_OUTPUT;
3637
import static com.alibaba.cloud.ai.constant.Constant.PLAN_CURRENT_STEP;
@@ -106,7 +107,13 @@ public Map<String, Object> apply(OverAllState state) throws Exception {
106107
"Validation failed: The plan is not a valid JSON structure. Error: " + e.getMessage());
107108
}
108109

109-
// 2. Execute the Plan if validation passes
110+
// 2. If开启人工复核,则在执行前暂停,跳转到human_feedback节点
111+
Boolean humanReviewEnabled = state.value(HUMAN_REVIEW_ENABLED, false);
112+
if (Boolean.TRUE.equals(humanReviewEnabled)) {
113+
logger.info("Human review enabled: routing to human_feedback node");
114+
return Map.of(PLAN_VALIDATION_STATUS, true, PLAN_NEXT_NODE, "human_feedback");
115+
}
116+
110117
Plan plan = getPlan(state);
111118
Integer currentStep = getCurrentStepNumber(state);
112119
List<ExecutionStep> executionPlan = plan.getExecutionPlan();
@@ -140,6 +147,10 @@ private Map<String, Object> determineNextNode(String toolToUse) {
140147
logger.info("Determined next execution node: {}", toolToUse);
141148
return Map.of(PLAN_NEXT_NODE, toolToUse, PLAN_VALIDATION_STATUS, true);
142149
}
150+
else if ("human_feedback".equals(toolToUse)) {
151+
logger.info("Determined next execution node: {}", toolToUse);
152+
return Map.of(PLAN_NEXT_NODE, toolToUse, PLAN_VALIDATION_STATUS, true);
153+
}
143154
else {
144155
// This case should ideally not be reached if validation is done correctly
145156
// before.

spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/java/com/alibaba/cloud/ai/node/PlannerNode.java

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,44 +55,60 @@ public PlannerNode(ChatClient.Builder chatClientBuilder) {
5555

5656
@Override
5757
public Map<String, Object> apply(OverAllState state) throws Exception {
58-
logger.info("Entering {} node", this.getClass().getSimpleName());
5958
String input = (String) state.value(INPUT_KEY).orElseThrow();
60-
// load prompt template
61-
String businessKnowledgePrompt = (String) state.value(BUSINESS_KNOWLEDGE).orElse("");
62-
String semanticModelPrompt = (String) state.value(SEMANTIC_MODEL).orElse("");
63-
64-
// 是否为NL2SQL模式
6559
Boolean onlyNl2sql = state.value(IS_ONLY_NL2SQL, false);
6660

67-
SchemaDTO schemaDTO = (SchemaDTO) state.value(TABLE_RELATION_OUTPUT).orElseThrow();
68-
String schemaStr = PromptHelper.buildMixMacSqlDbPrompt(schemaDTO, true);
69-
70-
// Check if this is a repair attempt
61+
// 检查是否为修复模式
7162
String validationError = StateUtils.getStringValue(state, PLAN_VALIDATION_ERROR, null);
72-
String userPrompt;
7363
if (validationError != null) {
74-
logger.warn("This is a plan repair attempt. Previous error: {}", validationError);
75-
String previousPlan = StateUtils.getStringValue(state, PLANNER_NODE_OUTPUT, "");
76-
userPrompt = String.format(
77-
"The previous plan you generated failed validation with the following error: %s\n\nHere is the faulty plan:\n%s\n\nPlease correct the plan and provide a new, valid one to answer the original question: %s",
78-
validationError, previousPlan, input);
64+
logger.info("Regenerating plan with user feedback: {}", validationError);
7965
}
8066
else {
81-
userPrompt = input;
67+
logger.info("Generating initial plan");
8268
}
8369

70+
// 构建提示参数
71+
String businessKnowledge = (String) state.value(BUSINESS_KNOWLEDGE).orElse("");
72+
String semanticModel = (String) state.value(SEMANTIC_MODEL).orElse("");
73+
SchemaDTO schemaDTO = (SchemaDTO) state.value(TABLE_RELATION_OUTPUT).orElseThrow();
74+
String schemaStr = PromptHelper.buildMixMacSqlDbPrompt(schemaDTO, true);
75+
76+
// 构建用户提示
77+
String userPrompt = buildUserPrompt(input, validationError, state);
78+
79+
// 构建模板参数
8480
Map<String, Object> params = Map.of("user_question", userPrompt, "schema", schemaStr, "business_knowledge",
85-
businessKnowledgePrompt, "semantic_model", semanticModelPrompt);
86-
// 根据模式选择planer的Prompt
81+
businessKnowledge, "semantic_model", semanticModel, "plan_validation_error",
82+
formatValidationError(validationError));
83+
84+
// 生成计划
8785
String plannerPrompt = (onlyNl2sql ? PromptConstant.getPlannerNl2sqlOnlyTemplate()
8886
: PromptConstant.getPlannerPromptTemplate())
8987
.render(params);
90-
Flux<ChatResponse> chatResponseFlux = chatClient.prompt().user(plannerPrompt).stream().chatResponse();
9188

89+
Flux<ChatResponse> chatResponseFlux = chatClient.prompt().user(plannerPrompt).stream().chatResponse();
9290
var generator = StreamingChatGeneratorUtil.createStreamingGeneratorWithMessages(this.getClass(), state,
9391
v -> Map.of(PLANNER_NODE_OUTPUT, v), chatResponseFlux, StreamResponseType.PLAN_GENERATION);
9492

9593
return Map.of(PLANNER_NODE_OUTPUT, generator);
9694
}
9795

96+
private String buildUserPrompt(String input, String validationError, OverAllState state) {
97+
if (validationError == null) {
98+
return input;
99+
}
100+
101+
String previousPlan = StateUtils.getStringValue(state, PLANNER_NODE_OUTPUT, "");
102+
return String.format(
103+
"IMPORTANT: User rejected previous plan with feedback: \"%s\"\n\n" + "Original question: %s\n\n"
104+
+ "Previous rejected plan:\n%s\n\n"
105+
+ "CRITICAL: Generate new plan incorporating user feedback (\"%s\")",
106+
validationError, input, previousPlan, validationError);
107+
}
108+
109+
private String formatValidationError(String validationError) {
110+
return validationError != null ? String
111+
.format("**USER FEEDBACK (CRITICAL)**: %s\n\n**Must incorporate this feedback.**", validationError) : "";
112+
}
113+
98114
}

spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/resources/prompts/planner-nl2sql-only.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ You are a Senior NL2SQL Agent. Your primary function is to interpret a user's na
44

55
**CRITICAL: You MUST only output a valid JSON object. Do not include any explanations, comments, or additional text outside the JSON structure.**
66

7+
# USER FEEDBACK HANDLING (CRITICAL PRIORITY)
8+
9+
{plan_validation_error}
10+
11+
**If user feedback is provided above:**
12+
1. **MANDATORY REQUIREMENT**: The feedback contains critical requirements that MUST be satisfied in the new plan
13+
2. **ABSOLUTE COMPLIANCE**: Every aspect of the user feedback must be incorporated into your plan
14+
3. **NO EXCEPTIONS**: If user says "需要用Python" (need to use Python), you MUST include PYTHON_GENERATE_NODE steps
15+
4. **PRIORITY OVERRIDE**: User feedback requirements take precedence over any default analysis approach
16+
717
# CORE TASK
818

919
1. **Understand the Question**: Analyze the user's natural language query to determine the required metrics, dimensions, and timeframes.

spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-chat/src/main/resources/prompts/planner.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ You are a Senior Data Analysis Agent. Your primary function is to interpret a us
44

55
**CRITICAL: You MUST only output a valid JSON object. Do not include any explanations, comments, or additional text outside the JSON structure.**
66

7+
# USER FEEDBACK HANDLING (CRITICAL PRIORITY)
8+
9+
{plan_validation_error}
10+
11+
**If user feedback is provided above:**
12+
1. **MANDATORY REQUIREMENT**: The feedback contains critical requirements that MUST be satisfied in the new plan
13+
2. **ABSOLUTE COMPLIANCE**: Every aspect of the user feedback must be incorporated into your plan
14+
3. **NO EXCEPTIONS**: If user says "需要用Python" (need to use Python), you MUST include PYTHON_GENERATE_NODE steps
15+
4. **PRIORITY OVERRIDE**: User feedback requirements take precedence over any default analysis approach
16+
717
# CORE TASK
818

919
1. **Deconstruct the Request**: Deeply analyze the user's question to understand the core business objective, required metrics (e.g., conversion rates), dimensions (e.g., by region, by channel), and timeframes.

spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-management/src/main/java/com/alibaba/cloud/ai/controller/AgentSchemaController.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,9 @@ public Flux<ServerSentEvent<String>> agentChat(@PathVariable Long agentId,
259259
log.info("Agent {} chat request: {}", agentId, query);
260260

261261
// Directly call streamSearch method of Nl2sqlForGraphController
262-
return nl2sqlForGraphController.streamSearch(query.trim(), String.valueOf(agentId), response);
262+
// 生成一个threadId用于图执行
263+
String threadId = String.valueOf(System.currentTimeMillis());
264+
return nl2sqlForGraphController.streamSearch(query.trim(), String.valueOf(agentId), threadId, response);
263265

264266
}
265267
catch (Exception e) {

0 commit comments

Comments
 (0)