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();
+ }
+
+}