diff --git a/docker-compose.middleware.yml b/docker-compose.middleware.yml index c6f529c..6750edb 100644 --- a/docker-compose.middleware.yml +++ b/docker-compose.middleware.yml @@ -118,7 +118,32 @@ services: minio-init: condition: service_completed_successfully ports: - - "19530:19530" + - "39530:19530" - "9091:9091" volumes: - ./data/milvus:/var/lib/milvus + + elasticsearch: + image: docker.elastic.co/elasticsearch/elasticsearch:8.15.5 + container_name: easyflow-elasticsearch + restart: unless-stopped + environment: + TZ: Asia/Shanghai + discovery.type: single-node + ELASTIC_PASSWORD: elastic + xpack.security.enabled: "true" + ES_JAVA_OPTS: -Xms1g -Xmx1g + ports: + - "39200:9200" + volumes: + - ./data/elasticsearch:/usr/share/elasticsearch/data + healthcheck: + test: + [ + "CMD-SHELL", + "curl -fsS -u elastic:elastic http://127.0.0.1:9200 >/dev/null || exit 1", + ] + interval: 15s + timeout: 10s + retries: 20 + start_period: 60s diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotDocumentCollectionController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotDocumentCollectionController.java index ac50067..224041e 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotDocumentCollectionController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/BotDocumentCollectionController.java @@ -1,6 +1,7 @@ package tech.easyflow.admin.controller.ai; import org.springframework.web.bind.annotation.PostMapping; +import tech.easyflow.ai.dto.BotKnowledgeBindingRequest; import tech.easyflow.ai.entity.BotDocumentCollection; import tech.easyflow.ai.entity.DocumentCollection; import tech.easyflow.ai.permission.KnowledgeReadAccessSnapshot; @@ -63,20 +64,21 @@ public class BotDocumentCollectionController extends BaseCurdController save(@JsonBody("botId") BigInteger botId, @JsonBody("knowledgeIds") BigInteger [] knowledgeIds) { - if (knowledgeIds != null) { - for (BigInteger knowledgeId : knowledgeIds) { - if (knowledgeId == null) { + public Result save(@JsonBody("botId") BigInteger botId, + @JsonBody("knowledgeBindings") List knowledgeBindings) { + if (knowledgeBindings != null) { + for (BotKnowledgeBindingRequest binding : knowledgeBindings) { + if (binding == null || binding.getKnowledgeId() == null) { continue; } - DocumentCollection collection = documentCollectionService.getById(knowledgeId); + DocumentCollection collection = documentCollectionService.getById(binding.getKnowledgeId()); if (collection == null) { continue; } resourceAccessService.assertAccess(CategoryResourceType.KNOWLEDGE, collection, ResourceAction.READ, "无权限绑定知识库"); } } - service.saveBotAndKnowledge(botId, knowledgeIds); + service.saveBotAndKnowledge(botId, knowledgeBindings); return Result.ok(); } } diff --git a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java index e9c9a8e..a249ab4 100644 --- a/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java +++ b/easyflow-api/easyflow-api-admin/src/main/java/tech/easyflow/admin/controller/ai/DocumentCollectionController.java @@ -2,6 +2,7 @@ package tech.easyflow.admin.controller.ai; import cn.dev33.satoken.annotation.SaCheckPermission; import com.easyagents.core.document.Document; +import com.easyagents.rag.retrieval.RagRetrievalMetadataKeys; import com.mybatisflex.core.paginate.Page; import com.mybatisflex.core.query.QueryWrapper; import org.springframework.util.StringUtils; @@ -12,9 +13,12 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import tech.easyflow.ai.permission.KnowledgeVisibilityQueryHelper; import tech.easyflow.ai.documentimport.DocumentImportDtos; +import tech.easyflow.ai.dto.KnowledgeSearchResultItem; import tech.easyflow.ai.entity.BotDocumentCollection; import tech.easyflow.ai.entity.DocumentCollection; import tech.easyflow.ai.entity.Model; +import tech.easyflow.ai.rag.KnowledgeRetrievalRequest; +import tech.easyflow.ai.rag.KnowledgeRetrievalModes; import tech.easyflow.ai.service.BotDocumentCollectionService; import tech.easyflow.ai.service.DocumentChunkService; import tech.easyflow.ai.service.DocumentCollectionService; @@ -32,11 +36,15 @@ import tech.easyflow.system.service.ResourceAccessService; import javax.annotation.Resource; import java.io.Serializable; +import java.math.BigDecimal; import java.math.BigInteger; +import java.math.RoundingMode; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; /** * 控制层。 @@ -105,13 +113,11 @@ public class DocumentCollectionController extends BaseCurdController options = entity.getOptions() == null ? new HashMap<>() : new HashMap<>(entity.getOptions()); - if (entity.getSearchEngineEnable() == null){ - entity.setSearchEngineEnable(false); - } options.putIfAbsent(DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL, true); options.putIfAbsent(DocumentCollection.KEY_RERANK_ENABLE, entity.getRerankModelId() != null); entity.setOptions(options); } + normalizeInfrastructureFields(entity, isSave); return super.onSaveOrUpdateBefore(entity, isSave); } @@ -124,8 +130,16 @@ public class DocumentCollectionController extends BaseCurdController> search(@RequestParam BigInteger knowledgeId, @RequestParam String keyword) { - return Result.ok(service.search(knowledgeId, keyword)); + public Result> search(@RequestParam BigInteger knowledgeId, + @RequestParam String keyword, + @RequestParam(required = false) String retrievalMode) { + KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest(); + request.setKnowledgeId(knowledgeId); + request.setQuery(keyword); + request.setRetrievalMode(KnowledgeRetrievalModes.parse(retrievalMode)); + request.setCallerType("API"); + request.setCallerId(String.valueOf(knowledgeId)); + return Result.ok(toKnowledgeSearchResult(service.search(request))); } @@ -234,4 +248,85 @@ public class DocumentCollectionController extends BaseCurdController toKnowledgeSearchResult(List documents) { + List results = new ArrayList<>(); + if (documents == null) { + return results; + } + for (int index = 0; index < documents.size(); index++) { + Document document = documents.get(index); + if (document == null) { + continue; + } + KnowledgeSearchResultItem item = new KnowledgeSearchResultItem(); + item.setSorting(index + 1); + item.setContent(document.getContent()); + item.setScore(roundScore(document.getScore())); + item.setHitSource(readMetadataAsString(document, RagRetrievalMetadataKeys.HIT_SOURCE)); + item.setVectorScore(roundScore(readMetadataAsDouble(document, RagRetrievalMetadataKeys.VECTOR_SCORE))); + item.setKeywordScore(roundScore(readMetadataAsDouble(document, RagRetrievalMetadataKeys.KEYWORD_SCORE))); + results.add(item); + } + return results; + } + + private String readMetadataAsString(Document document, String key) { + Object value = document == null ? null : document.getMetadata(key); + if (value == null) { + return null; + } + String text = String.valueOf(value); + return StringUtils.hasText(text) ? text : null; + } + + private Double readMetadataAsDouble(Document document, String key) { + Object value = document == null ? null : document.getMetadata(key); + if (value instanceof Number) { + return ((Number) value).doubleValue(); + } + if (value instanceof String && StringUtils.hasText((String) value)) { + try { + return Double.valueOf((String) value); + } catch (NumberFormatException ignore) { + return null; + } + } + return null; + } + + private Double roundScore(Double value) { + if (value == null) { + return null; + } + return new BigDecimal(String.valueOf(value)) + .setScale(4, RoundingMode.HALF_UP) + .doubleValue(); + } } diff --git a/easyflow-modules/easyflow-module-ai/pom.xml b/easyflow-modules/easyflow-module-ai/pom.xml index a14c3b8..e0a6d7f 100644 --- a/easyflow-modules/easyflow-module-ai/pom.xml +++ b/easyflow-modules/easyflow-module-ai/pom.xml @@ -41,6 +41,10 @@ com.easyagents easy-agents-support + + com.easyagents + easy-agents-rag-retrieval + com.easyagents easy-agents-spring-boot-starter diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/AiEsConfig.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/AiEsConfig.java index 104ffcc..950b2b6 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/AiEsConfig.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/AiEsConfig.java @@ -1,35 +1,10 @@ package tech.easyflow.ai.config; import com.easyagents.engine.es.ESConfig; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component +@ConfigurationProperties(prefix = "rag.searcher.elastic") public class AiEsConfig extends ESConfig { - - @Value("${rag.searcher.elastic.host}") - @Override - public void setHost(String host) { - super.setHost(host); - } - - @Value("${rag.searcher.elastic.userName}") - @Override - public void setUserName(String userName) { - super.setUserName(userName); - } - - @Value("${rag.searcher.elastic.password}") - @Override - public void setPassword(String password) { - super.setPassword(password); - } - - @Value("${rag.searcher.elastic.indexName}") - @Override - public void setIndexName(String indexName) { - super.setIndexName(indexName); - } - } - diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/AiLuceneConfig.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/AiLuceneConfig.java index 0cbbf59..7d81773 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/AiLuceneConfig.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/AiLuceneConfig.java @@ -1,17 +1,10 @@ package tech.easyflow.ai.config; import com.easyagents.search.engine.lucene.LuceneConfig; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component +@ConfigurationProperties(prefix = "rag.searcher.lucene") public class AiLuceneConfig extends LuceneConfig { - - @Value("${rag.searcher.lucene.indexDirPath}") - @Override - public void setIndexDirPath(String indexDirPath) { - super.setIndexDirPath(indexDirPath); - } - - } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/AiMilvusConfig.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/AiMilvusConfig.java new file mode 100644 index 0000000..9921106 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/AiMilvusConfig.java @@ -0,0 +1,22 @@ +package tech.easyflow.ai.config; + +import com.easyagents.store.milvus.MilvusVectorStoreConfig; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "rag.milvus") +public class AiMilvusConfig extends MilvusVectorStoreConfig { + + public MilvusVectorStoreConfig copyForCollection(String collectionName) { + MilvusVectorStoreConfig config = new MilvusVectorStoreConfig(); + config.setUri(getUri()); + config.setToken(getToken()); + config.setDatabaseName(getDatabaseName()); + config.setUsername(getUsername()); + config.setPassword(getPassword()); + config.setAutoCreateCollection(isAutoCreateCollection()); + config.setDefaultCollectionName(collectionName); + return config; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/RagInfrastructureValidator.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/RagInfrastructureValidator.java new file mode 100644 index 0000000..d149d95 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/RagInfrastructureValidator.java @@ -0,0 +1,103 @@ +package tech.easyflow.ai.config; + +import com.easyagents.engine.es.ElasticSearcher; +import com.easyagents.search.engine.service.DocumentSearcher; +import com.easyagents.store.milvus.MilvusVectorStore; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.stereotype.Component; +import tech.easyflow.ai.rag.KeywordEngineType; +import tech.easyflow.common.util.SpringContextUtil; +import tech.easyflow.common.util.StringUtil; +import tech.easyflow.common.web.exceptions.BusinessException; + +import javax.annotation.Resource; +import java.io.File; + +@Component +public class RagInfrastructureValidator implements SmartInitializingSingleton { + + private static final int STARTUP_CHECK_RETRY_TIMES = 10; + private static final long STARTUP_CHECK_RETRY_INTERVAL_MS = 1000L; + + @Resource + private AiMilvusConfig aiMilvusConfig; + + @Resource + private AiLuceneConfig aiLuceneConfig; + + @Resource + private SearcherFactory searcherFactory; + + @Override + public void afterSingletonsInstantiated() { + validateMilvus(); + validateKeywordSearcher(); + } + + private void validateMilvus() { + Exception lastException = null; + for (int i = 0; i < STARTUP_CHECK_RETRY_TIMES; i++) { + try { + MilvusVectorStore vectorStore = new MilvusVectorStore(aiMilvusConfig.copyForCollection("__rag_boot_probe__")); + if (vectorStore.checkAvailable()) { + return; + } + } catch (Exception e) { + lastException = e; + } + sleepBeforeRetry(); + } + if (lastException != null) { + throw new BusinessException("Milvus 服务不可用,项目启动失败,请检查 rag.milvus 配置与服务状态: " + lastException.getMessage()); + } + throw new BusinessException("Milvus 服务不可用,项目启动失败,请检查 rag.milvus 配置与服务状态"); + } + + private void validateKeywordSearcher() { + KeywordEngineType engineType = KeywordEngineType.from( + SpringContextUtil.getProperty("rag.engine", "ES") + ); + if (engineType == KeywordEngineType.LUCENE) { + validateLuceneDirectory(); + return; + } + + DocumentSearcher searcher = searcherFactory.getSearcher(); + if (!(searcher instanceof ElasticSearcher) || !checkElasticAvailable((ElasticSearcher) searcher)) { + throw new BusinessException("ES 服务不可用,项目启动失败,请检查 rag.engine 与 rag.searcher.elastic 配置"); + } + } + + private boolean checkElasticAvailable(ElasticSearcher elasticSearcher) { + for (int i = 0; i < STARTUP_CHECK_RETRY_TIMES; i++) { + if (elasticSearcher.checkAvailable()) { + return true; + } + sleepBeforeRetry(); + } + return false; + } + + private void validateLuceneDirectory() { + String indexDirPath = aiLuceneConfig.getIndexDirPath(); + if (StringUtil.noText(indexDirPath)) { + throw new BusinessException("Lucene 索引目录未配置,请检查 rag.searcher.lucene.indexDirPath"); + } + File indexDir = new File(indexDirPath); + if (!indexDir.exists() && !indexDir.mkdirs()) { + throw new BusinessException("Lucene 索引目录创建失败: " + indexDirPath); + } + if (!indexDir.isDirectory() || !indexDir.canRead() || !indexDir.canWrite()) { + throw new BusinessException("Lucene 索引目录不可读写: " + indexDirPath); + } + } + + private void sleepBeforeRetry() { + try { + Thread.sleep(STARTUP_CHECK_RETRY_INTERVAL_MS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new BusinessException("中间件启动校验被中断"); + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/SearcherFactory.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/SearcherFactory.java index a254c03..c602b71 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/SearcherFactory.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/config/SearcherFactory.java @@ -3,40 +3,37 @@ package tech.easyflow.ai.config; import com.easyagents.engine.es.ElasticSearcher; import com.easyagents.search.engine.lucene.LuceneSearcher; import com.easyagents.search.engine.service.DocumentSearcher; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SearcherFactory { - @Autowired - private AiLuceneConfig luceneConfig; + private final ObjectProvider documentSearcherProvider; - @Autowired - private AiEsConfig aiEsConfig; + public SearcherFactory(ObjectProvider documentSearcherProvider) { + this.documentSearcherProvider = documentSearcherProvider; + } @Bean - public LuceneSearcher luceneSearcher() { + @ConditionalOnProperty(prefix = "rag", name = "engine", havingValue = "LUCENE") + public LuceneSearcher luceneSearcher(AiLuceneConfig luceneConfig) { return new LuceneSearcher(luceneConfig); } @Bean - public ElasticSearcher elasticSearcher() { + @ConditionalOnProperty(prefix = "rag", name = "engine", havingValue = "ES", matchIfMissing = true) + public ElasticSearcher elasticSearcher(AiEsConfig aiEsConfig) { return new ElasticSearcher(aiEsConfig); } + public DocumentSearcher getSearcher() { + return documentSearcherProvider.getIfAvailable(); + } - public DocumentSearcher getSearcher(String defaultSearcherType) { - if (defaultSearcherType == null) { - defaultSearcherType = "lucene"; - } - switch (defaultSearcherType) { - case "elasticSearch": - return new ElasticSearcher(aiEsConfig); - case "lucene": - default: - return new LuceneSearcher(luceneConfig); - } + public DocumentSearcher getSearcher(String ignored) { + return getSearcher(); } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/dto/BotKnowledgeBindingRequest.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/dto/BotKnowledgeBindingRequest.java new file mode 100644 index 0000000..17f5e38 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/dto/BotKnowledgeBindingRequest.java @@ -0,0 +1,32 @@ +package tech.easyflow.ai.dto; + +import com.easyagents.rag.retrieval.RetrievalMode; +import tech.easyflow.ai.rag.KnowledgeRetrievalModes; + +import java.math.BigInteger; + +public class BotKnowledgeBindingRequest { + + private BigInteger knowledgeId; + private String retrievalMode; + + public BigInteger getKnowledgeId() { + return knowledgeId; + } + + public void setKnowledgeId(BigInteger knowledgeId) { + this.knowledgeId = knowledgeId; + } + + public String getRetrievalMode() { + return retrievalMode; + } + + public void setRetrievalMode(String retrievalMode) { + this.retrievalMode = retrievalMode; + } + + public RetrievalMode resolveRetrievalMode() { + return KnowledgeRetrievalModes.parse(retrievalMode); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/dto/KnowledgeSearchResultItem.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/dto/KnowledgeSearchResultItem.java new file mode 100644 index 0000000..4d61697 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/dto/KnowledgeSearchResultItem.java @@ -0,0 +1,59 @@ +package tech.easyflow.ai.dto; + +public class KnowledgeSearchResultItem { + + private Integer sorting; + private String content; + private Double score; + private String hitSource; + private Double vectorScore; + private Double keywordScore; + + public Integer getSorting() { + return sorting; + } + + public void setSorting(Integer sorting) { + this.sorting = sorting; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Double getScore() { + return score; + } + + public void setScore(Double score) { + this.score = score; + } + + public String getHitSource() { + return hitSource; + } + + public void setHitSource(String hitSource) { + this.hitSource = hitSource; + } + + public Double getVectorScore() { + return vectorScore; + } + + public void setVectorScore(Double vectorScore) { + this.vectorScore = vectorScore; + } + + public Double getKeywordScore() { + return keywordScore; + } + + public void setKeywordScore(Double keywordScore) { + this.keywordScore = keywordScore; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagents/tool/DocumentCollectionTool.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagents/tool/DocumentCollectionTool.java index c499366..6651920 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagents/tool/DocumentCollectionTool.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagents/tool/DocumentCollectionTool.java @@ -3,7 +3,9 @@ package tech.easyflow.ai.easyagents.tool; import com.easyagents.core.document.Document; import com.easyagents.core.model.chat.tool.BaseTool; import com.easyagents.core.model.chat.tool.Parameter; +import com.easyagents.rag.retrieval.RetrievalMode; import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.rag.KnowledgeRetrievalRequest; import tech.easyflow.ai.service.DocumentCollectionService; import tech.easyflow.common.util.SpringContextUtil; @@ -14,12 +16,18 @@ import java.util.Map; public class DocumentCollectionTool extends BaseTool { private BigInteger knowledgeId; + private RetrievalMode retrievalMode = RetrievalMode.HYBRID; public DocumentCollectionTool() { } public DocumentCollectionTool(DocumentCollection documentCollection, boolean needEnglishName) { + this(documentCollection, needEnglishName, RetrievalMode.HYBRID); + } + + public DocumentCollectionTool(DocumentCollection documentCollection, boolean needEnglishName, RetrievalMode retrievalMode) { this.knowledgeId = documentCollection.getId(); + this.retrievalMode = retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode; if (needEnglishName) { this.name = documentCollection.getEnglishName(); } else { @@ -47,11 +55,25 @@ public class DocumentCollectionTool extends BaseTool { this.knowledgeId = knowledgeId; } + public RetrievalMode getRetrievalMode() { + return retrievalMode; + } + + public void setRetrievalMode(RetrievalMode retrievalMode) { + this.retrievalMode = retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode; + } + @Override public Object invoke(Map argsMap) { DocumentCollectionService knowledgeService = SpringContextUtil.getBean(DocumentCollectionService.class); - List documents = knowledgeService.search(this.knowledgeId, (String) argsMap.get("input")); + KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest(); + request.setKnowledgeId(this.knowledgeId); + request.setQuery((String) argsMap.get("input")); + request.setRetrievalMode(this.retrievalMode); + request.setCallerType("BOT_TOOL"); + request.setCallerId(this.knowledgeId == null ? null : this.knowledgeId.toString()); + List documents = knowledgeService.search(request); StringBuilder sb = new StringBuilder(); if (documents != null) { diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/knowledge/KnowledgeProviderImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/knowledge/KnowledgeProviderImpl.java index 78ba97d..57a243e 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/knowledge/KnowledgeProviderImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/easyagentsflow/knowledge/KnowledgeProviderImpl.java @@ -7,6 +7,8 @@ import com.easyagents.flow.core.knowledge.Knowledge; import com.easyagents.flow.core.knowledge.KnowledgeProvider; import com.easyagents.flow.core.node.KnowledgeNode; import org.springframework.stereotype.Component; +import tech.easyflow.ai.rag.KnowledgeRetrievalRequest; +import tech.easyflow.ai.rag.KnowledgeRetrievalModes; import tech.easyflow.ai.service.DocumentCollectionService; import javax.annotation.Resource; @@ -30,7 +32,17 @@ public class KnowledgeProviderImpl implements KnowledgeProvider { return new Knowledge() { @Override public List> search(String keyword, int limit, KnowledgeNode knowledgeNode, Chain chain) { - List documents = documentCollectionService.search(new BigInteger(id.toString()), keyword); + KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest(); + request.setKnowledgeId(new BigInteger(id.toString())); + request.setQuery(keyword); + request.setLimit(limit); + request.setRetrievalMode(KnowledgeRetrievalModes.parse(knowledgeNode.getRetrievalMode())); + request.setCallerType("WORKFLOW"); + request.setCallerId(knowledgeNode.getId()); + List documents = documentCollectionService.search(request); + if (limit > 0 && documents.size() > limit) { + documents = new ArrayList<>(documents.subList(0, limit)); + } List> res = new ArrayList<>(); for (Document document : documents) { res.add(JSONObject.from(document)); diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/BotDocumentCollection.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/BotDocumentCollection.java index eac0b00..404a8b6 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/BotDocumentCollection.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/BotDocumentCollection.java @@ -1,9 +1,14 @@ package tech.easyflow.ai.entity; +import com.easyagents.rag.retrieval.RetrievalMode; import tech.easyflow.ai.entity.base.BotDocumentCollectionBase; +import tech.easyflow.ai.rag.KnowledgeRetrievalModes; import com.mybatisflex.annotation.RelationOneToOne; import com.mybatisflex.annotation.Table; +import java.util.HashMap; +import java.util.Map; + /** * 实体类。 * @@ -14,6 +19,8 @@ import com.mybatisflex.annotation.Table; @Table("tb_bot_document_collection") public class BotDocumentCollection extends BotDocumentCollectionBase { + public static final String OPTION_KEY_RETRIEVAL_MODE = "retrievalMode"; + @RelationOneToOne(selfField = "documentCollectionId", targetField = "id") private DocumentCollection knowledge; @@ -24,4 +31,21 @@ public class BotDocumentCollection extends BotDocumentCollectionBase { public void setKnowledge(DocumentCollection knowledge) { this.knowledge = knowledge; } + + public RetrievalMode getRetrievalMode() { + Map options = getOptions(); + if (options == null) { + return RetrievalMode.HYBRID; + } + Object value = options.get(OPTION_KEY_RETRIEVAL_MODE); + return KnowledgeRetrievalModes.parse(value == null ? null : String.valueOf(value)); + } + + public void setRetrievalMode(RetrievalMode retrievalMode) { + Map options = getOptions() == null + ? new HashMap<>() + : new HashMap<>(getOptions()); + options.put(OPTION_KEY_RETRIEVAL_MODE, (retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode).name()); + setOptions(options); + } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/DocumentCollection.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/DocumentCollection.java index 18a1c2c..1cee4d8 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/DocumentCollection.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/entity/DocumentCollection.java @@ -2,24 +2,16 @@ package tech.easyflow.ai.entity; import com.easyagents.core.model.chat.tool.Tool; import com.easyagents.core.store.DocumentStore; -import com.easyagents.store.aliyun.AliyunVectorStore; -import com.easyagents.store.aliyun.AliyunVectorStoreConfig; -import com.easyagents.store.elasticsearch.ElasticSearchVectorStore; -import com.easyagents.store.elasticsearch.ElasticSearchVectorStoreConfig; +import com.easyagents.rag.retrieval.RetrievalMode; import com.easyagents.store.milvus.MilvusVectorStore; import com.easyagents.store.milvus.MilvusVectorStoreConfig; -import com.easyagents.store.opensearch.OpenSearchVectorStore; -import com.easyagents.store.opensearch.OpenSearchVectorStoreConfig; -import com.easyagents.store.qcloud.QCloudVectorStore; -import com.easyagents.store.qcloud.QCloudVectorStoreConfig; -import com.easyagents.store.redis.RedisVectorStore; -import com.easyagents.store.redis.RedisVectorStoreConfig; import com.mybatisflex.annotation.Table; +import tech.easyflow.ai.config.AiMilvusConfig; import tech.easyflow.ai.easyagents.tool.DocumentCollectionTool; import tech.easyflow.ai.entity.base.DocumentCollectionBase; -import tech.easyflow.common.util.PropertiesUtil; +import tech.easyflow.ai.rag.KnowledgeRetrievalModes; +import tech.easyflow.common.util.SpringContextUtil; import tech.easyflow.common.util.StringUtil; -import tech.easyflow.common.web.exceptions.BusinessException; import tech.easyflow.system.permission.resource.VisibilityResource; import java.math.BigDecimal; @@ -58,11 +50,6 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi */ public static final String KEY_SIMILARITY_THRESHOLD = "simThreshold"; - /** - * 搜索引擎类型 - */ - public static final String KEY_SEARCH_ENGINE_TYPE = "searchEngineType"; - /** * 是否允许更新向量模型 */ @@ -78,28 +65,10 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi public static final String KEY_SPLITTER_STRATEGY_PROFILES = "splitter.strategyProfiles"; public DocumentStore toDocumentStore() { - String storeType = this.getVectorStoreType(); - if (StringUtil.noText(storeType)) { - throw new BusinessException("向量数据库类型未设置"); - } - if (storeType == null) { + if (StringUtil.noText(this.getVectorStoreCollection())) { return null; } - switch (storeType.toLowerCase()) { - case "redis": - return redisStore(); - case "milvus": - return milvusStore(); - case "opensearch": - return openSearchStore(); - case "elasticsearch": - return elasticSearchStore(); - case "aliyun": - return aliyunStore(); - case "qcloud": - return qcloudStore(); - } - return null; + return milvusStore(); } public boolean isVectorStoreEnabled() { @@ -115,53 +84,31 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi } public boolean isSearchEngineEnabled() { - return this.getSearchEngineEnable() != null && this.getSearchEngineEnable(); - } - - - private DocumentStore redisStore() { - RedisVectorStoreConfig redisVectorStoreConfig = getStoreConfig(RedisVectorStoreConfig.class); - return new RedisVectorStore(redisVectorStoreConfig); + return true; } private DocumentStore milvusStore() { - MilvusVectorStoreConfig milvusVectorStoreConfig = getStoreConfig(MilvusVectorStoreConfig.class); - if (milvusVectorStoreConfig != null && StringUtil.noText(milvusVectorStoreConfig.getDefaultCollectionName())) { - milvusVectorStoreConfig.setDefaultCollectionName(this.getVectorStoreCollection()); - } + AiMilvusConfig aiMilvusConfig = SpringContextUtil.getBean(AiMilvusConfig.class); + MilvusVectorStoreConfig milvusVectorStoreConfig = aiMilvusConfig.copyForCollection(this.getVectorStoreCollection()); return new MilvusVectorStore(milvusVectorStoreConfig); } - private DocumentStore openSearchStore() { - OpenSearchVectorStoreConfig openSearchVectorStoreConfig = getStoreConfig(OpenSearchVectorStoreConfig.class); - return new OpenSearchVectorStore(openSearchVectorStoreConfig); - } - - private DocumentStore elasticSearchStore() { - ElasticSearchVectorStoreConfig elasticSearchVectorStoreConfig = getStoreConfig(ElasticSearchVectorStoreConfig.class); - return new ElasticSearchVectorStore(elasticSearchVectorStoreConfig); - } - - private DocumentStore aliyunStore() { - AliyunVectorStoreConfig aliyunVectorStoreConfig = getStoreConfig(AliyunVectorStoreConfig.class); - return new AliyunVectorStore(aliyunVectorStoreConfig); - } - - private DocumentStore qcloudStore() { - QCloudVectorStoreConfig qCloudVectorStoreConfig = getStoreConfig(QCloudVectorStoreConfig.class); - return new QCloudVectorStore(qCloudVectorStoreConfig); - } - - private T getStoreConfig(Class clazz) { - return PropertiesUtil.propertiesTextToEntity(this.getVectorStoreConfig(), clazz); - } - public Tool toFunction(boolean needEnglishName) { - return new DocumentCollectionTool(this, needEnglishName); + return toFunction(needEnglishName, RetrievalMode.HYBRID.name()); + } + + public Tool toFunction(boolean needEnglishName, String retrievalMode) { + return new DocumentCollectionTool(this, needEnglishName, KnowledgeRetrievalModes.parse(retrievalMode)); } public Object getOptionsByKey(String key) { Map options = this.getOptions(); + if (KEY_DOC_RECALL_MAX_NUM.equals(key) && (options == null || !options.containsKey(KEY_DOC_RECALL_MAX_NUM))) { + return 5; + } + if (KEY_SIMILARITY_THRESHOLD.equals(key) && (options == null || !options.containsKey(KEY_SIMILARITY_THRESHOLD))) { + return 0.6f; + } if (KEY_RERANK_ENABLE.equals(key)) { if (options == null || !options.containsKey(KEY_RERANK_ENABLE)) { return this.getRerankModelId() != null; @@ -182,21 +129,9 @@ public class DocumentCollection extends DocumentCollectionBase implements Visibi if (options == null) { return null; } - if (KEY_DOC_RECALL_MAX_NUM.equals(key) && !options.containsKey(KEY_DOC_RECALL_MAX_NUM)) { - return 5; - } if (KEY_SIMILARITY_THRESHOLD.equals(key)) { - if (!options.containsKey(KEY_SIMILARITY_THRESHOLD)) { - return 0.6f; - } else { - BigDecimal score = (BigDecimal) options.get(key); - return (float) score.doubleValue(); - } - } - if (KEY_SEARCH_ENGINE_TYPE.equals(key)) { - if (!options.containsKey(KEY_SEARCH_ENGINE_TYPE)) { - return "lucene"; - } + BigDecimal score = (BigDecimal) options.get(key); + return (float) score.doubleValue(); } return options.get(key); } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/rag/KeywordEngineType.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/rag/KeywordEngineType.java new file mode 100644 index 0000000..63a2151 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/rag/KeywordEngineType.java @@ -0,0 +1,21 @@ +package tech.easyflow.ai.rag; + +import tech.easyflow.common.util.StringUtil; +import tech.easyflow.common.web.exceptions.BusinessException; + +public enum KeywordEngineType { + ES, + LUCENE; + + public static KeywordEngineType from(String value) { + if (StringUtil.noText(value)) { + return ES; + } + for (KeywordEngineType type : values()) { + if (type.name().equalsIgnoreCase(value.trim())) { + return type; + } + } + throw new BusinessException("不支持的关键词检索引擎: " + value); + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/rag/KnowledgeRetrievalModes.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/rag/KnowledgeRetrievalModes.java new file mode 100644 index 0000000..9311359 --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/rag/KnowledgeRetrievalModes.java @@ -0,0 +1,18 @@ +package tech.easyflow.ai.rag; + +import com.easyagents.rag.retrieval.RetrievalMode; +import tech.easyflow.common.web.exceptions.BusinessException; + +public final class KnowledgeRetrievalModes { + + private KnowledgeRetrievalModes() { + } + + public static RetrievalMode parse(String value) { + try { + return RetrievalMode.from(value); + } catch (IllegalArgumentException e) { + throw new BusinessException("不支持的检索方式: " + value); + } + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/rag/KnowledgeRetrievalRequest.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/rag/KnowledgeRetrievalRequest.java new file mode 100644 index 0000000..f0ddc9e --- /dev/null +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/rag/KnowledgeRetrievalRequest.java @@ -0,0 +1,63 @@ +package tech.easyflow.ai.rag; + +import com.easyagents.rag.retrieval.RetrievalMode; + +import java.math.BigInteger; + +public class KnowledgeRetrievalRequest { + + private BigInteger knowledgeId; + private String query; + private Integer limit; + private RetrievalMode retrievalMode = RetrievalMode.HYBRID; + private String callerType; + private String callerId; + + public BigInteger getKnowledgeId() { + return knowledgeId; + } + + public void setKnowledgeId(BigInteger knowledgeId) { + this.knowledgeId = knowledgeId; + } + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public Integer getLimit() { + return limit; + } + + public void setLimit(Integer limit) { + this.limit = limit; + } + + public RetrievalMode getRetrievalMode() { + return retrievalMode; + } + + public void setRetrievalMode(RetrievalMode retrievalMode) { + this.retrievalMode = retrievalMode == null ? RetrievalMode.HYBRID : retrievalMode; + } + + public String getCallerType() { + return callerType; + } + + public void setCallerType(String callerType) { + this.callerType = callerType; + } + + public String getCallerId() { + return callerId; + } + + public void setCallerId(String callerId) { + this.callerId = callerId; + } +} diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/BotDocumentCollectionService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/BotDocumentCollectionService.java index 9c8c10a..6dbb865 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/BotDocumentCollectionService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/BotDocumentCollectionService.java @@ -1,5 +1,6 @@ package tech.easyflow.ai.service; +import tech.easyflow.ai.dto.BotKnowledgeBindingRequest; import tech.easyflow.ai.entity.BotDocumentCollection; import com.mybatisflex.core.service.IService; @@ -16,5 +17,5 @@ public interface BotDocumentCollectionService extends IService listByBotId(BigInteger botId); - void saveBotAndKnowledge(BigInteger botId, BigInteger[] knowledgeIds); + void saveBotAndKnowledge(BigInteger botId, List knowledgeBindings); } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/DocumentCollectionService.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/DocumentCollectionService.java index b5dd444..c8748a2 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/DocumentCollectionService.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/DocumentCollectionService.java @@ -2,6 +2,7 @@ package tech.easyflow.ai.service; import com.easyagents.core.document.Document; import tech.easyflow.ai.entity.DocumentCollection; +import tech.easyflow.ai.rag.KnowledgeRetrievalRequest; import com.mybatisflex.core.service.IService; import java.math.BigInteger; @@ -17,6 +18,8 @@ public interface DocumentCollectionService extends IService List search(BigInteger id, String keyword); + List search(KnowledgeRetrievalRequest request); + DocumentCollection getDetail(String idOrAlias); DocumentCollection getByAlias(String idOrAlias); diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotDocumentCollectionServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotDocumentCollectionServiceImpl.java index 18cad7f..59205b1 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotDocumentCollectionServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotDocumentCollectionServiceImpl.java @@ -1,5 +1,7 @@ package tech.easyflow.ai.service.impl; +import com.easyagents.rag.retrieval.RetrievalMode; +import tech.easyflow.ai.dto.BotKnowledgeBindingRequest; import tech.easyflow.ai.entity.BotDocumentCollection; import tech.easyflow.ai.mapper.BotDocumentCollectionMapper; import tech.easyflow.ai.service.BotDocumentCollectionService; @@ -11,9 +13,10 @@ import tech.easyflow.common.cache.RedisLockExecutor; import javax.annotation.Resource; import java.math.BigInteger; import java.util.ArrayList; -import java.util.LinkedHashSet; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; -import java.util.Set; +import java.util.Map; import java.time.Duration; import com.mybatisflex.core.query.QueryWrapper; @@ -44,26 +47,30 @@ public class BotDocumentCollectionServiceImpl extends ServiceImpl knowledgeBindings) { redisLockExecutor.executeWithLock(BOT_BINDING_LOCK_KEY_PREFIX + botId, LOCK_WAIT_TIMEOUT, LOCK_LEASE_TIMEOUT, () -> { this.remove(QueryWrapper.create().eq(BotDocumentCollection::getBotId, botId)); - Set uniqueKnowledgeIds = new LinkedHashSet<>(); - if (knowledgeIds != null) { - for (BigInteger knowledgeId : knowledgeIds) { - if (knowledgeId != null) { - uniqueKnowledgeIds.add(knowledgeId); + Map bindingMap = new LinkedHashMap<>(); + if (knowledgeBindings != null) { + for (BotKnowledgeBindingRequest binding : knowledgeBindings) { + if (binding == null || binding.getKnowledgeId() == null) { + continue; } + bindingMap.put(binding.getKnowledgeId(), binding.resolveRetrievalMode()); } } - if (uniqueKnowledgeIds.isEmpty()) { + if (bindingMap.isEmpty()) { return; } - List list = new ArrayList<>(uniqueKnowledgeIds.size()); - for (BigInteger knowledgeId : uniqueKnowledgeIds) { + List list = new ArrayList<>(bindingMap.size()); + for (Map.Entry entry : bindingMap.entrySet()) { BotDocumentCollection botDocumentCollection = new BotDocumentCollection(); botDocumentCollection.setBotId(botId); - botDocumentCollection.setDocumentCollectionId(knowledgeId); + botDocumentCollection.setDocumentCollectionId(entry.getKey()); + Map options = new HashMap<>(); + options.put(BotDocumentCollection.OPTION_KEY_RETRIEVAL_MODE, entry.getValue().name()); + botDocumentCollection.setOptions(options); list.add(botDocumentCollection); } this.saveBatch(list); diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotServiceImpl.java index 9b9efaf..cc3bde5 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/BotServiceImpl.java @@ -401,7 +401,8 @@ public class BotServiceImpl extends ServiceImpl implements BotSe .selectListWithRelationsByQuery(queryWrapper); if (botDocumentCollections != null && !botDocumentCollections.isEmpty()) { for (BotDocumentCollection botDocumentCollection : botDocumentCollections) { - Tool function = botDocumentCollection.getKnowledge().toFunction(needEnglishName); + Tool function = botDocumentCollection.getKnowledge() + .toFunction(needEnglishName, botDocumentCollection.getRetrievalMode().name()); functionList.add(function); } } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/DocumentChunkServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/DocumentChunkServiceImpl.java index 46221db..263aa3f 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/DocumentChunkServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/DocumentChunkServiceImpl.java @@ -12,8 +12,6 @@ import org.springframework.stereotype.Service; import java.math.BigInteger; -import static tech.easyflow.ai.entity.DocumentCollection.KEY_SEARCH_ENGINE_TYPE; - /** * 服务层实现。 * @@ -28,10 +26,9 @@ public class DocumentChunkServiceImpl extends ServiceImpl search(BigInteger id, String keyword) { - DocumentCollection documentCollection = getById(id); + KnowledgeRetrievalRequest request = new KnowledgeRetrievalRequest(); + request.setKnowledgeId(id); + request.setQuery(keyword); + request.setRetrievalMode(RetrievalMode.HYBRID); + return search(request); + } + + @Override + public List search(KnowledgeRetrievalRequest request) { + if (request == null || request.getKnowledgeId() == null) { + throw new BusinessException("知识库ID不能为空"); + } + String keyword = request.getQuery(); + if (StringUtil.noText(keyword)) { + return Collections.emptyList(); + } + + RetrievalMode retrievalMode = request.getRetrievalMode() == null + ? RetrievalMode.HYBRID + : request.getRetrievalMode(); + DocumentCollection documentCollection = getById(request.getKnowledgeId()); if (documentCollection == null) { throw new BusinessException("知识库不存在"); } + int docRecallMaxNum = readIntegerOption(documentCollection, KEY_DOC_RECALL_MAX_NUM, 5); + float minSimilarity = readFloatOption(documentCollection, KEY_SIMILARITY_THRESHOLD, 0.6F); + + RagQuery ragQuery = new RagQuery(); + ragQuery.setQuery(keyword); + ragQuery.setRetrievalMode(retrievalMode); + ragQuery.setTopK(docRecallMaxNum); + ragQuery.setMinScore((double) minSimilarity); + + RagRetrievalExecutor retrievalExecutor = new RagRetrievalExecutor( + buildVectorRetriever(documentCollection, docRecallMaxNum, retrievalMode == RetrievalMode.VECTOR ? minSimilarity : null), + buildKeywordRetriever(documentCollection, docRecallMaxNum), + new RrfFusionStrategy() + ); + + RagRetrievalResult retrievalResult = retrievalExecutor.retrieve(ragQuery); + List searchDocuments = toDocuments(retrievalResult.getHits()); + fillSearchContent(documentCollection, searchDocuments); + if (searchDocuments.isEmpty()) { + return Collections.emptyList(); + } + + RerankModel rerankModel = resolveRerankModel(documentCollection); + boolean reranked = false; + if (rerankModel != null) { + try { + RagRetrievalResult rerankResult = retrievalExecutor.rerank(keyword, toRagHits(searchDocuments), rerankModel, docRecallMaxNum); + searchDocuments = toDocuments(rerankResult.getHits()); + reranked = true; + } catch (RerankException e) { + LOG.warn("Rerank failed for collectionId={}, modelId={}, fallback to retrieved results. message={}", + documentCollection.getId(), documentCollection.getRerankModelId(), e.getMessage()); + } + } + + RagScoreNormalizer.normalize(searchDocuments, retrievalMode, reranked); + return formatDocuments(searchDocuments, shouldApplyMinSimilarityFilter(retrievalMode, reranked), minSimilarity, docRecallMaxNum); + } + + private VectorRetriever buildVectorRetriever(DocumentCollection documentCollection, + int docRecallMaxNum, + Float minSimilarity) { + return new VectorRetriever() { + @Override + public List retrieve(RagQuery query) { + return adaptDocuments(searchVectorDocuments(documentCollection, query.getQuery(), docRecallMaxNum, minSimilarity), HitSource.VECTOR); + } + }; + } + + private KeywordRetriever buildKeywordRetriever(DocumentCollection documentCollection, int docRecallMaxNum) { + return new KeywordRetriever() { + @Override + public List retrieve(RagQuery query) { + return adaptDocuments(searchKeywordDocuments(documentCollection, query.getQuery(), docRecallMaxNum), HitSource.KEYWORD); + } + }; + } + + private List searchVectorDocuments(DocumentCollection documentCollection, + String keyword, + int docRecallMaxNum, + Float minSimilarity) { DocumentStore documentStore = documentCollection.toDocumentStore(); if (documentStore == null) { throw new BusinessException("知识库没有配置向量库"); @@ -88,88 +183,92 @@ public class DocumentCollectionServiceImpl extends ServiceImpl documents = documentStore.search(wrapper, options); + return documents == null ? Collections.emptyList() : documents; + } - // 并行查询:向量库 + 搜索引擎 - CompletableFuture> vectorFuture = CompletableFuture.supplyAsync(() -> - documentStore.search(wrapper, options) - ); - - CompletableFuture> searcherFuture = CompletableFuture.supplyAsync(() -> { - DocumentSearcher searcher = searcherFactory.getSearcher((String) documentCollection.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE)); - if (searcher == null || !documentCollection.isSearchEngineEnabled()) { - return Collections.emptyList(); - } - List documents = searcher.searchDocuments(keyword); - return documents == null ? Collections.emptyList() : documents; - }); - - // 合并两个查询结果 - CompletableFuture> combinedFuture = vectorFuture.thenCombine( - searcherFuture, - (vectorDocs, searcherDocs) -> { - Map uniqueDocs = new HashMap<>(); - vectorDocs.forEach(doc -> uniqueDocs.putIfAbsent(doc.getId().toString(), doc)); - searcherDocs.forEach(doc -> uniqueDocs.putIfAbsent(doc.getId().toString(), doc)); - return uniqueDocs; - } - ); - - try { - Map uniqueDocs = combinedFuture.get(); // 阻塞等待所有查询完成 - List searchDocuments = new ArrayList<>(uniqueDocs.values()); - searchDocuments.sort((doc1, doc2) -> Double.compare(doc2.getScore(), doc1.getScore())); - fillSearchContent(documentCollection, searchDocuments); - if (searchDocuments.isEmpty()) { - return Collections.emptyList(); - } - boolean rerankEnable = Boolean.TRUE.equals(documentCollection.getOptionsByKey(KEY_RERANK_ENABLE)); - if (!rerankEnable || documentCollection.getRerankModelId() == null) { - return formatDocuments(searchDocuments, minSimilarity, docRecallMaxNum); - } - - Model modelRerank = llmService.getModelInstance(documentCollection.getRerankModelId()); - if (modelRerank == null) { - return formatDocuments(searchDocuments, minSimilarity, docRecallMaxNum); - } - - RerankModel rerankModel = modelRerank.toRerankModel(); - if (rerankModel == null) { - return formatDocuments(searchDocuments, minSimilarity, docRecallMaxNum); - } - - Map originalScores = new HashMap<>(); - searchDocuments.forEach(item -> originalScores.put(item.getId(), item.getScore())); - searchDocuments.forEach(item -> item.setScore(null)); - try { - List rerankDocuments = rerankModel.rerank(keyword, searchDocuments); - return formatDocuments(rerankDocuments, minSimilarity, docRecallMaxNum); - } catch (RerankException e) { - searchDocuments.forEach(item -> item.setScore(originalScores.get(item.getId()))); - LOG.warn("Rerank failed for collectionId={}, modelId={}, fallback to vector results. message={}", - documentCollection.getId(), documentCollection.getRerankModelId(), e.getMessage()); - return formatDocuments(searchDocuments, minSimilarity, docRecallMaxNum); - } - } catch (InterruptedException | ExecutionException e) { - Thread.currentThread().interrupt(); - e.printStackTrace(); - throw new RuntimeException(e); + private List searchKeywordDocuments(DocumentCollection documentCollection, String keyword, int docRecallMaxNum) { + DocumentSearcher searcher = searcherFactory.getSearcher(); + if (searcher == null) { + return Collections.emptyList(); } + KeywordSearchRequest request = KeywordSearchRequest.of(keyword, docRecallMaxNum); + request.setKnowledgeId(documentCollection == null || documentCollection.getId() == null + ? null + : documentCollection.getId().toString()); + List documents = searcher.searchDocuments(request); + return documents == null ? Collections.emptyList() : documents; + } + + private List adaptDocuments(List documents, HitSource hitSource) { + List hits = new ArrayList<>(); + if (documents == null) { + return hits; + } + for (Document document : documents) { + RagHit hit = RagHit.fromDocument(document, hitSource); + if (hit != null) { + hits.add(hit); + } + } + return hits; + } + + private List toDocuments(List hits) { + List documents = new ArrayList<>(); + if (hits == null) { + return documents; + } + for (RagHit hit : hits) { + if (hit == null) { + continue; + } + documents.add(hit.toDocument()); + } + return documents; + } + + private List toRagHits(List documents) { + List hits = new ArrayList<>(); + if (documents == null) { + return hits; + } + for (Document document : documents) { + RagHit hit = RagHit.fromDocument(document); + if (hit != null) { + hits.add(hit); + } + } + return hits; + } + + private RerankModel resolveRerankModel(DocumentCollection documentCollection) { + boolean rerankEnable = Boolean.TRUE.equals(documentCollection.getOptionsByKey(KEY_RERANK_ENABLE)); + if (!rerankEnable || documentCollection.getRerankModelId() == null) { + return null; + } + Model modelRerank = llmService.getModelInstance(documentCollection.getRerankModelId()); + if (modelRerank == null) { + return null; + } + return modelRerank.toRerankModel(); + } + + private boolean shouldApplyMinSimilarityFilter(RetrievalMode retrievalMode, boolean reranked) { + return !reranked && retrievalMode == RetrievalMode.VECTOR; } @Override public DocumentCollection getDetail(String idOrAlias) { - DocumentCollection knowledge = null; if (idOrAlias.matches(RegexUtils.ALL_NUMBER)) { @@ -188,15 +287,11 @@ public class DocumentCollectionServiceImpl extends ServiceImpl formatDocuments(List documents, float minSimilarity, int maxResults) { + public List formatDocuments(List documents, + boolean applyMinSimilarity, + float minSimilarity, + int maxResults) { return documents.stream() - // 过滤掉分数为空 或 分数低于最小值的文档 - .filter(document -> { - Double score = document.getScore(); - return score != null && score >= minSimilarity; - }) - // 格式化保留四位小数 - .map(document -> { - Double score = document.getScore(); - BigDecimal bd = new BigDecimal(score.toString()); - bd = bd.setScale(4, RoundingMode.HALF_UP); - Double roundedScore = bd.doubleValue(); - document.setScore(roundedScore); - return document; - }) - // 按score降序排序(分数最高的排前面) - .sorted(Comparator.comparing(Document::getScore, Comparator.reverseOrder())) - // 限制只保留前maxResults条 - .limit(maxResults) - .collect(Collectors.toList()); + .filter(Objects::nonNull) + .filter(document -> !applyMinSimilarity + || (document.getScore() != null && document.getScore() >= minSimilarity)) + .map(this::roundDocumentScore) + .sorted(Comparator.comparing(Document::getScore, Comparator.nullsLast(Comparator.reverseOrder()))) + .limit(maxResults) + .collect(Collectors.toList()); + } + + private Document roundDocumentScore(Document document) { + if (document == null || document.getScore() == null) { + return document; + } + document.setScore(roundDouble(document.getScore())); + return document; } private void fillSearchContent(DocumentCollection documentCollection, List searchDocuments) { @@ -249,33 +335,42 @@ public class DocumentCollectionServiceImpl extends ServiceImpl ids = searchDocuments.stream() - .map(item -> (Serializable) item.getId()) - .collect(Collectors.toList()); + .map(item -> (Serializable) item.getId()) + .collect(Collectors.toList()); if (documentCollection.isFaqCollection()) { - Map faqItemMap = faqItemMapper.selectListByIds(ids).stream() - .collect(Collectors.toMap(item -> item.getId().toString(), item -> item, (a, b) -> a)); + QueryWrapper queryWrapper = QueryWrapper.create(); + queryWrapper.in(FaqItem::getId, ids); + queryWrapper.eq(FaqItem::getCollectionId, documentCollection.getId()); + Map faqItemMap = faqItemMapper.selectListByQuery(queryWrapper).stream() + .collect(Collectors.toMap(item -> item.getId().toString(), item -> item, (a, b) -> a)); + searchDocuments.removeIf(item -> !faqItemMap.containsKey(String.valueOf(item.getId()))); searchDocuments.forEach(item -> { FaqItem faqItem = faqItemMap.get(String.valueOf(item.getId())); - if (faqItem != null) { - List> faqImages = readFaqImages(faqItem); - item.setContent(buildFaqPromptContent(faqItem, faqImages)); - - Map metadataMap = item.getMetadataMap() == null - ? new HashMap<>() - : new HashMap<>(item.getMetadataMap()); - List imageUrls = faqImages.stream() - .map(image -> image.get("url")) - .filter(Objects::nonNull) - .collect(Collectors.toList()); - metadataMap.put("imageUrls", imageUrls); - item.setMetadataMap(metadataMap); + if (faqItem == null) { + return; } + List> faqImages = readFaqImages(faqItem); + item.setContent(buildFaqPromptContent(faqItem, faqImages)); + + Map metadataMap = item.getMetadataMap() == null + ? new HashMap() + : new HashMap(item.getMetadataMap()); + List imageUrls = faqImages.stream() + .map(image -> image.get("url")) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + metadataMap.put("imageUrls", imageUrls); + item.setMetadataMap(metadataMap); }); return; } - Map chunkMap = documentChunkMapper.selectListByIds(ids).stream() - .collect(Collectors.toMap(item -> item.getId().toString(), item -> item, (a, b) -> a)); + QueryWrapper queryWrapper = QueryWrapper.create(); + queryWrapper.in(DocumentChunk::getId, ids); + queryWrapper.eq(DocumentChunk::getDocumentCollectionId, documentCollection.getId()); + Map chunkMap = documentChunkMapper.selectListByQuery(queryWrapper).stream() + .collect(Collectors.toMap(item -> item.getId().toString(), item -> item, (a, b) -> a)); + searchDocuments.removeIf(item -> !chunkMap.containsKey(String.valueOf(item.getId()))); searchDocuments.forEach(item -> { DocumentChunk documentChunk = chunkMap.get(String.valueOf(item.getId())); if (documentChunk != null && !StringUtil.noText(documentChunk.getContent())) { @@ -354,4 +449,43 @@ public class DocumentCollectionServiceImpl extends ServiceImpl i List chunkIds = documentChunkMapper.selectListByQueryAs(queryWrapper, BigInteger.class); documentStore.delete(chunkIds, options); // 删除搜索引擎中的数据 - if (searcherFactory.getSearcher((String) knowledge.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE)) != null) { - DocumentSearcher searcher = searcherFactory.getSearcher((String) knowledge.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE)); + DocumentSearcher searcher = searcherFactory.getSearcher(); + if (searcher != null) { chunkIds.forEach(searcher::deleteDocument); } int ck = documentChunkMapper.deleteByQuery(QueryWrapper.create().eq(DocumentChunk::getDocumentId, id)); @@ -691,9 +691,7 @@ public class DocumentServiceImpl extends ServiceImpl i options.setIndexName(options.getCollectionName()); DocumentSearcher searcher = null; - if (knowledge.isSearchEngineEnabled()) { - searcher = searcherFactory.getSearcher((String) knowledge.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE)); - } + searcher = searcherFactory.getSearcher(); return new StoreExecutionContext(knowledge, model, embeddingModel, documentStore, options, searcher); } @@ -703,6 +701,8 @@ public class DocumentServiceImpl extends ServiceImpl i com.easyagents.core.document.Document document = new com.easyagents.core.document.Document(); document.setId(item.getId()); document.setContent(item.getContent()); + document.addMetadata(KeywordSearchMetadataKeys.KNOWLEDGE_ID, + storeContext.knowledge.getId() == null ? null : storeContext.knowledge.getId().toString()); documents.add(document); } diff --git a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/FaqItemServiceImpl.java b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/FaqItemServiceImpl.java index 6ffdd07..fe73546 100644 --- a/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/FaqItemServiceImpl.java +++ b/easyflow-modules/easyflow-module-ai/src/main/java/tech/easyflow/ai/service/impl/FaqItemServiceImpl.java @@ -15,6 +15,7 @@ import com.easyagents.core.store.DocumentStore; import com.easyagents.core.store.StoreOptions; import com.easyagents.core.store.StoreResult; import com.easyagents.search.engine.service.DocumentSearcher; +import com.easyagents.search.engine.service.KeywordSearchMetadataKeys; import com.mybatisflex.core.query.QueryWrapper; import com.mybatisflex.spring.service.impl.ServiceImpl; import org.jsoup.Jsoup; @@ -52,7 +53,6 @@ import java.util.*; import java.util.function.Supplier; import static tech.easyflow.ai.entity.DocumentCollection.KEY_CAN_UPDATE_EMBEDDING_MODEL; -import static tech.easyflow.ai.entity.DocumentCollection.KEY_SEARCH_ENGINE_TYPE; @Service public class FaqItemServiceImpl extends ServiceImpl implements FaqItemService { @@ -356,14 +356,12 @@ public class FaqItemServiceImpl extends ServiceImpl impl throw new BusinessException("FAQ向量化失败"); } - if (collection.isSearchEngineEnabled()) { - DocumentSearcher searcher = searcherFactory.getSearcher((String) collection.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE)); - if (searcher != null) { - if (isUpdate) { - searcher.deleteDocument(entity.getId()); - } - searcher.addDocument(doc); + DocumentSearcher searcher = searcherFactory.getSearcher(); + if (searcher != null) { + if (isUpdate) { + searcher.deleteDocument(entity.getId()); } + searcher.addDocument(doc); } markCollectionEmbedded(collection, preparedStore.embeddingModel); } @@ -375,14 +373,11 @@ public class FaqItemServiceImpl extends ServiceImpl impl throw new BusinessException("FAQ向量删除失败"); } - if (collection.isSearchEngineEnabled()) { - DocumentSearcher searcher = searcherFactory.getSearcher((String) collection.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE)); - if (searcher != null) { - boolean removed = searcher.deleteDocument(entity.getId()); - if (!removed) { - LOG.warn("Delete faq search index failed. faqId={}, searcherType={}", - entity.getId(), collection.getOptionsByKey(KEY_SEARCH_ENGINE_TYPE)); - } + DocumentSearcher searcher = searcherFactory.getSearcher(); + if (searcher != null) { + boolean removed = searcher.deleteDocument(entity.getId()); + if (!removed) { + LOG.warn("Delete faq search index failed. faqId={}", entity.getId()); } } } @@ -443,6 +438,7 @@ public class FaqItemServiceImpl extends ServiceImpl impl metadata.put("answerText", entity.getAnswerText()); metadata.put("categoryId", entity.getCategoryId()); metadata.put("imageUrls", readImageUrls(entity.getOptions())); + metadata.put(KeywordSearchMetadataKeys.KNOWLEDGE_ID, entity.getCollectionId() == null ? null : entity.getCollectionId().toString()); doc.setMetadataMap(metadata); return doc; } diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml b/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml index 0ae352a..17b462f 100644 --- a/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml @@ -208,12 +208,19 @@ jetcache: # 多路召回搜索引擎配置 rag: + engine: ES + milvus: + uri: http://127.0.0.1:39530 + databaseName: default + token: + username: easyflowadmin + password: easyflowadmin123 + autoCreateCollection: true searcher: - # 搜索方式 默认lucene lucene: indexDirPath: /Users/slience/data/easyflow/luceneKnowledge elastic: - host: https://127.0.0.1:9200 + host: http://127.0.0.1:39200 userName: elastic password: elastic indexName: easyflow diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json b/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json index ef438b2..46548f4 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/bot.json @@ -49,6 +49,12 @@ "publicChatCopySuccess": "Copied", "publicChatCopyFail": "Copy failed", "basicInfo": "Basic Info", + "knowledgeRetrievalMode": "Retrieval Mode", + "retrievalModes": { + "hybrid": "Hybrid Retrieval", + "vector": "Vector Retrieval", + "keyword": "Keyword Retrieval" + }, "modal": { "createDescription": "Set the assistant appearance, identity and base availability.", "editDescription": "Update the assistant presentation and base availability.", diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollection.json b/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollection.json index dab2314..dbe7040 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollection.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollection.json @@ -159,7 +159,13 @@ "documentPreview": "DocumentPreview", "total": "Total", "segments": "Segments", - "similarityScore": "SimilarityScore", + "similarityScore": "Relevance", + "searchFailed": "Search failed. Please try again later.", + "hitSources": { + "vector": "Vector Hit", + "keyword": "Keyword Hit", + "both": "Dual Hit" + }, "alibabaCloud": "AlibabaCloud", "tencentCloud": "tencentCloud", "vectorEmbedModelTips": "After successful vector data, it is not allowed to modify the vector model", diff --git a/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollectionSearch.json b/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollectionSearch.json index f07592f..152aa8d 100644 --- a/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollectionSearch.json +++ b/easyflow-ui-admin/app/src/locales/langs/en-US/documentCollectionSearch.json @@ -11,6 +11,7 @@ "button": { "save": "Save Configuration" }, + "engineHint": "The keyword search engine is controlled by platform-level configuration and is no longer configured per knowledge base.", "message": { "saveSuccess": "Configuration saved successfully", "saveFailed": "Configuration saved failed" diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json index 89d4180..b170b12 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/bot.json @@ -49,6 +49,12 @@ "publicChatCopySuccess": "复制成功", "publicChatCopyFail": "复制失败", "basicInfo": "基础信息", + "knowledgeRetrievalMode": "检索方式", + "retrievalModes": { + "hybrid": "混合检索", + "vector": "向量检索", + "keyword": "关键词检索" + }, "modal": { "createDescription": "设置助手的外观、标识和基础发布状态。", "editDescription": "更新助手的展示信息与基础状态。", diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollection.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollection.json index 8be603f..352398b 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollection.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollection.json @@ -159,7 +159,13 @@ "documentPreview": "文档预览", "total": "共", "segments": "个分段", - "similarityScore": "相似度", + "similarityScore": "相关度", + "searchFailed": "检索失败,请稍后重试", + "hitSources": { + "vector": "语义命中", + "keyword": "关键词命中", + "both": "双路命中" + }, "alibabaCloud": "阿里云", "tencentCloud": "腾讯云", "vectorEmbedModelTips": "成功向量数据之后不允许修改向量模型", diff --git a/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollectionSearch.json b/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollectionSearch.json index bde9391..0185111 100644 --- a/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollectionSearch.json +++ b/easyflow-ui-admin/app/src/locales/langs/zh-CN/documentCollectionSearch.json @@ -11,6 +11,7 @@ "button": { "save": "保存配置" }, + "engineHint": "关键词检索引擎由平台全局配置决定,知识库侧不再单独配置。", "message": { "saveSuccess": "配置保存成功", "saveFailed": "配置保存失败" diff --git a/easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue b/easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue index 88068a2..c812f2b 100644 --- a/easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue +++ b/easyflow-ui-admin/app/src/views/ai/bots/pages/setting/config.vue @@ -18,6 +18,7 @@ import { import { useDebounceFn } from '@vueuse/core'; import { ElAlert, + ElAvatar, ElButton, ElCol, ElCollapse, @@ -26,6 +27,7 @@ import { ElInput, ElInputNumber, ElMessage, + ElMessageBox, ElOption, ElRow, ElSelect, @@ -63,6 +65,13 @@ interface ApiKeyOption { label: string; } +type RetrievalMode = 'HYBRID' | 'KEYWORD' | 'VECTOR'; + +interface BotKnowledgeBindingItem { + knowledgeId: string; + retrievalMode: RetrievalMode; +} + const props = defineProps<{ bot?: BotInfo; hasSavePermission?: boolean; @@ -70,6 +79,7 @@ const props = defineProps<{ const botStore = useBotStore(); const route = useRoute(); const botId = ref((route.params.id as string) || ''); +const fallbackAvatarUrl = `${import.meta.env.BASE_URL || '/'}favicon.svg`; const options = ref([]); const selectedId = ref(''); const llmConfig = ref({ @@ -170,11 +180,11 @@ const iframeCodeHighlighted = computed(() => { } const escaped = escapeHtml(iframeCode.value); return escaped - .replace( + .replaceAll( /(<\/?)([a-zA-Z][\w-]*)/g, '$1$2', ) - .replace( + .replaceAll( /([:@a-zA-Z_][\w:-]*)=(".*?")/g, '$1=$2', ); @@ -202,6 +212,32 @@ const mcpToolData = ref([]); const workflowData = ref([]); const knowledgeData = ref([]); const pluginToolData = ref([]); +const retrievalModeOptions = computed(() => [ + { label: $t('bot.retrievalModes.hybrid'), value: 'HYBRID' }, + { label: $t('bot.retrievalModes.vector'), value: 'VECTOR' }, + { label: $t('bot.retrievalModes.keyword'), value: 'KEYWORD' }, +]); +const normalizeRetrievalMode = (value?: string): RetrievalMode => { + if (value === 'VECTOR' || value === 'KEYWORD' || value === 'HYBRID') { + return value; + } + return 'HYBRID'; +}; +const buildKnowledgeBindingsPayload = ( + selectedIds: Array, +): BotKnowledgeBindingItem[] => { + return [...new Set((selectedIds || []).map(String).filter(Boolean))].map( + (knowledgeId) => { + const existing = knowledgeData.value.find( + (item) => String(item.id) === knowledgeId, + ); + return { + knowledgeId, + retrievalMode: normalizeRetrievalMode(existing?.retrievalMode), + }; + }, + ); +}; const getAiBotPluginToolList = async () => { api .post('/api/v1/pluginItem/tool/list', { botId: botId.value }) @@ -226,6 +262,7 @@ const getAiBotKnowledgeList = async () => { knowledgeData.value = res.data.map((item: any) => { return { recordId: item.id, + retrievalMode: normalizeRetrievalMode(item.options?.retrievalMode), ...item.knowledge, }; }); @@ -419,7 +456,7 @@ const copyText = async (value: string) => { textarea.style.opacity = '0'; textarea.style.pointerEvents = 'none'; - document.body.appendChild(textarea); + document.body.append(textarea); const selection = document.getSelection(); const previousRange = @@ -430,7 +467,7 @@ const copyText = async (value: string) => { textarea.setSelectionRange(0, textarea.value.length); const copied = document.execCommand('copy'); - document.body.removeChild(textarea); + textarea.remove(); if (selection) { selection.removeAllRanges(); if (previousRange) { @@ -543,7 +580,7 @@ const confirmUpdateAiBotKnowledge = (data: any) => { api .post('/api/v1/botKnowledge/updateBotKnowledgeIds', { botId: botId.value, - knowledgeIds: data, + knowledgeBindings: buildKnowledgeBindingsPayload(data), }) .then((res) => { if (res.errorCode === 0) { @@ -555,6 +592,31 @@ const confirmUpdateAiBotKnowledge = (data: any) => { }); }; +const updateKnowledgeBindings = (bindings: BotKnowledgeBindingItem[]) => { + return api.post('/api/v1/botKnowledge/updateBotKnowledgeIds', { + botId: botId.value, + knowledgeBindings: bindings, + }); +}; + +const handleKnowledgeRetrievalModeChange = async ( + item: any, + value: RetrievalMode | string, +) => { + item.retrievalMode = normalizeRetrievalMode(value); + const bindings = knowledgeData.value.map((knowledgeItem: any) => ({ + knowledgeId: String(knowledgeItem.id), + retrievalMode: normalizeRetrievalMode(knowledgeItem.retrievalMode), + })); + const res = await updateKnowledgeBindings(bindings); + if (res.errorCode === 0) { + ElMessage.success($t('message.updateOkMessage')); + getAiBotKnowledgeList(); + return; + } + ElMessage.error(res.message || $t('message.updateFailMessage')); +}; + const confirmUpdateAiBotWorkflow = (data: any) => { api .post('/api/v1/botWorkflow/updateBotWorkflowIds', { @@ -616,7 +678,20 @@ const deleteBotMcpTool = (item: any) => { }); }; -const deleteKnowledge = (item: any) => { +const deleteKnowledge = async (item: any) => { + try { + await ElMessageBox.confirm( + $t('message.deleteAlert'), + $t('message.noticeTitle'), + { + confirmButtonText: $t('button.confirm'), + cancelButtonText: $t('button.cancel'), + type: 'warning', + }, + ); + } catch { + return; + } api .post('/api/v1/botKnowledge/remove', { id: item.recordId, @@ -840,10 +915,10 @@ const handleBasicInfoChange = async (
{{ $t('common.avatar') }}
- +
+
+
+ +
+
+ {{ item.title }} +
+
+ {{ item.description }} +
+
+
+
+ + + + + + +
+
+