diff --git a/pom.xml b/pom.xml
index 7eb61bf..52b4ae8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -39,7 +39,7 @@
2.17.2
1.17.2
- 4.4.0
+ 2.6.15
@@ -49,6 +49,30 @@
spring-boot-starter-web
+
+
+ io.milvus
+ milvus-sdk-java
+ ${milvus.version}
+
+
+
+ dev.langchain4j
+ langchain4j-embeddings-all-minilm-l6-v2
+ ${langchain4j.version}
+
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
org.springframework.boot
spring-boot-starter-validation
@@ -63,6 +87,12 @@
spring-boot-starter-aop
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
org.springframework.cloud
@@ -270,69 +300,12 @@
1.3.2
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
jakarta.persistence
jakarta.persistence-api
3.1.0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -341,23 +314,13 @@
${langchain4j.version}
-
-
-
-
-
-
dev.langchain4j
langchain4j
${langchain4j.version}
-
-
-
-
-
+
dev.langchain4j
langchain4j-ollama
@@ -371,13 +334,6 @@
1.17.0
-
-
-
-
-
-
-
@@ -392,12 +348,6 @@
2.9.1
-
-
-
-
-
-
diff --git a/src/main/java/com/xly/agent/ErpAiAgent.java b/src/main/java/com/xly/agent/ErpAiAgent.java
index 727c684..e120d7d 100644
--- a/src/main/java/com/xly/agent/ErpAiAgent.java
+++ b/src/main/java/com/xly/agent/ErpAiAgent.java
@@ -27,10 +27,13 @@ public interface ErpAiAgent {
* 入参:用户问题、执行的SQL、表结构、JSON格式结果
*/
@SystemMessage("""
- 你是专业的业务数据分析师,严格遵循以下**通用规则**解释查询结果,适用于所有业务场景:
+ 你是专业的业务数据分析师,严格遵循以下**通用规则**解释查询结果,适用于所有业务场景:
1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇;
2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致;
- 3. 输出格式:仅返回解释内容,不要列出ID,无多余标题、换行、符号,结果为空时直接返回“未查询到相关数据”;
+ 3. 输出格式:仅返回解释内容,不要列出ID,无多余标题、换行、符号,结果为空时直接返回“未查询到相关数据”
+ 3.1. 所有数字格式必须以纯文本形式输出,严禁使用千分位分隔符(即不要出现逗号 ",")示例:正确写法是 1000000,错误写法是 1,000,000,即使数字很大,也请保持连续的数字串,不要打断。
+ 3.2 所有日期请转换为 YYYY-MM-DD 格式(例如:2026-03-15),严禁包含时间部分(如小时、分钟、秒)(例如:2026-03-15 00:00:00),也不要包含时区信息。”
+ 3.3. 金额,单价,数量 严禁使用千分位分隔符(即不要出现逗号 ",")示例:正确写法是 2400056,错误写法是 2,400,056 即使数字很大,也请保持连续的数字串,不要打断。
4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势;
5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。
""")
@@ -48,4 +51,132 @@ public interface ErpAiAgent {
@V("sql") String sql,
@V("tableStruct") String tableStruct,
@V("result") String result);
+
+ /**
+ * 动态表结构:自然语言解释SQL执行结果
+ * 入参:用户问题、执行的SQL、表结构、JSON格式结果
+ */
+ @SystemMessage("""
+ 你是专业的业务数据分析师,请分析以下查询结果:
+ 【用户问题】
+ {{userInput}}
+ 【数据字段说明】
+ {{sMilvusFiledDescription}}
+ 【查询结果数据(JSON格式)】
+ {{result}}
+ 【分析要求】
+ 1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇;
+ 2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致;
+ 3. 输出格式:
+ 3.1. 如果用户要求"表格形式展示",先输出简短文字说明,然后输出Markdown格式的表格
+ 3.2. 如果用户未要求表格,仅返回解释内容,不要列出ID,无多余标题、换行、符号
+ 3.3. 结果为空时直接返回"未查询到相关数据"
+ 3.4. 所有数字格式必须以纯文本形式输出,严禁使用千分位分隔符(即不要出现逗号 ",")
+ 3.5. 所有日期请转换为 YYYY-MM-DD 格式,严禁包含时间部分
+ 4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势;
+ 5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。
+ """)
+ @UserMessage("""
+ 【用户查询】
+ {{userInput}}
+ 【字段说明】
+ {{sMilvusFiledDescription}}
+ 【查询结果】
+ 用户原始查询:{{userInput}}
+ 执行查询向量库后结果(JSON格式):{{result}}
+ 请根据上述信息+通用规则,对查询结果做业务解释:
+ """)
+ String explainMilvusResult(@MemoryId String userId,
+ @V("userInput") String userInput,
+ @V("sMilvusFiledDescription") String sMilvusFiledDescription,
+ @V("result") String result);
+
+ /**
+ * AI路由判断接口
+ * true: 走聚合查询(MySQL)
+ * false: 走向量检索(Milvus)
+ */
+// @SystemMessage("""
+// 你是一个智能查询路由专家,请根据用户需求判断应该使用哪种查询方式。
+//
+// 判断标准:
+// 1. 返回 true(聚合查询/MySQL)的场景:
+// - 需要计算统计指标:总数、总和、平均值、最大/最小值、占比
+// - 需要数据汇总:分组统计、排行榜、TopN
+// - 包含关键词:统计、求和、汇总、排名、平均、数量、总额、最高、最低、占比、分组、分析、趋势
+// - 示例:统计本月销售总额、查询销量前10的商品、各品类占比分析
+//
+// 2. 返回 false(向量检索/Milvus)的场景:
+// - 查询明细数据:XXX的销售订单明细、XXX的客户信息、具体内容详情
+// - 查找相似内容:根据语义查找相关文档、推荐相似商品
+// - 模糊匹配:不确定具体关键词,需要语义理解
+// - 内容检索:查找包含特定概念的文档
+// - 包含关键词:明细、详情、查询明细、查找、搜索、匹配、推荐、相似、相关、类似
+// - 示例:李留记的销售订单明细、查询关于人工智能的文档、找相似的图片
+//
+// 重要规则:
+// - 只返回 true 或 false,不要返回其他内容
+// - 不要解释,不要添加额外文字
+// - 如果用户要求"表格形式展示",返回 false(明细查询)
+// - 如果用户指定具体人名、具体对象,返回 false(明细查询)
+// """)
+// @UserMessage("用户需求:{{userInput}}")
+ @SystemMessage("""
+ 你是一个智能查询路由专家。请根据【用户需求】,只返回 true 或 false
+ - 如果用户需求包含以下关键词:统计、求和、汇总、排名、TopN、平均、数量、总额、最高、最低、占比、分组,则返回true
+ - 如果用户需求属于模糊匹配、普通语义检索,查询明细,(例如:查询报价单明细,查询客户信息),则返回false
+ - 查询明细数据:XXX的销售订单明细、XXX的客户信息、具体内容详情,则返回false
+ - 模糊匹配:不确定具体关键词,需要语义理解,则返回false
+ """)
+ @UserMessage("""
+ 【用户需求】
+ {{userInput}}
+ """)
+ Boolean routeQuery(@MemoryId String userId, @V("userInput") String userInput);
+
+ /**
+ * 生成 Milvus 过滤条件
+ */
+ @SystemMessage("""
+ MILVUS 标量过滤条件生成规则(严格遵守):
+ 1. 语法规范:
+ - 允许的操作符:==, !=, like
+ - 逻辑组合:&& (AND), || (OR)
+ - 所有字段都是字符串类型,值必须使用单引号包裹
+ - 字符串中的单引号需要转义:'O''Reilly'
+ 2. 可用字段(只能使用这些字段):
+ - {{sMilvusFiled}}
+ 字段说明:
+ - {{sMilvusFiledDescription}}
+ 3. 重要规则:
+ - 只使用上述可用字段,不要创建新字段
+ - 如果用户提到了文档类型(如"报价单"、"订单"等),但可用字段中没有类型字段,则忽略该条件
+ - 只提取有明确值的字段条件
+ 4. 生成规则:
+ - 如果没有提取到任何具体条件,返回空字符串
+ - 从用户输入中提取明确的字段条件
+ - 识别模式:字段名 + 操作符 + 值
+ - 示例:
+ * "单据号 INV001" → sBillNo == 'INV001'
+ * "客户编号 C001" → sCustomerNo == 'C001'
+ * "销售人员张三" → sSalesManName == '张三'
+ * "产品包含手机" → sProductStyle like '%手机%'
+ 5. 输出格式:
+ - 仅返回纯过滤条件,无任何解释、换行、备注
+ - 单条件:sBillNo == 'INV001'
+ - 多条件:(sBillNo == 'INV001' && sCustomerNo == 'C001')
+ - 无条件:直接返回空字符串
+ """)
+ @UserMessage("""
+ 【用户查询】
+ - {{userInput}}
+ 【可用字段】
+ - {{sMilvusFiled}}
+ 【字段说明】
+ - {{sMilvusFiledDescription}}
+ """)
+ String getMilvusFilter(@MemoryId String userId,
+ @V("userInput") String userInput,
+ @V("sMilvusFiled") String sMilvusFiled,
+ @V("sMilvusFiledDescription") String sMilvusFiledDescription);
}
diff --git a/src/main/java/com/xly/config/OperableChatMemoryProvider.java b/src/main/java/com/xly/config/OperableChatMemoryProvider.java
index 24f2952..c7aa9bd 100644
--- a/src/main/java/com/xly/config/OperableChatMemoryProvider.java
+++ b/src/main/java/com/xly/config/OperableChatMemoryProvider.java
@@ -121,6 +121,30 @@ public class OperableChatMemoryProvider implements ChatMemoryProvider {
// 步骤4: 完全重新设置消息列表
return rebuildMemoryWithMessages(memoryId, currentMessages);
}
+
+ public List deleteUserLasterMessageBySize(Object memoryId,Integer size) {
+ if (Objects.isNull(memoryId) || size==0) {
+ return getCurrentChatMessages(memoryId);
+ }
+ // 步骤1: 获取当前所有消息
+ ChatMemory currentMemory = this.get(memoryId);
+ List currentMessages = new ArrayList<>(currentMemory.messages());
+ // 从后往前查找内容匹配的最后一条消息
+ int indexToDelete = currentMessages.size();
+ // 如果找到匹配的消息
+ if (indexToDelete >= 0) {
+ List filteredMessages = new ArrayList<>(currentMessages);
+ for(int i=0;i1){
+ filteredMessages.remove(indexToDelete);
+ }
+ }
+ return rebuildMemoryWithMessages(memoryId, filteredMessages);
+ }
+ // 步骤4: 完全重新设置消息列表
+ return rebuildMemoryWithMessages(memoryId, currentMessages);
+ }
/**
* 批量删除多条消息
* @param memoryId 会话ID
diff --git a/src/main/java/com/xly/entity/AiResponseAccumulator.java b/src/main/java/com/xly/entity/AiResponseAccumulator.java
new file mode 100644
index 0000000..4dde7dd
--- /dev/null
+++ b/src/main/java/com/xly/entity/AiResponseAccumulator.java
@@ -0,0 +1,89 @@
+package com.xly.entity;
+
+import com.xly.entity.AiResponseDTO;
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * AI响应累积器
+ */
+@Slf4j
+public class AiResponseAccumulator {
+
+ private final String requestId;
+ private final StringBuilder aiTextBuilder = new StringBuilder();
+ private final StringBuilder systemTextBuilder = new StringBuilder();
+ private String sSceneName;
+ private String sMethodName;
+ private String sReturnType;
+ private int totalChunks = 0;
+ private int processedChunks = 0;
+ private final long startTime;
+
+ public AiResponseAccumulator(String requestId) {
+ this.requestId = requestId;
+ this.startTime = System.currentTimeMillis();
+ }
+
+ /**
+ * 累积单个AI响应
+ */
+ public void accumulate(AiResponseDTO response) {
+ processedChunks++;
+
+ // 更新总块数
+ if (response.getTotalChunks() != null && response.getTotalChunks() > 0) {
+ this.totalChunks = response.getTotalChunks();
+ }
+
+ // 累积AI文本片段
+ if (StrUtil.isNotBlank(response.getTextFragment())) {
+ aiTextBuilder.append(response.getTextFragment());
+ }
+
+ // 累积系统文本片段
+ if (StrUtil.isNotBlank(response.getSystemTextFragment())) {
+ systemTextBuilder.append(response.getSystemTextFragment());
+ }
+
+ // 更新元数据(取最后一次非空值)
+ if (StrUtil.isNotBlank(response.getSSceneName())) {
+ this.sSceneName = response.getSSceneName();
+ }
+ if (StrUtil.isNotBlank(response.getSMethodName())) {
+ this.sMethodName = response.getSMethodName();
+ }
+ if (StrUtil.isNotBlank(response.getSReturnType())) {
+ this.sReturnType = response.getSReturnType();
+ }
+
+ log.debug("累积进度: requestId={}, 已处理={}/{}块, AI文本长度={}",
+ requestId, processedChunks, totalChunks, aiTextBuilder.length());
+ }
+
+ /**
+ * 获取完整的响应
+ */
+ public AiResponseDTO getCompleteResponse() {
+ AiResponseDTO response = new AiResponseDTO();
+ response.setRequestId(requestId);
+ response.setAiText(aiTextBuilder.toString());
+ response.setSystemText(systemTextBuilder.toString());
+ response.setFullAiText(aiTextBuilder.toString());
+ response.setFullSystemText(systemTextBuilder.toString());
+ response.setSSceneName(sSceneName);
+ response.setSMethodName(sMethodName);
+ response.setSReturnType(sReturnType != null ? sReturnType : "MARKDOWN");
+ response.setTotalChunks(totalChunks);
+ response.setChunkIndex(processedChunks - 1);
+ response.setIsLastChunk(true);
+ response.setElapsedTime(System.currentTimeMillis() - startTime);
+
+ log.info("ERP累积完成: requestId={}, 总块数={}, AI文本长度={}, 系统文本长度={}, 耗时={}ms",
+ requestId, totalChunks,
+ aiTextBuilder.length(), systemTextBuilder.length(),
+ response.getElapsedTime());
+
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/xly/entity/AiResponseDTO.java b/src/main/java/com/xly/entity/AiResponseDTO.java
index edf20d2..bc5fa66 100644
--- a/src/main/java/com/xly/entity/AiResponseDTO.java
+++ b/src/main/java/com/xly/entity/AiResponseDTO.java
@@ -9,9 +9,12 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
/**
* TTS响应数据传输对象
+ * 增强版:支持流式处理
*/
@Data
@Builder
@@ -20,14 +23,230 @@ import java.io.Serializable;
public class AiResponseDTO implements Serializable {
private static final long serialVersionUID = 1L;
- // AI文字部分
+
+ // ============ 原有字段 ============
+
+ /**
+ * AI文字部分
+ */
private String aiText;
- //系统拼接返回的文字部分
+
+ /**
+ * 系统拼接返回的文字部分
+ */
private String systemText;
- //业务场景名称
+
+ /**
+ * 业务场景名称
+ */
private String sSceneName;
- //业务方法名称
+
+ /**
+ * 业务方法名称
+ */
private String sMethodName;
+
+ /**
+ * 返回类型,默认MARKDOWN
+ */
private String sReturnType = ReturnTypeCode.MAKEDOWN.getCode();
+ // ============ 新增字段:流式处理支持 ============
+
+ /**
+ * 请求ID,用于追踪整个流式请求
+ */
+ private String requestId;
+
+ /**
+ * 响应码
+ */
+ private Integer code;
+
+ /**
+ * 响应消息
+ */
+ private String message;
+
+ /**
+ * 处理状态:PROCESSING, COMPLETED, FAILED
+ */
+ private String status;
+
+ /**
+ * 当前处理的块编号(从0开始)
+ */
+ private Integer chunkIndex;
+
+ /**
+ * 总块数
+ */
+ private Integer totalChunks;
+
+ /**
+ * 是否是最后一块
+ */
+ private Boolean isLastChunk;
+
+ /**
+ * 文本片段(用于流式传输)
+ * 当aiText过大时,可以分段传输
+ */
+ private String textFragment;
+
+ /**
+ * 系统文本片段(用于流式传输)
+ */
+ private String systemTextFragment;
+
+ /**
+ * 累积的完整AI文本(仅在最后一块时返回)
+ */
+ private String fullAiText;
+
+ /**
+ * 累积的完整系统文本(仅在最后一块时返回)
+ */
+ private String fullSystemText;
+
+ /**
+ * 处理进度(0-100)
+ */
+ private Integer progress;
+
+ /**
+ * 时间戳
+ */
+ private Long timestamp;
+
+ /**
+ * 处理耗时(毫秒)
+ */
+ private Long elapsedTime;
+
+ /**
+ * 扩展字段,用于存储额外的元数据
+ */
+ private Map metadata;
+
+ /**
+ * 错误信息(当status=FAILED时)
+ */
+ private String errorMessage;
+
+ /**
+ * 错误码(当status=FAILED时)
+ */
+ private String errorCode;
+
+ // ============ 便捷方法 ============
+
+ /**
+ * 判断是否处理成功
+ */
+ public boolean isSuccess() {
+ return code != null && code == 200;
+ }
+
+ /**
+ * 判断是否处理中
+ */
+ public boolean isProcessing() {
+ return "PROCESSING".equals(status);
+ }
+
+ /**
+ * 判断是否已完成
+ */
+ public boolean isCompleted() {
+ return "COMPLETED".equals(status);
+ }
+
+ /**
+ * 判断是否失败
+ */
+ public boolean isFailed() {
+ return "FAILED".equals(status);
+ }
+
+ /**
+ * 获取完整的文本(AI文本 + 系统文本)
+ */
+ public String getFullText() {
+ StringBuilder sb = new StringBuilder();
+ if (StrUtil.isNotBlank(aiText)) {
+ sb.append(aiText);
+ }
+ if (StrUtil.isNotBlank(systemText)) {
+ sb.append(systemText);
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 创建处理中的响应
+ */
+ public static AiResponseDTO processing(String requestId, String textFragment,
+ Integer chunkIndex, Integer totalChunks) {
+ return AiResponseDTO.builder()
+ .requestId(requestId)
+ .code(200)
+ .message("Processing")
+ .status("PROCESSING")
+ .textFragment(textFragment)
+ .chunkIndex(chunkIndex)
+ .totalChunks(totalChunks)
+ .timestamp(System.currentTimeMillis())
+ .progress(calculateProgress(chunkIndex, totalChunks))
+ .build();
+ }
+
+ /**
+ * 创建完成响应
+ */
+ public static AiResponseDTO completed(String requestId, String fullAiText,
+ String fullSystemText, String sSceneName,
+ String sMethodName, String sReturnType) {
+ return AiResponseDTO.builder()
+ .requestId(requestId)
+ .code(200)
+ .message("Completed")
+ .status("COMPLETED")
+ .fullAiText(fullAiText)
+ .fullSystemText(fullSystemText)
+ .aiText(fullAiText)
+ .systemText(fullSystemText)
+ .sSceneName(sSceneName)
+ .sMethodName(sMethodName)
+ .sReturnType(sReturnType)
+ .progress(100)
+ .timestamp(System.currentTimeMillis())
+ .isLastChunk(true)
+ .build();
+ }
+
+ /**
+ * 创建失败响应
+ */
+ public static AiResponseDTO failed(String requestId, String errorMessage, String errorCode) {
+ return AiResponseDTO.builder()
+ .requestId(requestId)
+ .code(500)
+ .message("Failed")
+ .status("FAILED")
+ .errorMessage(errorMessage)
+ .errorCode(errorCode)
+ .timestamp(System.currentTimeMillis())
+ .build();
+ }
+
+ /**
+ * 计算进度
+ */
+ private static Integer calculateProgress(Integer chunkIndex, Integer totalChunks) {
+ if (chunkIndex == null || totalChunks == null || totalChunks == 0) {
+ return 0;
+ }
+ return (int) ((chunkIndex + 1) * 100.0 / totalChunks);
+ }
}
\ No newline at end of file
diff --git a/src/main/java/com/xly/entity/ToolMeta.java b/src/main/java/com/xly/entity/ToolMeta.java
index c64fe5e..42e14cb 100644
--- a/src/main/java/com/xly/entity/ToolMeta.java
+++ b/src/main/java/com/xly/entity/ToolMeta.java
@@ -47,4 +47,7 @@ public class ToolMeta {
private List paramRuleListCheck;//需要校验
private List paramRuleListAll;//所有的
private LocalDateTime tMakeDate;
+ private String sVectorfiled;
+ private String sVectorjson;
+
}
diff --git a/src/main/java/com/xly/milvus/bean/CustomSearchResultsWrapper.java b/src/main/java/com/xly/milvus/bean/CustomSearchResultsWrapper.java
new file mode 100644
index 0000000..ce73bee
--- /dev/null
+++ b/src/main/java/com/xly/milvus/bean/CustomSearchResultsWrapper.java
@@ -0,0 +1,22 @@
+package com.xly.milvus.bean;
+
+import io.milvus.grpc.SearchResultData;
+import io.milvus.response.SearchResultsWrapper;
+import java.util.List;
+
+/**
+ * 自定义SearchResultsWrapper,用于访问protected方法
+ */
+public class CustomSearchResultsWrapper extends SearchResultsWrapper {
+
+ public CustomSearchResultsWrapper(SearchResultData results) {
+ super(results);
+ }
+
+ /**
+ * 公开访问getOutputFields方法
+ */
+ public List getOutputFieldsPublic() {
+ return super.getOutputFields();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/xly/milvus/bean/SimilaritySearchRequest.java b/src/main/java/com/xly/milvus/bean/SimilaritySearchRequest.java
new file mode 100644
index 0000000..54c3719
--- /dev/null
+++ b/src/main/java/com/xly/milvus/bean/SimilaritySearchRequest.java
@@ -0,0 +1,30 @@
+package com.xly.milvus.bean;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 相似度查询请求实体
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SimilaritySearchRequest {
+ private List queryVector; // 查询向量
+ private String queryText; // 查询文本(如果有文本转向量服务)
+ private Integer topK = 10; // 返回数量
+ private Double minScore; // 最小相似度得分
+ private Double maxScore; // 最大相似度得分
+ private String metricType = "IP"; // 距离类型: IP(内积), L2(欧氏距离), COSINE(余弦)
+ private List outputFields; // 输出字段
+ private Map filter; // 过滤条件
+ private String partitionName; // 分区名称
+ private Boolean withScore = true; // 是否返回得分
+ private Boolean withDistance = false; // 是否返回距离
+}
\ No newline at end of file
diff --git a/src/main/java/com/xly/milvus/bean/SimilaritySearchResult.java b/src/main/java/com/xly/milvus/bean/SimilaritySearchResult.java
new file mode 100644
index 0000000..1bd32e5
--- /dev/null
+++ b/src/main/java/com/xly/milvus/bean/SimilaritySearchResult.java
@@ -0,0 +1,63 @@
+package com.xly.milvus.bean;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+/**
+ * 相似度查询结果实体 - 增强版
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SimilaritySearchResult {
+ private Long id; // 整数ID
+ private String strId; // 字符串ID
+ private Float score; // 相似度得分
+ private Double normalizedScore; // 归一化后的得分(0-1之间)
+ private Map fields; // 字段数据
+ private String collectionName; // 集合名称
+
+ /**
+ * 获取ID(优先返回字符串ID,如果没有则返回整数ID)
+ */
+ public String getId() {
+ if (strId != null && !strId.isEmpty()) {
+ return strId;
+ }
+ return id != null ? String.valueOf(id) : null;
+ }
+
+ /**
+ * 获取字段值
+ */
+ public Object getField(String fieldName) {
+ if (fields != null) {
+ return fields.get(fieldName);
+ }
+ return null;
+ }
+
+ /**
+ * 获取字符串字段值
+ */
+ public String getStringField(String fieldName) {
+ Object value = getField(fieldName);
+ return value != null ? value.toString() : null;
+ }
+
+ /**
+ * 获取整数字段值
+ */
+ public Integer getIntField(String fieldName) {
+ Object value = getField(fieldName);
+ if (value instanceof Number) {
+ return ((Number) value).intValue();
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/xly/milvus/config/EmbeddingConfig.java b/src/main/java/com/xly/milvus/config/EmbeddingConfig.java
new file mode 100644
index 0000000..fd5c348
--- /dev/null
+++ b/src/main/java/com/xly/milvus/config/EmbeddingConfig.java
@@ -0,0 +1,20 @@
+package com.xly.milvus.config;
+
+import dev.langchain4j.model.embedding.EmbeddingModel;
+import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * LangChain4j 嵌入模型配置类
+ */
+@Configuration
+public class EmbeddingConfig {
+
+ @Bean
+ public EmbeddingModel embeddingModel() {
+ // 使用 All-MiniLM-L6-v2 嵌入模型
+ // 这是一个轻量级的模型,维度为384,适合在本地运行
+ return new AllMiniLmL6V2EmbeddingModel();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/xly/milvus/config/MilvusConfig.java b/src/main/java/com/xly/milvus/config/MilvusConfig.java
new file mode 100644
index 0000000..a38e673
--- /dev/null
+++ b/src/main/java/com/xly/milvus/config/MilvusConfig.java
@@ -0,0 +1,162 @@
+package com.xly.milvus.config;
+
+import io.milvus.v2.client.ConnectConfig;
+import io.milvus.v2.client.MilvusClientV2;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.retry.annotation.EnableRetry;
+import org.springframework.retry.support.RetryTemplate;
+
+@Slf4j
+@Configuration
+@EnableRetry
+public class MilvusConfig {
+
+ @Value("${milvus.host:localhost}")
+ private String host;
+
+ @Value("${milvus.port:19530}")
+ private Integer port;
+
+ @Value("${milvus.database:default}")
+ private String database;
+
+ @Value("${milvus.username:}")
+ private String username;
+
+ @Value("${milvus.password:}")
+ private String password;
+
+ @Value("${milvus.connect-timeout:10000}")
+ private Long connectTimeout;
+
+ @Value("${milvus.rpc-deadline:10000}")
+ private Long rpcDeadline;
+
+ @Value("${milvus.keep-alive-time:300000}")
+ private Long keepAliveTime;
+
+ @Value("${milvus.keep-alive-timeout:5000}")
+ private Long keepAliveTimeout;
+
+ @Value("${milvus.secure:false}")
+ private Boolean secure;
+
+ @Value("${milvus.validate-on-startup:true}")
+ private boolean validateOnStartup;
+
+ @Value("${milvus.fail-on-connect-error:false}")
+ private boolean failOnConnectError;
+
+ @Value("${milvus.enable-precheck:true}")
+ private boolean enablePrecheck;
+
+ @Bean(destroyMethod = "close")
+ public MilvusClientV2 milvusClient() {
+ try {
+ String uri = String.format("http://%s:%d", host, port);
+ // 构建连接配置
+ ConnectConfig.ConnectConfigBuilder configBuilder = ConnectConfig.builder()
+ .uri(uri)
+ .dbName("default")
+ .connectTimeoutMs(connectTimeout)
+ .rpcDeadlineMs(rpcDeadline)
+ .keepAliveTimeMs(keepAliveTime)
+ .keepAliveTimeoutMs(keepAliveTimeout)
+ .keepAliveWithoutCalls(true)
+ .secure(secure)
+ .enablePrecheck(enablePrecheck);
+
+ // 添加认证信息(如果有)
+ if (username != null && !username.isEmpty()) {
+ configBuilder.username(username);
+ if (password != null) {
+ configBuilder.password(password);
+ }
+ }
+
+ ConnectConfig connectConfig = configBuilder.build();
+
+ // 创建客户端
+ MilvusClientV2 client = new MilvusClientV2(connectConfig);
+ // 检查xlyerp数据库是否存在,如果不存在则创建
+ var databases = client.listDatabases();
+ if (!databases.getDatabaseNames().contains(database)) {
+ log.info("数据库 xlyerp 不存在,正在创建...");
+ client.createDatabase(
+ io.milvus.v2.service.database.request.CreateDatabaseReq.builder()
+ .databaseName(database)
+ .build()
+ );
+ log.info("数据库 xlyerp 创建成功");
+ }
+
+ // 切换到xlyerp数据库
+ client.useDatabase (database);
+ log.info("切换到数据库: {}",database);
+
+ // 启动验证
+ if (validateOnStartup) {
+ validateConnection(client);
+ }
+ return client;
+ } catch (Exception e) {
+ log.error("创建Milvus客户端时发生异常: {}", e.getMessage(), e);
+ throw new RuntimeException("无法创建Milvus客户端", e);
+ }
+
+ }
+
+ /**
+ * 验证Milvus连接 - 基于源码中可用的方法
+ */
+ private void validateConnection(MilvusClientV2 client) {
+ try {
+ // 方法1: 检查服务器版本(源码中存在getServerVersion())
+ String serverVersion = client.getServerVersion();
+
+ // 方法2: 列出数据库(源码中存在listDatabases())
+ var databases = client.listDatabases();
+
+ // 方法3: 检查健康状态(源码中存在checkHealth())
+ var health = client.checkHealth();
+
+ System.out.println("✅ Milvus连接成功");
+ System.out.println(" - 服务器版本: " + serverVersion);
+ System.out.println(" - 数据库: " + databases);
+ System.out.println(" - 健康状态: " + health);
+
+ // 验证指定数据库是否存在
+ if (database != null && !database.isEmpty()) {
+ boolean dbExists = databases.getDatabaseNames().contains(database);
+ if (!dbExists && !"default".equals(database)) {
+ System.err.println("⚠️ 警告: 指定数据库 '" + database + "' 不存在");
+ }
+ }
+
+ } catch (Exception e) {
+ String errorMsg = String.format("❌ Milvus连接失败: %s:%d - %s",host, port, e.getMessage());
+
+ if (failOnConnectError) {
+ throw new IllegalStateException(errorMsg, e);
+ } else {
+ System.err.println(errorMsg);
+ System.err.println(" ⚠️ 应用将继续启动,但Milvus功能可能不可用");
+ }
+ }
+ }
+
+ /**
+ * 配置重试模板(可选)
+ */
+ @Bean
+ public RetryTemplate milvusRetryTemplate() {
+ return RetryTemplate.builder()
+ .maxAttempts(3)
+ .exponentialBackoff(1000, 2, 10000)
+ .retryOn(Exception.class)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/xly/milvus/config/MilvusHealthIndicator.java b/src/main/java/com/xly/milvus/config/MilvusHealthIndicator.java
new file mode 100644
index 0000000..5f5c819
--- /dev/null
+++ b/src/main/java/com/xly/milvus/config/MilvusHealthIndicator.java
@@ -0,0 +1,68 @@
+package com.xly.milvus.config;
+
+import io.milvus.v2.client.MilvusClientV2;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.actuate.health.Health;
+import org.springframework.boot.actuate.health.HealthIndicator;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+public class MilvusHealthIndicator implements HealthIndicator {
+
+ @Autowired(required = false)
+ private MilvusClientV2 milvusClient;
+
+ @Override
+ public Health health() {
+ if (milvusClient == null) {
+ return Health.down()
+ .withDetail("error", "Milvus客户端未初始化")
+ .build();
+ }
+
+ try {
+ // 检查客户端是否就绪(源码中存在clientIsReady())
+ boolean isReady = milvusClient.clientIsReady();
+
+ if (!isReady) {
+ return Health.down()
+ .withDetail("error", "客户端未就绪")
+ .build();
+ }
+
+ long startTime = System.currentTimeMillis();
+
+ // 执行健康检查 - 使用源码中存在的方法
+ String serverVersion = milvusClient.getServerVersion();
+ var databases = milvusClient.listDatabases();
+ var healthCheck = milvusClient.checkHealth();
+
+ long responseTime = System.currentTimeMillis() - startTime;
+
+ Map details = new HashMap<>();
+ details.put("serverVersion", serverVersion);
+ details.put("database", milvusClient.currentUsedDatabase());
+ details.put("databases", databases.getDatabaseNames());
+ details.put("healthStatus", healthCheck.getIsHealthy());
+ details.put("responseTime", responseTime + "ms");
+ details.put("clientReady", isReady);
+
+ if (healthCheck.getQuotaStates() != null) {
+ details.put("quotaStates", healthCheck.getQuotaStates());
+ }
+
+ return Health.up()
+ .withDetails(details)
+ .build();
+
+ } catch (Exception e) {
+ return Health.down()
+ .withDetail("error", e.getMessage())
+ .withDetail("clientReady", milvusClient.clientIsReady())
+ .build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/xly/milvus/config/MilvusRetryConfig.java b/src/main/java/com/xly/milvus/config/MilvusRetryConfig.java
new file mode 100644
index 0000000..1e3bb54
--- /dev/null
+++ b/src/main/java/com/xly/milvus/config/MilvusRetryConfig.java
@@ -0,0 +1,57 @@
+package com.xly.milvus.config;
+
+import io.milvus.v2.client.RetryConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MilvusRetryConfig {
+
+ @Value("${milvus.retry.max-retry-times:75}")
+ private int maxRetryTimes;
+
+ @Value("${milvus.retry.initial-backoff-ms:10}")
+ private long initialBackOffMs;
+
+ @Value("${milvus.retry.max-backoff-ms:3000}")
+ private long maxBackOffMs;
+
+ @Value("${milvus.retry.backoff-multiplier:3}")
+ private int backOffMultiplier;
+
+ @Value("${milvus.retry.retry-on-rate-limit:true}")
+ private boolean retryOnRateLimit;
+
+ @Value("${milvus.retry.max-retry-timeout-ms:0}")
+ private long maxRetryTimeoutMs;
+
+ @Value("${milvus.retry.enabled:true}")
+ private boolean retryEnabled;
+
+ /**
+ * 方法名改为 createRetryConfig,避免与类名冲突
+ */
+ @Bean
+ public RetryConfig createRetryConfig() {
+ if (!retryEnabled) {
+ return RetryConfig.builder()
+ .maxRetryTimes(1)
+ .initialBackOffMs(0)
+ .maxBackOffMs(0)
+ .backOffMultiplier(1)
+ .retryOnRateLimit(false)
+ .maxRetryTimeoutMs(0)
+ .build();
+ }
+
+ return RetryConfig.builder()
+ .maxRetryTimes(maxRetryTimes)
+ .initialBackOffMs(initialBackOffMs)
+ .maxBackOffMs(maxBackOffMs)
+ .backOffMultiplier(backOffMultiplier)
+ .retryOnRateLimit(retryOnRateLimit)
+ .maxRetryTimeoutMs(maxRetryTimeoutMs)
+ .build();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/xly/milvus/config/MilvusStartupValidator.java b/src/main/java/com/xly/milvus/config/MilvusStartupValidator.java
new file mode 100644
index 0000000..586e151
--- /dev/null
+++ b/src/main/java/com/xly/milvus/config/MilvusStartupValidator.java
@@ -0,0 +1,79 @@
+package com.xly.milvus.config;
+
+import io.milvus.v2.client.MilvusClientV2;
+import io.milvus.v2.service.collection.request.HasCollectionReq;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+@Slf4j
+@Component
+@Order(1)
+public class MilvusStartupValidator implements ApplicationRunner {
+
+ @Autowired
+ private MilvusClientV2 milvusClient;
+
+ @Value("${milvus.validation.collections:}")
+ private List validationCollections;
+
+ @Value("${milvus.validation.timeout:5000}")
+ private long validationTimeout;
+
+ @Override
+ public void run(ApplicationArguments args) throws Exception {
+ log.info("开始Milvus启动验证...");
+
+ try {
+ // 1. 验证客户端就绪状态
+ boolean isReady = milvusClient.clientIsReady();
+ log.info("客户端就绪状态: {}", isReady);
+
+ // 2. 获取服务器版本
+ String serverVersion = milvusClient.getServerVersion();
+ log.info("Milvus服务器版本: {}", serverVersion);
+
+ // 3. 列出所有数据库
+ var databases = milvusClient.listDatabases();
+ log.info("可用数据库: {}", databases.getDatabaseNames());
+
+ // 4. 检查当前使用的数据库
+ String currentDb = milvusClient.currentUsedDatabase();
+ log.info("当前数据库: {}", currentDb);
+
+ // 5. 健康检查
+ var healthCheck = milvusClient.checkHealth();
+ log.info("健康状态: {}", healthCheck.getIsHealthy());
+ if (healthCheck.getQuotaStates() != null && !healthCheck.getQuotaStates().isEmpty()) {
+ log.info("配额状态: {}", healthCheck.getQuotaStates());
+ }
+
+ // 6. 验证指定的集合(可选)
+ if (validationCollections != null && !validationCollections.isEmpty()) {
+ for (String collectionName : validationCollections) {
+ boolean exists = milvusClient.hasCollection(
+ HasCollectionReq.builder()
+ .collectionName(collectionName)
+ .build()
+ );
+ log.info("集合 '{}' 存在: {}", collectionName, exists);
+ }
+ }
+
+ log.info("Milvus启动验证完成");
+
+ } catch (Exception e) {
+ log.error("Milvus启动验证失败: {}", e.getMessage(), e);
+ // 可以根据配置决定是否抛出异常
+ // throw new RuntimeException("Milvus验证失败", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/xly/milvus/config/SearchResult.java b/src/main/java/com/xly/milvus/config/SearchResult.java
new file mode 100644
index 0000000..55da993
--- /dev/null
+++ b/src/main/java/com/xly/milvus/config/SearchResult.java
@@ -0,0 +1,22 @@
+package com.xly.milvus.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+/**
+ * 搜索结果实体
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class SearchResult {
+ private Long id;
+ private Float score;
+ private Map fields;
+ private String collectionName;
+}
\ No newline at end of file
diff --git a/src/main/java/com/xly/milvus/service/AiGlobalAgentQuestionSqlEmitterService.java b/src/main/java/com/xly/milvus/service/AiGlobalAgentQuestionSqlEmitterService.java
new file mode 100644
index 0000000..c98c0d7
--- /dev/null
+++ b/src/main/java/com/xly/milvus/service/AiGlobalAgentQuestionSqlEmitterService.java
@@ -0,0 +1,20 @@
+package com.xly.milvus.service;
+
+
+import java.util.Map;
+
+public interface AiGlobalAgentQuestionSqlEmitterService {
+
+ /***
+ * @Author 钱豹
+ * @Date 15:27 2026/3/19
+ * @Param [data, sQuestion, sSqlContent, collectionName]
+ * @return void
+ * @Description 插入向量库
+ **/
+ void addAiGlobalAgentQuestionSqlEmitter(String sKey,Map data, String sQuestion, String sSqlContent, String collectionName);
+
+
+ Map queryAiGlobalAgentQuestionSqlEmitter(String searchText, String collectionName);
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/xly/milvus/service/EmbeddingService.java b/src/main/java/com/xly/milvus/service/EmbeddingService.java
new file mode 100644
index 0000000..93985f7
--- /dev/null
+++ b/src/main/java/com/xly/milvus/service/EmbeddingService.java
@@ -0,0 +1,17 @@
+package com.xly.milvus.service;
+
+import java.util.List;
+
+public interface EmbeddingService {
+
+ /**
+ * 生成单个文本的向量
+ */
+ public List generateEmbedding(String text);
+
+ /**
+ * 批量生成向量(高效版)
+ * 利用 LangChain4j 内置的并行化能力,显著提升性能
+ */
+ public List> generateEmbeddings(List texts);
+}
\ No newline at end of file
diff --git a/src/main/java/com/xly/milvus/service/MilvusService.java b/src/main/java/com/xly/milvus/service/MilvusService.java
new file mode 100644
index 0000000..b0c800c
--- /dev/null
+++ b/src/main/java/com/xly/milvus/service/MilvusService.java
@@ -0,0 +1,58 @@
+package com.xly.milvus.service;
+
+import com.xly.tts.bean.TTSResponseDTO;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 向量化服务接口
+ */
+public interface MilvusService {
+
+ /***
+ * @Author 钱豹
+ * @Date 22:17 2026/3/24
+ * @Param
+ * @return
+ * @Description 初始化数据
+ **/
+
+ TTSResponseDTO initDataToMilvus(Map reqMap);
+
+
+ /**
+ * 创建集合(如果不存在)
+ */
+ void createCollectionIfNotExists(String collectionName, String sVectorfiled, String sVectorjson, Boolean bRset);
+
+
+ /***
+ * @Author 钱豹
+ * @Date 21:39 2026/3/24
+ * @Param [collectionName, sVectorfiled, sVectorjson, data]
+ * @return long
+ * @Description 批量插入数据
+ **/
+ long addDataToCollection(String collectionName, String sVectorfiled, String sVectorjson, List