diff --git a/.image/common/ai-feature.png b/.image/common/ai-feature.png index 552ed59b42..7f8c92f8cd 100644 Binary files a/.image/common/ai-feature.png and b/.image/common/ai-feature.png differ diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 70f698032f..6ef7ee85c5 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -585,6 +585,13 @@ com.xkcoding.justauth justauth-spring-boot-starter ${justauth-starter.version} + + + + cn.hutool + hutool-core + + diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java index c6e31f0e8d..b9daa95135 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/knowledge/AiKnowledgeController.java @@ -63,6 +63,15 @@ public class AiKnowledgeController { knowledgeService.updateKnowledge(updateReqVO); return success(true); } + + @DeleteMapping("/delete") + @Operation(summary = "删除知识库") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('ai:knowledge:delete')") + public CommonResult deleteKnowledge(@RequestParam("id") Long id) { + knowledgeService.deleteKnowledge(id); + return success(true); + } @GetMapping("/simple-list") @Operation(summary = "获得知识库的精简列表") diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/AiWorkflowController.http b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/AiWorkflowController.http new file mode 100644 index 0000000000..8dc1b0c0f7 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/AiWorkflowController.http @@ -0,0 +1,12 @@ +### 测试 AI 工作流 +POST {{baseUrl}}/ai/workflow/test +Content-Type: application/json +Authorization: {{token}} +tenant-id: {{adminTenantId}} + +{ + "id": 4, + "params": { + "message": "1 + 1 = ?" + } +} \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowTestReqVO.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowTestReqVO.java index 4dc509e89d..37b455cc03 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowTestReqVO.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/workflow/vo/AiWorkflowTestReqVO.java @@ -1,7 +1,8 @@ package cn.iocoder.yudao.module.ai.controller.admin.workflow.vo; +import cn.hutool.core.util.StrUtil; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.AssertTrue; import lombok.Data; import java.util.Map; @@ -10,11 +11,18 @@ import java.util.Map; @Data public class AiWorkflowTestReqVO { - @Schema(description = "工作流模型", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") - @NotEmpty(message = "工作流模型不能为空") + @Schema(description = "工作流编号", example = "1024") + private Long id; + + @Schema(description = "工作流模型", example = "{}") private String graph; @Schema(description = "参数", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}") private Map params; + @AssertTrue(message = "工作流或模型,必须传递一个") + public boolean isGraphValid() { + return id != null || StrUtil.isNotEmpty(graph); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java index 11a76cc57b..55f04bb328 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeDocumentMapper.java @@ -36,4 +36,8 @@ public interface AiKnowledgeDocumentMapper extends BaseMapperX selectListByKnowledgeId(Long knowledgeId) { + return selectList(AiKnowledgeDocumentDO::getKnowledgeId, knowledgeId); + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java index 00bacd9665..1b9ca867f5 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/dal/mysql/knowledge/AiKnowledgeSegmentMapper.java @@ -30,9 +30,9 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX selectListByVectorIds(List vectorIdList) { + default List selectListByVectorIds(List vectorIds) { return selectList(new LambdaQueryWrapperX() - .in(AiKnowledgeSegmentDO::getVectorId, vectorIdList) + .in(AiKnowledgeSegmentDO::getVectorId, vectorIds) .orderByDesc(AiKnowledgeSegmentDO::getId)); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java index f310ba69fd..672a3ae0c9 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java @@ -101,8 +101,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { ChatModel chatModel = modalService.getChatModel(model.getId()); // 2. 知识库找回 - List knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), - conversation); + List knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), conversation); // 3. 插入 user 发送消息 AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, @@ -122,11 +121,11 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { String newContent = chatResponse.getResult().getOutput().getText(); chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent)); // 3.4 响应结果 + Map documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( + convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); List segments = BeanUtils.toBean(knowledgeSegments, - AiChatMessageRespVO.KnowledgeSegment.class, - segment -> { - AiKnowledgeDocumentDO document = knowledgeDocumentService - .getKnowledgeDocument(segment.getDocumentId()); + AiChatMessageRespVO.KnowledgeSegment.class, segment -> { + AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); segment.setDocumentName(document != null ? document.getName() : null); }); return new AiChatMessageSendRespVO() @@ -173,12 +172,13 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { // 处理知识库的返回,只有首次才有 List segments = null; if (StrUtil.isEmpty(contentBuffer)) { - segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, - segment -> TenantUtils.executeIgnore(() -> { - AiKnowledgeDocumentDO document = knowledgeDocumentService - .getKnowledgeDocument(segment.getDocumentId()); - segment.setDocumentName(document != null ? document.getName() : null); - })); + Map documentMap = TenantUtils.executeIgnore(() -> + knowledgeDocumentService.getKnowledgeDocumentMap( + convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId))); + segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> { + AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); + segment.setDocumentName(document != null ? document.getName() : null); + }); } // 响应结果 String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; @@ -221,8 +221,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { } private Prompt buildPrompt(AiChatConversationDO conversation, List messages, - List knowledgeSegments, - AiModelDO model, AiChatMessageSendReqVO sendReqVO) { + List knowledgeSegments, + AiModelDO model, AiChatMessageSendReqVO sendReqVO) { List chatMessages = new ArrayList<>(); // 1.1 System Context 角色设定 if (StrUtil.isNotBlank(conversation.getSystemMessage())) { @@ -247,16 +247,18 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { // 2.1 查询 tool 工具 Set toolNames = null; + Map toolContext = Map.of(); if (conversation.getRoleId() != null) { AiChatRoleDO chatRole = chatRoleService.getChatRole(conversation.getRoleId()); if (chatRole != null && CollUtil.isNotEmpty(chatRole.getToolIds())) { toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName); + toolContext = AiUtils.buildCommonToolContext(); } } // 2.2 构建 ChatOptions 对象 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), - conversation.getTemperature(), conversation.getMaxTokens(), toolNames); + conversation.getTemperature(), conversation.getMaxTokens(), toolNames, toolContext); return new Prompt(chatMessages, chatOptions); } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java index 8ff137b331..66155d7727 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentService.java @@ -67,13 +67,6 @@ public interface AiKnowledgeDocumentService { */ void updateKnowledgeDocumentStatus(AiKnowledgeDocumentUpdateStatusReqVO reqVO); - /** - * 更新文档检索次数(增加 +1) - * - * @param ids 文档编号列表 - */ - void updateKnowledgeDocumentRetrievalCountIncr(Collection ids); - /** * 删除文档 * @@ -81,6 +74,13 @@ public interface AiKnowledgeDocumentService { */ void deleteKnowledgeDocument(Long id); + /** + * 根据知识库编号,批量删除文档 + * + * @param knowledgeId 知识库编号 + */ + void deleteKnowledgeDocumentByKnowledgeId(Long knowledgeId); + /** * 校验文档是否存在 * @@ -105,6 +105,14 @@ public interface AiKnowledgeDocumentService { */ List getKnowledgeDocumentList(Collection ids); + /** + * 根据知识库编号获取文档列表 + * + * @param knowledgeId 知识库编号 + * @return 文档列表 + */ + List getKnowledgeDocumentListByKnowledgeId(Long knowledgeId); + /** * 获取文档 Map * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java index 2d78f94f34..7de51ca0f2 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeDocumentServiceImpl.java @@ -161,14 +161,6 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic knowledgeSegmentService.deleteKnowledgeSegmentByDocumentId(id); } - @Override - public void updateKnowledgeDocumentRetrievalCountIncr(Collection ids) { - if (CollUtil.isEmpty(ids)) { - return; - } - knowledgeDocumentMapper.updateRetrievalCountIncr(ids); - } - @Override public AiKnowledgeDocumentDO validateKnowledgeDocumentExists(Long id) { AiKnowledgeDocumentDO knowledgeDocument = knowledgeDocumentMapper.selectById(id); @@ -211,4 +203,24 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic return knowledgeDocumentMapper.selectBatchIds(ids); } + @Override + public List getKnowledgeDocumentListByKnowledgeId(Long knowledgeId) { + return knowledgeDocumentMapper.selectListByKnowledgeId(knowledgeId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteKnowledgeDocumentByKnowledgeId(Long knowledgeId) { + // 1. 获取该知识库下的所有文档 + List documents = knowledgeDocumentMapper.selectListByKnowledgeId(knowledgeId); + if (CollUtil.isEmpty(documents)) { + return; + } + + // 2. 逐个删除文档及其对应的段落 + for (AiKnowledgeDocumentDO document : documents) { + deleteKnowledgeDocument(document.getId()); + } + } + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java index 5336570d27..6c552a40f0 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeService.java @@ -29,6 +29,13 @@ public interface AiKnowledgeService { */ void updateKnowledge(AiKnowledgeSaveReqVO updateReqVO); + /** + * 删除知识库 + * + * @param id 知识库编号 + */ + void deleteKnowledge(Long id); + /** * 获得知识库 * diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java index 59afd7d7bc..75b9943a8c 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeServiceImpl.java @@ -1,19 +1,18 @@ package cn.iocoder.yudao.module.ai.service.knowledge; import cn.hutool.core.util.ObjUtil; -import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; -import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper; import cn.iocoder.yudao.module.ai.service.model.AiModelService; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.util.List; @@ -36,6 +35,8 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { private AiModelService modelService; @Resource private AiKnowledgeSegmentService knowledgeSegmentService; + @Resource + private AiKnowledgeDocumentService knowledgeDocumentService; @Override public Long createKnowledge(AiKnowledgeSaveReqVO createReqVO) { @@ -67,6 +68,20 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { } } + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteKnowledge(Long id) { + // 1. 校验存在 + validateKnowledgeExists(id); + + // 2. 删除知识库下的所有文档及段落 + knowledgeDocumentService.deleteKnowledgeDocumentByKnowledgeId(id); + + // 3. 删除知识库 + // 特殊:知识库需要最后删除,不然相关的配置会找不到 + knowledgeMapper.deleteById(id); + } + @Override public AiKnowledgeDO getKnowledge(Long id) { return knowledgeMapper.selectById(id); @@ -74,11 +89,11 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService { @Override public AiKnowledgeDO validateKnowledgeExists(Long id) { - AiKnowledgeDO knowledgeBase = knowledgeMapper.selectById(id); - if (knowledgeBase == null) { + AiKnowledgeDO knowledge = knowledgeMapper.selectById(id); + if (knowledge == null) { throw exception(KNOWLEDGE_NOT_EXISTS); } - return knowledgeBase; + return knowledge; } @Override diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java index 127f72cc46..1b5aabbc51 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelService.java @@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReqVO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; +import dev.tinyflow.core.Tinyflow; import jakarta.validation.Valid; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.image.ImageModel; @@ -131,4 +132,12 @@ public interface AiModelService { */ VectorStore getOrCreateVectorStore(Long id, Map> metadataFields); + /** + * 获取 TinyFlow 所需 LLm Provider + * + * @param tinyflow tinyflow + * @param modelId AI 模型 ID + */ + void getLLmProvider4Tinyflow(Tinyflow tinyflow, Long modelId); + } diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java index b0e9e97172..3c7c3a952d 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java @@ -12,6 +12,11 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.model.AiModelSaveReq import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.model.AiChatMapper; +import com.agentsflex.llm.ollama.OllamaLlm; +import com.agentsflex.llm.ollama.OllamaLlmConfig; +import com.agentsflex.llm.qwen.QwenLlm; +import com.agentsflex.llm.qwen.QwenLlmConfig; +import dev.tinyflow.core.Tinyflow; import jakarta.annotation.Resource; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.embedding.EmbeddingModel; @@ -168,4 +173,29 @@ public class AiModelServiceImpl implements AiModelService { // return modelFactory.getOrCreateVectorStore(MilvusVectorStore.class, embeddingModel, metadataFields); } + // TODO @lesan:是不是返回 Llm 对象会好点哈? + @Override + public void getLLmProvider4Tinyflow(Tinyflow tinyflow, Long modelId) { + AiModelDO model = validateModel(modelId); + AiApiKeyDO apiKey = apiKeyService.validateApiKey(model.getKeyId()); + AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); + switch (platform) { + // TODO @lesan 考虑到未来不需要使用agents-flex 现在仅测试通义千问 + // TODO @lesan:【重要】是不是可以实现一个 SpringAiLlm,这样的话,内部全部用它就好了。只实现 chat 部分;这样,就把 flex 作为一个 agent 框架,内部调用,还是 spring ai 相关的。成本可能低一点?! + case TONG_YI: + QwenLlmConfig qwenLlmConfig = new QwenLlmConfig(); + qwenLlmConfig.setApiKey(apiKey.getApiKey()); + qwenLlmConfig.setModel(model.getModel()); + // TODO @lesan:这个有点奇怪。。。如果一个链式里,有多个模型,咋整呀。。。 + tinyflow.setLlmProvider(id -> new QwenLlm(qwenLlmConfig)); + break; + case OLLAMA: + OllamaLlmConfig ollamaLlmConfig = new OllamaLlmConfig(); + ollamaLlmConfig.setEndpoint(apiKey.getUrl()); + ollamaLlmConfig.setModel(model.getModel()); + tinyflow.setLlmProvider(id -> new OllamaLlm(ollamaLlmConfig)); + break; + } + } + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java new file mode 100644 index 0000000000..d8db05aeb6 --- /dev/null +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.ai.service.model.tool; + +import cn.iocoder.yudao.framework.ai.core.util.AiUtils; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.security.core.LoginUser; +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; +import com.fasterxml.jackson.annotation.JsonClassDescription; +import jakarta.annotation.Resource; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.ai.chat.model.ToolContext; +import org.springframework.stereotype.Component; + +import java.util.function.BiFunction; + +/** + * 工具:当前用户信息查询 + * + * 同时,也是展示 ToolContext 上下文的使用 + * + * @author Ren + */ +@Component("user_profile_query") +public class UserProfileQueryToolFunction + implements BiFunction { + + @Resource + private AdminUserApi adminUserApi; + + @Data + @JsonClassDescription("当前用户信息查询") + public static class Request { } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class Response { + + /** + * 用户ID + */ + private Long id; + /** + * 用户昵称 + */ + private String nickname; + + /** + * 手机号码 + */ + private String mobile; + /** + * 用户头像 + */ + private String avatar; + + } + + @Override + public UserProfileQueryToolFunction.Response apply(UserProfileQueryToolFunction.Request request, ToolContext toolContext) { + LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER); + Long tenantId = (Long) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_TENANT_ID); + if (loginUser == null | tenantId == null) { + return null; + } + return TenantUtils.execute(tenantId, () -> { + AdminUserRespDTO user = adminUserApi.getUser(loginUser.getId()); + return BeanUtils.toBean(user, Response.class); + }); + } + +} diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java index 70d28496c8..ac16e8755e 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/workflow/AiWorkflowServiceImpl.java @@ -7,10 +7,9 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowSaveReqVO; import cn.iocoder.yudao.module.ai.controller.admin.workflow.vo.AiWorkflowTestReqVO; -import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiApiKeyDO; import cn.iocoder.yudao.module.ai.dal.dataobject.workflow.AiWorkflowDO; import cn.iocoder.yudao.module.ai.dal.mysql.workflow.AiWorkflowMapper; -import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; +import cn.iocoder.yudao.module.ai.service.model.AiModelService; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import dev.tinyflow.core.Tinyflow; @@ -37,11 +36,14 @@ public class AiWorkflowServiceImpl implements AiWorkflowService { private AiWorkflowMapper workflowMapper; @Resource - private AiApiKeyService apiKeyService; + private AiModelService apiModelService; @Override public Long createWorkflow(AiWorkflowSaveReqVO createReqVO) { - validateWorkflowForCreateOrUpdate(null, createReqVO.getCode()); + // 1. 参数校验 + validateCodeUnique(null, createReqVO.getCode()); + + // 2. 插入工作流配置 AiWorkflowDO workflow = BeanUtils.toBean(createReqVO, AiWorkflowDO.class); workflowMapper.insert(workflow); return workflow.getId(); @@ -49,47 +51,33 @@ public class AiWorkflowServiceImpl implements AiWorkflowService { @Override public void updateWorkflow(AiWorkflowSaveReqVO updateReqVO) { - validateWorkflowForCreateOrUpdate(updateReqVO.getId(), updateReqVO.getCode()); + // 1. 参数校验 + validateWorkflowExists(updateReqVO.getId()); + validateCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); + + // 2. 更新工作流配置 AiWorkflowDO workflow = BeanUtils.toBean(updateReqVO, AiWorkflowDO.class); workflowMapper.updateById(workflow); } @Override public void deleteWorkflow(Long id) { + // 1. 校验存在 validateWorkflowExists(id); + + // 2. 删除工作流配置 workflowMapper.deleteById(id); } - @Override - public AiWorkflowDO getWorkflow(Long id) { - return workflowMapper.selectById(id); - } - - @Override - public PageResult getWorkflowPage(AiWorkflowPageReqVO pageReqVO) { - return workflowMapper.selectPage(pageReqVO); - } - - @Override - public Object testWorkflow(AiWorkflowTestReqVO testReqVO) { - Map variables = testReqVO.getParams(); - Tinyflow tinyflow = parseFlowParam(testReqVO.getGraph()); - return tinyflow.toChain().executeForResult(variables); - } - - private void validateWorkflowForCreateOrUpdate(Long id, String code) { - validateWorkflowExists(id); - validateCodeUnique(id, code); - } - - private void validateWorkflowExists(Long id) { + private AiWorkflowDO validateWorkflowExists(Long id) { if (ObjUtil.isNull(id)) { - return; + throw exception(WORKFLOW_NOT_EXISTS); } AiWorkflowDO workflow = workflowMapper.selectById(id); if (ObjUtil.isNull(workflow)) { throw exception(WORKFLOW_NOT_EXISTS); } + return workflow; } private void validateCodeUnique(Long id, String code) { @@ -108,6 +96,30 @@ public class AiWorkflowServiceImpl implements AiWorkflowService { } } + @Override + public AiWorkflowDO getWorkflow(Long id) { + return workflowMapper.selectById(id); + } + + @Override + public PageResult getWorkflowPage(AiWorkflowPageReqVO pageReqVO) { + return workflowMapper.selectPage(pageReqVO); + } + + @Override + public Object testWorkflow(AiWorkflowTestReqVO testReqVO) { + // 加载 graph + String graph = testReqVO.getGraph() != null ? testReqVO.getGraph() + : validateWorkflowExists(testReqVO.getId()).getGraph(); + + // 构建 TinyFlow 执行链 + Tinyflow tinyflow = parseFlowParam(graph); + + // 执行 + Map variables = testReqVO.getParams(); + return tinyflow.toChain().executeForResult(variables); + } + private Tinyflow parseFlowParam(String graph) { // TODO @lesan:可以使用 jackson 哇? JSONObject json = JSONObject.parseObject(graph); @@ -118,25 +130,7 @@ public class AiWorkflowServiceImpl implements AiWorkflowService { switch (node.getString("type")) { case "llmNode": JSONObject data = node.getJSONObject("data"); - AiApiKeyDO apiKey = apiKeyService.getApiKey(data.getLong("llmId")); - switch (apiKey.getPlatform()) { - // TODO @lesan 需要讨论一下这里怎么弄 - // TODO @lesan llmId 对应 model 的编号如何?这样的话,就是 apiModelService 提供一个获取 LLM 的方法。然后,创建的方法,也在 AiModelFactory 提供。可以先接个 deepseek 先。deepseek yyds! - case "OpenAI": - break; - case "Ollama": - break; - case "YiYan": - break; - case "XingHuo": - break; - case "TongYi": - break; - case "DeepSeek": - break; - case "ZhiPu": - break; - } + apiModelService.getLLmProvider4Tinyflow(tinyflow, data.getLong("llmId")); break; case "internalNode": break; diff --git a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java index 787f8d2046..eab2cd65b1 100644 --- a/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java +++ b/yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/write/AiWriteServiceImpl.java @@ -76,7 +76,7 @@ public class AiWriteServiceImpl implements AiWriteService { ? writeRole.getSystemMessage() : AiChatRoleEnum.AI_WRITE_ROLE.getSystemMessage(); // 1.3 校验平台 AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); - StreamingChatModel chatModel = modalService.getChatModel(model.getKeyId()); + StreamingChatModel chatModel = modalService.getChatModel(model.getId()); // 2. 插入写作信息 AiWriteDO writeDO = BeanUtils.toBean(generateReqVO, AiWriteDO.class, write -> write.setUserId(userId) diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml index ba1c923f79..a3d681fd7c 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/pom.xml @@ -15,7 +15,7 @@ AI 大模型拓展,接入国内外大模型 1.0.0-M6 - 1.0.0-rc.3 + 1.0.2 @@ -24,6 +24,18 @@ yudao-common + + + cn.iocoder.boot + yudao-spring-boot-starter-biz-tenant + + + + + cn.iocoder.boot + yudao-spring-boot-starter-security + + org.springframework.ai @@ -98,6 +110,13 @@ org.springframework.ai spring-ai-milvus-store ${spring-ai.version} + + + + org.slf4j + slf4j-reload4j + + @@ -124,6 +143,10 @@ tinyflow-java-core ${tinyflow.version} + + com.jfinal + enjoy + com.agentsflex @@ -134,6 +157,19 @@ org.codehaus.groovy groovy-all + + + org.slf4j + slf4j-simple + + + org.apache.logging.log4j + log4j-slf4j-impl + + + org.slf4j + slf4j-reload4j + diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java index 09370c6363..10f15b7b32 100644 --- a/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java @@ -3,6 +3,8 @@ package cn.iocoder.yudao.framework.ai.core.util; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.chat.messages.*; @@ -15,6 +17,8 @@ import org.springframework.ai.qianfan.QianFanChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import java.util.Collections; +import java.util.HashMap; +import java.util.Map; import java.util.Set; /** @@ -24,29 +28,32 @@ import java.util.Set; */ public class AiUtils { + public static final String TOOL_CONTEXT_LOGIN_USER = "LOGIN_USER"; + public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID"; + public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) { - return buildChatOptions(platform, model, temperature, maxTokens, null); + return buildChatOptions(platform, model, temperature, maxTokens, null, null); } public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens, - Set toolNames) { + Set toolNames, Map toolContext) { toolNames = ObjUtil.defaultIfNull(toolNames, Collections.emptySet()); // noinspection EnhancedSwitchMigration switch (platform) { case TONG_YI: return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) - .withFunctions(toolNames).build(); + .withFunctions(toolNames).withToolContext(toolContext).build(); case YI_YAN: return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case ZHI_PU: return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .functions(toolNames).build(); + .functions(toolNames).toolContext(toolContext).build(); case MINI_MAX: return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .functions(toolNames).build(); + .functions(toolNames).toolContext(toolContext).build(); case MOONSHOT: return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .functions(toolNames).build(); + .functions(toolNames).toolContext(toolContext).build(); case OPENAI: case DEEP_SEEK: // 复用 OpenAI 客户端 case DOU_BAO: // 复用 OpenAI 客户端 @@ -55,14 +62,14 @@ public class AiUtils { case SILICON_FLOW: // 复用 OpenAI 客户端 case BAI_CHUAN: // 复用 OpenAI 客户端 return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .toolNames(toolNames).build(); + .toolNames(toolNames).toolContext(toolContext).build(); case AZURE_OPENAI: // TODO 芋艿:貌似没 model 字段???! return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens) - .toolNames(toolNames).build(); + .toolNames(toolNames).toolContext(toolContext).build(); case OLLAMA: return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens) - .toolNames(toolNames).build(); + .toolNames(toolNames).toolContext(toolContext).build(); default: throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); } @@ -84,4 +91,11 @@ public class AiUtils { throw new IllegalArgumentException(StrUtil.format("未知消息类型({})", type)); } + public static Map buildCommonToolContext() { + Map context = new HashMap<>(); + context.put(TOOL_CONTEXT_LOGIN_USER, SecurityFrameworkUtils.getLoginUser()); + context.put(TOOL_CONTEXT_TENANT_ID, TenantContextHolder.getTenantId()); + return context; + } + } \ No newline at end of file diff --git a/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/CozeChatModelTests.java b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/CozeChatModelTests.java new file mode 100644 index 0000000000..11f7dd60e7 --- /dev/null +++ b/yudao-module-ai/yudao-spring-boot-starter-ai/src/test/java/cn/iocoder/yudao/framework/ai/chat/CozeChatModelTests.java @@ -0,0 +1,63 @@ +package cn.iocoder.yudao.framework.ai.chat; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.openai.OpenAiChatModel; +import org.springframework.ai.openai.api.OpenAiApi; +import reactor.core.publisher.Flux; + +import java.util.ArrayList; +import java.util.List; + +/** + * 基于 {@link OpenAiChatModel} 集成 Coze 测试 + * + * @author 芋道源码 + */ +public class CozeChatModelTests { + + private final OpenAiChatModel chatModel = OpenAiChatModel.builder() + .openAiApi(OpenAiApi.builder() + .baseUrl("http://127.0.0.1:3000") + .apiKey("app-4hy2d7fJauSbrKbzTKX1afuP") // apiKey + .build()) + .build(); + + @Test + @Disabled + public void testCall() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + ChatResponse response = chatModel.call(new Prompt(messages)); + // 打印结果 + System.out.println(response); + System.out.println(response.getResult().getOutput()); + } + + @Test + @Disabled + public void testStream() { + // 准备参数 + List messages = new ArrayList<>(); + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); + messages.add(new UserMessage("1 + 1 = ?")); + + // 调用 + Flux flux = chatModel.stream(new Prompt(messages)); + // 打印结果 + flux.doOnNext(response -> { +// System.out.println(response); + System.out.println(response.getResult().getOutput()); + }).then().block(); + } + +}