diff --git a/README.md b/README.md
index 33bc02fe9..e566146e4 100644
--- a/README.md
+++ b/README.md
@@ -117,7 +117,13 @@ spring-ai-alibaba-nl2sql/
将表和数据导入到你的MySQL数据库中。
-### 2. 配置management数据库
+
+
+### 2. 配置
+
+#### 2.1 配置management数据库
+
+
在`spring-ai-alibaba-nl2sql-management/src/main/resources/application.yml`中配置你的MySQL数据库连接信息。
@@ -133,7 +139,9 @@ spring:
type: com.alibaba.druid.pool.DruidDataSource
```
-### 2.1 可选:启用/关闭自动初始化(schema.sql + data.sql)
+#### 2.2 可选:启用/关闭自动初始化(schema.sql + data.sql)
+
+
- 默认配置:`application.yml` 中已设置为开启
@@ -162,7 +170,7 @@ spring:

-### 3. 配置 API Key
+#### 2.3 配置 API Key
```yaml
spring:
@@ -178,15 +186,52 @@ spring:
```
-### 4. 启动管理端
+
+#### 2.4 嵌入模型批处理策略配置
+
+
+
+| 属性 | 说明 | 默认值 |
+| ----------------------------------------------------------- | ------------------------------------------------------------ | ----------- |
+| spring.ai.alibaba.nl2sql.embedding-batch.encoding-type | 文本编码类型,可参考com.knuddels.jtokkit.api.EncodingType | cl100k_base |
+| spring.ai.alibaba.nl2sql.embedding-batch.max-token-count | 每批次最大令牌数 值越小,每批次文档越少,但更安全 值越大,处理效率越高,但可能超出API限制 建议值:2000-8000,根据实际API限制调整 | 2000 |
+| spring.ai.alibaba.nl2sql.embedding-batch.reserve-percentage | 预留百分比 用于预留缓冲空间,避免超出限制 建议值:0.1-0.2(10%-20%) | 0.2 |
+| spring.ai.alibaba.nl2sql.embedding-batch.max-text-count | 每批次最大文本数量 适用于DashScope等有文本数量限制的API DashScope限制为10 | 10 |
+
+
+
+#### 2.5 向量库配置
+
+
+
+| 属性 | 说明 | 默认值 |
+| ------------------------------------------------------------ | ------------------------------------------------------------ | --------- |
+| spring.ai.alibaba.nl2sql.vector-store.similarity-threshold | 相似度阈值配置,用于过滤相似度分数大于等于此阈值的文档 | 0.2 |
+| spring.ai.alibaba.nl2sql.vector-store.batch-del-topk-limit | 一次删除操作中,最多删除的文档数量 | 5000 |
+| spring.ai.alibaba.nl2sql.vector-store.topk-limit | 查询返回最大文档数 | 30 |
+| **spring.ai.alibaba.nl2sql.vector-store.enable-hybrid-search** | 是否启用混合搜索。**注意**:**项目目前默认只提供ES的混合检索能力,
如需要扩展其他向量库可自行继承重写 com.alibaba.cloud.ai.service.hybrid.retrieval
.AbstractHybridRetrievalStrategy#retrieve 该方法**
**并且修改com.alibaba.cloud.ai.service.hybrid.
factory.HybridRetrievalStrategyFactory#getObject
注册相应的bean** | **false** |
+| spring.ai.alibaba.nl2sql.vector-store.elasticsearch-min-score | Elasticsearch最小分数阈值,用于es执行关键词搜索时过滤相关性较低的文档。
**开发时使用的es服务端版本 8.15.0** | 0.5 |
+
+
+
+#### 2.6 检索融合策略
+
+| 属性 | 说明 | 默认值 |
+| ---------------------------------------- | -------------------- | ------ |
+| spring.ai.alibaba.nl2sql.fusion-strategy | 多路召回结果融合策略 | rrf |
+| | | |
+
+
+
+### 3. 启动管理端
在`spring-ai-alibaba-nl2sql-management`目录下,运行 `spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-management/src/main/java/com/alibaba/cloud/ai/DataAgentApplication.java` 类。
-### 5. 启动WEB页面
+### 4. 启动WEB页面
进入 `spring-ai-alibaba-nl2sql/spring-ai-alibaba-nl2sql-web-ui` 目录
-#### 安装依赖
+#### 4.1 安装依赖
```bash
@@ -197,7 +242,7 @@ npm install
yarn install
```
-### 启动服务
+#### 4.2 启动服务
```bash
# 使用 npm
diff --git a/spring-ai-alibaba-data-agent-chat/pom.xml b/spring-ai-alibaba-data-agent-chat/pom.xml
index 802411eda..ea9620c7f 100644
--- a/spring-ai-alibaba-data-agent-chat/pom.xml
+++ b/spring-ai-alibaba-data-agent-chat/pom.xml
@@ -39,6 +39,12 @@
spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-aop
+
+
org.springframework.boot
spring-boot-starter-test
@@ -199,7 +205,10 @@
elasticsearch-rest-client
${elasticsearch-client.version}
-
+
+ jakarta.validation
+ jakarta.validation-api
+
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/aop/NodeEntryLoggingAspect.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/aop/NodeEntryLoggingAspect.java
new file mode 100644
index 000000000..7653652fa
--- /dev/null
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/aop/NodeEntryLoggingAspect.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.alibaba.cloud.ai.aop;
+
+import com.alibaba.cloud.ai.graph.OverAllState;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.stereotype.Component;
+
+/**
+ * AOP切面类,用于记录所有Node类的入口日志
+ *
+ *
+ */
+@Aspect
+@Component
+@Slf4j
+public class NodeEntryLoggingAspect {
+
+ @Pointcut("execution(* com.alibaba.cloud.ai.node..*.apply(com.alibaba.cloud.ai.graph.OverAllState))")
+ public void nodeEntry() {
+ }
+
+ /**
+ * 在所有实现NodeAction接口的类的apply方法执行前记录日志
+ * @param joinPoint 连接点
+ */
+ @Before("nodeEntry()")
+ public void logNodeEntry(JoinPoint joinPoint) {
+ String className = joinPoint.getTarget().getClass().getSimpleName();
+ log.info("Entering {} node", className);
+
+ // 获取方法参数并打印状态信息
+ Object[] args = joinPoint.getArgs();
+ if (args != null && args.length > 0 && args[0] instanceof OverAllState state) {
+ log.debug("State: {}", state);
+ }
+ }
+
+}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/config/DataAgentConfiguration.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/config/DataAgentConfiguration.java
index 4d5c39481..9af906f83 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/config/DataAgentConfiguration.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/config/DataAgentConfiguration.java
@@ -108,22 +108,23 @@ public StateGraph nl2sqlGraph(NodeBeanUtil nodeBeanUtil) throws GraphStateExcept
keyStrategyHashMap.put(INPUT_KEY, new ReplaceStrategy());
// Agent ID
keyStrategyHashMap.put(AGENT_ID, new ReplaceStrategy());
- // Business knowledge
- keyStrategyHashMap.put(BUSINESS_KNOWLEDGE, new ReplaceStrategy());
+ // Intent recognition
+ keyStrategyHashMap.put(INTENT_RECOGNITION_NODE_OUTPUT, new ReplaceStrategy());
+ // QUERY_ENHANCE_NODE节点输出
+ keyStrategyHashMap.put(QUERY_ENHANCE_NODE_OUTPUT, new ReplaceStrategy());
// Semantic model
- keyStrategyHashMap.put(SEMANTIC_MODEL, new ReplaceStrategy());
- // queryWrite节点输出
- keyStrategyHashMap.put(QUERY_REWRITE_NODE_OUTPUT, new ReplaceStrategy());
- // keyword extract节点输出
- keyStrategyHashMap.put(KEYWORD_EXTRACT_NODE_OUTPUT, new ReplaceStrategy());
- keyStrategyHashMap.put(EVIDENCES, new ReplaceStrategy());
+ keyStrategyHashMap.put(GENEGRATED_SEMANTIC_MODEL_PROMPT, new ReplaceStrategy());
+ // EVIDENCE节点输出
+ keyStrategyHashMap.put(EVIDENCE, new ReplaceStrategy());
// schema recall节点输出
keyStrategyHashMap.put(TABLE_DOCUMENTS_FOR_SCHEMA_OUTPUT, new ReplaceStrategy());
- keyStrategyHashMap.put(COLUMN_DOCUMENTS_BY_KEYWORDS_OUTPUT, new ReplaceStrategy());
+ keyStrategyHashMap.put(COLUMN_DOCUMENTS__FOR_SCHEMA_OUTPUT, new ReplaceStrategy());
// table relation节点输出
keyStrategyHashMap.put(TABLE_RELATION_OUTPUT, new ReplaceStrategy());
keyStrategyHashMap.put(TABLE_RELATION_EXCEPTION_OUTPUT, new ReplaceStrategy());
keyStrategyHashMap.put(TABLE_RELATION_RETRY_COUNT, new ReplaceStrategy());
+ // Feasibility Assessment 节点输出
+ keyStrategyHashMap.put(FEASIBILITY_ASSESSMENT_NODE_OUTPUT, new ReplaceStrategy());
// sql generate节点输出
keyStrategyHashMap.put(SQL_GENERATE_SCHEMA_MISSING_ADVICE, new ReplaceStrategy());
keyStrategyHashMap.put(SQL_GENERATE_OUTPUT, new ReplaceStrategy());
@@ -160,10 +161,12 @@ public StateGraph nl2sqlGraph(NodeBeanUtil nodeBeanUtil) throws GraphStateExcept
};
StateGraph stateGraph = new StateGraph(NL2SQL_GRAPH_NAME, keyStrategyFactory)
- .addNode(QUERY_REWRITE_NODE, nodeBeanUtil.getNodeBeanAsync(QueryRewriteNode.class))
- .addNode(KEYWORD_EXTRACT_NODE, nodeBeanUtil.getNodeBeanAsync(KeywordExtractNode.class))
+ .addNode(INTENT_RECOGNITION_NODE, nodeBeanUtil.getNodeBeanAsync(IntentRecognitionNode.class))
+ .addNode(EVIDENCE_RECALL_NODE, nodeBeanUtil.getNodeBeanAsync(EvidenceRecallNode.class))
+ .addNode(QUERY_ENHANCE_NODE, nodeBeanUtil.getNodeBeanAsync(QueryEnhanceNode.class))
.addNode(SCHEMA_RECALL_NODE, nodeBeanUtil.getNodeBeanAsync(SchemaRecallNode.class))
.addNode(TABLE_RELATION_NODE, nodeBeanUtil.getNodeBeanAsync(TableRelationNode.class))
+ .addNode(FEASIBILITY_ASSESSMENT_NODE, nodeBeanUtil.getNodeBeanAsync(FeasibilityAssessmentNode.class))
.addNode(SQL_GENERATE_NODE, nodeBeanUtil.getNodeBeanAsync(SqlGenerateNode.class))
.addNode(PLANNER_NODE, nodeBeanUtil.getNodeBeanAsync(PlannerNode.class))
.addNode(PLAN_EXECUTOR_NODE, nodeBeanUtil.getNodeBeanAsync(PlanExecutorNode.class))
@@ -175,13 +178,19 @@ public StateGraph nl2sqlGraph(NodeBeanUtil nodeBeanUtil) throws GraphStateExcept
.addNode(SEMANTIC_CONSISTENCY_NODE, nodeBeanUtil.getNodeBeanAsync(SemanticConsistencyNode.class))
.addNode(HUMAN_FEEDBACK_NODE, nodeBeanUtil.getNodeBeanAsync(HumanFeedbackNode.class));
- stateGraph.addEdge(START, QUERY_REWRITE_NODE)
- .addConditionalEdges(QUERY_REWRITE_NODE, edge_async(new QueryRewriteDispatcher()),
- Map.of(KEYWORD_EXTRACT_NODE, KEYWORD_EXTRACT_NODE, END, END))
- .addEdge(KEYWORD_EXTRACT_NODE, SCHEMA_RECALL_NODE)
+ stateGraph.addEdge(START, INTENT_RECOGNITION_NODE)
+ .addConditionalEdges(INTENT_RECOGNITION_NODE, edge_async(new IntentRecognitionDispatcher()),
+ Map.of(EVIDENCE_RECALL_NODE, EVIDENCE_RECALL_NODE, END, END))
+ .addEdge(EVIDENCE_RECALL_NODE, QUERY_ENHANCE_NODE)
+ .addConditionalEdges(QUERY_ENHANCE_NODE, edge_async(new QueryEnhanceDispatcher()),
+ Map.of(SCHEMA_RECALL_NODE, SCHEMA_RECALL_NODE, END, END))
.addEdge(SCHEMA_RECALL_NODE, TABLE_RELATION_NODE)
.addConditionalEdges(TABLE_RELATION_NODE, edge_async(new TableRelationDispatcher()),
- Map.of(PLANNER_NODE, PLANNER_NODE, END, END, TABLE_RELATION_NODE, TABLE_RELATION_NODE)) // retry
+ Map.of(FEASIBILITY_ASSESSMENT_NODE, FEASIBILITY_ASSESSMENT_NODE, END, END, TABLE_RELATION_NODE,
+ TABLE_RELATION_NODE)) // retry
+ .addConditionalEdges(FEASIBILITY_ASSESSMENT_NODE, edge_async(new FeasibilityAssessmentDispatcher()),
+ Map.of(PLANNER_NODE, PLANNER_NODE, END, END))
+
// The edge from PlannerNode now goes to PlanExecutorNode for validation and
// execution
.addEdge(PLANNER_NODE, PLAN_EXECUTOR_NODE)
@@ -214,16 +223,15 @@ public StateGraph nl2sqlGraph(NodeBeanUtil nodeBeanUtil) throws GraphStateExcept
.addConditionalEdges(SQL_EXECUTE_NODE, edge_async(new SQLExecutorDispatcher()),
Map.of(SQL_GENERATE_NODE, SQL_GENERATE_NODE, SEMANTIC_CONSISTENCY_NODE, SEMANTIC_CONSISTENCY_NODE))
.addConditionalEdges(SQL_GENERATE_NODE, edge_async(new SqlGenerateDispatcher()),
- Map.of(KEYWORD_EXTRACT_NODE, KEYWORD_EXTRACT_NODE, END, END, SQL_EXECUTE_NODE, SQL_EXECUTE_NODE))
+ Map.of(FEASIBILITY_ASSESSMENT_NODE, FEASIBILITY_ASSESSMENT_NODE, END, END, SQL_EXECUTE_NODE,
+ SQL_EXECUTE_NODE))
.addConditionalEdges(SEMANTIC_CONSISTENCY_NODE, edge_async(new SemanticConsistenceDispatcher()),
Map.of(SQL_GENERATE_NODE, SQL_GENERATE_NODE, PLAN_EXECUTOR_NODE, PLAN_EXECUTOR_NODE));
GraphRepresentation graphRepresentation = stateGraph.getGraph(GraphRepresentation.Type.PLANTUML,
"workflow graph");
- log.info("\n\n");
- log.info(graphRepresentation.content());
- log.info("\n\n");
+ log.info("workflow in PlantUML format as follows \n\n" + graphRepresentation.content() + "\n\n");
return stateGraph;
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/config/DataAgentProperties.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/config/DataAgentProperties.java
index 51e7f5200..51abd8d8f 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/config/DataAgentProperties.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/config/DataAgentProperties.java
@@ -37,6 +37,8 @@ public class DataAgentProperties {
*/
private EmbeddingBatch embeddingBatch = new EmbeddingBatch();
+ private VectorStoreProperties vectorStore = new VectorStoreProperties();
+
@Getter
@Setter
public static class EmbeddingBatch {
@@ -63,4 +65,35 @@ public static class EmbeddingBatch {
}
+ @Getter
+ @Setter
+ public static class VectorStoreProperties {
+
+ /**
+ * 相似度阈值配置,用于过滤相似度分数大于等于此阈值的文档
+ */
+ private double similarityThreshold = 0.2;
+
+ /**
+ * 一次删除操作中,最多删除的文档数量
+ */
+ private int batchDelTopkLimit = 5000;
+
+ /**
+ * 查询时返回的最大文档数量
+ */
+ private int topkLimit = 30;
+
+ /**
+ * 是否启用混合搜索
+ */
+ private boolean enableHybridSearch = false;
+
+ /**
+ * Elasticsearch最小分数阈值,用于es执行关键词搜索时过滤相关性较低的文档
+ */
+ private double elasticsearchMinScore = 0.5;
+
+ }
+
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/constant/Constant.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/constant/Constant.java
index 35bd698b7..758dad4fe 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/constant/Constant.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/constant/Constant.java
@@ -35,17 +35,19 @@ private Constant() {
public static final String NL2SQL_GRAPH_NAME = "nl2sqlGraph";
- public static final String QUERY_REWRITE_NODE_OUTPUT = "QUERY_REWRITE_NODE_OUTPUT";
+ public static final String INTENT_RECOGNITION_NODE_OUTPUT = "INTENT_RECOGNITION_NODE_OUTPUT";
- public static final String KEYWORD_EXTRACT_NODE_OUTPUT = "KEYWORD_EXTRACT_NODE_OUTPUT";
+ public static final String QUERY_ENHANCE_NODE_OUTPUT = "QUERY_ENHANCE_NODE_OUTPUT";
- public static final String EVIDENCES = "EVIDENCES";
+ public static final String FEASIBILITY_ASSESSMENT_NODE_OUTPUT = "FEASIBILITY_ASSESSMENT_NODE_OUTPUT";
+
+ public static final String EVIDENCE = "EVIDENCE";
public static final String TABLE_DOCUMENTS_FOR_SCHEMA_OUTPUT = "TABLE_DOCUMENTS_FOR_SCHEMA";
public static final String SCHEMA_RECALL_NODE_OUTPUT = "SCHEMA_RECALL_NODE_OUTPUT";
- public static final String COLUMN_DOCUMENTS_BY_KEYWORDS_OUTPUT = "COLUMN_DOCUMENTS_BY_KEYWORDS_OUTPUT";
+ public static final String COLUMN_DOCUMENTS__FOR_SCHEMA_OUTPUT = "COLUMN_DOCUMENTS__FOR_SCHEMA_OUTPUT";
public static final String TABLE_RELATION_OUTPUT = "TABLE_RELATION_OUTPUT";
@@ -53,9 +55,7 @@ private Constant() {
public static final String TABLE_RELATION_RETRY_COUNT = "TABLE_RELATION_RETRY_COUNT";
- public static final String BUSINESS_KNOWLEDGE = "BUSINESS_KNOWLEDGE";
-
- public static final String SEMANTIC_MODEL = "SEMANTIC_MODEL";
+ public static final String GENEGRATED_SEMANTIC_MODEL_PROMPT = "GENEGRATED_SEMANTIC_MODEL_PROMPT";
public static final String SQL_GENERATE_OUTPUT = "SQL_GENERATE_OUTPUT";
@@ -93,6 +93,14 @@ private Constant() {
public static final String PLAN_EXECUTOR_NODE = "PLAN_EXECUTOR_NODE";
+ public static final String INTENT_RECOGNITION_NODE = "INTENT_RECOGNITION_NODE";
+
+ public static final String EVIDENCE_RECALL_NODE = "EVIDENCE_RECALL_NODE";
+
+ public static final String QUERY_ENHANCE_NODE = "QUERY_ENHANCE_NODE";
+
+ public static final String FEASIBILITY_ASSESSMENT_NODE = "FEASIBILITY_ASSESSMENT_NODE";
+
public static final String QUERY_REWRITE_NODE = "QUERY_REWRITE_NODE";
public static final String REPORT_GENERATOR_NODE = "REPORT_GENERATOR_NODE";
@@ -144,16 +152,4 @@ private Constant() {
// 人类复核相关
public static final String HUMAN_REVIEW_ENABLED = "HUMAN_REVIEW_ENABLED";
- // column
- public static final String COLUMN = "column";
-
- // table
- public static final String TABLE = "table";
-
- // vectorType
- public static final String VECTOR_TYPE = "vectorType";
-
- // knowledgeId
- public static final String KNOWLEDGE_ID = "knowledgeId";
-
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/constant/DocumentMetadataConstant.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/constant/DocumentMetadataConstant.java
new file mode 100644
index 000000000..3379e2303
--- /dev/null
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/constant/DocumentMetadataConstant.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.cloud.ai.constant;
+
+// Document中metadata中存储的key
+public final class DocumentMetadataConstant {
+
+ private DocumentMetadataConstant() {
+
+ }
+
+ // column
+ public static final String COLUMN = "column";
+
+ // table
+ public static final String TABLE = "table";
+
+ // vectorType
+ public static final String VECTOR_TYPE = "vectorType";
+
+ // knowledgeId
+ public static final String KNOWLEDGE_ID = "knowledgeId";
+
+ // businessTerm
+ public static final String BUSINESS_TERM = "businessTerm";
+
+ // 智能体的知识
+ public static final String AGENT_KNOWLEDGE = "agentKnowledge";
+
+ // 关联该文档在数据库中的记录id
+ public static final String DB_RECORD_ID = "dbRecordId";
+
+ // isRecall
+ public static final String IS_RECALL = "isRecall";
+
+}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/QueryRewriteDispatcher.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/FeasibilityAssessmentDispatcher.java
similarity index 54%
rename from spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/QueryRewriteDispatcher.java
rename to spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/FeasibilityAssessmentDispatcher.java
index 5f901210d..b35ca74b7 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/QueryRewriteDispatcher.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/FeasibilityAssessmentDispatcher.java
@@ -23,30 +23,25 @@
import static com.alibaba.cloud.ai.constant.Constant.*;
import static com.alibaba.cloud.ai.graph.StateGraph.END;
-/**
- * @author zhangshenghang
- */
@Slf4j
-public class QueryRewriteDispatcher implements EdgeAction {
+public class FeasibilityAssessmentDispatcher implements EdgeAction {
@Override
- public String apply(OverAllState state) {
- // value的值是和 resources/init-rewrite.txt的输出一致,例如
- // 需求类型:《自由闲聊》.........
- // 语种类型:《中文》
- // 需求内容:2025-10-17天气如何
- String value = state.value(QUERY_REWRITE_NODE_OUTPUT, END);
- log.info("[QueryRewriteDispatcher]apply方法被调用,QUERY_REWRITE_NODE_OUTPUT的value如下\n {}", value);
-
- if (value != null && value.contains("需求类型:《数据分析》")) {
- log.info("[QueryRewriteDispatcher]需求类型为数据分析,进入KEYWORD_EXTRACT_NODE节点");
- return KEYWORD_EXTRACT_NODE;
+ public String apply(OverAllState state) throws Exception {
+ // value的值是和 resources/feasibility-assessment.txt的输出一致,例如
+ // 【需求类型】:《数据分析》
+ // 【语种类型】:《中文》
+ // 【需求内容】:查询所有“核心用户”的数量
+ String value = state.value(FEASIBILITY_ASSESSMENT_NODE_OUTPUT, END);
+
+ if (value != null && value.contains("【需求类型】:《数据分析》")) {
+ log.info("[FeasibilityAssessmentNodeDispatcher]需求类型为数据分析,进入PlannerNode节点");
+ return PLANNER_NODE;
}
else {
- log.info("[QueryRewriteDispatcher]需求类型非数据分析,返回END节点");
+ log.info("[FeasibilityAssessmentNodeDispatcher]需求类型非数据分析,返回END节点");
return END;
}
-
}
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/IntentRecognitionDispatcher.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/IntentRecognitionDispatcher.java
new file mode 100644
index 000000000..ceeece468
--- /dev/null
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/IntentRecognitionDispatcher.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.alibaba.cloud.ai.dispatcher;
+
+import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.action.EdgeAction;
+import com.alibaba.cloud.ai.util.JsonUtil;
+import com.alibaba.cloud.ai.util.MarkdownParserUtil;
+import com.alibaba.cloud.ai.util.StateUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+
+import static com.alibaba.cloud.ai.constant.Constant.*;
+import static com.alibaba.cloud.ai.graph.StateGraph.END;
+
+/**
+ * 根据意图识别结果决定下一个节点的分发器
+ */
+@Slf4j
+public class IntentRecognitionDispatcher implements EdgeAction {
+
+ @Override
+ public String apply(OverAllState state) throws Exception {
+ // 获取意图识别结果
+ String intentResult = StateUtil.getStringValue(state, INTENT_RECOGNITION_NODE_OUTPUT);
+
+ if (intentResult == null || intentResult.trim().isEmpty()) {
+ log.warn("Intent recognition result is null or empty, defaulting to END");
+ return END;
+ }
+
+ try {
+ // 解析JSON格式的意图识别结果
+ JsonNode jsonNode = JsonUtil.getObjectMapper()
+ .readTree(MarkdownParserUtil.extractText(intentResult.trim()));
+ String classification = jsonNode.path("classification").asText();
+
+ // 根据分类结果决定下一个节点
+ if ("《闲聊或无关指令》".equals(classification)) {
+ log.warn("Intent classified as chat or irrelevant, ending conversation");
+ return END;
+ }
+ else {
+ log.info("Intent classified as potential data analysis request, proceeding to evidence recall");
+ return EVIDENCE_RECALL_NODE;
+ }
+ }
+ catch (JsonProcessingException e) {
+ log.error("Failed to parse intent recognition result as JSON: {}", intentResult, e);
+ return END;
+ }
+ }
+
+}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/QueryEnhanceDispatcher.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/QueryEnhanceDispatcher.java
new file mode 100644
index 000000000..c7c73aacc
--- /dev/null
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/QueryEnhanceDispatcher.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.cloud.ai.dispatcher;
+
+import com.alibaba.cloud.ai.dto.QueryEnhanceOutputDTO;
+import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.action.EdgeAction;
+import com.alibaba.cloud.ai.util.StateUtil;
+import lombok.extern.slf4j.Slf4j;
+
+import static com.alibaba.cloud.ai.constant.Constant.QUERY_ENHANCE_NODE_OUTPUT;
+import static com.alibaba.cloud.ai.constant.Constant.SCHEMA_RECALL_NODE;
+import static com.alibaba.cloud.ai.graph.StateGraph.END;
+
+/**
+ * 根据查询增强结果决定下一个节点的分发器
+ */
+@Slf4j
+public class QueryEnhanceDispatcher implements EdgeAction {
+
+ @Override
+ public String apply(OverAllState state) throws Exception {
+ // 获取查询处理结果
+ QueryEnhanceOutputDTO queryProcessOutput = StateUtil.getObjectValue(state, QUERY_ENHANCE_NODE_OUTPUT,
+ QueryEnhanceOutputDTO.class);
+
+ // 检查查询处理结果是否为空
+ if (queryProcessOutput == null) {
+ log.warn("Query process output is null, ending conversation");
+ return END;
+ }
+
+ // 检查各个字段是否为空
+ boolean isCanonicalQueryEmpty = queryProcessOutput.getCanonicalQuery() == null
+ || queryProcessOutput.getCanonicalQuery().trim().isEmpty();
+ boolean isExpandedQueriesEmpty = queryProcessOutput.getExpandedQueries() == null
+ || queryProcessOutput.getExpandedQueries().isEmpty();
+ boolean isConsolidatedKeywordsEmpty = queryProcessOutput.getConsolidatedKeywords() == null
+ || queryProcessOutput.getConsolidatedKeywords().isEmpty();
+
+ if (isCanonicalQueryEmpty || isExpandedQueriesEmpty || isConsolidatedKeywordsEmpty) {
+ log.warn(
+ "Query process output contains empty fields - canonicalQuery: {}, expandedQueries: {}, consolidatedKeywords: {}",
+ isCanonicalQueryEmpty, isExpandedQueriesEmpty, isConsolidatedKeywordsEmpty);
+ return END;
+ }
+ else {
+ log.info("Query process output is valid, proceeding to schema recall");
+ return SCHEMA_RECALL_NODE;
+ }
+ }
+
+}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/SqlGenerateDispatcher.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/SqlGenerateDispatcher.java
index 530f1f9aa..64a600bf0 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/SqlGenerateDispatcher.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/SqlGenerateDispatcher.java
@@ -38,9 +38,10 @@ public String apply(OverAllState state) {
log.info("检测到流程结束标志: {}", END);
yield END;
}
+ // TODO 需要优化,不能简单跳转
case SQL_GENERATE_SCHEMA_MISSING -> {
- log.warn("SQL生成缺少Schema,跳转到{}节点", KEYWORD_EXTRACT_NODE);
- yield KEYWORD_EXTRACT_NODE;
+ log.warn("SQL生成缺少Schema,跳转到{}节点", FEASIBILITY_ASSESSMENT_NODE);
+ yield FEASIBILITY_ASSESSMENT_NODE;
}
default -> {
log.info("SQL生成成功,进入SQL执行节点: {}", SQL_EXECUTE_NODE);
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/TableRelationDispatcher.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/TableRelationDispatcher.java
index 7ab10b354..9b3094817 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/TableRelationDispatcher.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dispatcher/TableRelationDispatcher.java
@@ -46,7 +46,7 @@ public String apply(OverAllState state) throws Exception {
Optional tableRelationOutput = state.value(TABLE_RELATION_OUTPUT);
if (tableRelationOutput.isPresent()) {
- return PLANNER_NODE; // next node is planner
+ return FEASIBILITY_ASSESSMENT_NODE;
}
// no output, end
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/BusinessKnowledgeDTO.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/BusinessKnowledgeDTO.java
index 60b659055..7b5ebf775 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/BusinessKnowledgeDTO.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/BusinessKnowledgeDTO.java
@@ -16,6 +16,8 @@
package com.alibaba.cloud.ai.dto;
import com.alibaba.cloud.ai.entity.BusinessKnowledge;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@@ -30,8 +32,10 @@
@Builder
public class BusinessKnowledgeDTO {
+ @NotBlank(message = "Business term cannot be empty")
private String businessTerm; // Business term
+ @NotBlank(message = "Description cannot be empty")
private String description; // Description
private String synonyms; // Synonyms, comma separated
@@ -39,6 +43,7 @@ public class BusinessKnowledgeDTO {
@Builder.Default
private Boolean isRecall = true; // is recall
+ @NotNull(message = "Agent ID cannot be Null")
private Long agentId; // Associated agent ID
public BusinessKnowledge toEntity() {
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/QueryEnhanceOutputDTO.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/QueryEnhanceOutputDTO.java
new file mode 100644
index 000000000..0ea499a7c
--- /dev/null
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/QueryEnhanceOutputDTO.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.cloud.ai.dto;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+// 对应 模板query-enhancement.txt的输出
+@Data
+@NoArgsConstructor
+public class QueryEnhanceOutputDTO {
+
+ // 经LLM重写后的 规范化查询
+ @JsonProperty("canonical_query")
+ private String canonicalQuery;
+
+ // 基于canonicalQuery的扩展查询
+ @JsonProperty("expanded_queries")
+ private List expandedQueries;
+
+ // 从"规范化查询"、所有"扩展问题"以及 "业务知识涉及的关键概念和数值"中,提取出的所有核心关键词。
+ @JsonProperty("consolidated_keywords")
+ private List consolidatedKeywords;
+
+}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/SemanticModelAddDTO.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/SemanticModelAddDTO.java
new file mode 100644
index 000000000..17e3e6430
--- /dev/null
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/SemanticModelAddDTO.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.cloud.ai.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+
+/**
+ * 添加语义模型的DTO类
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SemanticModelAddDTO {
+
+ /**
+ * 关联的智能体ID
+ */
+ @NotNull(message = "智能体ID不能为空")
+ private Integer agentId;
+
+ /**
+ * 关联的表名
+ */
+ @NotBlank(message = "表名不能为空")
+ private String tableName;
+
+ /**
+ * 数据库中的物理字段名 (例如: csat_score)
+ */
+ @NotBlank(message = "数据库字段名不能为空")
+ private String columnName;
+
+ /**
+ * 业务名/别名 (例如: 客户满意度分数)
+ */
+ @NotBlank(message = "业务名称不能为空")
+ private String businessName;
+
+ /**
+ * 业务名的同义词 (例如: 满意度,客户评分)
+ */
+ private String synonyms;
+
+ /**
+ * 业务描述 (用于向LLM解释字段的业务含义)
+ */
+ private String businessDescription;
+
+ /**
+ * 数据库中的物理字段的原始注释
+ */
+ private String columnComment;
+
+ /**
+ * 物理数据类型 (例如: int, varchar(20))
+ */
+ @NotBlank(message = "数据类型不能为空")
+ private String dataType;
+
+}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/schema/ColumnDTO.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/schema/ColumnDTO.java
index 6b4a6b14c..adc05b14f 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/schema/ColumnDTO.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/dto/schema/ColumnDTO.java
@@ -38,8 +38,6 @@ public class ColumnDTO {
private String type;
- private List samples;
-
private List data;
private Map mapping;
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/entity/SemanticModel.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/entity/SemanticModel.java
index 8298a4c85..9cce5d0f5 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/entity/SemanticModel.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/entity/SemanticModel.java
@@ -31,29 +31,77 @@
@AllArgsConstructor
public class SemanticModel {
+ /**
+ * 主键ID
+ */
private Long id;
- private Long agentId;
+ /**
+ * 关联的智能体ID
+ */
+ private Integer agentId;
- private String fieldName;
+ /**
+ * 关联的数据源ID
+ */
+ private Integer datasourceId;
- private String conversationName;
+ /**
+ * 关联的表名
+ */
+ private String tableName;
+ /**
+ * 数据库中的物理字段名 (例如: csat_score)
+ */
+ private String columnName;
+
+ /**
+ * 业务名/别名 (例如: 客户满意度分数)
+ */
+ private String businessName;
+
+ /**
+ * 业务名的同义词 (例如: 满意度,客户评分)
+ */
private String synonyms;
- private String description;
+ /**
+ * 业务描述 (用于向LLM解释字段的业务含义)
+ */
+ private String businessDescription;
- private String type;
+ /**
+ * 数据库中的物理字段的原始注释
+ */
+ private String columnComment;
+ /**
+ * 物理数据类型 (例如: int, varchar(20))
+ */
+ private String dataType;
+
+ /**
+ * 状态: 0 停用 1 启用
+ */
+ private Integer status;
+
+ /**
+ * 创建时间
+ */
private LocalDateTime createdTime;
+ /**
+ * 更新时间
+ */
private LocalDateTime updateTime;
- private Integer status;
-
+ /**
+ * 获取用于提示的信息
+ */
public String getPromptInfo() {
- return String.format("对话字段名称: %s, 数据库字段名: %s, 字段同义词: %s, 字段描述: %s, 字段类型: %s", conversationName, fieldName,
- synonyms, description, type);
+ return String.format("业务名称: %s, 表名: %s, 数据库字段名: %s, 字段同义词: %s, 业务描述: %s, 数据类型: %s", businessName, tableName,
+ columnName, synonyms, businessDescription, dataType);
}
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/mapper/SemanticModelMapper.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/mapper/SemanticModelMapper.java
index 49afb1052..c88af1a53 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/mapper/SemanticModelMapper.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/mapper/SemanticModelMapper.java
@@ -51,9 +51,9 @@ public interface SemanticModelMapper {
*/
@Select("""
SELECT * FROM semantic_model
- WHERE field_name LIKE CONCAT('%', #{keyword}, '%')
- OR conversation_name LIKE CONCAT('%', #{keyword}, '%')
- OR description LIKE CONCAT('%', #{keyword}, '%')
+ WHERE column_name LIKE CONCAT('%', #{keyword}, '%')
+ OR business_name LIKE CONCAT('%', #{keyword}, '%')
+ OR business_description LIKE CONCAT('%', #{keyword}, '%')
OR synonyms LIKE CONCAT('%', #{keyword}, '%')
ORDER BY created_time DESC
""")
@@ -92,9 +92,9 @@ OR synonyms LIKE CONCAT('%', #{keyword}, '%')
@Insert("""
INSERT INTO semantic_model
- (agent_id, field_name, conversation_name, synonyms, description, type, created_time, updated_time, status)
+ (agent_id, datasource_id, table_name, column_name, business_name, synonyms, business_description, column_comment, data_type, created_time, updated_time, status)
VALUES
- (#{agentId}, #{fieldName}, #{conversationName}, #{synonyms}, #{description}, #{type}, NOW(), NOW(), #{status})
+ (#{agentId}, #{datasourceId}, #{tableName}, #{columnName}, #{businessName}, #{synonyms}, #{businessDescription}, #{columnComment}, #{dataType}, NOW(), NOW(), #{status})
""")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int insert(SemanticModel model);
@@ -104,11 +104,14 @@ OR synonyms LIKE CONCAT('%', #{keyword}, '%')
UPDATE semantic_model
agent_id = #{agentId},
- field_name = #{fieldName},
- conversation_name = #{conversationName},
+ datasource_id = #{datasourceId},
+ table_name = #{tableName},
+ column_name = #{columnName},
+ business_name = #{businessName},
synonyms = #{synonyms},
- description = #{description},
- type = #{type},
+ business_description = #{businessDescription},
+ column_comment = #{columnComment},
+ data_type = #{dataType},
status = #{status},
updated_time = NOW()
@@ -123,4 +126,22 @@ OR synonyms LIKE CONCAT('%', #{keyword}, '%')
""")
int deleteById(@Param("id") Long id);
+ /**
+ * Query semantic models by datasource ID, status and table names
+ */
+ @Select("""
+
+ """)
+ List selectByDatasourceIdAndTableNames(@Param("datasourceId") Integer datasourceId,
+ @Param("tableNames") List tableNames);
+
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/EvidenceRecallNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/EvidenceRecallNode.java
new file mode 100644
index 000000000..ee6807f35
--- /dev/null
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/EvidenceRecallNode.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.cloud.ai.node;
+
+import com.alibaba.cloud.ai.constant.DocumentMetadataConstant;
+import com.alibaba.cloud.ai.enums.TextType;
+import com.alibaba.cloud.ai.graph.GraphResponse;
+import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.action.NodeAction;
+import com.alibaba.cloud.ai.graph.streaming.FluxConverter;
+import com.alibaba.cloud.ai.graph.streaming.StreamingOutput;
+import com.alibaba.cloud.ai.prompt.PromptConstant;
+import com.alibaba.cloud.ai.prompt.PromptHelper;
+import com.alibaba.cloud.ai.service.llm.LlmService;
+import com.alibaba.cloud.ai.service.vectorstore.AgentVectorStoreService;
+import com.alibaba.cloud.ai.util.*;
+import com.fasterxml.jackson.core.type.TypeReference;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.jetbrains.annotations.Nullable;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.ai.document.Document;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Sinks;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static com.alibaba.cloud.ai.constant.Constant.*;
+
+@Slf4j
+@Component
+@AllArgsConstructor
+public class EvidenceRecallNode implements NodeAction {
+
+ private final LlmService llmService;
+
+ private final AgentVectorStoreService vectorStoreService;
+
+ @Override
+ public Map apply(OverAllState state) throws Exception {
+
+ // 从state中提取question和agentId
+ String question = StateUtil.getStringValue(state, INPUT_KEY);
+ String agentId = StateUtil.getStringValue(state, AGENT_ID);
+ Assert.hasText(agentId, "Agent ID cannot be empty.");
+
+ log.info("Extracting keywords before getting evidence in question: {}", question);
+ log.debug("Agent ID: {}", agentId);
+
+ // 构建关键词提取提示词
+ String prompt = PromptConstant.getQuestionToKeywordsPromptTemplate().render(Map.of("question", question));
+ log.debug("Built evidence keyword extraction prompt as follows \n {} \n", prompt);
+
+ // 调用LLM提取关键词
+ Flux responseFlux = llmService.callUser(prompt);
+ Sinks.Many evidenceDisplaySink = Sinks.many().multicast().onBackpressureBuffer();
+
+ final Map resultMap = new HashMap<>();
+ Flux> generator = FluxUtil.createStreamingGenerator(this.getClass(), state,
+ responseFlux,
+ Flux.just(ChatResponseUtil.createResponse("正在获取关键词..."),
+ ChatResponseUtil.createPureResponse(TextType.JSON.getStartSign())),
+ Flux.just(ChatResponseUtil.createPureResponse(TextType.JSON.getEndSign()),
+ ChatResponseUtil.createResponse("\n关键词获取完成!")),
+ result -> {
+ resultMap.putAll(getEvidences(result, agentId, evidenceDisplaySink));
+ return resultMap;
+ });
+
+ Flux> evidenceFlux = FluxConverter.builder()
+ .startingNode(this.getClass().getSimpleName())
+ .startingState(state)
+ .mapResult(r -> resultMap)
+ .build(evidenceDisplaySink.asFlux().map(ChatResponseUtil::createPureResponse));
+ return Map.of(EVIDENCE, generator.concatWith(evidenceFlux));
+ }
+
+ private Map getEvidences(String llmOutput, String agentId, Sinks.Many sink) {
+ try {
+ List keywords = extractKeywords(llmOutput);
+
+ if (null == keywords || keywords.isEmpty()) {
+ log.debug("No keywords extracted from LLM output");
+ sink.tryEmitNext("未找到关键词!\n");
+ return Map.of(EVIDENCE, "无");
+ }
+
+ // 将关键词列表用空格拼接成字符串
+ sink.tryEmitNext("关键词:\n");
+ keywords.forEach(keyword -> sink.tryEmitNext(keyword + " "));
+ sink.tryEmitNext("\n");
+ String keywordsString = String.join(" ", keywords);
+ log.debug("Joined keywords string: {}", keywordsString);
+ sink.tryEmitNext("正在获取证据...");
+
+ // 获取业务知识和智能体的知识
+ List businessTermDocuments = vectorStoreService
+ .getDocumentsForAgent(agentId, keywordsString, DocumentMetadataConstant.BUSINESS_TERM)
+ .stream()
+ .filter(doc -> Boolean.TRUE.equals(doc.getMetadata().get(DocumentMetadataConstant.IS_RECALL)))
+ .toList();
+
+ // 检查是否有证据文档
+ if (businessTermDocuments.isEmpty()) {
+ log.debug("No evidence documents found for agent: {} with keywords: {}", agentId, keywordsString);
+ sink.tryEmitNext("未找到证据!\n");
+ return Map.of(EVIDENCE, "无");
+ }
+
+ // 构建业务知识提示
+ String businessKnowledgePrompt = PromptHelper.buildBusinessKnowledgePrompt(
+ businessTermDocuments.stream().map(Document::getText).collect(Collectors.joining(";\n")));
+ // TODO 根据知识库模板渲染智能体的知识,然后拼接成EVIDENCE。 businessKnowledgePrompt + "\n\n" +
+ // agentKnowledgePrompt;
+ String evidence = businessKnowledgePrompt + "\n\n";
+
+ // 输出证据内容
+ sink.tryEmitNext("证据内容:\n");
+ businessTermDocuments.forEach(e -> sink.tryEmitNext(e.getText() + "\n"));
+ // TODO agentKnowledge.forEach
+
+ // 返回结果
+ return Map.of(EVIDENCE, evidence);
+ }
+ catch (Exception e) {
+ log.error("Error occurred while getting evidences", e);
+ sink.tryEmitError(e);
+ return Map.of(EVIDENCE, "");
+ }
+ finally {
+ sink.tryEmitComplete();
+ }
+ }
+
+ @Nullable
+ private static List extractKeywords(String llmOutput) {
+ // 解析关键词列表
+ List keywords;
+ try {
+ String content = MarkdownParserUtil.extractText(llmOutput.trim());
+ keywords = JsonUtil.getObjectMapper().readValue(content, new TypeReference>() {
+ });
+ log.info("For getting evidence keyword,extracted {} keywords: {}", keywords != null ? keywords.size() : 0,
+ keywords);
+ }
+ catch (Exception e) {
+ log.error("Failed to parse keywords from LLM response", e);
+ keywords = List.of(); // 使用空列表作为默认值
+ }
+ return keywords;
+ }
+
+}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/FeasibilityAssessmentNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/FeasibilityAssessmentNode.java
new file mode 100644
index 000000000..03dad7cb0
--- /dev/null
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/FeasibilityAssessmentNode.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.cloud.ai.node;
+
+import com.alibaba.cloud.ai.dto.schema.SchemaDTO;
+import com.alibaba.cloud.ai.graph.GraphResponse;
+import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.action.NodeAction;
+import com.alibaba.cloud.ai.graph.streaming.StreamingOutput;
+import com.alibaba.cloud.ai.prompt.PromptHelper;
+import com.alibaba.cloud.ai.service.llm.LlmService;
+import com.alibaba.cloud.ai.util.FluxUtil;
+import com.alibaba.cloud.ai.util.StateUtil;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+
+import java.util.Map;
+
+import static com.alibaba.cloud.ai.constant.Constant.*;
+
+// 可行性评估节点,看需求是 数据分析/需要澄清 或者最终确认为自由闲聊
+@Slf4j
+@Component
+@AllArgsConstructor
+public class FeasibilityAssessmentNode implements NodeAction {
+
+ private final LlmService llmService;
+
+ @Override
+ public Map apply(OverAllState state) throws Exception {
+ // 获取canonical_query
+ String canonicalQuery = StateUtil.getCanonicalQuery(state);
+
+ // 获取召回的Schema
+ SchemaDTO recalledSchema = StateUtil.getObjectValue(state, TABLE_RELATION_OUTPUT, SchemaDTO.class);
+
+ // 获取证据信息
+ String evidence = StateUtil.getStringValue(state, EVIDENCE);
+
+ // 构建可行性评估提示词,多轮对话暂时为空
+ String prompt = PromptHelper.buildFeasibilityAssessmentPrompt(canonicalQuery, recalledSchema, evidence, null);
+ log.debug("Built feasibility assessment prompt as follows \n {} \n", prompt);
+
+ // 调用LLM进行可行性评估
+ Flux responseFlux = llmService.callUser(prompt);
+
+ Flux> generator = FluxUtil.createStreamingGeneratorWithMessages(this.getClass(),
+ state, "正在进行可行性评估...", "可行性评估完成!", llmOutput -> {
+ // 获取评估结果
+ String assessmentResult = llmOutput.trim();
+ log.info("Feasibility assessment result: {}", assessmentResult);
+ // 返回评估结果
+ return Map.of(FEASIBILITY_ASSESSMENT_NODE_OUTPUT, assessmentResult);
+ }, responseFlux);
+ return Map.of(FEASIBILITY_ASSESSMENT_NODE_OUTPUT, generator);
+ }
+
+}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/HumanFeedbackNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/HumanFeedbackNode.java
index 61d63317a..2f401ded2 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/HumanFeedbackNode.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/HumanFeedbackNode.java
@@ -39,7 +39,6 @@ public class HumanFeedbackNode implements NodeAction {
@Override
public Map apply(OverAllState state) throws Exception {
- log.info("Processing human feedback");
Map updated = new HashMap<>();
// 检查最大修复次数
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/IntentRecognitionNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/IntentRecognitionNode.java
new file mode 100644
index 000000000..2d6054891
--- /dev/null
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/IntentRecognitionNode.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.alibaba.cloud.ai.node;
+
+import com.alibaba.cloud.ai.enums.TextType;
+import com.alibaba.cloud.ai.graph.GraphResponse;
+import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.action.NodeAction;
+import com.alibaba.cloud.ai.graph.streaming.StreamingOutput;
+import com.alibaba.cloud.ai.prompt.PromptHelper;
+import com.alibaba.cloud.ai.service.llm.LlmService;
+import com.alibaba.cloud.ai.util.ChatResponseUtil;
+import com.alibaba.cloud.ai.util.FluxUtil;
+import com.alibaba.cloud.ai.util.StateUtil;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+
+import java.util.Map;
+
+import static com.alibaba.cloud.ai.constant.Constant.INPUT_KEY;
+import static com.alibaba.cloud.ai.constant.Constant.INTENT_RECOGNITION_NODE_OUTPUT;
+
+/**
+ * 意图识别节点,用于识别用户输入是闲聊还是数据分析请求
+ */
+@Slf4j
+@Component
+@AllArgsConstructor
+public class IntentRecognitionNode implements NodeAction {
+
+ private final LlmService llmService;
+
+ @Override
+ public Map apply(OverAllState state) throws Exception {
+
+ // 获取用户输入
+ String userInput = StateUtil.getStringValue(state, INPUT_KEY);
+ log.info("User input for intent recognition: {}", userInput);
+
+ // 构建意图识别提示,多轮对话暂时为空
+ String prompt = PromptHelper.buildIntentRecognitionPrompt(null, userInput);
+ log.debug("Built intent recognition prompt as follows \n {} \n", prompt);
+
+ // 调用LLM进行意图识别
+ Flux responseFlux = llmService.callUser(prompt);
+
+ Flux> generator = FluxUtil.createStreamingGenerator(this.getClass(), state,
+ responseFlux,
+ Flux.just(ChatResponseUtil.createResponse("正在进行意图识别..."),
+ ChatResponseUtil.createPureResponse(TextType.JSON.getStartSign())),
+ Flux.just(ChatResponseUtil.createPureResponse(TextType.JSON.getEndSign()),
+ ChatResponseUtil.createResponse("\n意图识别完成!")),
+ result -> Map.of(INTENT_RECOGNITION_NODE_OUTPUT, result));
+ return Map.of(INTENT_RECOGNITION_NODE_OUTPUT, generator);
+ }
+
+}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/KeywordExtractNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/KeywordExtractNode.java
deleted file mode 100644
index 1e2c6fdeb..000000000
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/KeywordExtractNode.java
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright 2025 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.alibaba.cloud.ai.node;
-
-import com.alibaba.cloud.ai.enums.TextType;
-import com.alibaba.cloud.ai.graph.GraphResponse;
-import com.alibaba.cloud.ai.graph.streaming.StreamingOutput;
-import com.alibaba.cloud.ai.pojo.KeywordExtractionResult;
-import com.alibaba.cloud.ai.graph.OverAllState;
-import com.alibaba.cloud.ai.graph.action.NodeAction;
-import com.alibaba.cloud.ai.service.processing.QueryProcessingService;
-import com.alibaba.cloud.ai.util.ChatResponseUtil;
-import com.alibaba.cloud.ai.util.FluxUtil;
-import com.alibaba.cloud.ai.util.StateUtil;
-import lombok.AllArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.ai.chat.model.ChatResponse;
-import org.springframework.stereotype.Component;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-import reactor.core.publisher.Sinks;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Consumer;
-
-import static com.alibaba.cloud.ai.constant.Constant.*;
-
-/**
- * Keyword, entity, and temporal information extraction node to prepare for subsequent
- * schema recall.
- *
- * This node is responsible for: - Extracting evidences from user input - Extracting
- * keywords based on evidences - Preparing structured information for schema recall -
- * Providing streaming feedback during extraction process
- *
- * @author zhangshenghang
- */
-@Slf4j
-@Component
-@AllArgsConstructor
-public class KeywordExtractNode implements NodeAction {
-
- private final QueryProcessingService queryProcessingService;
-
- /**
- * Process multiple question variants, extract keywords and merge results Use parallel
- * stream processing to improve multi-question processing efficiency
- * @param questions list of question variants
- * @return list of extraction results
- */
- private Flux processMultipleQuestions(List questions, String agentId,
- Consumer> resultConsumer) {
- List resultList = new ArrayList<>(questions.size());
- AtomicReference> fluxRef = new AtomicReference<>(Flux.empty());
- questions.forEach(question -> {
- List evidences = queryProcessingService.extractEvidences(question, agentId);
- fluxRef
- .set(fluxRef.get().concatWith(queryProcessingService.extractKeywords(question, evidences, keywords -> {
- log.info("成功从问题变体提取关键词: 问题=\"{}\", 关键词={}", question, keywords);
- resultList.add(new KeywordExtractionResult(question, evidences, keywords));
- }).doOnError(e -> {
- log.warn("从问题变体提取关键词失败: 问题={}", question, e);
- resultList.add(new KeywordExtractionResult(question, false));
- })));
- });
- return fluxRef.get().doOnComplete(() -> resultConsumer.accept(resultList));
- }
-
- /**
- * Merge keywords from multiple question variants, deduplicate and keep original
- * question keywords priority
- * @param extractionResults list of extraction results
- * @param originalQuestion original question
- * @return merged keyword list
- */
- private List mergeKeywords(List extractionResults, String originalQuestion) {
- if (extractionResults.isEmpty()) {
- return List.of();
- }
-
- Set mergedKeywords = new LinkedHashSet<>();
-
- extractionResults.stream()
- .filter(result -> result.isSuccessful() && result.getQuestion().equals(originalQuestion))
- .findFirst()
- .ifPresent(result -> mergedKeywords.addAll(result.getKeywords()));
-
- extractionResults.stream()
- .filter(result -> result.isSuccessful() && !result.getQuestion().equals(originalQuestion))
- .forEach(result -> mergedKeywords.addAll(result.getKeywords()));
-
- return new ArrayList<>(mergedKeywords);
- }
-
- /**
- * Merge evidences from multiple question variants, deduplicate
- * @param extractionResults list of extraction results
- * @return merged evidence list
- */
- private List mergeEvidences(List extractionResults) {
- Set mergedEvidences = new HashSet<>();
-
- extractionResults.stream()
- .filter(KeywordExtractionResult::isSuccessful)
- .forEach(result -> mergedEvidences.addAll(result.getEvidences()));
-
- return new ArrayList<>(mergedEvidences);
- }
-
- @Override
- public Map apply(OverAllState state) throws Exception {
- log.info("Entering {} node", this.getClass().getSimpleName());
-
- String input = StateUtil.getStringValue(state, QUERY_REWRITE_NODE_OUTPUT,
- StateUtil.getStringValue(state, INPUT_KEY));
-
- try {
- log.info("开始增强关键词提取处理...");
-
- Map resultMap = new HashMap<>();
-
- AtomicReference> expandedQuestionsRef = new AtomicReference<>(null);
- Sinks.Many sink = Sinks.many().multicast().onBackpressureBuffer();
- Flux displayFlux = FluxUtil
- .cascadeFlux(queryProcessingService.expandQuestion(input, expandedQuestions -> {
- log.info("问题扩展结果: {}", expandedQuestions);
- expandedQuestionsRef.set(expandedQuestions);
- }), r -> processMultipleQuestions(expandedQuestionsRef.get(), StateUtil.getStringValue(state, AGENT_ID),
- extractionResults -> {
- List mergedKeywords = mergeKeywords(extractionResults, input);
- List mergedEvidences = mergeEvidences(extractionResults);
-
- log.info("[{}] 增强提取结果 - 证据: {}, 关键词: {}", this.getClass().getSimpleName(), mergedEvidences,
- mergedKeywords);
- resultMap.putAll(Map.of(KEYWORD_EXTRACT_NODE_OUTPUT, mergedKeywords, EVIDENCES,
- mergedEvidences, RESULT, mergedKeywords));
- sink.tryEmitNext(ChatResponseUtil.createPureResponse(TextType.JSON.getEndSign()));
- sink.tryEmitNext(ChatResponseUtil
- .createResponse("\n\n合并后的证据: " + String.join(", ", mergedEvidences)));
- sink.tryEmitNext(
- ChatResponseUtil.createResponse("合并后的关键词: " + String.join(", ", mergedKeywords)));
- sink.tryEmitNext(ChatResponseUtil.createResponse("关键词提取完成."));
- sink.tryEmitComplete();
- }), flux -> Mono.just(""),
- Flux.just(ChatResponseUtil.createResponse("开始增强关键词提取..."),
- ChatResponseUtil.createResponse("正在扩展问题理解...\n"),
- ChatResponseUtil.createPureResponse(TextType.JSON.getStartSign())),
- Flux.defer(() -> Flux.just(ChatResponseUtil.createPureResponse(TextType.JSON.getEndSign()),
- ChatResponseUtil.createResponse("\n\n问题扩展结束!"),
- ChatResponseUtil.createResponse("合并多个问题变体的结果...\n"),
- ChatResponseUtil.createPureResponse(TextType.JSON.getStartSign()))),
- sink.asFlux());
-
- Flux> generator = FluxUtil
- .createStreamingGeneratorWithMessages(this.getClass(), state, v -> resultMap, displayFlux);
-
- return Map.of(KEYWORD_EXTRACT_NODE_OUTPUT, generator);
-
- }
- catch (Exception e) {
- log.error("增强关键词提取失败{}", e.getMessage());
- throw new RuntimeException(e);
- }
- }
-
-}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PlanExecutorNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PlanExecutorNode.java
index f1a684583..0799ad8db 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PlanExecutorNode.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PlanExecutorNode.java
@@ -18,12 +18,12 @@
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.StateGraph;
+import com.alibaba.cloud.ai.graph.action.NodeAction;
import com.alibaba.cloud.ai.pojo.ExecutionStep;
import com.alibaba.cloud.ai.pojo.Plan;
+import com.alibaba.cloud.ai.util.PlanProcessUtil;
import com.alibaba.cloud.ai.util.StateUtil;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.ai.converter.BeanOutputConverter;
-import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Component;
import java.util.List;
@@ -34,7 +34,6 @@
import static com.alibaba.cloud.ai.constant.Constant.IS_ONLY_NL2SQL;
import static com.alibaba.cloud.ai.constant.Constant.HUMAN_REVIEW_ENABLED;
import static com.alibaba.cloud.ai.constant.Constant.ONLY_NL2SQL_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.PLANNER_NODE_OUTPUT;
import static com.alibaba.cloud.ai.constant.Constant.PLAN_CURRENT_STEP;
import static com.alibaba.cloud.ai.constant.Constant.PLAN_NEXT_NODE;
import static com.alibaba.cloud.ai.constant.Constant.PLAN_REPAIR_COUNT;
@@ -52,29 +51,19 @@
*/
@Slf4j
@Component
-public class PlanExecutorNode extends AbstractPlanBasedNode {
+public class PlanExecutorNode implements NodeAction {
// Supported node types
private static final Set SUPPORTED_NODES = Set.of(SQL_EXECUTE_NODE, PYTHON_GENERATE_NODE,
REPORT_GENERATOR_NODE);
- private final BeanOutputConverter converter;
-
- public PlanExecutorNode() {
- this.converter = new BeanOutputConverter<>(new ParameterizedTypeReference<>() {
- });
- }
-
@Override
public Map apply(OverAllState state) throws Exception {
- logNodeEntry();
-
// 1. Validate the Plan
- String plannerOutput = StateUtil.getStringValue(state, PLANNER_NODE_OUTPUT);
try {
- Plan plan = converter.convert(plannerOutput);
+ Plan plan = PlanProcessUtil.getPlan(state);
- if (plan == null || plan.getExecutionPlan() == null || plan.getExecutionPlan().isEmpty()) {
+ if (plan.getExecutionPlan() == null || plan.getExecutionPlan().isEmpty()) {
return buildValidationResult(state, false,
"Validation failed: The generated plan is empty or has no execution steps.");
}
@@ -115,8 +104,8 @@ public Map apply(OverAllState state) throws Exception {
return Map.of(PLAN_VALIDATION_STATUS, true, PLAN_NEXT_NODE, HUMAN_FEEDBACK_NODE);
}
- Plan plan = getPlan(state);
- Integer currentStep = getCurrentStepNumber(state);
+ Plan plan = PlanProcessUtil.getPlan(state);
+ int currentStep = PlanProcessUtil.getCurrentStepNumber(state);
List executionPlan = plan.getExecutionPlan();
// Check if the plan is completed
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PlannerNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PlannerNode.java
index 5349cabb9..c9dbd17b9 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PlannerNode.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PlannerNode.java
@@ -36,14 +36,7 @@
import java.util.Map;
-import static com.alibaba.cloud.ai.constant.Constant.BUSINESS_KNOWLEDGE;
-import static com.alibaba.cloud.ai.constant.Constant.INPUT_KEY;
-import static com.alibaba.cloud.ai.constant.Constant.IS_ONLY_NL2SQL;
-import static com.alibaba.cloud.ai.constant.Constant.PLANNER_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.PLAN_VALIDATION_ERROR;
-import static com.alibaba.cloud.ai.constant.Constant.QUERY_REWRITE_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.SEMANTIC_MODEL;
-import static com.alibaba.cloud.ai.constant.Constant.TABLE_RELATION_OUTPUT;
+import static com.alibaba.cloud.ai.constant.Constant.*;
/**
* @author zhangshenghang
@@ -57,11 +50,9 @@ public class PlannerNode implements NodeAction {
@Override
public Map apply(OverAllState state) throws Exception {
- String input = StateUtil.getStringValue(state, QUERY_REWRITE_NODE_OUTPUT,
- StateUtil.getStringValue(state, INPUT_KEY));
- // 使用经过时间表达式处理的重写查询,如果没有则回退到原始输入
- String processedQuery = StateUtil.getStringValue(state, QUERY_REWRITE_NODE_OUTPUT, input);
- log.info("Using processed query for planning: {}", processedQuery);
+ // 获取查询增强节点的输出
+ String canonicalQuery = StateUtil.getCanonicalQuery(state);
+ log.info("Using processed query for planning: {}", canonicalQuery);
// 是否为NL2SQL模式
Boolean onlyNl2sql = state.value(IS_ONLY_NL2SQL, false);
@@ -76,24 +67,23 @@ public Map apply(OverAllState state) throws Exception {
}
// 构建提示参数
- String businessKnowledge = (String) state.value(BUSINESS_KNOWLEDGE).orElse("");
- String semanticModel = (String) state.value(SEMANTIC_MODEL).orElse("");
+ String semanticModel = (String) state.value(GENEGRATED_SEMANTIC_MODEL_PROMPT).orElse("");
SchemaDTO schemaDTO = StateUtil.getObjectValue(state, TABLE_RELATION_OUTPUT, SchemaDTO.class);
String schemaStr = PromptHelper.buildMixMacSqlDbPrompt(schemaDTO, true);
// 构建用户提示
- String userPrompt = buildUserPrompt(processedQuery, validationError, state);
+ String userPrompt = buildUserPrompt(canonicalQuery, validationError, state);
+ String evidence = StateUtil.getStringValue(state, EVIDENCE);
// 构建模板参数
- Map params = Map.of("user_question", userPrompt, "schema", schemaStr, "business_knowledge",
- businessKnowledge, "semantic_model", semanticModel, "plan_validation_error",
- formatValidationError(validationError));
+ Map params = Map.of("user_question", userPrompt, "schema", schemaStr, "evidence", evidence,
+ "semantic_model", semanticModel, "plan_validation_error", formatValidationError(validationError));
// 生成计划
String plannerPrompt = (onlyNl2sql ? PromptConstant.getPlannerNl2sqlOnlyTemplate()
: PromptConstant.getPlannerPromptTemplate())
.render(params);
-
+ log.debug("Planner prompt: as follows \n{}\n", plannerPrompt);
Flux chatResponseFlux = Flux.concat(
Flux.just(ChatResponseUtil.createPureResponse(TextType.JSON.getStartSign())),
llmService.callUser(plannerPrompt),
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PythonAnalyzeNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PythonAnalyzeNode.java
index fb916b405..8fd436443 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PythonAnalyzeNode.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PythonAnalyzeNode.java
@@ -23,8 +23,10 @@
import com.alibaba.cloud.ai.prompt.PromptConstant;
import com.alibaba.cloud.ai.service.llm.LlmService;
import com.alibaba.cloud.ai.util.FluxUtil;
+import com.alibaba.cloud.ai.util.PlanProcessUtil;
import com.alibaba.cloud.ai.util.StateUtil;
-import com.alibaba.cloud.ai.util.StepResultUtil;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
@@ -32,11 +34,7 @@
import java.util.HashMap;
import java.util.Map;
-import static com.alibaba.cloud.ai.constant.Constant.PLAN_CURRENT_STEP;
-import static com.alibaba.cloud.ai.constant.Constant.PYTHON_ANALYSIS_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.PYTHON_EXECUTE_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.QUERY_REWRITE_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.SQL_EXECUTE_NODE_OUTPUT;
+import static com.alibaba.cloud.ai.constant.Constant.*;
/**
* 根据Python代码的运行结果做总结分析
@@ -44,24 +42,20 @@
* @author vlsmb
* @since 2025/7/30
*/
+@Slf4j
@Component
-public class PythonAnalyzeNode extends AbstractPlanBasedNode implements NodeAction {
+@AllArgsConstructor
+public class PythonAnalyzeNode implements NodeAction {
private final LlmService llmService;
- public PythonAnalyzeNode(LlmService llmService) {
- super();
- this.llmService = llmService;
- }
-
@Override
public Map apply(OverAllState state) throws Exception {
- this.logNodeEntry();
// Get context
- String userQuery = StateUtil.getStringValue(state, QUERY_REWRITE_NODE_OUTPUT);
+ String userQuery = StateUtil.getCanonicalQuery(state);
String pythonOutput = StateUtil.getStringValue(state, PYTHON_EXECUTE_NODE_OUTPUT);
- int currentStep = this.getCurrentStepNumber(state);
+ int currentStep = PlanProcessUtil.getCurrentStepNumber(state);
@SuppressWarnings("unchecked")
Map sqlExecuteResult = StateUtil.getObjectValue(state, SQL_EXECUTE_NODE_OUTPUT, Map.class,
new HashMap<>());
@@ -74,9 +68,9 @@ public Map apply(OverAllState state) throws Exception {
Flux> generator = FluxUtil.createStreamingGeneratorWithMessages(this.getClass(),
state, "正在分析代码运行结果...\n", "\n结果分析完成。", aiResponse -> {
- Map updatedSqlResult = StepResultUtil.addStepResult(sqlExecuteResult, currentStep,
+ Map updatedSqlResult = PlanProcessUtil.addStepResult(sqlExecuteResult, currentStep,
aiResponse);
- this.logNodeOutput("python_analysis_result", aiResponse);
+ log.info("python analyze result: {}", aiResponse);
return Map.of(SQL_EXECUTE_NODE_OUTPUT, updatedSqlResult, PLAN_CURRENT_STEP, currentStep + 1);
}, pythonAnalyzeFlux);
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PythonExecuteNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PythonExecuteNode.java
index 5a17c91d8..ddb0fcc27 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PythonExecuteNode.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PythonExecuteNode.java
@@ -24,6 +24,7 @@
import com.alibaba.cloud.ai.service.code.CodePoolExecutorService;
import com.alibaba.cloud.ai.util.ChatResponseUtil;
import com.alibaba.cloud.ai.util.FluxUtil;
+import com.alibaba.cloud.ai.util.JsonUtil;
import com.alibaba.cloud.ai.util.StateUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
@@ -47,21 +48,19 @@
*/
@Slf4j
@Component
-public class PythonExecuteNode extends AbstractPlanBasedNode implements NodeAction {
+public class PythonExecuteNode implements NodeAction {
private final CodePoolExecutorService codePoolExecutor;
private final ObjectMapper objectMapper;
public PythonExecuteNode(CodePoolExecutorService codePoolExecutor) {
- super();
this.codePoolExecutor = codePoolExecutor;
- this.objectMapper = new ObjectMapper();
+ this.objectMapper = JsonUtil.getObjectMapper();
}
@Override
public Map apply(OverAllState state) throws Exception {
- this.logNodeEntry();
try {
// Get context
@@ -98,7 +97,7 @@ public Map apply(OverAllState state) throws Exception {
emitter.next(ChatResponseUtil.createResponse("标准输出:"));
emitter.next(ChatResponseUtil.createPureResponse(TextType.JSON.getStartSign()));
emitter.next(ChatResponseUtil.createResponse(finalStdout));
- emitter.next(ChatResponseUtil.createResponse(TextType.JSON.getEndSign()));
+ emitter.next(ChatResponseUtil.createPureResponse(TextType.JSON.getEndSign()));
emitter.next(ChatResponseUtil.createResponse("Python代码执行成功!"));
emitter.complete();
});
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PythonGenerateNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PythonGenerateNode.java
index 0210ef27a..ae32c6b01 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PythonGenerateNode.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/PythonGenerateNode.java
@@ -29,6 +29,7 @@
import com.alibaba.cloud.ai.util.ChatResponseUtil;
import com.alibaba.cloud.ai.util.FluxUtil;
import com.alibaba.cloud.ai.util.MarkdownParserUtil;
+import com.alibaba.cloud.ai.util.PlanProcessUtil;
import com.alibaba.cloud.ai.util.StateUtil;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -40,13 +41,7 @@
import java.util.List;
import java.util.Map;
-import static com.alibaba.cloud.ai.constant.Constant.PYTHON_EXECUTE_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.PYTHON_GENERATE_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.PYTHON_IS_SUCCESS;
-import static com.alibaba.cloud.ai.constant.Constant.PYTHON_TRIES_COUNT;
-import static com.alibaba.cloud.ai.constant.Constant.QUERY_REWRITE_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.SQL_RESULT_LIST_MEMORY;
-import static com.alibaba.cloud.ai.constant.Constant.TABLE_RELATION_OUTPUT;
+import static com.alibaba.cloud.ai.constant.Constant.*;
/**
* 生成Python代码的节点
@@ -56,7 +51,7 @@
*/
@Slf4j
@Component
-public class PythonGenerateNode extends AbstractPlanBasedNode implements NodeAction {
+public class PythonGenerateNode implements NodeAction {
private static final int SAMPLE_DATA_NUMBER = 5;
@@ -69,7 +64,6 @@ public class PythonGenerateNode extends AbstractPlanBasedNode implements NodeAct
private final LlmService llmService;
public PythonGenerateNode(CodeExecutorProperties codeExecutorProperties, LlmService llmService) {
- super();
this.codeExecutorProperties = codeExecutorProperties;
this.llmService = llmService;
this.objectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
@@ -77,7 +71,6 @@ public PythonGenerateNode(CodeExecutorProperties codeExecutorProperties, LlmServ
@Override
public Map apply(OverAllState state) throws Exception {
- this.logNodeEntry();
// Get context
SchemaDTO schemaDTO = StateUtil.getObjectValue(state, TABLE_RELATION_OUTPUT, SchemaDTO.class);
@@ -85,7 +78,7 @@ public Map apply(OverAllState state) throws Exception {
boolean codeRunSuccess = StateUtil.getObjectValue(state, PYTHON_IS_SUCCESS, Boolean.class, true);
int triesCount = StateUtil.getObjectValue(state, PYTHON_TRIES_COUNT, Integer.class, MAX_TRIES_COUNT);
- String userPrompt = StateUtil.getStringValue(state, QUERY_REWRITE_NODE_OUTPUT);
+ String userPrompt = StateUtil.getCanonicalQuery(state);
if (!codeRunSuccess) {
// Last generated Python code failed to run, inform AI model of this
// information
@@ -104,7 +97,7 @@ public Map apply(OverAllState state) throws Exception {
""", lastCode, lastError);
}
- ExecutionStep executionStep = this.getCurrentExecutionStep(state);
+ ExecutionStep executionStep = PlanProcessUtil.getCurrentExecutionStep(state);
ExecutionStep.ToolParameters toolParameters = executionStep.getToolParameters();
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/QueryEnhanceNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/QueryEnhanceNode.java
new file mode 100644
index 000000000..e52a79107
--- /dev/null
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/QueryEnhanceNode.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.alibaba.cloud.ai.node;
+
+import com.alibaba.cloud.ai.dto.QueryEnhanceOutputDTO;
+import com.alibaba.cloud.ai.enums.TextType;
+import com.alibaba.cloud.ai.graph.GraphResponse;
+import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.action.NodeAction;
+import com.alibaba.cloud.ai.graph.streaming.StreamingOutput;
+import com.alibaba.cloud.ai.prompt.PromptHelper;
+import com.alibaba.cloud.ai.service.llm.LlmService;
+import com.alibaba.cloud.ai.util.ChatResponseUtil;
+import com.alibaba.cloud.ai.util.FluxUtil;
+import com.alibaba.cloud.ai.util.JsonUtil;
+import com.alibaba.cloud.ai.util.StateUtil;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.chat.model.ChatResponse;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+
+import java.util.Map;
+
+import static com.alibaba.cloud.ai.constant.Constant.*;
+
+/**
+ * 查询处理节点,用于查询改写、分解和关键词提取
+ */
+@Slf4j
+@Component
+@AllArgsConstructor
+public class QueryEnhanceNode implements NodeAction {
+
+ private final LlmService llmService;
+
+ @Override
+ public Map apply(OverAllState state) throws Exception {
+
+ // 获取用户输入
+ String userInput = StateUtil.getStringValue(state, INPUT_KEY);
+ log.info("User input for query enhance: {}", userInput);
+
+ String evidence = StateUtil.getStringValue(state, EVIDENCE);
+
+ // 构建查询处理提示,多轮对话暂时为空
+ String prompt = PromptHelper.buildQueryEnhancePrompt(null, userInput, evidence);
+ log.debug("Built query enhance prompt as follows \n {} \n", prompt);
+
+ // 调用LLM进行查询处理
+ Flux responseFlux = llmService.callUser(prompt);
+
+ Flux> generator = FluxUtil.createStreamingGenerator(this.getClass(), state,
+ responseFlux,
+ Flux.just(ChatResponseUtil.createResponse("正在进行问题增强..."),
+ ChatResponseUtil.createPureResponse(TextType.JSON.getStartSign())),
+ Flux.just(ChatResponseUtil.createPureResponse(TextType.JSON.getEndSign()),
+ ChatResponseUtil.createResponse("\n问题增强完成!")),
+ this::handleQueryEnhance);
+
+ return Map.of(QUERY_ENHANCE_NODE_OUTPUT, generator);
+ }
+
+ private Map handleQueryEnhance(String llmOutput) {
+ // 获取处理结果
+ String enhanceResult = llmOutput.trim();
+ log.info("Query enhance result: {}", enhanceResult);
+
+ // 解析处理结果,转成 QueryProcessOutputDTO
+ QueryEnhanceOutputDTO queryEnhanceOutputDTO = null;
+ try {
+ queryEnhanceOutputDTO = JsonUtil.getObjectMapper().readValue(enhanceResult, QueryEnhanceOutputDTO.class);
+ log.info("Successfully parsed query enhance result: {}", queryEnhanceOutputDTO);
+ }
+ catch (Exception e) {
+ log.error("Failed to parse query enhance result: {}", enhanceResult, e);
+ }
+
+ if (queryEnhanceOutputDTO == null)
+ return Map.of();
+ // 返回处理结果
+ return Map.of(QUERY_ENHANCE_NODE_OUTPUT, queryEnhanceOutputDTO);
+ }
+
+}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/QueryRewriteNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/QueryRewriteNode.java
deleted file mode 100644
index 3e6ef7bf7..000000000
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/QueryRewriteNode.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * Copyright 2025 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.alibaba.cloud.ai.node;
-
-import com.alibaba.cloud.ai.graph.GraphResponse;
-import com.alibaba.cloud.ai.graph.OverAllState;
-import com.alibaba.cloud.ai.graph.action.NodeAction;
-import com.alibaba.cloud.ai.graph.streaming.StreamingOutput;
-import com.alibaba.cloud.ai.service.processing.QueryProcessingService;
-import com.alibaba.cloud.ai.util.FluxUtil;
-import com.alibaba.cloud.ai.util.StateUtil;
-import lombok.AllArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Component;
-import reactor.core.publisher.Flux;
-
-import java.util.Map;
-
-import static com.alibaba.cloud.ai.constant.Constant.AGENT_ID;
-import static com.alibaba.cloud.ai.constant.Constant.INPUT_KEY;
-import static com.alibaba.cloud.ai.constant.Constant.QUERY_REWRITE_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.RESULT;
-
-/**
- * Query rewriting and intent clarification node to improve intent understanding accuracy.
- *
- * This node is responsible for: - Rewriting user queries to clarify intent - Improving
- * understanding accuracy through query transformation - Providing streaming feedback
- * during rewriting process
- *
- * @author zhangshenghang
- */
-@Slf4j
-@Component
-@AllArgsConstructor
-public class QueryRewriteNode implements NodeAction {
-
- private final QueryProcessingService queryProcessingService;
-
- @Override
- public Map apply(OverAllState state) throws Exception {
- log.info("Entering {} node", this.getClass().getSimpleName());
-
- String input = StateUtil.getStringValue(state, INPUT_KEY);
- String agentId = StateUtil.getStringValue(state, AGENT_ID); // Get agent ID
- log.info("[{}] Processing user input: {} for agentId: {}", this.getClass().getSimpleName(), input, agentId);
-
- final StringBuilder queryResultCollector = new StringBuilder();
- // Use streaming utility class for content collection and result mapping
- Flux> generator = FluxUtil.createStreamingGeneratorWithMessages(this.getClass(),
- state, "开始进行问题重写...", "\n\n问题重写完成!", finalResult -> {
- finalResult = queryResultCollector.toString();
- return Map.of(QUERY_REWRITE_NODE_OUTPUT, finalResult, RESULT, finalResult);
- }, queryProcessingService.rewriteStream(input, agentId, queryResultCollector));
-
- return Map.of(QUERY_REWRITE_NODE_OUTPUT, generator);
- }
-
-}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/ReportGeneratorNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/ReportGeneratorNode.java
index f2050b522..0f978f4e5 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/ReportGeneratorNode.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/ReportGeneratorNode.java
@@ -41,12 +41,7 @@
import java.util.List;
import java.util.Map;
-import static com.alibaba.cloud.ai.constant.Constant.INPUT_KEY;
-import static com.alibaba.cloud.ai.constant.Constant.PLANNER_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.PLAN_CURRENT_STEP;
-import static com.alibaba.cloud.ai.constant.Constant.QUERY_REWRITE_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.RESULT;
-import static com.alibaba.cloud.ai.constant.Constant.SQL_EXECUTE_NODE_OUTPUT;
+import static com.alibaba.cloud.ai.constant.Constant.*;
/**
* Report generation node that creates comprehensive analysis reports based on execution
@@ -77,12 +72,10 @@ public ReportGeneratorNode(LlmService llmService, UserPromptService promptConfig
@Override
public Map apply(OverAllState state) throws Exception {
- log.info("Entering {} node", this.getClass().getSimpleName());
// Get necessary input parameters
String plannerNodeOutput = StateUtil.getStringValue(state, PLANNER_NODE_OUTPUT);
- String userInput = StateUtil.getStringValue(state, QUERY_REWRITE_NODE_OUTPUT,
- StateUtil.getStringValue(state, INPUT_KEY));
+ String userInput = StateUtil.getCanonicalQuery(state);
Integer currentStep = StateUtil.getObjectValue(state, PLAN_CURRENT_STEP, Integer.class, 1);
@SuppressWarnings("unchecked")
HashMap executionResults = StateUtil.getObjectValue(state, SQL_EXECUTE_NODE_OUTPUT,
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SchemaRecallNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SchemaRecallNode.java
index 46eff0760..ce865e3a1 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SchemaRecallNode.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SchemaRecallNode.java
@@ -16,6 +16,7 @@
package com.alibaba.cloud.ai.node;
+import com.alibaba.cloud.ai.dto.QueryEnhanceOutputDTO;
import com.alibaba.cloud.ai.graph.GraphResponse;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.action.NodeAction;
@@ -35,13 +36,7 @@
import java.util.List;
import java.util.Map;
-import static com.alibaba.cloud.ai.constant.Constant.COLUMN_DOCUMENTS_BY_KEYWORDS_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.AGENT_ID;
-import static com.alibaba.cloud.ai.constant.Constant.INPUT_KEY;
-import static com.alibaba.cloud.ai.constant.Constant.KEYWORD_EXTRACT_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.QUERY_REWRITE_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.SCHEMA_RECALL_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.TABLE_DOCUMENTS_FOR_SCHEMA_OUTPUT;
+import static com.alibaba.cloud.ai.constant.Constant.*;
/**
* Schema recall node that retrieves relevant database schema information based on
@@ -62,50 +57,68 @@ public class SchemaRecallNode implements NodeAction {
@Override
public Map apply(OverAllState state) throws Exception {
- log.info("Entering {} node", this.getClass().getSimpleName());
- String input = StateUtil.getStringValue(state, QUERY_REWRITE_NODE_OUTPUT,
- StateUtil.getStringValue(state, INPUT_KEY));
- List keywords = StateUtil.getListValue(state, KEYWORD_EXTRACT_NODE_OUTPUT);
+ // get input information
+ QueryEnhanceOutputDTO queryEnhanceOutputDTO = StateUtil.getObjectValue(state, QUERY_ENHANCE_NODE_OUTPUT,
+ QueryEnhanceOutputDTO.class);
+ String input = queryEnhanceOutputDTO.getCanonicalQuery();
+ List keywords = queryEnhanceOutputDTO.getConsolidatedKeywords();
String agentId = StateUtil.getStringValue(state, AGENT_ID);
// Execute business logic first - recall schema information immediately
- List tableDocuments;
- List> columnDocumentsByKeywords;
-
- // If agentId exists, use agent-specific search, otherwise use global search
- if (agentId != null && !agentId.trim().isEmpty()) {
- log.info("Using agent-specific schema recall for agent: {}", agentId);
- tableDocuments = new ArrayList<>(schemaService.getTableDocumentsForAgent(agentId, input));
- columnDocumentsByKeywords = schemaService.getColumnDocumentsByKeywordsForAgent(agentId, keywords);
- }
- else {
- log.info("Using global schema recall (no agentId provided)");
- tableDocuments = schemaService.getTableDocuments(input);
- columnDocumentsByKeywords = schemaService.getColumnDocumentsByKeywords(keywords);
- }
+ List tableDocuments = new ArrayList<>(schemaService.getTableDocumentsForAgent(agentId, input));
+ List columnDocumentsByKeywords = schemaService.getColumnDocumentsByKeywordsForAgent(agentId, keywords,
+ tableDocuments);
- log.info("[{}] Schema recall results - table documents count: {}, keyword-related column document groups: {}",
- this.getClass().getSimpleName(), tableDocuments.size(), columnDocumentsByKeywords.size());
+ // extract table names
+ List recallTableNames = extractTableName(tableDocuments);
+ // extract column names
+ List recallColumnNames = extractColumnName(columnDocumentsByKeywords);
Flux displayFlux = Flux.create(emitter -> {
- emitter.next(ChatResponseUtil.createResponse("开始召回Schema信息..."));
- emitter.next(ChatResponseUtil.createResponse("表信息召回完成,数量: " + tableDocuments.size()));
- emitter.next(ChatResponseUtil.createResponse("列信息召回完成,数量: " + columnDocumentsByKeywords.size()));
- emitter.next(ChatResponseUtil.createResponse("Schema信息召回完成."));
+ emitter.next(ChatResponseUtil.createResponse("开始初步召回Schema信息..."));
+ emitter.next(ChatResponseUtil.createResponse(
+ "初步表信息召回完成,数量: " + tableDocuments.size() + ",表名: " + String.join(", ", recallTableNames)));
+ emitter.next(ChatResponseUtil.createResponse("初步列信息召回完成,数量: " + columnDocumentsByKeywords.size() + ",列名: "
+ + String.join(", ", recallColumnNames)));
+ emitter.next(ChatResponseUtil.createResponse("初步Schema信息召回完成."));
emitter.complete();
});
Flux> generator = FluxUtil.createStreamingGeneratorWithMessages(this.getClass(),
state, currentState -> {
- log.info("Table document details: {}", tableDocuments);
- log.info("Keyword-related column document details: {}", columnDocumentsByKeywords);
return Map.of(TABLE_DOCUMENTS_FOR_SCHEMA_OUTPUT, tableDocuments,
- COLUMN_DOCUMENTS_BY_KEYWORDS_OUTPUT, columnDocumentsByKeywords);
+ COLUMN_DOCUMENTS__FOR_SCHEMA_OUTPUT, columnDocumentsByKeywords);
}, displayFlux);
// Return the processing result
return Map.of(SCHEMA_RECALL_NODE_OUTPUT, generator);
}
+ private static List extractTableName(List tableDocuments) {
+ List tableNames = new ArrayList<>();
+ // metadata中的name字段
+ for (Document document : tableDocuments) {
+ String name = (String) document.getMetadata().get("name");
+ if (name != null && !name.isEmpty())
+ tableNames.add(name);
+ }
+ log.info("At this SchemaRecallNode, Recall tables are: {}", tableNames);
+ return tableNames;
+
+ }
+
+ private static List extractColumnName(List columnDocuments) {
+ // metadata中的 tableName + . + name字段
+ List columnNames = new ArrayList<>();
+ for (Document document : columnDocuments) {
+ String name = (String) document.getMetadata().get("name");
+ String tableName = (String) document.getMetadata().get("tableName");
+ if (name != null && !name.isEmpty() && tableName != null && !tableName.isEmpty())
+ columnNames.add(tableName + "." + name);
+ }
+ log.info("At this SchemaRecallNode, Recall columns are: {}", columnNames);
+ return columnNames;
+ }
+
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SemanticConsistencyNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SemanticConsistencyNode.java
index 4ca972bfc..e7662b5d2 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SemanticConsistencyNode.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SemanticConsistencyNode.java
@@ -19,26 +19,23 @@
import com.alibaba.cloud.ai.dto.schema.SchemaDTO;
import com.alibaba.cloud.ai.graph.GraphResponse;
import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.action.NodeAction;
import com.alibaba.cloud.ai.graph.streaming.StreamingOutput;
import com.alibaba.cloud.ai.pojo.ExecutionStep;
import com.alibaba.cloud.ai.prompt.PromptHelper;
import com.alibaba.cloud.ai.service.nl2sql.Nl2SqlService;
import com.alibaba.cloud.ai.util.FluxUtil;
+import com.alibaba.cloud.ai.util.PlanProcessUtil;
import com.alibaba.cloud.ai.util.StateUtil;
+import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.lang3.StringUtils;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
-import java.util.List;
import java.util.Map;
-import static com.alibaba.cloud.ai.constant.Constant.EVIDENCES;
-import static com.alibaba.cloud.ai.constant.Constant.PLAN_CURRENT_STEP;
-import static com.alibaba.cloud.ai.constant.Constant.SEMANTIC_CONSISTENCY_NODE_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.SEMANTIC_CONSISTENCY_NODE_RECOMMEND_OUTPUT;
-import static com.alibaba.cloud.ai.constant.Constant.TABLE_RELATION_OUTPUT;
+import static com.alibaba.cloud.ai.constant.Constant.*;
/**
* Semantic consistency validation node that checks SQL query semantic consistency.
@@ -51,34 +48,29 @@
*/
@Slf4j
@Component
-public class SemanticConsistencyNode extends AbstractPlanBasedNode {
+@AllArgsConstructor
+public class SemanticConsistencyNode implements NodeAction {
private final Nl2SqlService nl2SqlService;
- public SemanticConsistencyNode(Nl2SqlService nl2SqlService) {
- super();
- this.nl2SqlService = nl2SqlService;
- }
-
@Override
public Map apply(OverAllState state) throws Exception {
- logNodeEntry();
// Get necessary input parameters
- List evidenceList = StateUtil.getListValue(state, EVIDENCES);
+ String evidence = StateUtil.getStringValue(state, EVIDENCE);
SchemaDTO schemaDTO = StateUtil.getObjectValue(state, TABLE_RELATION_OUTPUT, SchemaDTO.class);
// Get current execution step and SQL query
- ExecutionStep executionStep = getCurrentExecutionStep(state);
- Integer currentStep = getCurrentStepNumber(state);
+ ExecutionStep executionStep = PlanProcessUtil.getCurrentExecutionStep(state);
+ Integer currentStep = PlanProcessUtil.getCurrentStepNumber(state);
ExecutionStep.ToolParameters toolParameters = executionStep.getToolParameters();
String sqlQuery = toolParameters.getSqlQuery();
log.info("Starting semantic consistency validation - SQL: {}", sqlQuery);
log.info("Step description: {}", toolParameters.getDescription());
- Flux validationResultFlux = performSemanticValidationStream(schemaDTO, evidenceList,
- toolParameters, sqlQuery);
+ Flux validationResultFlux = performSemanticValidationStream(schemaDTO, evidence, toolParameters,
+ sqlQuery);
Flux> generator = FluxUtil.createStreamingGeneratorWithMessages(this.getClass(),
state, "开始语义一致性校验", "语义一致性校验完成", validationResult -> {
@@ -95,11 +87,10 @@ public Map apply(OverAllState state) throws Exception {
/**
* Perform streaming semantic consistency validation
*/
- private Flux performSemanticValidationStream(SchemaDTO schemaDTO, List evidenceList,
+ private Flux performSemanticValidationStream(SchemaDTO schemaDTO, String evidence,
ExecutionStep.ToolParameters toolParameters, String sqlQuery) throws Exception {
// Build validation context
String schema = PromptHelper.buildMixMacSqlDbPrompt(schemaDTO, true);
- String evidence = StringUtils.join(evidenceList, ";\n");
String context = String.join("\n", schema, evidence, toolParameters.getDescription());
// Execute semantic consistency check
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SqlExecuteNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SqlExecuteNode.java
index f08c40841..840a7893b 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SqlExecuteNode.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SqlExecuteNode.java
@@ -26,6 +26,7 @@
import com.alibaba.cloud.ai.enums.TextType;
import com.alibaba.cloud.ai.graph.GraphResponse;
import com.alibaba.cloud.ai.graph.OverAllState;
+import com.alibaba.cloud.ai.graph.action.NodeAction;
import com.alibaba.cloud.ai.graph.streaming.StreamingOutput;
import com.alibaba.cloud.ai.pojo.ExecutionStep;
import com.alibaba.cloud.ai.service.datasource.DatasourceService;
@@ -33,8 +34,8 @@
import com.alibaba.cloud.ai.entity.Datasource;
import com.alibaba.cloud.ai.util.ChatResponseUtil;
import com.alibaba.cloud.ai.util.FluxUtil;
+import com.alibaba.cloud.ai.util.PlanProcessUtil;
import com.alibaba.cloud.ai.util.StateUtil;
-import com.alibaba.cloud.ai.util.StepResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Component;
@@ -58,7 +59,7 @@
*/
@Slf4j
@Component
-public class SqlExecuteNode extends AbstractPlanBasedNode {
+public class SqlExecuteNode implements NodeAction {
// todo: 根据数据库配置动态获取Accessor
private final Accessor dbAccessor;
@@ -68,7 +69,6 @@ public class SqlExecuteNode extends AbstractPlanBasedNode {
private final DbConfig dbConfig;
public SqlExecuteNode(AccessorFactory accessorFactory, DatasourceService datasourceService, DbConfig dbConfig) {
- super();
this.dbAccessor = accessorFactory.getAccessorByDbConfig(dbConfig);
this.datasourceService = datasourceService;
this.dbConfig = dbConfig;
@@ -76,10 +76,9 @@ public SqlExecuteNode(AccessorFactory accessorFactory, DatasourceService datasou
@Override
public Map apply(OverAllState state) throws Exception {
- logNodeEntry();
- ExecutionStep executionStep = getCurrentExecutionStep(state);
- Integer currentStep = getCurrentStepNumber(state);
+ ExecutionStep executionStep = PlanProcessUtil.getCurrentExecutionStep(state);
+ Integer currentStep = PlanProcessUtil.getCurrentStepNumber(state);
ExecutionStep.ToolParameters toolParameters = executionStep.getToolParameters();
String sqlQuery = toolParameters.getSqlQuery();
@@ -199,7 +198,7 @@ private Map executeSqlQuery(OverAllState state, Integer currentS
// Update step results with the query output
Map existingResults = StateUtil.getObjectValue(state, SQL_EXECUTE_NODE_OUTPUT, Map.class,
new HashMap<>());
- Map updatedResults = StepResultUtil.addStepResult(existingResults, currentStep, jsonStr);
+ Map updatedResults = PlanProcessUtil.addStepResult(existingResults, currentStep, jsonStr);
log.info("SQL execution successful, result count: {}",
resultSetBO.getData() != null ? resultSetBO.getData().size() : 0);
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SqlGenerateNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SqlGenerateNode.java
index 0e5141a2c..ebf0286e5 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SqlGenerateNode.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/SqlGenerateNode.java
@@ -27,18 +27,17 @@
import com.alibaba.cloud.ai.service.nl2sql.Nl2SqlService;
import com.alibaba.cloud.ai.util.ChatResponseUtil;
import com.alibaba.cloud.ai.util.FluxUtil;
+import com.alibaba.cloud.ai.util.PlanProcessUtil;
import com.alibaba.cloud.ai.util.StateUtil;
import com.google.common.util.concurrent.AtomicDouble;
+import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatResponse;
-import org.springframework.ai.converter.BeanOutputConverter;
-import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.HashMap;
-import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
@@ -61,31 +60,19 @@
*/
@Slf4j
@Component
+@AllArgsConstructor
public class SqlGenerateNode implements NodeAction {
private static final int MAX_OPTIMIZATION_ROUNDS = 3;
private final Nl2SqlService nl2SqlService;
- private final BeanOutputConverter converter;
-
- public SqlGenerateNode(Nl2SqlService nl2SqlService) {
- this.nl2SqlService = nl2SqlService;
- this.converter = new BeanOutputConverter<>(new ParameterizedTypeReference<>() {
- });
- }
-
@Override
public Map apply(OverAllState state) throws Exception {
- log.info("Entering {} node", this.getClass().getSimpleName());
// Get necessary input parameters
- String plannerNodeOutput = StateUtil.getStringValue(state, PLANNER_NODE_OUTPUT);
- Plan plan = converter.convert(plannerNodeOutput);
- Integer currentStep = StateUtil.getObjectValue(state, PLAN_CURRENT_STEP, Integer.class, 1);
-
- List executionPlan = Optional.ofNullable(plan).orElseThrow().getExecutionPlan();
- ExecutionStep executionStep = executionPlan.get(currentStep - 1);
+ Plan plan = PlanProcessUtil.getPlan(state);
+ ExecutionStep executionStep = PlanProcessUtil.getCurrentExecutionStep(state);
ExecutionStep.ToolParameters toolParameters = executionStep.getToolParameters();
// Execute business logic first - determine what needs to be regenerated
@@ -133,11 +120,11 @@ private Flux handleSqlExecutionException(OverAllState state, ExecutionSt
String sqlException = StateUtil.getStringValue(state, SQL_EXECUTE_NODE_EXCEPTION_OUTPUT);
log.info("Detected SQL execution exception, starting to regenerate SQL: {}", sqlException);
- List evidenceList = StateUtil.getListValue(state, EVIDENCES);
+ String evidence = StateUtil.getStringValue(state, EVIDENCE);
SchemaDTO schemaDTO = StateUtil.getObjectValue(state, TABLE_RELATION_OUTPUT, SchemaDTO.class);
- return regenerateSql(state, toolParameters.toJsonStr(), evidenceList, schemaDTO,
- SQL_EXECUTE_NODE_EXCEPTION_OUTPUT, toolParameters.getSqlQuery(), finalSqlConsumer);
+ return regenerateSql(state, toolParameters.toJsonStr(), evidence, schemaDTO, SQL_EXECUTE_NODE_EXCEPTION_OUTPUT,
+ toolParameters.getSqlQuery(), finalSqlConsumer);
}
/**
@@ -147,10 +134,10 @@ private Flux handleSemanticConsistencyFailure(OverAllState state,
ExecutionStep.ToolParameters toolParameters, Consumer finalSqlConsumer) {
log.info("Semantic consistency validation failed, starting to regenerate SQL");
- List evidenceList = StateUtil.getListValue(state, EVIDENCES);
+ String evidence = StateUtil.getStringValue(state, EVIDENCE);
SchemaDTO schemaDTO = StateUtil.getObjectValue(state, TABLE_RELATION_OUTPUT, SchemaDTO.class);
- return regenerateSql(state, toolParameters.toJsonStr(), evidenceList, schemaDTO,
+ return regenerateSql(state, toolParameters.toJsonStr(), evidence, schemaDTO,
SEMANTIC_CONSISTENCY_NODE_RECOMMEND_OUTPUT, toolParameters.getSqlQuery(), finalSqlConsumer);
}
@@ -167,7 +154,7 @@ private boolean isSemanticConsistencyFailed(OverAllState state) {
* @param finalSqlConsumer 处理最终生成SQL的消费者,如果失败,则sql为null
* @return AI中间输出过程的Flux
*/
- private Flux regenerateSql(OverAllState state, String input, List evidenceList, SchemaDTO schemaDTO,
+ private Flux regenerateSql(OverAllState state, String input, String evidence, SchemaDTO schemaDTO,
String exceptionOutputKey, String originalSql, Consumer finalSqlConsumer) {
String exceptionMessage = StateUtil.getStringValue(state, exceptionOutputKey);
log.info("开始增强SQL生成流程 - 原始SQL: {}, 异常信息: {}", originalSql, exceptionMessage);
@@ -239,7 +226,7 @@ public Flux get() {
}
};
- Flux sqlFlux = nl2SqlService.generateSql(evidenceList, input, schemaDTO, originalSql, exceptionMessage);
+ Flux sqlFlux = nl2SqlService.generateSql(evidence, input, schemaDTO, originalSql, exceptionMessage);
Mono sqlMono = sqlFlux.collect(StringBuilder::new, StringBuilder::append).map(StringBuilder::toString);
return Flux.just("正在生成SQL...\n", TextType.SQL.getStartSign())
.concatWith(sqlMono.flatMapMany(sql -> Flux.just(nl2SqlService.sqlTrim(sql)).expand(newSql -> {
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/TableRelationNode.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/TableRelationNode.java
index 0b6f8bc15..2629ee783 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/TableRelationNode.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/node/TableRelationNode.java
@@ -17,17 +17,17 @@
package com.alibaba.cloud.ai.node;
import com.alibaba.cloud.ai.connector.config.DbConfig;
-import com.alibaba.cloud.ai.dto.BusinessKnowledgeDTO;
+import com.alibaba.cloud.ai.dto.schema.SchemaDTO;
+import com.alibaba.cloud.ai.dto.schema.TableDTO;
import com.alibaba.cloud.ai.entity.Datasource;
import com.alibaba.cloud.ai.entity.SemanticModel;
import com.alibaba.cloud.ai.enums.TextType;
import com.alibaba.cloud.ai.graph.GraphResponse;
import com.alibaba.cloud.ai.graph.OverAllState;
import com.alibaba.cloud.ai.graph.action.NodeAction;
-import com.alibaba.cloud.ai.dto.schema.SchemaDTO;
import com.alibaba.cloud.ai.graph.streaming.StreamingOutput;
-import com.alibaba.cloud.ai.service.datasource.DatasourceService;
import com.alibaba.cloud.ai.service.business.BusinessKnowledgeService;
+import com.alibaba.cloud.ai.service.datasource.DatasourceService;
import com.alibaba.cloud.ai.service.nl2sql.Nl2SqlService;
import com.alibaba.cloud.ai.service.schema.SchemaService;
import com.alibaba.cloud.ai.service.semantic.SemanticModelService;
@@ -41,7 +41,6 @@
import org.springframework.ai.document.Document;
import org.springframework.dao.DataAccessException;
import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import java.util.HashMap;
@@ -50,7 +49,6 @@
import java.util.function.Consumer;
import static com.alibaba.cloud.ai.constant.Constant.*;
-import static com.alibaba.cloud.ai.prompt.PromptHelper.buildBusinessKnowledgePrompt;
import static com.alibaba.cloud.ai.prompt.PromptHelper.buildSemanticModelPrompt;
/**
@@ -80,52 +78,37 @@ public class TableRelationNode implements NodeAction {
@Override
public Map apply(OverAllState state) throws Exception {
- log.info("Entering {} node", this.getClass().getSimpleName());
-
- int retryCount = StateUtil.getObjectValue(state, TABLE_RELATION_RETRY_COUNT, Integer.class, 0);
// Get necessary input parameters
- String input = StateUtil.getStringValue(state, QUERY_REWRITE_NODE_OUTPUT,
- StateUtil.getStringValue(state, INPUT_KEY));
- List evidenceList = StateUtil.getListValue(state, EVIDENCES);
+ String canonicalQuery = StateUtil.getCanonicalQuery(state);
+
+ String evidence = StateUtil.getStringValue(state, EVIDENCE);
List tableDocuments = StateUtil.getDocumentList(state, TABLE_DOCUMENTS_FOR_SCHEMA_OUTPUT);
- List> columnDocumentsByKeywords = StateUtil.getDocumentListList(state,
- COLUMN_DOCUMENTS_BY_KEYWORDS_OUTPUT);
+ List columnDocuments = StateUtil.getDocumentList(state, COLUMN_DOCUMENTS__FOR_SCHEMA_OUTPUT);
String agentIdStr = StateUtil.getStringValue(state, AGENT_ID);
- if (!StringUtils.hasText(agentIdStr))
- throw new RuntimeException("Agent ID is empty.");
// Execute business logic first - get final result immediately
DbConfig agentDbConfig = getAgentDbConfig(Integer.valueOf(agentIdStr));
- SchemaDTO schemaDTO = buildInitialSchema(agentIdStr, columnDocumentsByKeywords, tableDocuments, agentDbConfig);
-
- List businessKnowledges;
- List semanticModel = List.of();
- try {
- // Extract business knowledge and semantic model
- businessKnowledges = businessKnowledgeService.getKnowledgeDtoRecalled(Long.parseLong(agentIdStr));
- // todo: 等待SemanticModelService写完再打开
- // semanticModel =
- // semanticModelService.getEnabledByAgentId(Long.parseLong(agentIdStr));
- }
- catch (DataAccessException e) {
- log.warn("Database query failed (attempt {}): {}", retryCount + 1, e.getMessage());
-
- String errorType = classifyDatabaseError(e);
- return Map.of(TABLE_RELATION_EXCEPTION_OUTPUT, errorType + ": " + e.getMessage(),
- TABLE_RELATION_RETRY_COUNT, retryCount + 1);
- }
- // load prompt template
- String businessKnowledgePrompt = buildBusinessKnowledgePrompt(businessKnowledges);
- String semanticModelPrompt = buildSemanticModelPrompt(semanticModel);
+ SchemaDTO initialSchema = buildInitialSchema(agentIdStr, columnDocuments, tableDocuments, agentDbConfig);
- Map resultMap = new HashMap<>(
- Map.of(BUSINESS_KNOWLEDGE, businessKnowledgePrompt, SEMANTIC_MODEL, semanticModelPrompt));
+ Map resultMap = new HashMap<>();
- Flux schemaFlux = processSchemaSelection(schemaDTO, input, evidenceList, state, agentDbConfig,
- result -> {
+ Flux schemaFlux = processSchemaSelection(initialSchema, canonicalQuery, evidence, state,
+ agentDbConfig, result -> {
log.info("[{}] Schema processing result: {}", this.getClass().getSimpleName(), result);
+ // 将处理后的SchemaDTO存储到resultMap中
resultMap.put(TABLE_RELATION_OUTPUT, result);
+
+ // 从最终的SchemaDTO中获取表名列表
+ List tableNames = result.getTable().stream().map(TableDTO::getName).toList();
+
+ // 根据agentId和表名列表获取语义模型
+ List semanticModels = semanticModelService
+ .getByAgentIdAndTableNames(Integer.valueOf(agentIdStr), tableNames);
+
+ // 构建语义模型提示并存储到resultMap中
+ String semanticModelPrompt = buildSemanticModelPrompt(semanticModels);
+ resultMap.put(GENEGRATED_SEMANTIC_MODEL_PROMPT, semanticModelPrompt);
});
// Create display stream for user experience only
@@ -146,8 +129,8 @@ public Map apply(OverAllState state) throws Exception {
state, v -> resultMap, displayFlux);
// need to reset retry count and exception
- return Map.of(TABLE_RELATION_OUTPUT, generator, BUSINESS_KNOWLEDGE, businessKnowledgePrompt, SEMANTIC_MODEL,
- semanticModelPrompt, TABLE_RELATION_RETRY_COUNT, 0, TABLE_RELATION_EXCEPTION_OUTPUT, "");
+ return Map.of(TABLE_RELATION_OUTPUT, generator, TABLE_RELATION_RETRY_COUNT, 0, TABLE_RELATION_EXCEPTION_OUTPUT,
+ "");
}
@@ -165,12 +148,12 @@ private String classifyDatabaseError(DataAccessException e) {
/**
* Builds initial schema from column and table documents.
*/
- private SchemaDTO buildInitialSchema(String agentId, List> columnDocumentsByKeywords,
- List tableDocuments, DbConfig agentDbConfig) {
+ private SchemaDTO buildInitialSchema(String agentId, List columnDocuments, List tableDocuments,
+ DbConfig agentDbConfig) {
SchemaDTO schemaDTO = new SchemaDTO();
schemaService.extractDatabaseName(schemaDTO, agentDbConfig);
- schemaService.buildSchemaFromDocuments(agentId, columnDocumentsByKeywords, tableDocuments, schemaDTO);
+ schemaService.buildSchemaFromDocuments(agentId, columnDocuments, tableDocuments, schemaDTO);
return schemaDTO;
}
@@ -191,7 +174,7 @@ private DbConfig getAgentDbConfig(Integer agentId) {
/**
* Processes schema selection based on input, evidence, and optional advice.
*/
- private Flux processSchemaSelection(SchemaDTO schemaDTO, String input, List evidenceList,
+ private Flux processSchemaSelection(SchemaDTO schemaDTO, String input, String evidence,
OverAllState state, DbConfig agentDbConfig, Consumer dtoConsumer) {
String schemaAdvice = StateUtil.getStringValue(state, SQL_GENERATE_SCHEMA_MISSING_ADVICE, null);
@@ -199,12 +182,11 @@ private Flux processSchemaSelection(SchemaDTO schemaDTO, String in
if (schemaAdvice != null) {
log.info("[{}] Processing with schema supplement advice: {}", this.getClass().getSimpleName(),
schemaAdvice);
- schemaFlux = nl2SqlService.fineSelect(schemaDTO, input, evidenceList, schemaAdvice, agentDbConfig,
- dtoConsumer);
+ schemaFlux = nl2SqlService.fineSelect(schemaDTO, input, evidence, schemaAdvice, agentDbConfig, dtoConsumer);
}
else {
log.info("[{}] Executing regular schema selection", this.getClass().getSimpleName());
- schemaFlux = nl2SqlService.fineSelect(schemaDTO, input, evidenceList, null, agentDbConfig, dtoConsumer);
+ schemaFlux = nl2SqlService.fineSelect(schemaDTO, input, evidence, null, agentDbConfig, dtoConsumer);
}
return Flux
.just(ChatResponseUtil.createResponse("正在选择合适的数据表...\n"),
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/prompt/PromptConstant.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/prompt/PromptConstant.java
index 72ed89fdf..ad4b3750f 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/prompt/PromptConstant.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/prompt/PromptConstant.java
@@ -30,6 +30,20 @@ public static PromptTemplate getInitRewritePromptTemplate() {
return new PromptTemplate(PromptLoader.loadPrompt("init-rewrite"));
}
+ // intent-recognition
+ public static PromptTemplate getIntentRecognitionPromptTemplate() {
+ return new PromptTemplate(PromptLoader.loadPrompt("intent-recognition"));
+ }
+
+ public static PromptTemplate getQueryEnhancementPromptTemplate() {
+ return new PromptTemplate(PromptLoader.loadPrompt("query-enhancement"));
+ }
+
+ // feasibility-assessment
+ public static PromptTemplate getFeasibilityAssessmentPromptTemplate() {
+ return new PromptTemplate(PromptLoader.loadPrompt("feasibility-assessment"));
+ }
+
public static PromptTemplate getQuestionToKeywordsPromptTemplate() {
return new PromptTemplate(PromptLoader.loadPrompt("question-to-keywords"));
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/prompt/PromptHelper.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/prompt/PromptHelper.java
index a685f989f..e447f9ddf 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/prompt/PromptHelper.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/prompt/PromptHelper.java
@@ -15,19 +15,17 @@
*/
package com.alibaba.cloud.ai.prompt;
-import com.alibaba.cloud.ai.entity.SemanticModel;
-import com.alibaba.cloud.ai.enums.BizDataSourceTypeEnum;
import com.alibaba.cloud.ai.connector.config.DbConfig;
-import com.alibaba.cloud.ai.dto.BusinessKnowledgeDTO;
import com.alibaba.cloud.ai.dto.schema.ColumnDTO;
import com.alibaba.cloud.ai.dto.schema.SchemaDTO;
import com.alibaba.cloud.ai.dto.schema.TableDTO;
+import com.alibaba.cloud.ai.entity.SemanticModel;
import com.alibaba.cloud.ai.entity.UserPromptConfig;
-
+import com.alibaba.cloud.ai.enums.BizDataSourceTypeEnum;
+import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.ai.chat.prompt.PromptTemplate;
-import org.apache.commons.collections.CollectionUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -44,6 +42,7 @@ public static String buildRewritePrompt(String query, SchemaDTO schemaDTO, List<
for (TableDTO tableDTO : schemaDTO.getTable()) {
dbContent.append(buildMacSqlTablePrompt(tableDTO)).append("\n");
}
+ // TODO 待完善多轮输入
StringBuilder multiTurn = new StringBuilder();
multiTurn.append("<最新>").append("用户: ").append(query);
@@ -88,13 +87,15 @@ public static String buildQueryToKeywordsPrompt(String question) {
return PromptConstant.getQuestionToKeywordsPromptTemplate().render(params);
}
- public static String buildMixSelectorPrompt(List evidences, String question, SchemaDTO schemaDTO) {
+ public static String buildMixSelectorPrompt(String evidence, String question, SchemaDTO schemaDTO) {
String schemaInfo = buildMixMacSqlDbPrompt(schemaDTO, true);
Map params = new HashMap<>();
params.put("schema_info", schemaInfo);
params.put("question", question);
- String evidence = CollectionUtils.isEmpty(evidences) ? "" : StringUtils.join(evidences, ";\n");
- params.put("evidence", evidence);
+ if (StringUtils.isBlank(evidence))
+ params.put("evidence", "无");
+ else
+ params.put("evidence", evidence);
return PromptConstant.getMixSelectorPromptTemplate().render(params);
}
@@ -170,15 +171,7 @@ public static String buildMixMacSqlTablePrompt(TableDTO tableDTO, Boolean withCo
List data = new ArrayList<>(enumData.subList(0, Math.min(3, enumData.size())));
line.append(StringUtils.join(data, ",")).append("]");
}
- else if (CollectionUtils.isNotEmpty(columnDTO.getSamples())) {
- List data = columnDTO.getSamples().subList(0, Math.min(3, columnDTO.getSamples().size()));
- data = data.stream().filter(item -> StringUtils.isNotBlank(item)).collect(Collectors.toList());
- if (CollectionUtils.isNotEmpty(data)) {
- line.append(", Examples: [");
- data = processSamples(data, columnDTO);
- line.append(StringUtils.join(data, ",")).append("]");
- }
- }
+
line.append(")");
columnLines.add(line.toString());
}
@@ -212,8 +205,7 @@ private static List processSamples(List samples, ColumnDTO colum
}
public static List buildMixSqlGeneratorPrompt(String question, DbConfig dbConfig, SchemaDTO schemaDTO,
- List evidenceList) {
- String evidence = StringUtils.join(evidenceList, ";\n");
+ String evidence) {
String schemaInfo = buildMixMacSqlDbPrompt(schemaDTO, true);
String dialect = BizDataSourceTypeEnum.fromTypeName(dbConfig.getDialectType()).getDialect();
Map params = new HashMap<>();
@@ -271,8 +263,7 @@ public static String buildReportGeneratorPromptWithOptimization(String userRequi
}
public static String buildSqlErrorFixerPrompt(String question, DbConfig dbConfig, SchemaDTO schemaDTO,
- List evidenceList, String errorSql, String errorMessage) {
- String evidence = StringUtils.join(evidenceList, ";\n");
+ String evidence, String errorSql, String errorMessage) {
String schemaInfo = buildMixMacSqlDbPrompt(schemaDTO, true);
String dialect = BizDataSourceTypeEnum.fromTypeName(dbConfig.getDialectType()).getDialect();
@@ -287,11 +278,12 @@ public static String buildSqlErrorFixerPrompt(String question, DbConfig dbConfig
return PromptConstant.getSqlErrorFixerPromptTemplate().render(params);
}
- public static String buildBusinessKnowledgePrompt(List businessKnowledgeDTOS) {
+ public static String buildBusinessKnowledgePrompt(String businessTerms) {
Map params = new HashMap<>();
- String businessKnowledge = CollectionUtils.isEmpty(businessKnowledgeDTOS) ? ""
- : StringUtils.join(businessKnowledgeDTOS, ";\n");
- params.put("businessKnowledge", businessKnowledge);
+ if (StringUtils.isNotBlank(businessTerms))
+ params.put("businessKnowledge", businessTerms);
+ else
+ params.put("businessKnowledge", "无");
return PromptConstant.getBusinessKnowledgePromptTemplate().render(params);
}
@@ -329,6 +321,56 @@ private static String buildOptimizationSection(List optimizati
return result.toString().trim();
}
+ /**
+ * 构建意图识别提示词
+ * @param multiTurn 多轮对话历史
+ * @param latestQuery 最新用户输入
+ * @return 意图识别提示词
+ */
+ public static String buildIntentRecognitionPrompt(String multiTurn, String latestQuery) {
+ Map params = new HashMap<>();
+ params.put("multi_turn", multiTurn != null ? multiTurn : "(无)");
+ params.put("latest_query", latestQuery);
+ return PromptConstant.getIntentRecognitionPromptTemplate().render(params);
+ }
+
+ /**
+ * 构建查询处理提示词
+ * @param multiTurn 多轮对话历史
+ * @param latestQuery 最新用户输入
+ * @return 查询处理提示词
+ */
+ public static String buildQueryEnhancePrompt(String multiTurn, String latestQuery, String evidence) {
+ Map params = new HashMap<>();
+ params.put("multi_turn", multiTurn != null ? multiTurn : "(无)");
+ params.put("latest_query", latestQuery);
+ if (StringUtils.isEmpty(evidence))
+ params.put("evidence", "无");
+ else
+ params.put("evidence", evidence);
+ params.put("current_time_info", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+ return PromptConstant.getQueryEnhancementPromptTemplate().render(params);
+ }
+
+ /**
+ * 构建可行性评估提示词
+ * @param canonicalQuery 规范化查询
+ * @param recalledSchema 召回的数据库Schema
+ * @param evidence 参考信息
+ * @param multiTurn 多轮对话历史
+ * @return 可行性评估提示词
+ */
+ public static String buildFeasibilityAssessmentPrompt(String canonicalQuery, SchemaDTO recalledSchema,
+ String evidence, String multiTurn) {
+ Map params = new HashMap<>();
+ String schemaInfo = buildMixMacSqlDbPrompt(recalledSchema, true);
+ params.put("canonical_query", canonicalQuery != null ? canonicalQuery : "");
+ params.put("recalled_schema", schemaInfo);
+ params.put("evidence", evidence != null ? evidence : "");
+ params.put("multi_turn", multiTurn != null ? multiTurn : "(无)");
+ return PromptConstant.getFeasibilityAssessmentPromptTemplate().render(params);
+ }
+
/**
* 渲染优化提示词模板
* @param optimizationPrompt 优化提示词模板
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/business/BusinessKnowledgeServiceImpl.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/business/BusinessKnowledgeServiceImpl.java
index 7019c404c..3be1e4415 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/business/BusinessKnowledgeServiceImpl.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/business/BusinessKnowledgeServiceImpl.java
@@ -16,20 +16,38 @@
package com.alibaba.cloud.ai.service.business;
+import com.alibaba.cloud.ai.constant.Constant;
+import com.alibaba.cloud.ai.constant.DocumentMetadataConstant;
import com.alibaba.cloud.ai.dto.BusinessKnowledgeDTO;
import com.alibaba.cloud.ai.entity.BusinessKnowledge;
import com.alibaba.cloud.ai.mapper.BusinessKnowledgeMapper;
+import com.alibaba.cloud.ai.service.vectorstore.AgentVectorStoreService;
+import com.alibaba.cloud.ai.util.DocumentConverterUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.document.Document;
+import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+@Slf4j
@Service
public class BusinessKnowledgeServiceImpl implements BusinessKnowledgeService {
private final BusinessKnowledgeMapper businessKnowledgeMapper;
- public BusinessKnowledgeServiceImpl(BusinessKnowledgeMapper businessKnowledgeMapper) {
+ private final VectorStore vectorStore;
+
+ private final AgentVectorStoreService agentVectorStoreService;
+
+ public BusinessKnowledgeServiceImpl(BusinessKnowledgeMapper businessKnowledgeMapper, VectorStore vectorStore,
+ AgentVectorStoreService agentVectorStoreService) {
this.businessKnowledgeMapper = businessKnowledgeMapper;
+ this.vectorStore = vectorStore;
+ this.agentVectorStoreService = agentVectorStoreService;
}
@Override
@@ -58,38 +76,125 @@ public BusinessKnowledge getKnowledgeById(Long id) {
}
@Override
+ @Transactional(rollbackFor = Exception.class)
public Long addKnowledge(BusinessKnowledgeDTO knowledgeDTO) {
BusinessKnowledge entity = knowledgeDTO.toEntity();
- if (businessKnowledgeMapper.insert(entity) > 0) {
+
+ // 插入数据库
+ if (businessKnowledgeMapper.insert(entity) <= 0) {
+ throw new RuntimeException("Failed to add knowledge to database");
+ }
+
+ try {
+ // 转换为文档并插入向量库
+ Document document = DocumentConverterUtil.convertBusinessTermToDocument(knowledgeDTO,
+ entity.getId().toString());
+ vectorStore.add(List.of(document));
return entity.getId();
}
- throw new RuntimeException("Failed to add knowledge");
+ catch (Exception e) {
+ // 向量库插入失败,清理数据库记录
+ try {
+ businessKnowledgeMapper.deleteById(entity.getId());
+ }
+ catch (Exception deleteException) {
+ log.error("Failed to clean up database record after vector store error: {}",
+ deleteException.getMessage());
+ }
+ throw new RuntimeException("Failed to add knowledge: " + e.getMessage(), e);
+ }
}
@Override
+ @Transactional(rollbackFor = Exception.class)
public void updateKnowledge(Long id, BusinessKnowledgeDTO knowledgeDTO) {
BusinessKnowledge entity = knowledgeDTO.toEntity();
entity.setId(id);
- if (businessKnowledgeMapper.updateById(entity) > 0) {
- return;
+
+ // 更新数据库
+ if (businessKnowledgeMapper.updateById(entity) <= 0) {
+ throw new RuntimeException("Failed to update knowledge in database");
+ }
+
+ try {
+ // 更新向量库中的文档
+ Map metadata = new HashMap<>();
+ metadata.put(Constant.AGENT_ID, entity.getAgentId());
+ metadata.put(DocumentMetadataConstant.VECTOR_TYPE, DocumentMetadataConstant.BUSINESS_TERM);
+ metadata.put(DocumentMetadataConstant.DB_RECORD_ID, id.toString());
+ agentVectorStoreService.deleteDocumentsByMetedata(knowledgeDTO.getAgentId().toString(), metadata);
+
+ Document document = DocumentConverterUtil.convertBusinessTermToDocument(knowledgeDTO, id.toString());
+ vectorStore.add(List.of(document));
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Failed to update knowledge in vector store: " + e.getMessage(), e);
}
- throw new RuntimeException("Failed to update knowledge");
}
@Override
+ @Transactional(rollbackFor = Exception.class)
public void deleteKnowledge(Long id) {
- if (businessKnowledgeMapper.deleteById(id) > 0) {
+ // 从数据库获取原始数据
+ BusinessKnowledge knowledge = businessKnowledgeMapper.selectById(id);
+ if (knowledge == null) {
+ log.warn("Knowledge not found with id: " + id);
return;
}
- throw new RuntimeException("Failed to delete knowledge");
+
+ try {
+ // 从向量库删除文档
+ Map metadata = new HashMap<>();
+ metadata.put(Constant.AGENT_ID, knowledge.getAgentId());
+ metadata.put(DocumentMetadataConstant.VECTOR_TYPE, DocumentMetadataConstant.BUSINESS_TERM);
+ metadata.put(DocumentMetadataConstant.DB_RECORD_ID, id.toString());
+ agentVectorStoreService.deleteDocumentsByMetedata(knowledge.getAgentId().toString(), metadata);
+ }
+ catch (Exception e) {
+ log.warn("Failed to delete document from vector store: {}", e.getMessage());
+ }
+
+ // 从数据库删除记录
+ if (businessKnowledgeMapper.deleteById(id) <= 0) {
+ throw new RuntimeException("Failed to delete knowledge from database");
+ }
}
@Override
+ @Transactional(rollbackFor = Exception.class)
public void recallKnowledge(Long id, boolean isRecall) {
- if (businessKnowledgeMapper.changeRecall(id, isRecall) > 0) {
- return;
+ // 从数据库获取原始数据
+ BusinessKnowledge knowledge = businessKnowledgeMapper.selectById(id);
+ if (knowledge == null) {
+ throw new RuntimeException("Knowledge not found with id: " + id);
+ }
+
+ // 更新数据库中的召回状态
+ if (businessKnowledgeMapper.changeRecall(id, isRecall) <= 0) {
+ throw new RuntimeException("Failed to change recall status in database");
+ }
+
+ try {
+ // 更新向量库中的文档
+ Map metadata = new HashMap<>();
+ metadata.put(Constant.AGENT_ID, knowledge.getAgentId());
+ metadata.put(DocumentMetadataConstant.VECTOR_TYPE, DocumentMetadataConstant.BUSINESS_TERM);
+ metadata.put(DocumentMetadataConstant.DB_RECORD_ID, id.toString());
+ agentVectorStoreService.deleteDocumentsByMetedata(knowledge.getAgentId().toString(), metadata);
+
+ BusinessKnowledgeDTO dto = new BusinessKnowledgeDTO();
+ dto.setBusinessTerm(knowledge.getBusinessTerm());
+ dto.setDescription(knowledge.getDescription());
+ dto.setSynonyms(knowledge.getSynonyms());
+ dto.setAgentId(knowledge.getAgentId());
+ dto.setIsRecall(isRecall);
+
+ Document document = DocumentConverterUtil.convertBusinessTermToDocument(dto, id.toString());
+ vectorStore.add(List.of(document));
+ }
+ catch (Exception e) {
+ throw new RuntimeException("Failed to update recall status in vector store: " + e.getMessage(), e);
}
- throw new RuntimeException("Failed to change recall status");
}
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/factory/FusionStrategyFactory.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/factory/FusionStrategyFactory.java
index f44f95df2..cc90ef449 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/factory/FusionStrategyFactory.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/factory/FusionStrategyFactory.java
@@ -20,28 +20,26 @@
import com.alibaba.cloud.ai.service.hybrid.fusion.impl.RrfFusionStrategy;
import com.alibaba.cloud.ai.service.hybrid.fusion.impl.WeightedAverageStrategy;
import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
/**
* FusionStrategy工厂类,根据配置创建相应的FusionStrategy实现类
*/
@Slf4j
-@Configuration
-public class FusionStrategyFactory {
+@Component
+public class FusionStrategyFactory implements FactoryBean {
@Value("${" + Constant.PROJECT_PROPERTIES_PREFIX + ".fusion-strategy:rrf}")
private String fusionStrategyType;
/**
- * 根据配置创建FusionStrategy实例
+ * 创建FusionStrategy实例
* @return FusionStrategy实例
*/
- @Bean
- @ConditionalOnMissingBean(FusionStrategy.class)
- public FusionStrategy fusionStrategy() {
+ @Override
+ public FusionStrategy getObject() throws Exception {
log.info("Creating FusionStrategy with type: {}", fusionStrategyType);
return switch (fusionStrategyType.toLowerCase()) {
@@ -60,4 +58,22 @@ public FusionStrategy fusionStrategy() {
};
}
+ /**
+ * 返回FusionStrategy的类型
+ * @return FusionStrategy类对象
+ */
+ @Override
+ public Class> getObjectType() {
+ return FusionStrategy.class;
+ }
+
+ /**
+ * 设置为单例模式
+ * @return true
+ */
+ @Override
+ public boolean isSingleton() {
+ return true;
+ }
+
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/factory/HybridRetrievalStrategyFactory.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/factory/HybridRetrievalStrategyFactory.java
index 759f43ec4..aa767ed59 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/factory/HybridRetrievalStrategyFactory.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/factory/HybridRetrievalStrategyFactory.java
@@ -16,6 +16,7 @@
package com.alibaba.cloud.ai.service.hybrid.factory;
+import com.alibaba.cloud.ai.config.DataAgentProperties;
import com.alibaba.cloud.ai.constant.Constant;
import com.alibaba.cloud.ai.service.hybrid.fusion.FusionStrategy;
import com.alibaba.cloud.ai.service.hybrid.retrieval.HybridRetrievalStrategy;
@@ -23,10 +24,10 @@
import com.alibaba.cloud.ai.service.hybrid.retrieval.impl.ElasticsearchHybridRetrievalStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.vectorstore.VectorStore;
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
+import org.springframework.stereotype.Component;
import java.util.concurrent.ExecutorService;
@@ -34,44 +35,64 @@
* 混合检索策略工厂类 根据配置条件创建并注册相应的 HybridRetrievalStrategy 实现类
*/
@Slf4j
-@Configuration
-public class HybridRetrievalStrategyFactory {
+@Component
+public class HybridRetrievalStrategyFactory implements FactoryBean {
+ // spring ai 的官方属性
@Value("${spring.ai.vectorstore.type:simple}")
private String vectorStoreType;
+ // spring ai Elasticsearch VectorStore的官方属性
@Value("${spring.ai.vectorstore.elasticsearch.index-name:spring-ai-document-index}")
private String elasticsearchIndexName;
@Value("${" + Constant.PROJECT_PROPERTIES_PREFIX + ".elasticsearch-min-score:0.5}")
private Double minScore;
- /**
- * 创建并注册 HybridRetrievalStrategy Bean
- * @param executorService 线程池执行器
- * @param vectorStore 向量存储
- * @param fusionStrategy 融合策略
- * @return HybridRetrievalStrategy 实例
- */
- @Bean
- @ConditionalOnMissingBean(HybridRetrievalStrategy.class)
- public HybridRetrievalStrategy hybridRetrievalStrategy(ExecutorService executorService, VectorStore vectorStore,
- FusionStrategy fusionStrategy) {
- log.info("Creating HybridRetrievalStrategy with vectorStore type: {}", vectorStoreType);
+ @Autowired
+ private ExecutorService executorService;
+ @Autowired
+ private VectorStore vectorStore;
+
+ @Autowired
+ private FusionStrategy fusionStrategy;
+
+ @Autowired
+ private DataAgentProperties dataAgentProperties;
+
+ @Override
+ public HybridRetrievalStrategy getObject() throws Exception {
+
+ if (!dataAgentProperties.getVectorStore().isEnableHybridSearch()) {
+ log.warn("Hybrid search is disabled. Returning null HybridRetrievalStrategy.");
+ return null;
+ }
if ("elasticsearch".equalsIgnoreCase(vectorStoreType)) {
log.info("Creating ElasticsearchHybridRetrievalStrategy with index: {}", elasticsearchIndexName);
ElasticsearchHybridRetrievalStrategy strategy = new ElasticsearchHybridRetrievalStrategy(executorService,
vectorStore, fusionStrategy);
// 设置索引名称
strategy.setIndexName(elasticsearchIndexName);
- strategy.setMinScore(minScore);
+ // 从DataAgentProperties获取最小分数
+ strategy.setMinScore(dataAgentProperties.getVectorStore().getElasticsearchMinScore());
return strategy;
}
else {
- log.warn("Creating DefaultHybridRetrievalStrategy (default) without keyword-search ability");
+ log.warn(
+ "Creating DefaultHybridRetrievalStrategy (default) without keyword-search ability,maybe you should implement interface -> HybridRetrievalStrategy ");
return new DefaultHybridRetrievalStrategy(executorService, vectorStore, fusionStrategy);
}
}
+ @Override
+ public Class> getObjectType() {
+ return HybridRetrievalStrategy.class;
+ }
+
+ @Override
+ public boolean isSingleton() {
+ return true;
+ }
+
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/fusion/FusionStrategy.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/fusion/FusionStrategy.java
index 5638cb58d..a7f6495e9 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/fusion/FusionStrategy.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/fusion/FusionStrategy.java
@@ -22,6 +22,7 @@
/*融合策略接口*/
public interface FusionStrategy {
- List fuseResults(List vectorResults, List keywordResults, int topK);
+ @SuppressWarnings("unchecked")
+ List fuseResults(int topK, List... resultLists);
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/fusion/impl/RrfFusionStrategy.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/fusion/impl/RrfFusionStrategy.java
index 6d0076de0..7da32d853 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/fusion/impl/RrfFusionStrategy.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/fusion/impl/RrfFusionStrategy.java
@@ -28,36 +28,31 @@
@Slf4j
public class RrfFusionStrategy implements FusionStrategy {
+ @SuppressWarnings("unchecked")
@Override
- public List fuseResults(List vectorResults, List keywordResults, int topK) {
- if (vectorResults == null) {
- vectorResults = List.of();
- }
- if (keywordResults == null) {
- keywordResults = List.of();
- }
-
- if (vectorResults.isEmpty() && keywordResults.isEmpty()) {
+ public List fuseResults(int topK, List... resultLists) {
+ if (resultLists == null || resultLists.length == 0) {
return List.of();
}
// RRF参数配置
int k = 60;
- // 将两个结果列表组合
- List> results = List.of(vectorResults, keywordResults);
-
// 使用Map存储每个文档的RRF分数
Map rrfScores = new HashMap<>();
// 使用Map存储文档ID到Document对象的映射
Map documentMap = new HashMap<>();
- // 遍历每个结果列表
- for (List resultList : results) {
+ for (List resultList : resultLists) {
+ if (resultList == null) {
+ continue;
+ }
+
for (int i = 0; i < resultList.size(); i++) {
Document doc = resultList.get(i);
// 排名从1开始
int rank = i + 1;
+ // 假设 getDocumentId 方法能稳定获取唯一ID
String docId = getDocumentId(doc);
// 计算并累加RRF分数: score = 1 / (k + rank)
@@ -67,6 +62,11 @@ public List fuseResults(List vectorResults, List k
}
}
+ // 如果处理完所有列表后没有任何文档,提前返回
+ if (rrfScores.isEmpty()) {
+ return List.of();
+ }
+
// 按RRF分数降序排序,取topK个
return rrfScores.entrySet()
.stream()
@@ -74,6 +74,7 @@ public List fuseResults(List vectorResults, List k
.limit(topK)
.map(entry -> documentMap.get(entry.getKey()))
.collect(Collectors.toList());
+
}
private String getDocumentId(Document document) {
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/fusion/impl/WeightedAverageStrategy.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/fusion/impl/WeightedAverageStrategy.java
index 45fce9163..8f948f33b 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/fusion/impl/WeightedAverageStrategy.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/fusion/impl/WeightedAverageStrategy.java
@@ -23,8 +23,9 @@
public class WeightedAverageStrategy implements FusionStrategy {
+ @SuppressWarnings("unchecked")
@Override
- public List fuseResults(List vectorResults, List keywordResults, int topK) {
+ public List fuseResults(int topK, List... resultLists) {
throw new UnsupportedOperationException("Not implemented");
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/retrieval/AbstractHybridRetrievalStrategy.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/retrieval/AbstractHybridRetrievalStrategy.java
index 6ffca80e2..9d907e854 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/retrieval/AbstractHybridRetrievalStrategy.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/retrieval/AbstractHybridRetrievalStrategy.java
@@ -16,16 +16,12 @@
package com.alibaba.cloud.ai.service.hybrid.retrieval;
-import com.alibaba.cloud.ai.constant.Constant;
import com.alibaba.cloud.ai.request.AgentSearchRequest;
import com.alibaba.cloud.ai.service.hybrid.fusion.FusionStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
-import org.springframework.ai.vectorstore.filter.Filter;
-import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
-import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.List;
@@ -34,6 +30,8 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
+import static com.alibaba.cloud.ai.util.SearchUtil.buildVectorSearchRequest;
+
@Slf4j
public abstract class AbstractHybridRetrievalStrategy implements HybridRetrievalStrategy {
@@ -67,40 +65,34 @@ public List retrieve(AgentSearchRequest request) {
if (Objects.isNull(request.getTopK()))
throw new IllegalArgumentException("topK cannot be null");
- SearchRequest vectorSearchRequest = buildVectorSearchRequest(request);
+ SearchRequest vectorSearchRequest = buildVectorSearchRequest(request, true);
// 异步执行向量搜索
CompletableFuture> vectorSearchFuture = CompletableFuture.supplyAsync(() -> {
List vectorResults = vectorStore.similaritySearch(vectorSearchRequest);
- log.info("Vector Search completed. Found {} documents for SearchRequest: {}", vectorResults.size(),
+ log.debug("Vector Search completed. Found {} documents for SearchRequest: {}", vectorResults.size(),
vectorSearchRequest);
return vectorResults;
}, executorService);
// 异步执行关键词搜索
- CompletableFuture> keywordSearchFuture = null;
- if (!CollectionUtils.isEmpty(request.getKeywords())) {
- keywordSearchFuture = CompletableFuture.supplyAsync(() -> {
- List results = getDocumentsByKeywords(request);
- log.info("Keyword Search completed. Found {} documents, using keywords: {}", results.size(),
- request.getKeywords());
- return results;
- }, executorService);
- }
+ CompletableFuture> keywordSearchFuture = CompletableFuture.supplyAsync(() -> {
+ List results = getDocumentsByKeywords(request);
+ log.debug("Keyword Search completed. Found {} documents, with query: {}", results.size(),
+ request.getQuery());
+ return results;
+ }, executorService);
try {
List vectorResults = vectorSearchFuture.get();
- if (keywordSearchFuture == null) {
- return vectorResults;
- }
// 等待关键词搜索完成
List keywordResults = keywordSearchFuture.get();
// 融合结果
- List finalDocuments = fusionStrategy.fuseResults(vectorResults, keywordResults,
- request.getTopK());
- log.info("Fusion completed. Found {} documents", finalDocuments.size());
+ List finalDocuments = fusionStrategy.fuseResults(request.getTopK(), vectorResults,
+ keywordResults);
+ log.debug("Fusion completed. Found {} documents", finalDocuments.size());
return finalDocuments;
}
catch (InterruptedException e) {
@@ -113,24 +105,6 @@ public List retrieve(AgentSearchRequest request) {
}
- private SearchRequest buildVectorSearchRequest(AgentSearchRequest request) {
- SearchRequest.Builder builder = SearchRequest.builder();
- if (StringUtils.hasText(request.getQuery()))
- builder.query(request.getQuery());
-
- // 和keyword查找一样设置2倍topK去查找,因为后续需要和keyword的查找融合返回topK条
- builder.topK(request.getTopK() * 2);
-
- FilterExpressionBuilder b = new FilterExpressionBuilder();
- Filter.Expression expression = b
- .and(b.eq(Constant.VECTOR_TYPE, request.getDocVectorType()), b.eq(Constant.AGENT_ID, request.getAgentId()))
- .build();
- builder.filterExpression(expression);
-
- builder.similarityThreshold(request.getSimilarityThreshold());
- return builder.build();
- }
-
public abstract List getDocumentsByKeywords(AgentSearchRequest agentSearchRequest);
}
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/retrieval/impl/DefaultHybridRetrievalStrategy.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/retrieval/impl/DefaultHybridRetrievalStrategy.java
index 3bf05641b..ce978a9b4 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/retrieval/impl/DefaultHybridRetrievalStrategy.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/retrieval/impl/DefaultHybridRetrievalStrategy.java
@@ -28,7 +28,7 @@
import java.util.concurrent.ExecutorService;
/**
- * 适合测试使用,无关键词搜索能力
+ * 适合测试以及没有继承实现AbstractHybridRetrievalStrategy的向量库(如Pg,milvus)使用,无关键词搜索能力
*/
@Slf4j
public class DefaultHybridRetrievalStrategy extends AbstractHybridRetrievalStrategy {
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/retrieval/impl/ElasticsearchHybridRetrievalStrategy.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/retrieval/impl/ElasticsearchHybridRetrievalStrategy.java
index a26978cde..a33599b4c 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/retrieval/impl/ElasticsearchHybridRetrievalStrategy.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/hybrid/retrieval/impl/ElasticsearchHybridRetrievalStrategy.java
@@ -32,7 +32,7 @@
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.elasticsearch.ElasticsearchVectorStore;
-import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.Collections;
@@ -74,7 +74,7 @@ public List getDocumentsByKeywords(AgentSearchRequest agentSearchReque
* "term": { "metadata.vectorType": "table" } } ] } }, "size": 20, "_source": true
* }
*/
- if (CollectionUtils.isEmpty(agentSearchRequest.getKeywords()))
+ if (!StringUtils.hasText(agentSearchRequest.getQuery()))
return Collections.emptyList();
ElasticsearchVectorStore vectorStore = (ElasticsearchVectorStore) this.vectorStore;
@@ -99,10 +99,9 @@ public List getDocumentsByKeywords(AgentSearchRequest agentSearchReque
private SearchRequest buildSearchRequest(AgentSearchRequest agentSearchRequest) {
// 拼接keywords 通过空格连接
- String keywords = String.join(" ", agentSearchRequest.getKeywords());
- log.debug("ElasticsearchClient search keywords: {}", keywords);
+ log.debug("ElasticsearchClient search with query: {}", agentSearchRequest.getQuery());
- Query matchQuery = MatchQuery.of(m -> m.field("content").query(keywords))._toQuery();
+ Query matchQuery = MatchQuery.of(m -> m.field("content").query(agentSearchRequest.getQuery()))._toQuery();
// 创建元数据过滤条件
Query agentIdFilter = TermQuery.of(t -> t.field("metadata.agentId").value(agentSearchRequest.getAgentId()))
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/knowledge/AgentKnowledgeServiceImpl.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/knowledge/AgentKnowledgeServiceImpl.java
index 66e55207f..9b80ad1af 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/knowledge/AgentKnowledgeServiceImpl.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/knowledge/AgentKnowledgeServiceImpl.java
@@ -17,6 +17,7 @@
package com.alibaba.cloud.ai.service.knowledge;
import com.alibaba.cloud.ai.constant.Constant;
+import com.alibaba.cloud.ai.constant.DocumentMetadataConstant;
import com.alibaba.cloud.ai.entity.AgentKnowledge;
import com.alibaba.cloud.ai.mapper.AgentKnowledgeMapper;
import com.alibaba.cloud.ai.service.vectorstore.AgentVectorStoreService;
@@ -159,7 +160,7 @@ public void deleteKnowledgeFromVectorStore(Long agentId, Integer knowledgeId) {
try {
String agentIdStr = String.valueOf(agentId);
Map metadata = new HashMap<>(Map.ofEntries(Map.entry(Constant.AGENT_ID, agentIdStr),
- Map.entry(Constant.KNOWLEDGE_ID, knowledgeId)));
+ Map.entry(DocumentMetadataConstant.KNOWLEDGE_ID, knowledgeId)));
vectorStoreService.deleteDocumentsByMetedata(agentIdStr, metadata);
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/nl2sql/Nl2SqlService.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/nl2sql/Nl2SqlService.java
index 01260537a..506b5ed5d 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/nl2sql/Nl2SqlService.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/nl2sql/Nl2SqlService.java
@@ -22,19 +22,17 @@
import org.springframework.ai.chat.model.ChatResponse;
import reactor.core.publisher.Flux;
-import java.util.List;
import java.util.function.Consumer;
public interface Nl2SqlService {
Flux semanticConsistencyStream(String sql, String queryPrompt);
- Flux generateSql(List evidenceList, String query, SchemaDTO schemaDTO, String sql,
- String exceptionMessage);
+ Flux generateSql(String evidence, String query, SchemaDTO schemaDTO, String sql, String exceptionMessage);
Flux generateOptimizedSql(String previousSql, String exceptionMessage, int round);
- Flux fineSelect(SchemaDTO schemaDTO, String query, List evidenceList,
+ Flux fineSelect(SchemaDTO schemaDTO, String query, String evidence,
String sqlGenerateSchemaMissingAdvice, DbConfig specificDbConfig, Consumer dtoConsumer);
default String sqlTrim(String sql) {
diff --git a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/nl2sql/Nl2SqlServiceImpl.java b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/nl2sql/Nl2SqlServiceImpl.java
index ac9e64cd3..03ae6d241 100644
--- a/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/nl2sql/Nl2SqlServiceImpl.java
+++ b/spring-ai-alibaba-data-agent-chat/src/main/java/com/alibaba/cloud/ai/service/nl2sql/Nl2SqlServiceImpl.java
@@ -17,29 +17,21 @@
import com.alibaba.cloud.ai.connector.accessor.Accessor;
import com.alibaba.cloud.ai.connector.accessor.AccessorFactory;
-import com.alibaba.cloud.ai.connector.bo.DbQueryParameter;
-import com.alibaba.cloud.ai.connector.bo.ResultSetBO;
import com.alibaba.cloud.ai.connector.config.DbConfig;
-import com.alibaba.cloud.ai.dto.schema.ColumnDTO;
import com.alibaba.cloud.ai.dto.schema.SchemaDTO;
-import com.alibaba.cloud.ai.dto.schema.TableDTO;
import com.alibaba.cloud.ai.prompt.PromptHelper;
import com.alibaba.cloud.ai.service.llm.LlmService;
-
import com.alibaba.cloud.ai.util.FluxUtil;
+import com.alibaba.cloud.ai.util.JsonUtil;
import com.alibaba.cloud.ai.util.MarkdownParserUtil;
import com.fasterxml.jackson.core.type.TypeReference;
-import com.alibaba.cloud.ai.util.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
-import java.util.ArrayList;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
@@ -73,7 +65,7 @@ public Flux semanticConsistencyStream(String sql, String queryProm
}
@Override
- public Flux generateSql(List evidenceList, String query, SchemaDTO schemaDTO, String sql,
+ public Flux generateSql(String evidence, String query, SchemaDTO schemaDTO, String sql,
String exceptionMessage) {
log.info("Generating SQL for query: {}, hasExistingSql: {}", query, sql != null && !sql.isEmpty());
@@ -81,15 +73,15 @@ public Flux generateSql(List evidenceList, String query, SchemaD
if (sql != null && !sql.isEmpty()) {
// Use professional SQL error repair prompt
log.debug("Using SQL error fixer for existing SQL: {}", sql);
- String errorFixerPrompt = PromptHelper.buildSqlErrorFixerPrompt(query, dbConfig, schemaDTO, evidenceList,
- sql, exceptionMessage);
+ String errorFixerPrompt = PromptHelper.buildSqlErrorFixerPrompt(query, dbConfig, schemaDTO, evidence, sql,
+ exceptionMessage);
newSqlFlux = llmService.toStringFlux(llmService.callUser(errorFixerPrompt));
log.info("SQL error fixing completed");
}
else {
// Normal SQL generation process
log.debug("Generating new SQL from scratch");
- List prompts = PromptHelper.buildMixSqlGeneratorPrompt(query, dbConfig, schemaDTO, evidenceList);
+ List prompts = PromptHelper.buildMixSqlGeneratorPrompt(query, dbConfig, schemaDTO, evidence);
newSqlFlux = llmService.toStringFlux(llmService.call(prompts.get(0), prompts.get(1)));
log.info("New SQL generation completed");
}
@@ -164,18 +156,13 @@ private Flux fineSelect(SchemaDTO schemaDTO, String sqlGenerateSch
}
@Override
- public Flux fineSelect(SchemaDTO schemaDTO, String query, List evidenceList,
+ public Flux fineSelect(SchemaDTO schemaDTO, String query, String evidence,
String sqlGenerateSchemaMissingAdvice, DbConfig specificDbConfig, Consumer dtoConsumer) {
- log.debug("Fine selecting schema for query: {} with {} evidences and specificDbConfig: {}", query,
- evidenceList.size(), specificDbConfig != null ? specificDbConfig.getUrl() : "default");
+ log.debug("Fine selecting schema for query: {} with evidences and specificDbConfig: {}", query,
+ specificDbConfig != null ? specificDbConfig.getUrl() : "default");
- // 增加具体的样例数据,让模型根据样例数据进行选择
- SchemaDTO enrichedSchema = enrichSchemaWithSampleData(schemaDTO, specificDbConfig);
- log.debug("Schema enriched with sample data for {} tables",
- enrichedSchema.getTable() != null ? enrichedSchema.getTable().size() : 0);
-
- String prompt = buildMixSelectorPrompt(evidenceList, query, enrichedSchema);
- log.debug("Calling LLM for schema fine selection");
+ String prompt = buildMixSelectorPrompt(evidence, query, schemaDTO);
+ log.debug("Built schema fine selection prompt as follows \n {} \n", prompt);
Set selectedTables = new HashSet<>();
@@ -209,11 +196,14 @@ public Flux fineSelect(SchemaDTO schemaDTO, String query, List !selectedTables.contains(table.getName().toLowerCase()));
- int finalTableCount = schemaDTO.getTable() != null ? schemaDTO.getTable().size() : 0;
- log.debug("Fine selection completed: {} -> {} tables, selected tables: {}", originalTableCount,
- finalTableCount, selectedTables);
+ if (schemaDTO.getTable() != null) {
+ int originalTableCount = schemaDTO.getTable().size();
+ schemaDTO.getTable()
+ .removeIf(table -> !selectedTables.contains(table.getName().toLowerCase()));
+ int finalTableCount = schemaDTO.getTable().size();
+ log.debug("Fine selection completed: {} -> {} tables, selected tables: {}",
+ originalTableCount, finalTableCount, selectedTables);
+ }
}
}
dtoConsumer.accept(schemaDTO);
@@ -223,233 +213,4 @@ public Flux fineSelect(SchemaDTO schemaDTO, String query, List columnHeaders = tableData.getColumn();
- List