toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName);
+ toolNames.forEach(toolName -> {
+ ToolCallback toolCallback = toolCallbackResolver.resolve(toolName);
+ if (toolCallback != null) {
+ toolCallbacks.add(toolCallback);
+ }
+ });
+ }
+ // 2. 通过 mcpClients
+ if (CollUtil.isNotEmpty(mcpClients) && CollUtil.isNotEmpty(chatRole.getMcpClientNames())) {
+ chatRole.getMcpClientNames().forEach(mcpClientName -> {
+ // 2.1 标准化名字,参考 McpClientAutoConfiguration 的 connectedClientName 方法
+ String finalMcpClientName = mcpClientCommonProperties.getName() + " - " + mcpClientName;
+ // 2.2 匹配对应的 McpSyncClient
+ mcpClients.forEach(mcpClient -> {
+ if (ObjUtil.notEqual(mcpClient.getClientInfo().name(), finalMcpClientName)) {
+ return;
+ }
+ ToolCallback[] mcpToolCallBacks = new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks();
+ CollUtil.addAll(toolCallbacks, mcpToolCallBacks);
+ });
+ });
+ }
+ return toolCallbacks;
+ }
+
/**
* 从历史消息中,获得倒序的 n 组消息作为消息上下文
*
@@ -302,14 +458,56 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
return contextMessages;
}
+ private UserMessage buildAttachmentUserMessage(List attachmentUrls) {
+ if (CollUtil.isEmpty(attachmentUrls)) {
+ return null;
+ }
+ // 读取文件内容
+ Map attachmentContents = Maps.newLinkedHashMapWithExpectedSize(attachmentUrls.size());
+ for (String attachmentUrl : attachmentUrls) {
+ try {
+ String name = FileNameUtil.getName(attachmentUrl);
+ String mineType = FileTypeUtils.getMineType(name);
+ String content;
+ if (FileTypeUtils.isImage(mineType)) {
+ // 特殊:图片则转为 Base64
+ byte[] bytes = HttpUtil.downloadBytes(attachmentUrl);
+ content = Base64.encode(bytes);
+ } else {
+ content = knowledgeDocumentService.readUrl(attachmentUrl);
+ }
+ if (StrUtil.isNotEmpty(content)) {
+ attachmentContents.put(name, content);
+ }
+ } catch (Exception e) {
+ log.error("[buildAttachmentUserMessage][读取附件({}) 发生异常]", attachmentUrl, e);
+ }
+ }
+ if (CollUtil.isEmpty(attachmentContents)) {
+ return null;
+ }
+
+ // 拼接 UserMessage 消息
+ String attachment = attachmentContents.entrySet().stream()
+ .map(entry -> "" + entry.getValue() + "")
+ .collect(Collectors.joining("\n\n"));
+ return new UserMessage(String.format(Attachment_USER_MESSAGE_TEMPLATE, attachment));
+ }
+
private AiChatMessageDO createChatMessage(Long conversationId, Long replyId,
- AiModelDO model, Long userId, Long roleId,
- MessageType messageType, String content, Boolean useContext,
- List knowledgeSegments) {
+ AiModelDO model, Long userId, Long roleId,
+ MessageType messageType, String content, Boolean useContext,
+ List knowledgeSegments,
+ List attachmentUrls,
+ AiWebSearchResponse webSearchResponse) {
AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId)
.setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId)
.setType(messageType.getValue()).setContent(content).setUseContext(useContext)
- .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId));
+ .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId))
+ .setAttachmentUrls(attachmentUrls);
+ if (webSearchResponse != null) {
+ message.setWebSearchPages(webSearchResponse.getLists());
+ }
message.setCreateTime(LocalDateTime.now());
chatMessageMapper.insert(message);
return message;
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java
index e3a6f08a1..dd0f91315 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java
@@ -18,6 +18,10 @@ import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO;
import cn.iocoder.yudao.module.ai.service.model.AiModelService;
+import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions;
+import com.alibaba.cloud.ai.model.RerankModel;
+import com.alibaba.cloud.ai.model.RerankRequest;
+import com.alibaba.cloud.ai.model.RerankResponse;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
@@ -27,6 +31,7 @@ import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -36,6 +41,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS;
+import static org.springframework.ai.vectorstore.SearchRequest.SIMILARITY_THRESHOLD_ACCEPT_ALL;
/**
* AI 知识库分片 Service 实现类
@@ -55,6 +61,11 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
VECTOR_STORE_METADATA_DOCUMENT_ID, String.class,
VECTOR_STORE_METADATA_SEGMENT_ID, String.class);
+ /**
+ * Rerank 在向量检索时,检索数量 * 该系数,目的是为了提升 Rerank 的效果
+ */
+ private static final Integer RERANK_RETRIEVAL_FACTOR = 4;
+
@Resource
private AiKnowledgeSegmentMapper segmentMapper;
@@ -69,6 +80,9 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
@Resource
private TokenCountEstimator tokenCountEstimator;
+ @Autowired(required = false) // 由于 spring.ai.model.rerank 配置项,可以关闭 RerankModel 的功能,所以这里只能不强制注入
+ private RerankModel rerankModel;
+
@Override
public PageResult getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) {
return segmentMapper.selectPage(pageReqVO);
@@ -211,28 +225,16 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
// 1. 校验
AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqBO.getKnowledgeId());
- // 2.1 向量检索
- VectorStore vectorStore = getVectorStoreById(knowledge);
- List documents = vectorStore.similaritySearch(SearchRequest.builder()
- .query(reqBO.getContent())
- .topK(ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK()))
- .similarityThreshold(
- ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold()))
- .filterExpression(new FilterExpressionBuilder()
- .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId().toString())
- .build())
- .build());
- if (CollUtil.isEmpty(documents)) {
- return ListUtil.empty();
- }
- // 2.2 段落召回
+ // 2. 检索
+ List documents = searchDocument(knowledge, reqBO);
+
+ // 3.1 段落召回
List segments = segmentMapper
.selectListByVectorIds(convertList(documents, Document::getId));
if (CollUtil.isEmpty(segments)) {
return ListUtil.empty();
}
-
- // 3. 增加召回次数
+ // 3.2 增加召回次数
segmentMapper.updateRetrievalCountIncrByIds(convertList(segments, AiKnowledgeSegmentDO::getId));
// 4. 构建结果
@@ -249,6 +251,42 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
return result;
}
+ /**
+ * 基于 Embedding + Rerank Model,检索知识库中的文档
+ *
+ * @param knowledge 知识库
+ * @param reqBO 检索请求
+ * @return 文档列表
+ */
+ private List searchDocument(AiKnowledgeDO knowledge, AiKnowledgeSegmentSearchReqBO reqBO) {
+ VectorStore vectorStore = getVectorStoreById(knowledge);
+ Integer topK = ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK());
+ Double similarityThreshold = ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold());
+
+ // 1. 向量检索
+ int searchTopK = rerankModel != null ? topK * RERANK_RETRIEVAL_FACTOR : topK;
+ double searchSimilarityThreshold = rerankModel != null ? SIMILARITY_THRESHOLD_ACCEPT_ALL : similarityThreshold;
+ SearchRequest.Builder searchRequestBuilder = SearchRequest.builder()
+ .query(reqBO.getContent())
+ .topK(searchTopK).similarityThreshold(searchSimilarityThreshold)
+ .filterExpression(new FilterExpressionBuilder()
+ .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId().toString()).build());
+ List documents = vectorStore.similaritySearch(searchRequestBuilder.build());
+ if (CollUtil.isEmpty(documents)) {
+ return documents;
+ }
+
+ // 2. Rerank 重排序
+ if (rerankModel != null) {
+ RerankResponse rerankResponse = rerankModel.call(new RerankRequest(reqBO.getContent(), documents,
+ DashScopeRerankOptions.builder().withTopN(topK).build()));
+ documents = convertList(rerankResponse.getResults(),
+ documentWithScore -> documentWithScore.getScore() >= similarityThreshold
+ ? documentWithScore.getOutput() : null);
+ }
+ return documents;
+ }
+
@Override
public List splitContent(String url, Integer segmentMaxTokens) {
// 1. 读取 URL 内容
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java
index ec807cf40..235e54a7f 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java
@@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.ai.service.model;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
-import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory;
+import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java
index cbf14ca36..cb25a3198 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java
@@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.ai.service.model;
-import cn.hutool.extra.spring.SpringUtil;
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.model.vo.tool.AiToolPageReqVO;
@@ -8,7 +7,8 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolSaveReqVO
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO;
import cn.iocoder.yudao.module.ai.dal.mysql.model.AiToolMapper;
import jakarta.annotation.Resource;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.ai.tool.ToolCallback;
+import org.springframework.ai.tool.resolution.ToolCallbackResolver;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@@ -31,6 +31,9 @@ public class AiToolServiceImpl implements AiToolService {
@Resource
private AiToolMapper toolMapper;
+ @Resource
+ private ToolCallbackResolver toolCallbackResolver;
+
@Override
public Long createTool(AiToolSaveReqVO createReqVO) {
// 校验名称是否存在
@@ -70,9 +73,8 @@ public class AiToolServiceImpl implements AiToolService {
}
private void validateToolNameExists(String name) {
- try {
- SpringUtil.getBean(name);
- } catch (NoSuchBeanDefinitionException e) {
+ ToolCallback toolCallback = toolCallbackResolver.resolve(name);
+ if (toolCallback == null) {
throw exception(TOOL_NAME_NOT_EXISTS, name);
}
}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java
similarity index 98%
rename from yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java
rename to yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java
index 787b2e772..8e75d5d9e 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java
@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.ai.service.model.tool;
+package cn.iocoder.yudao.module.ai.tool.function;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java
similarity index 97%
rename from yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java
rename to yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java
index 079f3a42f..06a2641da 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java
@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.ai.service.model.tool;
+package cn.iocoder.yudao.module.ai.tool.function;
import cn.iocoder.yudao.module.ai.util.AiUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java
similarity index 98%
rename from yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java
rename to yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java
index 99262fafa..689ea0046 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java
@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.ai.service.model.tool;
+package cn.iocoder.yudao.module.ai.tool.function;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.RandomUtil;
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java
new file mode 100644
index 000000000..0b5965635
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 参考 Tool Calling —— Methods as Tools
+ */
+package cn.iocoder.yudao.module.ai.tool.function;
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java
new file mode 100644
index 000000000..66bab5a7f
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.ai.tool.method;
+
+/**
+ * 来自 Spring AI 官方文档
+ *
+ * Represents a person with basic information.
+ * This is an immutable record.
+ */
+public record Person(
+ int id,
+ String firstName,
+ String lastName,
+ String email,
+ String sex,
+ String ipAddress,
+ String jobTitle,
+ int age
+) {
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java
new file mode 100644
index 000000000..52c895494
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java
@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.module.ai.tool.method;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * 来自 Spring AI 官方文档
+ *
+ * Service interface for managing Person data.
+ * Defines the contract for CRUD operations and search/filter functionalities.
+ */
+public interface PersonService {
+
+ /**
+ * Creates a new Person record.
+ * Assigns a unique ID to the person and stores it.
+ *
+ * @param personData The data for the new person (ID field is ignored). Must not be null.
+ * @return The created Person record, including the generated ID.
+ */
+ Person createPerson(Person personData);
+
+ /**
+ * Retrieves a Person by their unique ID.
+ *
+ * @param id The ID of the person to retrieve.
+ * @return An Optional containing the found Person, or an empty Optional if not found.
+ */
+ Optional getPersonById(int id);
+
+ /**
+ * Retrieves all Person records currently stored.
+ *
+ * @return An unmodifiable List containing all Persons. Returns an empty list if none exist.
+ */
+ List getAllPersons();
+
+ /**
+ * Updates an existing Person record identified by ID.
+ * Replaces the existing data with the provided data, keeping the original ID.
+ *
+ * @param id The ID of the person to update.
+ * @param updatedPersonData The new data for the person (ID field is ignored). Must not be null.
+ * @return true if the person was found and updated, false otherwise.
+ */
+ boolean updatePerson(int id, Person updatedPersonData);
+
+ /**
+ * Deletes a Person record identified by ID.
+ *
+ * @param id The ID of the person to delete.
+ * @return true if the person was found and deleted, false otherwise.
+ */
+ boolean deletePerson(int id);
+
+ /**
+ * Searches for Persons whose job title contains the given query string (case-insensitive).
+ *
+ * @param jobTitleQuery The string to search for within job titles. Can be null or blank.
+ * @return An unmodifiable List of matching Persons. Returns an empty list if no matches or query is invalid.
+ */
+ List searchByJobTitle(String jobTitleQuery);
+
+ /**
+ * Filters Persons by their exact sex (case-insensitive).
+ *
+ * @param sex The sex to filter by (e.g., "Male", "Female"). Can be null or blank.
+ * @return An unmodifiable List of matching Persons. Returns an empty list if no matches or filter is invalid.
+ */
+ List filterBySex(String sex);
+
+ /**
+ * Filters Persons by their exact age.
+ *
+ * @param age The age to filter by.
+ * @return An unmodifiable List of matching Persons. Returns an empty list if no matches.
+ */
+ List filterByAge(int age);
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java
new file mode 100644
index 000000000..3b8c31b42
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java
@@ -0,0 +1,336 @@
+package cn.iocoder.yudao.module.ai.tool.method;
+
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * 来自 Spring AI 官方文档
+ *
+ * Implementation of the PersonService interface using an in-memory data store.
+ * Manages a collection of Person objects loaded from embedded CSV data.
+ * This class is thread-safe due to the use of ConcurrentHashMap and AtomicInteger.
+ */
+@Service
+@Slf4j
+public class PersonServiceImpl implements PersonService {
+
+ private final Map personStore = new ConcurrentHashMap<>();
+
+ private AtomicInteger idGenerator;
+
+ /**
+ * Embedded CSV data for initial population
+ */
+ private static final String CSV_DATA = """
+ Id,FirstName,LastName,Email,Sex,IpAddress,JobTitle,Age
+ 1,Fons,Tollfree,ftollfree0@senate.gov,Male,55.1 Tollfree Lane,Research Associate,31
+ 2,Emlynne,Tabourier,etabourier1@networksolutions.com,Female,18 Tabourier Way,Associate Professor,38
+ 3,Shae,Johncey,sjohncey2@yellowpages.com,Male,1 Johncey Circle,Structural Analysis Engineer,30
+ 4,Sebastien,Bradly,sbradly3@mapquest.com,Male,2 Bradly Hill,Chief Executive Officer,40
+ 5,Harriott,Kitteringham,hkitteringham4@typepad.com,Female,3 Kitteringham Drive,VP Sales,47
+ 6,Anallise,Parradine,aparradine5@miibeian.gov.cn,Female,4 Parradine Street,Analog Circuit Design manager,44
+ 7,Gorden,Kirkbright,gkirkbright6@reuters.com,Male,5 Kirkbright Plaza,Senior Editor,40
+ 8,Veradis,Ledwitch,vledwitch7@google.com.au,Female,6 Ledwitch Avenue,Computer Systems Analyst IV,44
+ 9,Agnesse,Penhalurick,apenhalurick8@google.it,Female,7 Penhalurick Terrace,Automation Specialist IV,41
+ 10,Bibby,Hutable,bhutable9@craigslist.org,Female,8 Hutable Place,Account Representative I,43
+ 11,Karoly,Lightoller,klightollera@rakuten.co.jp,Female,9 Lightoller Parkway,Senior Developer,46
+ 12,Cristine,Durrad,cdurradb@aol.com,Female,10 Durrad Center,Senior Developer,48
+ 13,Aggy,Napier,anapierc@hostgator.com,Female,11 Napier Court,VP Product Management,44
+ 14,Prisca,Caddens,pcaddensd@vinaora.com,Female,12 Caddens Alley,Business Systems Development Analyst,41
+ 15,Khalil,McKernan,kmckernane@google.fr,Male,13 McKernan Pass,Engineer IV,44
+ 16,Lorry,MacTrusty,lmactrustyf@eventbrite.com,Male,14 MacTrusty Junction,Design Engineer,42
+ 17,Casandra,Worsell,cworsellg@goo.gl,Female,15 Worsell Point,Systems Administrator IV,45
+ 18,Ulrikaumeko,Haveline,uhavelineh@usgs.gov,Female,16 Haveline Trail,Financial Advisor,42
+ 19,Shurlocke,Albany,salbanyi@artisteer.com,Male,17 Albany Plaza,Software Test Engineer III,46
+ 20,Myrilla,Brimilcombe,mbrimilcombej@accuweather.com,Female,18 Brimilcombe Road,Programmer Analyst I,48
+ 21,Carlina,Scimonelli,cscimonellik@va.gov,Female,19 Scimonelli Pass,Help Desk Technician,45
+ 22,Tina,Goullee,tgoulleel@miibeian.gov.cn,Female,20 Goullee Crossing,Accountant IV,43
+ 23,Adriaens,Storek,astorekm@devhub.com,Female,21 Storek Avenue,Recruiting Manager,40
+ 24,Tedra,Giraudot,tgiraudotn@wiley.com,Female,22 Giraudot Terrace,Speech Pathologist,47
+ 25,Josiah,Soares,jsoareso@google.nl,Male,23 Soares Street,Tax Accountant,45
+ 26,Kayle,Gaukrodge,kgaukrodgep@wikispaces.com,Female,24 Gaukrodge Parkway,Accountant II,43
+ 27,Ardys,Chuter,achuterq@ustream.tv,Female,25 Chuter Drive,Engineer IV,41
+ 28,Francyne,Baudinet,fbaudinetr@newyorker.com,Female,26 Baudinet Center,VP Accounting,48
+ 29,Gerick,Bullan,gbullans@seesaa.net,Male,27 Bullan Way,Senior Financial Analyst,43
+ 30,Northrup,Grivori,ngrivorit@unc.edu,Male,28 Grivori Plaza,Systems Administrator I,45
+ 31,Town,Duguid,tduguidu@squarespace.com,Male,29 Duguid Pass,Safety Technician IV,46
+ 32,Pierette,Kopisch,pkopischv@google.com.br,Female,30 Kopisch Lane,Director of Sales,41
+ 33,Jacquenetta,Le Prevost,jleprevostw@netlog.com,Female,31 Le Prevost Trail,Senior Developer,47
+ 34,Garvy,Rusted,grustedx@aboutads.info,Male,32 Rusted Junction,Senior Developer,42
+ 35,Clarice,Aysh,cayshy@merriam-webster.com,Female,33 Aysh Avenue,VP Quality Control,40
+ 36,Tracie,Fedorski,tfedorskiz@bloglines.com,Male,34 Fedorski Terrace,Design Engineer,44
+ 37,Noelyn,Matushenko,nmatushenko10@globo.com,Female,35 Matushenko Place,VP Sales,48
+ 38,Rudiger,Klaesson,rklaesson11@usnews.com,Male,36 Klaesson Road,Database Administrator IV,43
+ 39,Mirella,Syddie,msyddie12@geocities.jp,Female,37 Syddie Circle,Geological Engineer,46
+ 40,Donalt,O'Lunny,dolunny13@elpais.com,Male,38 O'Lunny Center,Analog Circuit Design manager,41
+ 41,Guntar,Deniskevich,gdeniskevich14@google.com.hk,Male,39 Deniskevich Way,Structural Engineer,47
+ 42,Hort,Shufflebotham,hshufflebotham15@about.me,Male,40 Shufflebotham Court,Structural Analysis Engineer,45
+ 43,Dominique,Thickett,dthickett16@slashdot.org,Male,41 Thickett Crossing,Safety Technician I,42
+ 44,Zebulen,Piscopello,zpiscopello17@umich.edu,Male,42 Piscopello Parkway,Web Developer II,40
+ 45,Mellicent,Mac Giany,mmacgiany18@state.tx.us,Female,43 Mac Giany Pass,Assistant Manager,44
+ 46,Merle,Bounds,mbounds19@amazon.co.jp,Female,44 Bounds Alley,Systems Administrator III,41
+ 47,Madelle,Farbrace,mfarbrace1a@xinhuanet.com,Female,45 Farbrace Terrace,Quality Engineer,48
+ 48,Galvin,O'Sheeryne,gosheeryne1b@addtoany.com,Male,46 O'Sheeryne Way,Environmental Specialist,43
+ 49,Guillemette,Bootherstone,gbootherstone1c@nationalgeographic.com,Female,47 Bootherstone Plaza,Professor,46
+ 50,Letti,Aylmore,laylmore1d@vinaora.com,Female,48 Aylmore Circle,Automation Specialist I,40
+ 51,Nonie,Rivalland,nrivalland1e@weather.com,Female,49 Rivalland Avenue,Software Test Engineer IV,45
+ 52,Jacquelynn,Halfacre,jhalfacre1f@surveymonkey.com,Female,50 Halfacre Pass,Geologist II,42
+ 53,Anderea,MacKibbon,amackibbon1g@weibo.com,Female,51 MacKibbon Parkway,Automation Specialist II,47
+ 54,Wash,Klimko,wklimko1h@slashdot.org,Male,52 Klimko Alley,Database Administrator I,40
+ 55,Flori,Kynett,fkynett1i@auda.org.au,Female,53 Kynett Trail,Quality Control Specialist,46
+ 56,Libbey,Penswick,lpenswick1j@google.co.uk,Female,54 Penswick Point,VP Accounting,43
+ 57,Silvanus,Skellorne,sskellorne1k@booking.com,Male,55 Skellorne Drive,Account Executive,48
+ 58,Carmine,Mateos,cmateos1l@plala.or.jp,Male,56 Mateos Terrace,Systems Administrator I,41
+ 59,Sheffie,Blazewicz,sblazewicz1m@google.com.au,Male,57 Blazewicz Center,VP Sales,44
+ 60,Leanor,Worsnop,lworsnop1n@uol.com.br,Female,58 Worsnop Plaza,Systems Administrator III,45
+ 61,Caspar,Pamment,cpamment1o@google.co.jp,Male,59 Pamment Court,Senior Financial Analyst,42
+ 62,Justinian,Pentycost,jpentycost1p@sciencedaily.com,Male,60 Pentycost Way,Senior Quality Engineer,47
+ 63,Gerianne,Jarnell,gjarnell1q@bing.com,Female,61 Jarnell Avenue,Help Desk Operator,40
+ 64,Boycie,Zanetto,bzanetto1r@about.com,Male,62 Zanetto Place,Quality Engineer,46
+ 65,Camilla,Mac Giany,cmacgiany1s@state.gov,Female,63 Mac Giany Parkway,Senior Cost Accountant,43
+ 66,Hadlee,Piscopiello,hpiscopiello1t@artisteer.com,Male,64 Piscopiello Street,Account Representative III,48
+ 67,Bobbie,Penvarden,bpenvarden1u@google.cn,Male,65 Penvarden Lane,Help Desk Operator,41
+ 68,Ali,Gowlett,agowlett1v@parallels.com,Male,66 Gowlett Pass,VP Marketing,44
+ 69,Olivette,Acome,oacome1w@qq.com,Female,67 Acome Hill,VP Product Management,45
+ 70,Jehanna,Brotherheed,jbrotherheed1x@google.nl,Female,68 Brotherheed Junction,Database Administrator III,42
+ 71,Morgan,Berthomieu,mberthomieu1y@artisteer.com,Male,69 Berthomieu Alley,Systems Administrator II,47
+ 72,Linzy,Shilladay,lshilladay1z@icq.com,Female,70 Shilladay Trail,Research Assistant IV,40
+ 73,Faydra,Brimner,fbrimner20@mozilla.org,Female,71 Brimner Road,Senior Editor,46
+ 74,Gwenore,Oxlee,goxlee21@devhub.com,Female,72 Oxlee Terrace,Systems Administrator II,43
+ 75,Evangelin,Beinke,ebeinke22@mozilla.com,Female,73 Beinke Circle,Accountant I,48
+ 76,Missy,Cockling,mcockling23@si.edu,Female,74 Cockling Way,Software Engineer I,41
+ 77,Suzanne,Klimschak,sklimschak24@etsy.com,Female,75 Klimschak Plaza,Tax Accountant,44
+ 78,Candide,Goricke,cgoricke25@weebly.com,Female,76 Goricke Pass,Sales Associate,45
+ 79,Gerome,Pinsent,gpinsent26@google.com.au,Male,77 Pinsent Junction,Software Consultant,42
+ 80,Lezley,Mac Giany,lmacgiany27@scribd.com,Male,78 Mac Giany Alley,Operator,47
+ 81,Tobiah,Durn,tdurn28@state.tx.us,Male,79 Durn Court,VP Sales,40
+ 82,Sherlocke,Cockshoot,scockshoot29@yelp.com,Male,80 Cockshoot Street,Senior Financial Analyst,46
+ 83,Myrle,Speenden,mspeenden2a@utexas.edu,Female,81 Speenden Center,Senior Developer,43
+ 84,Isidore,Gorries,igorries2b@flavors.me,Male,82 Gorries Parkway,Sales Representative,48
+ 85,Isac,Kitchingman,ikitchingman2c@businessinsider.com,Male,83 Kitchingman Drive,VP Accounting,41
+ 86,Benedetta,Purrier,bpurrier2d@admin.ch,Female,84 Purrier Trail,VP Accounting,44
+ 87,Tera,Fitchell,tfitchell2e@fotki.com,Female,85 Fitchell Place,Software Engineer IV,45
+ 88,Abbe,Pamment,apamment2f@about.com,Male,86 Pamment Avenue,VP Sales,42
+ 89,Jandy,Gommowe,jgommowe2g@angelfire.com,Female,87 Gommowe Road,Financial Analyst,47
+ 90,Karena,Fussey,kfussey2h@google.com.au,Female,88 Fussey Point,Assistant Professor,40
+ 91,Gaspar,Pammenter,gpammenter2i@google.com.br,Male,89 Pammenter Hill,Help Desk Operator,46
+ 92,Stanwood,Mac Giany,smacgiany2j@prlog.org,Male,90 Mac Giany Terrace,Research Associate,43
+ 93,Byrom,Beedell,bbeedell2k@google.co.jp,Male,91 Beedell Way,VP Sales,48
+ 94,Annabella,Rowbottom,arowbottom2l@google.com.au,Female,92 Rowbottom Plaza,Help Desk Operator,41
+ 95,Rodolphe,Debell,rdebell2m@imageshack.us,Male,93 Debell Pass,Design Engineer,44
+ 96,Tyne,Gommey,tgommey2n@joomla.org,Female,94 Gommey Junction,VP Marketing,45
+ 97,Christoper,Pincked,cpincked2o@icq.com,Male,95 Pincked Alley,Human Resources Manager,42
+ 98,Kore,Le Prevost,kleprevost2p@tripadvisor.com,Female,96 Le Prevost Street,VP Quality Control,47
+ 99,Ceciley,Petrolli,cpetrolli2q@oaic.gov.au,Female,97 Petrolli Court,Senior Developer,40
+ 100,Elspeth,Mac Giany,emacgiany2r@icio.us,Female,98 Mac Giany Parkway,Internal Auditor,46
+ """;
+
+ /**
+ * Initializes the service after dependency injection by loading data from the CSV string.
+ * Uses @PostConstruct to ensure this runs after the bean is created.
+ */
+ @PostConstruct
+ private void initializeData() {
+ log.info("Initializing PersonService data store...");
+ int maxId = loadDataFromCsv();
+ idGenerator = new AtomicInteger(maxId);
+ log.info("PersonService initialized with {} records. Next ID: {}", personStore.size(), idGenerator.get() + 1);
+ }
+
+ /**
+ * Parses the embedded CSV data and populates the in-memory store.
+ * Calculates the maximum ID found in the data to initialize the ID generator.
+ *
+ * @return The maximum ID found in the loaded CSV data.
+ */
+ private int loadDataFromCsv() {
+ final AtomicInteger currentMaxId = new AtomicInteger(0);
+ // Clear existing data before loading (important for tests or re-initialization scenarios)
+ personStore.clear();
+ try (Stream lines = CSV_DATA.lines().skip(1)) { // Skip header row
+ lines.forEach(line -> {
+ try {
+ // Split carefully, handling potential commas within quoted fields if necessary (simple split here)
+ String[] fields = line.split(",", 8); // Limit split to handle potential commas in job title
+ if (fields.length == 8) {
+ int id = Integer.parseInt(fields[0].trim());
+ String firstName = fields[1].trim();
+ String lastName = fields[2].trim();
+ String email = fields[3].trim();
+ String sex = fields[4].trim();
+ String ipAddress = fields[5].trim();
+ String jobTitle = fields[6].trim();
+ int age = Integer.parseInt(fields[7].trim());
+
+ Person person = new Person(id, firstName, lastName, email, sex, ipAddress, jobTitle, age);
+ personStore.put(id, person);
+ currentMaxId.updateAndGet(max -> Math.max(max, id));
+ } else {
+ log.warn("Skipping malformed CSV line (expected 8 fields, found {}): {}", fields.length, line);
+ }
+ } catch (NumberFormatException e) {
+ log.warn("Skipping line due to parsing error (ID or Age): {} - Error: {}", line, e.getMessage());
+ } catch (Exception e) {
+ log.error("Skipping line due to unexpected error: {} - Error: {}", line, e.getMessage(), e);
+ }
+ });
+ } catch (Exception e) {
+ log.error("Fatal error reading embedded CSV data: {}", e.getMessage(), e);
+ // In a real application, might throw a specific initialization exception
+ }
+ return currentMaxId.get();
+ }
+
+ @Override
+ @Tool(
+ name = "ps_create_person",
+ description = "Create a new person record in the in-memory store."
+ )
+ public Person createPerson(Person personData) {
+ if (personData == null) {
+ throw new IllegalArgumentException("Person data cannot be null");
+ }
+ int newId = idGenerator.incrementAndGet();
+ // Create a new Person record using data from the input, but with the generated ID
+ Person newPerson = new Person(
+ newId,
+ personData.firstName(),
+ personData.lastName(),
+ personData.email(),
+ personData.sex(),
+ personData.ipAddress(),
+ personData.jobTitle(),
+ personData.age()
+ );
+ personStore.put(newId, newPerson);
+ log.debug("Created person: {}", newPerson);
+ return newPerson;
+ }
+
+ @Override
+ @Tool(
+ name = "ps_get_person_by_id",
+ description = "Retrieve a person record by ID from the in-memory store."
+ )
+ public Optional getPersonById(int id) {
+ Person person = personStore.get(id);
+ log.debug("Retrieved person by ID {}: {}", id, person);
+ return Optional.ofNullable(person);
+ }
+
+ @Override
+ @Tool(
+ name = "ps_get_all_persons",
+ description = "Retrieve all person records from the in-memory store."
+ )
+ public List getAllPersons() {
+ // Return an unmodifiable view of the values
+ List allPersons = personStore.values().stream().toList();
+ log.debug("Retrieved all persons (count: {})", allPersons.size());
+ return allPersons;
+ }
+
+ @Override
+ @Tool(
+ name = "ps_update_person",
+ description = "Update an existing person record by ID in the in-memory store."
+ )
+ public boolean updatePerson(int id, Person updatedPersonData) {
+ if (updatedPersonData == null) {
+ throw new IllegalArgumentException("Updated person data cannot be null");
+ }
+ // Use computeIfPresent for atomic update if the key exists
+ Person result = personStore.computeIfPresent(id, (key, existingPerson) ->
+ // Create a new Person record with the original ID but updated data
+ new Person(
+ id, // Keep original ID
+ updatedPersonData.firstName(),
+ updatedPersonData.lastName(),
+ updatedPersonData.email(),
+ updatedPersonData.sex(),
+ updatedPersonData.ipAddress(),
+ updatedPersonData.jobTitle(),
+ updatedPersonData.age()
+ )
+ );
+ boolean updated = result != null;
+ log.debug("Update attempt for ID {}: {}", id, updated ? "Successful" : "Failed (Not Found)");
+ if(updated) log.trace("Updated person data for ID {}: {}", id, result);
+ return updated;
+ }
+
+ @Override
+ @Tool(
+ name = "ps_delete_person",
+ description = "Delete a person record by ID from the in-memory store."
+ )
+ public boolean deletePerson(int id) {
+ boolean removed = personStore.remove(id) != null;
+ log.debug("Delete attempt for ID {}: {}", id, removed ? "Successful" : "Failed (Not Found)");
+ return removed;
+ }
+
+ @Override
+ @Tool(
+ name = "ps_search_by_job_title",
+ description = "Search for persons by job title in the in-memory store."
+ )
+ public List searchByJobTitle(String jobTitleQuery) {
+ if (jobTitleQuery == null || jobTitleQuery.isBlank()) {
+ log.debug("Search by job title skipped due to blank query.");
+ return Collections.emptyList();
+ }
+ String lowerCaseQuery = jobTitleQuery.toLowerCase();
+ List results = personStore.values().stream()
+ .filter(person -> person.jobTitle() != null && person.jobTitle().toLowerCase().contains(lowerCaseQuery))
+ .collect(Collectors.toList());
+ log.debug("Search by job title '{}' found {} results.", jobTitleQuery, results.size());
+ return Collections.unmodifiableList(results);
+ }
+
+ @Override
+ @Tool(
+ name = "ps_filter_by_sex",
+ description = "Filters Persons by sex (case-insensitive)."
+ )
+ public List filterBySex(String sex) {
+ if (sex == null || sex.isBlank()) {
+ log.debug("Filter by sex skipped due to blank filter.");
+ return Collections.emptyList();
+ }
+ List results = personStore.values().stream()
+ .filter(person -> person.sex() != null && person.sex().equalsIgnoreCase(sex))
+ .collect(Collectors.toList());
+ log.debug("Filter by sex '{}' found {} results.", sex, results.size());
+ return Collections.unmodifiableList(results);
+ }
+
+ @Override
+ @Tool(
+ name = "ps_filter_by_age",
+ description = "Filters Persons by age."
+ )
+ public List filterByAge(int age) {
+ if (age < 0) {
+ log.debug("Filter by age skipped due to negative age: {}", age);
+ return Collections.emptyList(); // Or throw IllegalArgumentException based on requirements
+ }
+ List results = personStore.values().stream()
+ .filter(person -> person.age() == age)
+ .collect(Collectors.toList());
+ log.debug("Filter by age {} found {} results.", age, results.size());
+ return Collections.unmodifiableList(results);
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java
new file mode 100644
index 000000000..44b53e197
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * 参考 Tool Calling —— Methods as Tools
+ */
+package cn.iocoder.yudao.module.ai.tool.method;
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java
index 0744ff630..d209c62d4 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java
@@ -8,18 +8,20 @@ import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.qianfan.QianFanChatOptions;
+import org.springframework.ai.anthropic.AnthropicChatOptions;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.*;
+import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
+import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
+import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.openai.OpenAiChatOptions;
+import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
/**
* Spring AI 工具类
@@ -36,40 +38,47 @@ public class AiUtils {
}
public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens,
- Set toolNames, Map toolContext) {
- toolNames = ObjUtil.defaultIfNull(toolNames, Collections.emptySet());
+ List toolCallbacks, Map toolContext) {
+ toolCallbacks = ObjUtil.defaultIfNull(toolCallbacks, Collections.emptyList());
toolContext = ObjUtil.defaultIfNull(toolContext, Collections.emptyMap());
// noinspection EnhancedSwitchMigration
switch (platform) {
case TONG_YI:
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
- .withToolNames(toolNames).withToolContext(toolContext).build();
+ .withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
+ .withToolCallbacks(toolCallbacks).withToolContext(toolContext).build();
case YI_YAN:
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
+ case DEEP_SEEK:
+ case DOU_BAO: // 复用 DeepSeek 客户端
+ case HUN_YUAN: // 复用 DeepSeek 客户端
+ case SILICON_FLOW: // 复用 DeepSeek 客户端
+ case XING_HUO: // 复用 DeepSeek 客户端
+ return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
+ .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case ZHI_PU:
return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
- .toolNames(toolNames).toolContext(toolContext).build();
+ .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case MINI_MAX:
return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
- .toolNames(toolNames).toolContext(toolContext).build();
+ .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case MOONSHOT:
return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
- .toolNames(toolNames).toolContext(toolContext).build();
+ .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case OPENAI:
- case DEEP_SEEK: // 复用 OpenAI 客户端
- case DOU_BAO: // 复用 OpenAI 客户端
- case HUN_YUAN: // 复用 OpenAI 客户端
- case XING_HUO: // 复用 OpenAI 客户端
- case SILICON_FLOW: // 复用 OpenAI 客户端
+ case GEMINI: // 复用 OpenAI 客户端
case BAI_CHUAN: // 复用 OpenAI 客户端
return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
- .toolNames(toolNames).toolContext(toolContext).build();
+ .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case AZURE_OPENAI:
return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens)
- .toolNames(toolNames).toolContext(toolContext).build();
+ .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
+ case ANTHROPIC:
+ return AnthropicChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
+ .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case OLLAMA:
return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens)
- .toolNames(toolNames).toolContext(toolContext).build();
+ .toolCallbacks(toolCallbacks).toolContext(toolContext).build();
default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
}
@@ -98,4 +107,27 @@ public class AiUtils {
return context;
}
+ @SuppressWarnings("ConstantValue")
+ public static String getChatResponseContent(ChatResponse response) {
+ if (response == null
+ || response.getResult() == null
+ || response.getResult().getOutput() == null) {
+ return null;
+ }
+ return response.getResult().getOutput().getText();
+ }
+
+ @SuppressWarnings("ConstantValue")
+ public static String getChatResponseReasoningContent(ChatResponse response) {
+ if (response == null
+ || response.getResult() == null
+ || response.getResult().getOutput() == null) {
+ return null;
+ }
+ if (response.getResult().getOutput() instanceof DeepSeekAssistantMessage) {
+ return ((DeepSeekAssistantMessage) (response.getResult().getOutput())).getReasoningContent();
+ }
+ return null;
+ }
+
}
\ No newline at end of file
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/util/FileTypeUtils.java b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/util/FileTypeUtils.java
new file mode 100644
index 000000000..9c3b202c4
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/util/FileTypeUtils.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.ai.util;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.tika.Tika;
+
+/**
+ * 文件类型 Utils
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+public class FileTypeUtils {
+
+ private static final Tika TIKA = new Tika();
+
+ /**
+ * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用 jar 文件时,通过名字更为准确
+ *
+ * @param name 文件名
+ * @return mineType 无法识别时会返回“application/octet-stream”
+ */
+ public static String getMineType(String name) {
+ return TIKA.detect(name);
+ }
+
+ /**
+ * 判断是否是图片
+ *
+ * @param mineType 类型
+ * @return 是否是图片
+ */
+ public static boolean isImage(String mineType) {
+ return StrUtil.startWith(mineType, "image/");
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/main/resources/application.yaml b/yudao-module-ai/yudao-module-ai-server/src/main/resources/application.yaml
index 157a6c047..121084036 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/main/resources/application.yaml
+++ b/yudao-module-ai/yudao-module-ai-server/src/main/resources/application.yaml
@@ -132,7 +132,8 @@ spring:
azure: # OpenAI 微软
openai:
endpoint: https://eastusprejade.openai.azure.com
- api-key: xxx
+ anthropic: # Anthropic Claude
+ api-key: sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942
ollama:
base-url: http://127.0.0.1:11434
chat:
@@ -140,7 +141,7 @@ spring:
stabilityai:
api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx
dashscope: # 通义千问
- api-key: sk-71800982914041848008480000000000
+ api-key: sk-47aa124781be4bfb95244cc62f6xxxx
minimax: # Minimax:https://www.minimaxi.com/
api-key: xxxx
moonshot: # 月之暗灭(KIMI)
@@ -150,9 +151,30 @@ spring:
chat:
options:
model: deepseek-chat
+ model:
+ rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启
+ mcp:
+ server:
+ enabled: true
+ name: yudao-mcp-server
+ version: 1.0.0
+ instructions: 一个 MCP 示例服务
+ sse-endpoint: /sse
+ client:
+ enabled: true
+ name: mcp
+ sse:
+ connections:
+ filesystem:
+ url: http://127.0.0.1:8089
+ sse-endpoint: /sse
yudao:
ai:
+ gemini: # 谷歌 Gemini
+ enable: true
+ api-key: AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ
+ model: gemini-2.5-flash
doubao: # 字节豆包
enable: true
api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272
@@ -169,7 +191,7 @@ yudao:
enable: true
appKey: 75b161ed2aef4719b275d6e7f2a4d4cd
secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz
- model: generalv3.5
+ model: x1
baichuan: # 百川智能
enable: true
api-key: sk-abc
@@ -184,6 +206,9 @@ yudao:
enable: true
# base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
base-url: http://127.0.0.1:3001
+ web-search:
+ enable: true
+ api-key: sk-40500e52840f4d24b956d0b1d80d9abe
--- #################### 芋道相关配置 ####################
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AnthropicChatModelTest.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AnthropicChatModelTest.java
new file mode 100644
index 000000000..454fad47b
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AnthropicChatModelTest.java
@@ -0,0 +1,87 @@
+package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.ai.anthropic.AnthropicChatModel;
+import org.springframework.ai.anthropic.AnthropicChatOptions;
+import org.springframework.ai.anthropic.api.AnthropicApi;
+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 reactor.core.publisher.Flux;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@link AnthropicChatModel} 集成测试类
+ *
+ * @author 芋道源码
+ */
+public class AnthropicChatModelTest {
+
+ private final AnthropicChatModel chatModel = AnthropicChatModel.builder()
+ .anthropicApi(AnthropicApi.builder()
+ .apiKey("sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942")
+ .baseUrl("https://aihubmix.com")
+ .build())
+ .defaultOptions(AnthropicChatOptions.builder()
+ .model(AnthropicApi.ChatModel.CLAUDE_SONNET_4)
+ .temperature(0.7)
+ .maxTokens(4096)
+ .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);
+ }
+
+ @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(System.out::println).then().block();
+ }
+
+ // TODO @芋艿:需要等 spring ai 升级:https://github.com/spring-projects/spring-ai/pull/2800
+ @Test
+ @Disabled
+ public void testStream_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("thkinking 下,1+1 为什么等于 2 "));
+ AnthropicChatOptions options = AnthropicChatOptions.builder()
+ .model(AnthropicApi.ChatModel.CLAUDE_SONNET_4)
+ .thinking(AnthropicApi.ThinkingType.ENABLED, 3096)
+ .temperature(1D)
+ .build();
+
+ // 调用
+ Flux flux = chatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult());
+ }).then().block();
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java
index 7b51df166..2fbe0ee5d 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java
@@ -60,4 +60,23 @@ public class DeepSeekChatModelTests {
flux.doOnNext(System.out::println).then().block();
}
+ @Test
+ @Disabled
+ public void testStream_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
+ DeepSeekChatOptions options = DeepSeekChatOptions.builder()
+ .model("deepseek-reasoner")
+ .build();
+
+ // 调用
+ Flux flux = chatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult().getOutput());
+ }).then().block();
+ }
+
}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java
index 7cd3d43bb..38c4f0b01 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java
@@ -8,9 +8,9 @@ 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.OpenAiChatOptions;
-import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.ai.deepseek.DeepSeekChatModel;
+import org.springframework.ai.deepseek.DeepSeekChatOptions;
+import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
@@ -23,13 +23,18 @@ import java.util.List;
*/
public class DouBaoChatModelTests {
- private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
- .openAiApi(OpenAiApi.builder()
+ /**
+ * 相比 OpenAIChatModel 来说,DeepSeekChatModel 可以兼容豆包的 thinking 能力!
+ */
+ private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
+ .deepSeekApi(DeepSeekApi.builder()
.baseUrl(DouBaoChatModel.BASE_URL)
+ .completionsPath(DouBaoChatModel.COMPLETE_PATH)
.apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey
.build())
- .defaultOptions(OpenAiChatOptions.builder()
+ .defaultOptions(DeepSeekChatOptions.builder()
.model("doubao-1-5-lite-32k-250115") // 模型(doubao)
+// .model("doubao-seed-1-6-thinking-250715") // 模型(doubao)
// .model("deepseek-r1-250120") // 模型(deepseek)
.temperature(0.7)
.build())
@@ -51,14 +56,13 @@ public class DouBaoChatModelTests {
System.out.println(response);
}
- // TODO @芋艿:因为使用的是 v1 api,导致 deepseek-r1-250120 不返回 think 过程,后续需要优化
@Test
@Disabled
public void testStream() {
// 准备参数
List messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
- messages.add(new UserMessage("1 + 1 = ?"));
+ messages.add(new UserMessage("详细推理下,帮我设计一个用户中心!"));
// 调用
Flux flux = chatModel.stream(new Prompt(messages));
@@ -66,4 +70,23 @@ public class DouBaoChatModelTests {
flux.doOnNext(System.out::println).then().block();
}
+ @Test
+ @Disabled
+ public void testStream_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
+ DeepSeekChatOptions options = DeepSeekChatOptions.builder()
+ .model("doubao-seed-1-6-thinking-250715")
+ .build();
+
+ // 调用
+ Flux flux = chatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult().getOutput());
+ }).then().block();
+ }
+
}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GeminiChatModelTests.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GeminiChatModelTests.java
new file mode 100644
index 000000000..964a5f3c3
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GeminiChatModelTests.java
@@ -0,0 +1,68 @@
+package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
+
+import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
+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.OpenAiChatOptions;
+import org.springframework.ai.openai.api.OpenAiApi;
+import reactor.core.publisher.Flux;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@link GeminiChatModel} 集成测试
+ *
+ * @author 芋道源码
+ */
+public class GeminiChatModelTests {
+
+ private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
+ .openAiApi(OpenAiApi.builder()
+ .baseUrl(GeminiChatModel.BASE_URL)
+ .completionsPath(GeminiChatModel.COMPLETE_PATH)
+ .apiKey("AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ")
+ .build())
+ .defaultOptions(OpenAiChatOptions.builder()
+ .model(GeminiChatModel.MODEL_DEFAULT) // 模型
+ .temperature(0.7)
+ .build())
+ .build();
+
+ private final GeminiChatModel chatModel = new GeminiChatModel(openAiChatModel);
+
+ @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);
+ }
+
+ @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(System.out::println).then().block();
+ }
+
+}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java
index b568f5ac4..eeafef261 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java
@@ -8,9 +8,9 @@ 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.OpenAiChatOptions;
-import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.ai.deepseek.DeepSeekChatModel;
+import org.springframework.ai.deepseek.DeepSeekChatOptions;
+import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
@@ -23,12 +23,13 @@ import java.util.List;
*/
public class HunYuanChatModelTests {
- private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
- .openAiApi(OpenAiApi.builder()
+ private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
+ .deepSeekApi(DeepSeekApi.builder()
.baseUrl(HunYuanChatModel.BASE_URL)
- .apiKey("sk-bcd") // apiKey
+ .completionsPath(HunYuanChatModel.COMPLETE_PATH)
+ .apiKey("sk-abc") // apiKey
.build())
- .defaultOptions(OpenAiChatOptions.builder()
+ .defaultOptions(DeepSeekChatOptions.builder()
.model(HunYuanChatModel.MODEL_DEFAULT) // 模型
.temperature(0.7)
.build())
@@ -64,12 +65,33 @@ public class HunYuanChatModelTests {
flux.doOnNext(System.out::println).then().block();
}
- private final OpenAiChatModel deepSeekOpenAiChatModel = OpenAiChatModel.builder()
- .openAiApi(OpenAiApi.builder()
+ @Test
+ @Disabled
+ public void testStream_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
+ DeepSeekChatOptions options = DeepSeekChatOptions.builder()
+ .model("hunyuan-a13b")
+// .model("hunyuan-turbos-latest")
+ .build();
+
+ // 调用
+ Flux flux = chatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult().getOutput());
+ }).then().block();
+ }
+
+ private final DeepSeekChatModel deepSeekOpenAiChatModel = DeepSeekChatModel.builder()
+ .deepSeekApi(DeepSeekApi.builder()
.baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL)
+ .completionsPath(HunYuanChatModel.COMPLETE_PATH)
.apiKey("sk-abc") // apiKey
.build())
- .defaultOptions(OpenAiChatOptions.builder()
+ .defaultOptions(DeepSeekChatOptions.builder()
// .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型("deepseek-v3")
.model("deepseek-r1") // 模型("deepseek-r1")
.temperature(0.7)
@@ -94,7 +116,7 @@ public class HunYuanChatModelTests {
@Test
@Disabled
- public void testStream_deekseek() {
+ public void testStream_deepseek() {
// 准备参数
List messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
@@ -106,5 +128,23 @@ public class HunYuanChatModelTests {
flux.doOnNext(System.out::println).then().block();
}
+ @Test
+ @Disabled
+ public void testStream_deepseek_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
+ DeepSeekChatOptions options = DeepSeekChatOptions.builder()
+ .model("deepseek-r1")
+ .build();
+
+ // 调用
+ Flux flux = deepSeekChatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult().getOutput());
+ }).then().block();
+ }
}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java
index 69e2c1daa..14f32e06e 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java
@@ -1,6 +1,20 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.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.ollama.OllamaChatModel;
+import org.springframework.ai.ollama.api.OllamaApi;
+import org.springframework.ai.ollama.api.OllamaModel;
+import org.springframework.ai.ollama.api.OllamaOptions;
+import reactor.core.publisher.Flux;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* {@link OllamaChatModel} 集成测试
@@ -9,43 +23,65 @@ import org.springframework.ai.ollama.OllamaChatModel;
*/
public class LlamaChatModelTests {
-// private final OllamaChatModel chatModel = OllamaChatModel.builder()
-// .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址
-// .defaultOptions(OllamaOptions.builder()
-// .model(OllamaModel.LLAMA3.getName()) // 模型
-// .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();
-// }
+ private final OllamaChatModel chatModel = OllamaChatModel.builder()
+ .ollamaApi(OllamaApi.builder()
+ .baseUrl("http://127.0.0.1:11434") // Ollama 服务地址
+ .build())
+ .defaultOptions(OllamaOptions.builder()
+ .model(OllamaModel.LLAMA3.getName()) // 模型
+ .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();
+ }
+
+ @Test
+ @Disabled
+ public void testStream_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
+ OllamaOptions options = OllamaOptions.builder()
+ .model("qwen3")
+ .build();
+
+ // 调用
+ Flux flux = chatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult().getOutput());
+ }).then().block();
+ }
+
}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java
index ce350ddd2..8fb133dbb 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java
@@ -59,4 +59,24 @@ public class MiniMaxChatModelTests {
}).then().block();
}
+ // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
+ @Test
+ @Disabled
+ public void testStream_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
+ MiniMaxChatOptions options = MiniMaxChatOptions.builder()
+ .model("MiniMax-M1")
+ .build();
+
+ // 调用
+ Flux flux = chatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult().getOutput());
+ }).then().block();
+ }
+
}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java
index 992334b4d..b50ab80f4 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java
@@ -63,4 +63,25 @@ public class MoonshotChatModelTests {
}).then().block();
}
+ // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
+ @Test
+ @Disabled
+ public void testStream_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
+ MoonshotChatOptions options = MoonshotChatOptions.builder()
+// .model("kimi-k2-0711-preview")
+ .model("kimi-thinking-preview")
+ .build();
+
+ // 调用
+ Flux flux = chatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult().getOutput());
+ }).then().block();
+ }
+
}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java
index c650fd042..5bae6c694 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
+import com.azure.ai.openai.models.ReasoningEffortValue;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
@@ -25,10 +26,11 @@ public class OpenAIChatModelTests {
private final OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl("https://api.holdai.top")
- .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") // apiKey
+ .apiKey("sk-z5joyRoV1iFEnh2SAi8QPNrIZTXyQSyxTmD5CoNDQbFixK2l") // apiKey
.build())
.defaultOptions(OpenAiChatOptions.builder()
- .model(OpenAiApi.ChatModel.GPT_4_1_NANO) // 模型
+ .model("gpt-5-nano-2025-08-07") // 模型
+// .model(OpenAiApi.ChatModel.O1) // 模型
.temperature(0.7)
.build())
.build();
@@ -54,7 +56,7 @@ public class OpenAIChatModelTests {
// 准备参数
List messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
- messages.add(new UserMessage("1 + 1 = ?"));
+ messages.add(new UserMessage("帮我推理下,怎么实现一个用户中心!"));
// 调用
Flux flux = chatModel.stream(new Prompt(messages));
@@ -65,4 +67,29 @@ public class OpenAIChatModelTests {
}).then().block();
}
+ // TODO @芋艿:无法触发思考的字段返回,需要 response api:https://github.com/spring-projects/spring-ai/issues/2962
+ @Test
+ @Disabled
+ public void testStream_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
+ OpenAiChatOptions options = OpenAiChatOptions.builder()
+ .model("gpt-5")
+// .model(OpenAiApi.ChatModel.O4_MINI)
+// .model("o3-pro")
+ .reasoningEffort(ReasoningEffortValue.LOW.getValue())
+ .build();
+
+ // 调用
+ Flux flux = chatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult().getOutput());
+ }).then().block();
+ }
+
+
+
}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/SiliconFlowChatModelTests.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/SiliconFlowChatModelTests.java
index f34c662db..3bb58e68e 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/SiliconFlowChatModelTests.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/SiliconFlowChatModelTests.java
@@ -9,9 +9,9 @@ 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.OpenAiChatOptions;
-import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.ai.deepseek.DeepSeekChatModel;
+import org.springframework.ai.deepseek.DeepSeekChatOptions;
+import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
@@ -24,12 +24,12 @@ import java.util.List;
*/
public class SiliconFlowChatModelTests {
- private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
- .openAiApi(OpenAiApi.builder()
+ private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
+ .deepSeekApi(DeepSeekApi.builder()
.baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL)
.apiKey("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // apiKey
.build())
- .defaultOptions(OpenAiChatOptions.builder()
+ .defaultOptions(DeepSeekChatOptions.builder()
.model(SiliconFlowApiConstants.MODEL_DEFAULT) // 模型
// .model("deepseek-ai/DeepSeek-R1") // 模型(deepseek-ai/DeepSeek-R1)可用赠费
// .model("Pro/deepseek-ai/DeepSeek-R1") // 模型(Pro/deepseek-ai/DeepSeek-R1)需要付费
@@ -67,4 +67,23 @@ public class SiliconFlowChatModelTests {
flux.doOnNext(System.out::println).then().block();
}
+ @Test
+ @Disabled
+ public void testStream_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
+ DeepSeekChatOptions options = DeepSeekChatOptions.builder()
+ .model("deepseek-ai/DeepSeek-R1")
+ .build();
+
+ // 调用
+ Flux flux = chatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult().getOutput());
+ }).then().block();
+ }
+
}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java
index 4f2e27edd..23bd5d9e0 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java
@@ -1,8 +1,15 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
+import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankModel;
+import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions;
+import com.alibaba.cloud.ai.model.RerankModel;
+import com.alibaba.cloud.ai.model.RerankOptions;
+import com.alibaba.cloud.ai.model.RerankRequest;
+import com.alibaba.cloud.ai.model.RerankResponse;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
@@ -10,11 +17,14 @@ 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.document.Document;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
+import static java.util.Arrays.asList;
+
/**
* {@link DashScopeChatModel} 集成测试类
*
@@ -26,11 +36,13 @@ public class TongYiChatModelTests {
.dashScopeApi(DashScopeApi.builder()
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.build())
- .defaultOptions( DashScopeChatOptions.builder()
- .withModel("qwen1.5-72b-chat") // 模型
+ .defaultOptions(DashScopeChatOptions.builder()
+// .withModel("qwen1.5-72b-chat") // 模型
+ .withModel("qwen3-235b-a22b-thinking-2507") // 模型
// .withModel("deepseek-r1") // 模型(deepseek-r1)
// .withModel("deepseek-v3") // 模型(deepseek-v3)
// .withModel("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b)
+// .withEnableThinking(true)
.build())
.build();
@@ -54,8 +66,8 @@ public class TongYiChatModelTests {
public void testStream() {
// 准备参数
List messages = new ArrayList<>();
- messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
- messages.add(new UserMessage("1 + 1 = ?"));
+// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
+ messages.add(new UserMessage("帮我推理下,怎么实现一个用户中心!"));
// 调用
Flux flux = chatModel.stream(new Prompt(messages));
@@ -66,4 +78,52 @@ public class TongYiChatModelTests {
}).then().block();
}
+ @Test
+ @Disabled
+ public void testStream_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
+ DashScopeChatOptions options = DashScopeChatOptions.builder()
+ .withModel("qwen3-235b-a22b-thinking-2507")
+// .withModel("qwen-max-2025-01-25")
+ .withEnableThinking(true) // 必须设置,否则会报错
+ .build();
+
+ // 调用
+ Flux flux = chatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult().getOutput());
+ }).then().block();
+ }
+
+ @Test
+ @Disabled
+ public void testRerank() {
+ // 准备环境
+ RerankModel rerankModel = new DashScopeRerankModel(
+ DashScopeApi.builder()
+ .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
+ .build());
+ // 准备参数
+ String query = "spring";
+ Document document01 = new Document("abc");
+ Document document02 = new Document("sapring");
+ RerankOptions options = DashScopeRerankOptions.builder()
+ .withTopN(1)
+ .withModel("gte-rerank-v2")
+ .build();
+ RerankRequest rerankRequest = new RerankRequest(
+ query,
+ asList(document01, document02),
+ options);
+
+ // 调用
+ RerankResponse call = rerankModel.call(rerankRequest);
+ // 打印结果
+ System.out.println(JsonUtils.toJsonPrettyString(call));
+ }
+
}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java
index 5d8dae201..77dbd2bc6 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java
@@ -8,9 +8,9 @@ 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.OpenAiChatOptions;
-import org.springframework.ai.openai.api.OpenAiApi;
+import org.springframework.ai.deepseek.DeepSeekChatModel;
+import org.springframework.ai.deepseek.DeepSeekChatOptions;
+import org.springframework.ai.deepseek.api.DeepSeekApi;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
@@ -23,13 +23,15 @@ import java.util.List;
*/
public class XingHuoChatModelTests {
- private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
- .openAiApi(OpenAiApi.builder()
- .baseUrl(XingHuoChatModel.BASE_URL)
+ private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
+ .deepSeekApi(DeepSeekApi.builder()
+ .baseUrl(XingHuoChatModel.BASE_URL_V2)
+ .completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2)
.apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey
.build())
- .defaultOptions(OpenAiChatOptions.builder()
- .model("generalv3.5") // 模型
+ .defaultOptions(DeepSeekChatOptions.builder()
+// .model("generalv3.5") // 模型
+ .model("x1") // 模型
.temperature(0.7)
.build())
.build();
@@ -64,4 +66,23 @@ public class XingHuoChatModelTests {
flux.doOnNext(System.out::println).then().block();
}
+ @Test
+ @Disabled
+ public void testStream_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
+ DeepSeekChatOptions options = DeepSeekChatOptions.builder()
+ .model("x1")
+ .build();
+
+ // 调用
+ Flux flux = chatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult().getOutput());
+ }).then().block();
+ }
+
}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java
index ffdb51892..0b0b00693 100644
--- a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java
@@ -23,7 +23,7 @@ import java.util.List;
public class ZhiPuAiChatModelTests {
private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel(
- new ZhiPuAiApi("32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs"), // 密钥
+ new ZhiPuAiApi("2f35fb6ca4ea41fab898729b7fac086c.6ESSfPcCkxaKEUlR"), // 密钥
ZhiPuAiChatOptions.builder()
.model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型
.build()
@@ -61,4 +61,24 @@ public class ZhiPuAiChatModelTests {
}).then().block();
}
+ // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
+ @Test
+ @Disabled
+ public void testStream_thinking() {
+ // 准备参数
+ List messages = new ArrayList<>();
+ messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
+ ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder()
+ .model("GLM-4.5")
+ .build();
+
+ // 调用
+ Flux flux = chatModel.stream(new Prompt(messages, options));
+ // 打印结果
+ flux.doOnNext(response -> {
+// System.out.println(response);
+ System.out.println(response.getResult().getOutput());
+ }).then().block();
+ }
+
}
diff --git a/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/websearch/AiBoChaWebSearchClientTest.java b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/websearch/AiBoChaWebSearchClientTest.java
new file mode 100644
index 000000000..0a02ab589
--- /dev/null
+++ b/yudao-module-ai/yudao-module-ai-server/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/websearch/AiBoChaWebSearchClientTest.java
@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.ai.framework.ai.core.websearch;
+
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest;
+import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
+import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
+import org.junit.jupiter.api.Test;
+
+/**
+ * {@link AiBoChaWebSearchClient} 集成测试类
+ *
+ * @author 芋道源码
+ */
+public class AiBoChaWebSearchClientTest {
+
+ private final AiBoChaWebSearchClient webSearchClient = new AiBoChaWebSearchClient(
+ "sk-40500e52840f4d24b956d0b1d80d9abe");
+
+ @Test
+ public void testSearch() {
+ AiWebSearchRequest request = new AiWebSearchRequest()
+ .setQuery("阿里巴巴")
+ .setCount(3);
+ AiWebSearchResponse response = webSearchClient.search(request);
+ System.out.println(JsonUtils.toJsonPrettyString(response));
+ }
+
+}
\ No newline at end of file
diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java
index d8a13e953..28d6a9fa1 100644
--- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java
+++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java
@@ -80,17 +80,17 @@ public class FileTypeUtils {
*/
public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException {
// 设置 header 和 contentType
- String contentType = getMineType(content, filename);
- response.setContentType(contentType);
+ String mineType = getMineType(content, filename);
+ response.setContentType(mineType);
// 设置内容显示、下载文件名:https://www.cnblogs.com/wq-9/articles/12165056.html
- if (StrUtil.containsIgnoreCase(contentType, "image/")) {
+ if (isImage(mineType)) {
// 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论
response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename));
} else {
response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename));
}
// 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题
- if (StrUtil.containsIgnoreCase(contentType, "video")) {
+ if (StrUtil.containsIgnoreCase(mineType, "video")) {
response.setHeader("Content-Length", String.valueOf(content.length));
response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length);
response.setHeader("Accept-Ranges", "bytes");
@@ -99,4 +99,14 @@ public class FileTypeUtils {
IoUtil.write(response.getOutputStream(), false, content);
}
+ /**
+ * 判断是否是图片
+ *
+ * @param mineType 类型
+ * @return 是否是图片
+ */
+ public static boolean isImage(String mineType) {
+ return StrUtil.startWith(mineType, "image/");
+ }
+
}
diff --git a/yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
index ba607922f..946980f01 100644
--- a/yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
+++ b/yudao-module-mall/yudao-module-promotion-server/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java
@@ -166,6 +166,10 @@ public class CouponServiceImpl implements CouponService {
public void invalidateCouponsByAdmin(List giveCouponIds, Long userId) {
// 循环收回
for (Long couponId : giveCouponIds) {
+ // couponId 为空或 0 则跳过
+ if (null == couponId || couponId <= 0) {
+ continue;
+ }
try {
getSelf().invalidateCoupon(couponId, userId);
} catch (Exception e) {
diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java
index a9e3d919b..e5f774190 100644
--- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java
+++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/captcha/core/PictureWordCaptchaServiceImpl.java
@@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.system.framework.captcha.core;
-import cn.hutool.core.util.RandomUtil;
import com.anji.captcha.model.common.RepCodeEnum;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
@@ -9,7 +8,8 @@ import com.anji.captcha.service.impl.CaptchaServiceFactory;
import com.anji.captcha.util.AESUtil;
import com.anji.captcha.util.ImageUtils;
import com.anji.captcha.util.RandomUtils;
-import org.apache.commons.lang3.StringUtils;
+import cn.hutool.core.util.RandomUtil;
+import org.apache.commons.lang3.Strings;
import java.awt.*;
import java.awt.geom.AffineTransform;
@@ -82,7 +82,7 @@ public class PictureWordCaptchaServiceImpl extends AbstractCaptchaService {
// 用户输入的验证码(CaptchaVO 中 没有预留字段,暂时用 pointJson 无需加解密)
String userCode = captchaVO.getPointJson();
- if (!StringUtils.equalsIgnoreCase(code, userCode)) {
+ if (!Strings.CI.equals(code, userCode)) {
afterValidateFail(captchaVO);
return ResponseModel.errorMsg(RepCodeEnum.API_CAPTCHA_COORDINATE_ERROR);
}
@@ -209,4 +209,4 @@ public class PictureWordCaptchaServiceImpl extends AbstractCaptchaService {
return RandomUtil.randomString(CHARACTERS, length);
}
-}
\ No newline at end of file
+}
diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java
index 4ae8b78c6..830b728ed 100644
--- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java
+++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java
@@ -78,7 +78,6 @@ public class AuthRequestFactory {
.keySet()
.stream()
.filter(x -> names.contains(x.toUpperCase()))
- .map(String::toUpperCase)
.collect(Collectors.toList());
}
@@ -318,4 +317,4 @@ public class AuthRequestFactory {
.proxy(new Proxy(Proxy.Type.valueOf(proxyConfig.getType()), new InetSocketAddress(proxyConfig.getHostname(), proxyConfig.getPort())))
.build());
}
-}
\ No newline at end of file
+}
diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
index a931efbc6..22d159a8c 100644
--- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
+++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
@@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
import cn.iocoder.yudao.module.system.api.logger.dto.LoginLogCreateReqDTO;
import cn.iocoder.yudao.module.system.api.sms.SmsCodeApi;
import cn.iocoder.yudao.module.system.api.sms.dto.code.SmsCodeUseReqDTO;
@@ -97,6 +98,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
}
@Override
+ @DataPermission(enable = false)
public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
// 校验验证码
validateCaptcha(reqVO);
@@ -134,7 +136,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@Override
public AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) {
// 校验验证码
- smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP())).checkError();
+ smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP()));
// 获得用户信息
AdminUserDO user = userService.getUserByMobile(reqVO.getMobile());
@@ -296,7 +298,7 @@ public class AdminAuthServiceImpl implements AdminAuthService {
.setMobile(reqVO.getMobile())
.setScene(SmsSceneEnum.ADMIN_MEMBER_RESET_PASSWORD.getScene())
.setUsedIp(getClientIP())
- ).checkError();
+ );
userService.updateUserPassword(userByMobile.getId(), reqVO.getPassword());
}
diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml
index fd68a9ab5..ef77602a8 100644
--- a/yudao-server/src/main/resources/application.yaml
+++ b/yudao-server/src/main/resources/application.yaml
@@ -199,7 +199,8 @@ spring:
azure: # OpenAI 微软
openai:
endpoint: https://eastusprejade.openai.azure.com
- api-key: xxx
+ anthropic: # Anthropic Claude
+ api-key: sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942
ollama:
base-url: http://127.0.0.1:11434
chat:
@@ -207,7 +208,7 @@ spring:
stabilityai:
api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx
dashscope: # 通义千问
- api-key: sk-71800982914041848008480000000000
+ api-key: sk-47aa124781be4bfb95244cc62f6xxxx
minimax: # Minimax:https://www.minimaxi.com/
api-key: xxxx
moonshot: # 月之暗灭(KIMI)
@@ -217,9 +218,30 @@ spring:
chat:
options:
model: deepseek-chat
+ model:
+ rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启
+ mcp:
+ server:
+ enabled: true
+ name: yudao-mcp-server
+ version: 1.0.0
+ instructions: 一个 MCP 示例服务
+ sse-endpoint: /sse
+ client:
+ enabled: true
+ name: mcp
+ sse:
+ connections:
+ filesystem:
+ url: http://127.0.0.1:8089
+ sse-endpoint: /sse
yudao:
ai:
+ gemini: # 谷歌 Gemini
+ enable: true
+ api-key: AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ
+ model: gemini-2.5-flash
doubao: # 字节豆包
enable: true
api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272
@@ -236,7 +258,7 @@ yudao:
enable: true
appKey: 75b161ed2aef4719b275d6e7f2a4d4cd
secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz
- model: generalv3.5
+ model: x1
baichuan: # 百川智能
enable: true
api-key: sk-abc
@@ -251,6 +273,9 @@ yudao:
enable: true
# base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app
base-url: http://127.0.0.1:3001
+ web-search:
+ enable: true
+ api-key: sk-40500e52840f4d24b956d0b1d80d9abe
--- #################### 芋道相关配置 ####################