【同步】BOOT 和 CLOUD 的功能
parent
59429be4df
commit
2503432067
|
@ -17,11 +17,11 @@
|
|||
<revision>2025.08-SNAPSHOT</revision>
|
||||
<flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
|
||||
<!-- 统一依赖管理 -->
|
||||
<spring.boot.version>3.5.4</spring.boot.version>
|
||||
<spring.boot.version>3.5.5</spring.boot.version>
|
||||
<spring.cloud.version>2025.0.0</spring.cloud.version>
|
||||
<spring.cloud.alibaba.version>2023.0.3.3</spring.cloud.alibaba.version>
|
||||
<!-- Web 相关 -->
|
||||
<springdoc.version>2.8.9</springdoc.version>
|
||||
<springdoc.version>2.8.11</springdoc.version>
|
||||
<knife4j.version>4.5.0</knife4j.version>
|
||||
<!-- DB 相关 -->
|
||||
<druid.version>1.2.27</druid.version>
|
||||
|
@ -30,11 +30,11 @@
|
|||
<mybatis-plus-join.version>1.5.4</mybatis-plus-join.version>
|
||||
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
|
||||
<easy-trans.version>3.0.6</easy-trans.version>
|
||||
<redisson.version>3.50.0</redisson.version>
|
||||
<redisson.version>3.51.0</redisson.version>
|
||||
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
|
||||
<kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
|
||||
<opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
|
||||
<taos.version>3.3.3</taos.version>
|
||||
<taos.version>3.7.3</taos.version>
|
||||
<!-- 消息队列 -->
|
||||
<rocketmq-spring.version>2.3.4</rocketmq-spring.version>
|
||||
<!-- RPC 相关 -->
|
||||
|
@ -55,12 +55,12 @@
|
|||
<flowable.version>7.0.1</flowable.version>
|
||||
<!-- 工具类相关 -->
|
||||
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
|
||||
<jsoup.version>1.21.1</jsoup.version>
|
||||
<jsoup.version>1.21.2</jsoup.version>
|
||||
<lombok.version>1.18.38</lombok.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<hutool-5.version>5.8.39</hutool-5.version>
|
||||
<hutool-5.version>5.8.40</hutool-5.version>
|
||||
<hutool-6.version>6.0.0-M22</hutool-6.version>
|
||||
<fastexcel.version>1.2.0</fastexcel.version>
|
||||
<fastexcel.version>1.3.0</fastexcel.version>
|
||||
<velocity.version>2.4.1</velocity.version>
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
<guava.version>33.4.8-jre</guava.version>
|
||||
|
@ -82,7 +82,7 @@
|
|||
<justauth-starter.version>1.4.0</justauth-starter.version>
|
||||
<jimureport.version>2.0.0</jimureport.version>
|
||||
<jimubi.version>1.9.5</jimubi.version>
|
||||
<weixin-java.version>4.7.5.B</weixin-java.version>
|
||||
<weixin-java.version>4.7.7-20250808.182223</weixin-java.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
|
|
|
@ -75,7 +75,7 @@ public class TenantDatabaseInterceptor implements TenantLineHandler {
|
|||
if (TenantBaseDO.class.isAssignableFrom(tableInfo.getEntityType())) {
|
||||
return false;
|
||||
}
|
||||
// 如果添加了 @TenantIgnore 注解,显然也不忽略租户
|
||||
// 如果添加了 @TenantIgnore 注解,则忽略租户
|
||||
TenantIgnore tenantIgnore = tableInfo.getEntityType().getAnnotation(TenantIgnore.class);
|
||||
return tenantIgnore != null;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
|
|||
public class TenantRabbitMQInitializer implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof RabbitTemplate) {
|
||||
RabbitTemplate rabbitTemplate = (RabbitTemplate) bean;
|
||||
|
@ -20,4 +21,4 @@ public class TenantRabbitMQInitializer implements BeanPostProcessor {
|
|||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
|
|||
public class TenantRocketMQInitializer implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof DefaultRocketMQListenerContainer) {
|
||||
DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean;
|
||||
|
@ -50,4 +51,4 @@ public class TenantRocketMQInitializer implements BeanPostProcessor {
|
|||
consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ public class YudaoAsyncAutoConfiguration {
|
|||
return new BeanPostProcessor() {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
|
||||
// 处理 ThreadPoolTaskExecutor
|
||||
if (bean instanceof ThreadPoolTaskExecutor) {
|
||||
|
|
|
@ -42,6 +42,8 @@ public class YudaoMybatisAutoConfiguration {
|
|||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
|
||||
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
|
||||
// ↓↓↓ 按需开启,可能会影响到 updateBatch 的地方:例如说文件配置管理 ↓↓↓
|
||||
// mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 拦截没有指定条件的 update 和 delete 语句
|
||||
return mybatisPlusInterceptor;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.mybatis.core.handler;
|
|||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
|
||||
|
@ -19,6 +18,7 @@ import java.util.Objects;
|
|||
public class DefaultDBFieldHandler implements MetaObjectHandler {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
|
||||
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
|
||||
|
|
|
@ -182,6 +182,7 @@ public class GlobalExceptionHandler {
|
|||
* 例如说,接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String
|
||||
*/
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) {
|
||||
log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex);
|
||||
if (ex.getCause() instanceof InvalidFormatException) {
|
||||
|
|
|
@ -148,6 +148,7 @@ public class WebFrameworkUtils {
|
|||
return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT);
|
||||
}
|
||||
|
||||
@SuppressWarnings("PatternVariableCanBeUsed")
|
||||
public static HttpServletRequest getRequest() {
|
||||
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
if (!(requestAttributes instanceof ServletRequestAttributes)) {
|
||||
|
|
|
@ -184,6 +184,10 @@ spring:
|
|||
- Path=/admin-api/ai/**
|
||||
filters:
|
||||
- RewritePath=/admin-api/ai/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
|
||||
- id: ai-mcp-server # 路由的编号(MCP Server)
|
||||
uri: grayLb://ai-server
|
||||
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
|
||||
- Path=/sse, /mcp/message
|
||||
## iot-server 服务
|
||||
- id: iot-admin-api # 路由的编号
|
||||
uri: grayLb://iot-server
|
||||
|
|
|
@ -33,6 +33,8 @@ public enum AiPlatformEnum implements ArrayValuable<String> {
|
|||
|
||||
OPENAI("OpenAI", "OpenAI"), // OpenAI 官方
|
||||
AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软
|
||||
ANTHROPIC("Anthropic", "Anthropic"), // Anthropic Claude
|
||||
GEMINI("Gemini", "Gemini"), // 谷歌 Gemini
|
||||
OLLAMA("Ollama", "Ollama"),
|
||||
|
||||
STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>yudao-module-ai-server</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>
|
||||
|
@ -18,8 +19,8 @@
|
|||
国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
|
||||
</description>
|
||||
<properties>
|
||||
<spring-ai.version>1.0.0</spring-ai.version>
|
||||
<alibaba-ai.version>1.0.0.2</alibaba-ai.version>
|
||||
<spring-ai.version>1.0.1</spring-ai.version>
|
||||
<alibaba-ai.version>1.0.0.3</alibaba-ai.version>
|
||||
<tinyflow.version>1.0.2</tinyflow.version>
|
||||
</properties>
|
||||
|
||||
|
@ -119,6 +120,11 @@
|
|||
<artifactId>spring-ai-starter-model-azure-openai</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-anthropic</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-deepseek</artifactId>
|
||||
|
@ -217,6 +223,24 @@
|
|||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- MCP 相关 -->
|
||||
<!--
|
||||
特殊说明:不能使用 spring-ai-starter-mcp-server-webflux 或 spring-ai-starter-mcp-client-webflux !!!
|
||||
原因:项目使用了 SpringMVC,而不是 WebFlux。引入上述 2 个,会导致 SSE Server 失效。
|
||||
-->
|
||||
<dependency>
|
||||
<!-- 服务端 -->
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- 客户端 -->
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-client</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- TinyFlow:AI 工作流 -->
|
||||
<dependency>
|
||||
<groupId>dev.tinyflow</groupId>
|
||||
|
|
|
@ -20,9 +20,46 @@ tenant-id: {{adminTenantId}}
|
|||
"content": "1+1=?"
|
||||
}
|
||||
|
||||
### 获得指定对话的消息列表
|
||||
GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649
|
||||
### 发送消息(流式)【带文件】
|
||||
POST {{baseUrl}}/ai/chat/message/send-stream
|
||||
Content-Type: application/json
|
||||
Authorization: {{token}}
|
||||
tenant-id: {{adminTenantId}}
|
||||
|
||||
{
|
||||
"conversationId": "1781604279872581797",
|
||||
"content": "图片里有什么?",
|
||||
"attachmentUrls": ["http://test.yudao.iocoder.cn/1755531278.jpeg"]
|
||||
}
|
||||
|
||||
### 发送消息(流式)【追问带文件】
|
||||
POST {{baseUrl}}/ai/chat/message/send-stream
|
||||
Content-Type: application/json
|
||||
Authorization: {{token}}
|
||||
tenant-id: {{adminTenantId}}
|
||||
|
||||
{
|
||||
"conversationId": "1781604279872581799",
|
||||
"content": "说下图片里,有哪些字?",
|
||||
"useContext": true
|
||||
}
|
||||
|
||||
### 发送消息(流式)【联网搜索】
|
||||
POST {{baseUrl}}/ai/chat/message/send-stream
|
||||
Content-Type: application/json
|
||||
Authorization: {{token}}
|
||||
tenant-id: {{adminTenantId}}
|
||||
|
||||
{
|
||||
"conversationId": "1781604279872581799",
|
||||
"content": "今天是周几?",
|
||||
"useSearch": true
|
||||
}
|
||||
|
||||
### 获得指定对话的消息列表
|
||||
GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581799
|
||||
Authorization: {{token}}
|
||||
tenant-id: {{adminTenantId}}
|
||||
|
||||
### 删除消息
|
||||
DELETE {{baseUrl}}/ai/chat/message/delete?id=50
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
|
||||
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
|
@ -37,6 +38,9 @@ public class AiChatMessageRespVO {
|
|||
@Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "推理内容", example = "要达到这个目标,你需要...")
|
||||
private String reasoningContent;
|
||||
|
||||
@Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
private Boolean useContext;
|
||||
|
||||
|
@ -46,6 +50,12 @@ public class AiChatMessageRespVO {
|
|||
@Schema(description = "知识库段落数组")
|
||||
private List<KnowledgeSegment> segments;
|
||||
|
||||
@Schema(description = "联网搜索的网页内容数组")
|
||||
private List<AiWebSearchResponse.WebPage> webSearchPages;
|
||||
|
||||
@Schema(description = "附件 URL 数组", example = "https://www.iocoder.cn/1.png")
|
||||
private List<String> attachmentUrls;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
|
|||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Schema(description = "管理后台 - AI 聊天消息发送 Request VO")
|
||||
@Data
|
||||
|
@ -22,4 +22,10 @@ public class AiChatMessageSendReqVO {
|
|||
@Schema(description = "是否携带上下文", example = "true")
|
||||
private Boolean useContext;
|
||||
|
||||
@Schema(description = "是否联网搜索", example = "true")
|
||||
private Boolean useSearch;
|
||||
|
||||
@Schema(description = "附件 URL 数组", example = "https://www.iocoder.cn/1.png")
|
||||
private List<String> attachmentUrls;
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message;
|
||||
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
|
@ -29,12 +30,18 @@ public class AiChatMessageSendRespVO {
|
|||
@Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊")
|
||||
private String content;
|
||||
|
||||
@Schema(description = "推理内容", example = "要达到这个目标,你需要...")
|
||||
private String reasoningContent;
|
||||
|
||||
@Schema(description = "知识库段落编号数组", example = "[1,2,3]")
|
||||
private List<Long> segmentIds;
|
||||
|
||||
@Schema(description = "知识库段落数组")
|
||||
private List<AiChatMessageRespVO.KnowledgeSegment> segments;
|
||||
|
||||
@Schema(description = "联网搜索的网页内容数组")
|
||||
private List<AiWebSearchResponse.WebPage> webSearchPages;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
|
|
|
@ -52,6 +52,9 @@ public class AiChatRoleRespVO implements VO {
|
|||
@Schema(description = "引用的工具编号列表", example = "1,2,3")
|
||||
private List<Long> toolIds;
|
||||
|
||||
@Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
|
||||
private List<String> mcpClientNames;
|
||||
|
||||
@Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Boolean publicStatus;
|
||||
|
||||
|
|
|
@ -37,4 +37,7 @@ public class AiChatRoleSaveMyReqVO {
|
|||
@Schema(description = "引用的工具编号列表", example = "1,2,3")
|
||||
private List<Long> toolIds;
|
||||
|
||||
@Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
|
||||
private List<String> mcpClientNames;
|
||||
|
||||
}
|
|
@ -50,6 +50,9 @@ public class AiChatRoleSaveReqVO {
|
|||
@Schema(description = "引用的工具编号列表", example = "1,2,3")
|
||||
private List<Long> toolIds;
|
||||
|
||||
@Schema(description = "引用的 MCP Client 名字列表", example = "filesystem")
|
||||
private List<String> mcpClientNames;
|
||||
|
||||
@Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@NotNull(message = "是否公开不能为空")
|
||||
private Boolean publicStatus;
|
||||
|
|
|
@ -2,14 +2,20 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
|
|||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.*;
|
||||
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.ai.chat.messages.MessageType;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -87,6 +93,10 @@ public class AiChatMessageDO extends BaseDO {
|
|||
* 聊天内容
|
||||
*/
|
||||
private String content;
|
||||
/**
|
||||
* 推理内容
|
||||
*/
|
||||
private String reasoningContent;
|
||||
|
||||
/**
|
||||
* 是否携带上下文
|
||||
|
@ -101,4 +111,16 @@ public class AiChatMessageDO extends BaseDO {
|
|||
@TableField(typeHandler = LongListTypeHandler.class)
|
||||
private List<Long> segmentIds;
|
||||
|
||||
/**
|
||||
* 联网搜索的网页内容数组
|
||||
*/
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<AiWebSearchResponse.WebPage> webSearchPages;
|
||||
|
||||
/**
|
||||
* 附件 URL 数组
|
||||
*/
|
||||
@TableField(typeHandler = StringListTypeHandler.class)
|
||||
private List<String> attachmentUrls;
|
||||
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model;
|
|||
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
|
||||
import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
|
||||
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
|
@ -80,6 +81,13 @@ public class AiChatRoleDO extends BaseDO {
|
|||
*/
|
||||
@TableField(typeHandler = LongListTypeHandler.class)
|
||||
private List<Long> toolIds;
|
||||
/**
|
||||
* 引用的 MCP Client 名字列表
|
||||
*
|
||||
* 关联 spring.ai.mcp.client 下的名字
|
||||
*/
|
||||
@TableField(typeHandler = StringListTypeHandler.class)
|
||||
private List<String> mcpClientNames;
|
||||
|
||||
/**
|
||||
* 是否公开
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package cn.iocoder.yudao.module.ai.dal.dataobject.model;
|
||||
|
||||
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||
import cn.iocoder.yudao.module.ai.service.model.tool.DirectoryListToolFunction;
|
||||
import cn.iocoder.yudao.module.ai.service.model.tool.WeatherQueryToolFunction;
|
||||
import cn.iocoder.yudao.module.ai.tool.function.DirectoryListToolFunction;
|
||||
import cn.iocoder.yudao.module.ai.tool.function.WeatherQueryToolFunction;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
|
|
|
@ -2,25 +2,34 @@ package cn.iocoder.yudao.module.ai.framework.ai.config;
|
|||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactoryImpl;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactoryImpl;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient;
|
||||
import cn.iocoder.yudao.module.ai.tool.method.PersonService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatModel;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatOptions;
|
||||
import org.springframework.ai.deepseek.api.DeepSeekApi;
|
||||
import org.springframework.ai.embedding.BatchingStrategy;
|
||||
import org.springframework.ai.embedding.TokenCountBatchingStrategy;
|
||||
import org.springframework.ai.model.tool.ToolCallingManager;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.ai.openai.OpenAiChatOptions;
|
||||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.support.ToolCallbacks;
|
||||
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
|
||||
import org.springframework.ai.tokenizer.TokenCountEstimator;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
|
||||
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
|
||||
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
|
||||
|
@ -30,6 +39,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
|
|||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 芋道 AI 自动配置
|
||||
*
|
||||
|
@ -51,20 +62,49 @@ public class AiAutoConfiguration {
|
|||
|
||||
// ========== 各种 AI Client 创建 ==========
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "yudao.ai.gemini.enable", havingValue = "true")
|
||||
public GeminiChatModel geminiChatModel(YudaoAiProperties yudaoAiProperties) {
|
||||
YudaoAiProperties.Gemini properties = yudaoAiProperties.getGemini();
|
||||
return buildGeminiChatClient(properties);
|
||||
}
|
||||
|
||||
public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.Gemini properties) {
|
||||
if (StrUtil.isEmpty(properties.getModel())) {
|
||||
properties.setModel(GeminiChatModel.MODEL_DEFAULT);
|
||||
}
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
.baseUrl(GeminiChatModel.BASE_URL)
|
||||
.completionsPath(GeminiChatModel.COMPLETE_PATH)
|
||||
.apiKey(properties.getApiKey())
|
||||
.build())
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.model(properties.getModel())
|
||||
.temperature(properties.getTemperature())
|
||||
.maxTokens(properties.getMaxTokens())
|
||||
.topP(properties.getTopP())
|
||||
.build())
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
.build();
|
||||
return new GeminiChatModel(openAiChatModel);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true")
|
||||
public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) {
|
||||
YudaoAiProperties.DouBaoProperties properties = yudaoAiProperties.getDoubao();
|
||||
YudaoAiProperties.DouBao properties = yudaoAiProperties.getDoubao();
|
||||
return buildDouBaoChatClient(properties);
|
||||
}
|
||||
|
||||
public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBaoProperties properties) {
|
||||
public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBao properties) {
|
||||
if (StrUtil.isEmpty(properties.getModel())) {
|
||||
properties.setModel(DouBaoChatModel.MODEL_DEFAULT);
|
||||
}
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
.baseUrl(DouBaoChatModel.BASE_URL)
|
||||
.completionsPath(DouBaoChatModel.COMPLETE_PATH)
|
||||
.apiKey(properties.getApiKey())
|
||||
.build())
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
|
@ -81,20 +121,20 @@ public class AiAutoConfiguration {
|
|||
@Bean
|
||||
@ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true")
|
||||
public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) {
|
||||
YudaoAiProperties.SiliconFlowProperties properties = yudaoAiProperties.getSiliconflow();
|
||||
YudaoAiProperties.SiliconFlow properties = yudaoAiProperties.getSiliconflow();
|
||||
return buildSiliconFlowChatClient(properties);
|
||||
}
|
||||
|
||||
public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlowProperties properties) {
|
||||
public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlow properties) {
|
||||
if (StrUtil.isEmpty(properties.getModel())) {
|
||||
properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT);
|
||||
}
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
|
||||
.deepSeekApi(DeepSeekApi.builder()
|
||||
.baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL)
|
||||
.apiKey(properties.getApiKey())
|
||||
.build())
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.defaultOptions(DeepSeekChatOptions.builder()
|
||||
.model(properties.getModel())
|
||||
.temperature(properties.getTemperature())
|
||||
.maxTokens(properties.getMaxTokens())
|
||||
|
@ -108,11 +148,11 @@ public class AiAutoConfiguration {
|
|||
@Bean
|
||||
@ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true")
|
||||
public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) {
|
||||
YudaoAiProperties.HunYuanProperties properties = yudaoAiProperties.getHunyuan();
|
||||
YudaoAiProperties.HunYuan properties = yudaoAiProperties.getHunyuan();
|
||||
return buildHunYuanChatClient(properties);
|
||||
}
|
||||
|
||||
public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuanProperties properties) {
|
||||
public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuan properties) {
|
||||
if (StrUtil.isEmpty(properties.getModel())) {
|
||||
properties.setModel(HunYuanChatModel.MODEL_DEFAULT);
|
||||
}
|
||||
|
@ -122,13 +162,14 @@ public class AiAutoConfiguration {
|
|||
StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL
|
||||
: HunYuanChatModel.BASE_URL);
|
||||
}
|
||||
// 创建 OpenAiChatModel、HunYuanChatModel 对象
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
// 创建 DeepSeekChatModel、HunYuanChatModel 对象
|
||||
DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder()
|
||||
.deepSeekApi(DeepSeekApi.builder()
|
||||
.baseUrl(properties.getBaseUrl())
|
||||
.completionsPath(HunYuanChatModel.COMPLETE_PATH)
|
||||
.apiKey(properties.getApiKey())
|
||||
.build())
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.defaultOptions(DeepSeekChatOptions.builder()
|
||||
.model(properties.getModel())
|
||||
.temperature(properties.getTemperature())
|
||||
.maxTokens(properties.getMaxTokens())
|
||||
|
@ -142,25 +183,30 @@ public class AiAutoConfiguration {
|
|||
@Bean
|
||||
@ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true")
|
||||
public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) {
|
||||
YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo();
|
||||
YudaoAiProperties.XingHuo properties = yudaoAiProperties.getXinghuo();
|
||||
return buildXingHuoChatClient(properties);
|
||||
}
|
||||
|
||||
public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuoProperties properties) {
|
||||
public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuo properties) {
|
||||
if (StrUtil.isEmpty(properties.getModel())) {
|
||||
properties.setModel(XingHuoChatModel.MODEL_DEFAULT);
|
||||
}
|
||||
OpenAiApi.Builder builder = OpenAiApi.builder()
|
||||
.baseUrl(XingHuoChatModel.BASE_URL_V1)
|
||||
.apiKey(properties.getAppKey() + ":" + properties.getSecretKey());
|
||||
if ("x1".equals(properties.getModel())) {
|
||||
builder.baseUrl(XingHuoChatModel.BASE_URL_V2)
|
||||
.completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2);
|
||||
}
|
||||
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
|
||||
.openAiApi(OpenAiApi.builder()
|
||||
.baseUrl(XingHuoChatModel.BASE_URL)
|
||||
.apiKey(properties.getAppKey() + ":" + properties.getSecretKey())
|
||||
.build())
|
||||
.openAiApi(builder.build())
|
||||
.defaultOptions(OpenAiChatOptions.builder()
|
||||
.model(properties.getModel())
|
||||
.temperature(properties.getTemperature())
|
||||
.maxTokens(properties.getMaxTokens())
|
||||
.topP(properties.getTopP())
|
||||
.build())
|
||||
// TODO @芋艿:星火的 function call 有 bug,会报 ToolResponseMessage must have an id 错误!!!
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
.build();
|
||||
return new XingHuoChatModel(openAiChatModel);
|
||||
|
@ -169,11 +215,11 @@ public class AiAutoConfiguration {
|
|||
@Bean
|
||||
@ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true")
|
||||
public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) {
|
||||
YudaoAiProperties.BaiChuanProperties properties = yudaoAiProperties.getBaichuan();
|
||||
YudaoAiProperties.BaiChuan properties = yudaoAiProperties.getBaichuan();
|
||||
return buildBaiChuanChatClient(properties);
|
||||
}
|
||||
|
||||
public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuanProperties properties) {
|
||||
public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuan properties) {
|
||||
if (StrUtil.isEmpty(properties.getModel())) {
|
||||
properties.setModel(BaiChuanChatModel.MODEL_DEFAULT);
|
||||
}
|
||||
|
@ -196,7 +242,7 @@ public class AiAutoConfiguration {
|
|||
@Bean
|
||||
@ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true")
|
||||
public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) {
|
||||
YudaoAiProperties.MidjourneyProperties config = yudaoAiProperties.getMidjourney();
|
||||
YudaoAiProperties.Midjourney config = yudaoAiProperties.getMidjourney();
|
||||
return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl());
|
||||
}
|
||||
|
||||
|
@ -222,4 +268,22 @@ public class AiAutoConfiguration {
|
|||
return SpringUtil.getBean(ToolCallingManager.class);
|
||||
}
|
||||
|
||||
// ========== Web Search 相关 ==========
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "yudao.ai.web-search.enable", havingValue = "true")
|
||||
public AiWebSearchClient webSearchClient(YudaoAiProperties yudaoAiProperties) {
|
||||
return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey());
|
||||
}
|
||||
|
||||
// ========== MCP 相关 ==========
|
||||
|
||||
/**
|
||||
* 参考自 <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">MCP Server Boot Starter</>
|
||||
*/
|
||||
@Bean
|
||||
public List<ToolCallback> toolCallbacks(PersonService personService) {
|
||||
return List.of(ToolCallbacks.from(personService));
|
||||
}
|
||||
|
||||
}
|
|
@ -13,49 +13,54 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||
@Data
|
||||
public class YudaoAiProperties {
|
||||
|
||||
/**
|
||||
* 谷歌 Gemini
|
||||
*/
|
||||
private Gemini gemini;
|
||||
|
||||
/**
|
||||
* 字节豆包
|
||||
*/
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
private DouBaoProperties doubao;
|
||||
private DouBao doubao;
|
||||
|
||||
/**
|
||||
* 腾讯混元
|
||||
*/
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
private HunYuanProperties hunyuan;
|
||||
private HunYuan hunyuan;
|
||||
|
||||
/**
|
||||
* 硅基流动
|
||||
*/
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
private SiliconFlowProperties siliconflow;
|
||||
private SiliconFlow siliconflow;
|
||||
|
||||
/**
|
||||
* 讯飞星火
|
||||
*/
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
private XingHuoProperties xinghuo;
|
||||
private XingHuo xinghuo;
|
||||
|
||||
/**
|
||||
* 百川
|
||||
*/
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
private BaiChuanProperties baichuan;
|
||||
private BaiChuan baichuan;
|
||||
|
||||
/**
|
||||
* Midjourney 绘图
|
||||
*/
|
||||
private MidjourneyProperties midjourney;
|
||||
private Midjourney midjourney;
|
||||
|
||||
/**
|
||||
* Suno 音乐
|
||||
*/
|
||||
@SuppressWarnings("SpellCheckingInspection")
|
||||
private SunoProperties suno;
|
||||
private Suno suno;
|
||||
|
||||
/**
|
||||
* 网络搜索
|
||||
*/
|
||||
private WebSearch webSearch;
|
||||
|
||||
@Data
|
||||
public static class DouBaoProperties {
|
||||
public static class Gemini {
|
||||
|
||||
private String enable;
|
||||
private String apiKey;
|
||||
|
@ -68,7 +73,20 @@ public class YudaoAiProperties {
|
|||
}
|
||||
|
||||
@Data
|
||||
public static class HunYuanProperties {
|
||||
public static class DouBao {
|
||||
|
||||
private String enable;
|
||||
private String apiKey;
|
||||
|
||||
private String model;
|
||||
private Double temperature;
|
||||
private Integer maxTokens;
|
||||
private Double topP;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class HunYuan {
|
||||
|
||||
private String enable;
|
||||
private String baseUrl;
|
||||
|
@ -82,7 +100,7 @@ public class YudaoAiProperties {
|
|||
}
|
||||
|
||||
@Data
|
||||
public static class SiliconFlowProperties {
|
||||
public static class SiliconFlow {
|
||||
|
||||
private String enable;
|
||||
private String apiKey;
|
||||
|
@ -95,7 +113,7 @@ public class YudaoAiProperties {
|
|||
}
|
||||
|
||||
@Data
|
||||
public static class XingHuoProperties {
|
||||
public static class XingHuo {
|
||||
|
||||
private String enable;
|
||||
private String appId;
|
||||
|
@ -110,7 +128,7 @@ public class YudaoAiProperties {
|
|||
}
|
||||
|
||||
@Data
|
||||
public static class BaiChuanProperties {
|
||||
public static class BaiChuan {
|
||||
|
||||
private String enable;
|
||||
private String apiKey;
|
||||
|
@ -123,7 +141,7 @@ public class YudaoAiProperties {
|
|||
}
|
||||
|
||||
@Data
|
||||
public static class MidjourneyProperties {
|
||||
public static class Midjourney {
|
||||
|
||||
private String enable;
|
||||
private String baseUrl;
|
||||
|
@ -134,12 +152,21 @@ public class YudaoAiProperties {
|
|||
}
|
||||
|
||||
@Data
|
||||
public static class SunoProperties {
|
||||
public static class Suno {
|
||||
|
||||
private boolean enable = false;
|
||||
private boolean enable;
|
||||
|
||||
private String baseUrl;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class WebSearch {
|
||||
|
||||
private boolean enable;
|
||||
|
||||
private String apiKey;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package cn.iocoder.yudao.module.ai.framework.ai.core;
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.model;
|
||||
|
||||
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
|
|
@ -1,4 +1,4 @@
|
|||
package cn.iocoder.yudao.module.ai.framework.ai.core;
|
||||
package cn.iocoder.yudao.module.ai.framework.ai.core.model;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
|
@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration;
|
|||
import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants;
|
||||
|
@ -67,6 +68,7 @@ import org.springframework.ai.minimax.MiniMaxChatOptions;
|
|||
import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
|
||||
import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
|
||||
import org.springframework.ai.minimax.api.MiniMaxApi;
|
||||
import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration;
|
||||
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration;
|
||||
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration;
|
||||
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties;
|
||||
|
@ -93,6 +95,8 @@ import org.springframework.ai.openai.OpenAiImageModel;
|
|||
import org.springframework.ai.openai.api.OpenAiApi;
|
||||
import org.springframework.ai.openai.api.OpenAiImageApi;
|
||||
import org.springframework.ai.openai.api.common.OpenAiApiConstants;
|
||||
import org.springframework.ai.anthropic.AnthropicChatModel;
|
||||
import org.springframework.ai.anthropic.api.AnthropicApi;
|
||||
import org.springframework.ai.stabilityai.StabilityAiImageModel;
|
||||
import org.springframework.ai.stabilityai.api.StabilityAiApi;
|
||||
import org.springframework.ai.vectorstore.SimpleVectorStore;
|
||||
|
@ -168,6 +172,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
return buildOpenAiChatModel(apiKey, url);
|
||||
case AZURE_OPENAI:
|
||||
return buildAzureOpenAiChatModel(apiKey, url);
|
||||
case ANTHROPIC:
|
||||
return buildAnthropicChatModel(apiKey, url);
|
||||
case GEMINI:
|
||||
return buildGeminiChatModel(apiKey);
|
||||
case OLLAMA:
|
||||
return buildOllamaChatModel(url);
|
||||
default:
|
||||
|
@ -206,6 +214,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
return SpringUtil.getBean(OpenAiChatModel.class);
|
||||
case AZURE_OPENAI:
|
||||
return SpringUtil.getBean(AzureOpenAiChatModel.class);
|
||||
case ANTHROPIC:
|
||||
return SpringUtil.getBean(AnthropicChatModel.class);
|
||||
case GEMINI:
|
||||
return SpringUtil.getBean(GeminiChatModel.class);
|
||||
case OLLAMA:
|
||||
return SpringUtil.getBean(OllamaChatModel.class);
|
||||
default:
|
||||
|
@ -260,7 +272,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey,
|
||||
url);
|
||||
return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> {
|
||||
YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class)
|
||||
YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class)
|
||||
.getMidjourney();
|
||||
return new MidjourneyApi(url, apiKey, properties.getNotifyUrl());
|
||||
});
|
||||
|
@ -347,7 +359,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
* 可参考 {@link DashScopeImageAutoConfiguration} 的 dashScopeImageModel 方法
|
||||
*/
|
||||
private static DashScopeImageModel buildTongYiImagesModel(String key) {
|
||||
DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key);
|
||||
DashScopeImageApi dashScopeImageApi = DashScopeImageApi.builder().apiKey(key).build();
|
||||
return DashScopeImageModel.builder()
|
||||
.dashScopeApi(dashScopeImageApi)
|
||||
.build();
|
||||
|
@ -397,7 +409,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
* 可参考 {@link AiAutoConfiguration#douBaoChatClient(YudaoAiProperties)}
|
||||
*/
|
||||
private ChatModel buildDouBaoChatModel(String apiKey) {
|
||||
YudaoAiProperties.DouBaoProperties properties = new YudaoAiProperties.DouBaoProperties()
|
||||
YudaoAiProperties.DouBao properties = new YudaoAiProperties.DouBao()
|
||||
.setApiKey(apiKey);
|
||||
return new AiAutoConfiguration().buildDouBaoChatClient(properties);
|
||||
}
|
||||
|
@ -406,7 +418,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
* 可参考 {@link AiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)}
|
||||
*/
|
||||
private ChatModel buildHunYuanChatModel(String apiKey, String url) {
|
||||
YudaoAiProperties.HunYuanProperties properties = new YudaoAiProperties.HunYuanProperties()
|
||||
YudaoAiProperties.HunYuan properties = new YudaoAiProperties.HunYuan()
|
||||
.setBaseUrl(url).setApiKey(apiKey);
|
||||
return new AiAutoConfiguration().buildHunYuanChatClient(properties);
|
||||
}
|
||||
|
@ -415,7 +427,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
* 可参考 {@link AiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)}
|
||||
*/
|
||||
private ChatModel buildSiliconFlowChatModel(String apiKey) {
|
||||
YudaoAiProperties.SiliconFlowProperties properties = new YudaoAiProperties.SiliconFlowProperties()
|
||||
YudaoAiProperties.SiliconFlow properties = new YudaoAiProperties.SiliconFlow()
|
||||
.setApiKey(apiKey);
|
||||
return new AiAutoConfiguration().buildSiliconFlowChatClient(properties);
|
||||
}
|
||||
|
@ -473,7 +485,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
private static XingHuoChatModel buildXingHuoChatModel(String key) {
|
||||
List<String> keys = StrUtil.split(key, '|');
|
||||
Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式");
|
||||
YudaoAiProperties.XingHuoProperties properties = new YudaoAiProperties.XingHuoProperties()
|
||||
YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo()
|
||||
.setAppKey(keys.get(0)).setSecretKey(keys.get(1));
|
||||
return new AiAutoConfiguration().buildXingHuoChatClient(properties);
|
||||
}
|
||||
|
@ -482,7 +494,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
* 可参考 {@link AiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)}
|
||||
*/
|
||||
private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) {
|
||||
YudaoAiProperties.BaiChuanProperties properties = new YudaoAiProperties.BaiChuanProperties()
|
||||
YudaoAiProperties.BaiChuan properties = new YudaoAiProperties.BaiChuan()
|
||||
.setApiKey(apiKey);
|
||||
return new AiAutoConfiguration().buildBaiChuanChatClient(properties);
|
||||
}
|
||||
|
@ -512,6 +524,30 @@ public class AiModelFactoryImpl implements AiModelFactory {
|
|||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link AnthropicChatAutoConfiguration} 的 anthropicApi 方法
|
||||
*/
|
||||
private static AnthropicChatModel buildAnthropicChatModel(String apiKey, String url) {
|
||||
AnthropicApi.Builder builder = AnthropicApi.builder().apiKey(apiKey);
|
||||
if (StrUtil.isNotEmpty(url)) {
|
||||
builder.baseUrl(url);
|
||||
}
|
||||
AnthropicApi anthropicApi = builder.build();
|
||||
return AnthropicChatModel.builder()
|
||||
.anthropicApi(anthropicApi)
|
||||
.toolCallingManager(getToolCallingManager())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link AiAutoConfiguration#buildGeminiChatClient(YudaoAiProperties.Gemini)}
|
||||
*/
|
||||
private static GeminiChatModel buildGeminiChatModel(String apiKey) {
|
||||
YudaoAiProperties.Gemini properties = SpringUtil.getBean(YudaoAiProperties.class)
|
||||
.getGemini().setApiKey(apiKey);
|
||||
return new AiAutoConfiguration().buildGeminiChatClient(properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法
|
||||
*/
|
|
@ -0,0 +1,174 @@
|
|||
# AiBoChaWebSearchClient 使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
`AiBoChaWebSearchClient` 是基于博查AI开放平台提供的网页搜索服务的Java客户端,实现了符合项目架构风格的HTTP客户端封装。
|
||||
|
||||
## 特性
|
||||
|
||||
- **统一的API调用风格**:参考 SunoApi 和 XunFeiPptApi 的实现方式
|
||||
- **Record 类型数据结构**:使用 Record 类型定义请求和响应数据
|
||||
- **简洁的响应数据模型**:包含网页搜索结果
|
||||
- **灵活的搜索配置**:支持时间范围、域名过滤、结果数量等参数
|
||||
- **错误处理机制**:统一的异常处理和日志记录
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 创建客户端实例
|
||||
|
||||
```java
|
||||
// 使用默认base URL
|
||||
AiBoChaWebSearchClient client = new AiBoChaWebSearchClient("your-api-key");
|
||||
|
||||
// 使用自定义base URL
|
||||
AiBoChaWebSearchClient client = new AiBoChaWebSearchClient("https://custom.api.com", "your-api-key");
|
||||
```
|
||||
|
||||
### 2. 基本搜索
|
||||
|
||||
```java
|
||||
// 基本搜索
|
||||
WebSearchRequest request = new WebSearchRequest(
|
||||
"Spring Boot 教程",
|
||||
null, null, null, null, null
|
||||
);
|
||||
AiWebSearchResponse result = client.search(request);
|
||||
```
|
||||
|
||||
### 3. 高级搜索
|
||||
|
||||
```java
|
||||
// 构建详细的搜索请求
|
||||
WebSearchRequest request = new WebSearchRequest(
|
||||
"人工智能最新进展",
|
||||
FreshnessType.ONE_WEEK.getValue(), // 搜索一周内的内容
|
||||
true, // 显示摘要
|
||||
"zhihu.com|csdn.net", // 只搜索指定域名
|
||||
"spam.com", // 排除指定域名
|
||||
20 // 返回20条结果
|
||||
);
|
||||
|
||||
AiWebSearchResponse result = client.search(request);
|
||||
```
|
||||
|
||||
## API参数说明
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| query | String | 是 | 用户的搜索词 |
|
||||
| freshness | String | 否 | 搜索时间范围,默认为 noLimit |
|
||||
| summary | Boolean | 否 | 是否显示文本摘要,默认为 false |
|
||||
| include | String | 否 | 指定搜索的网站范围,多个域名使用\|或,分隔 |
|
||||
| exclude | String | 否 | 排除搜索的网站范围,多个域名使用\|或,分隔 |
|
||||
| count | Integer | 否 | 返回结果条数,范围1-50,默认为10 |
|
||||
|
||||
### 时间范围选项
|
||||
|
||||
使用 `FreshnessType` 枚举:
|
||||
|
||||
```java
|
||||
FreshnessType.NO_LIMIT // 不限(默认)
|
||||
FreshnessType.ONE_DAY // 一天内
|
||||
FreshnessType.ONE_WEEK // 一周内
|
||||
FreshnessType.ONE_MONTH // 一个月内
|
||||
FreshnessType.ONE_YEAR // 一年内
|
||||
```
|
||||
|
||||
也可以使用自定义日期范围:
|
||||
- 日期范围:`"2025-01-01..2025-04-06"`
|
||||
- 指定日期:`"2025-04-06"`
|
||||
|
||||
### 响应数据结构
|
||||
|
||||
```java
|
||||
// 主要响应数据
|
||||
AiWebSearchResponse result = client.search(request);
|
||||
|
||||
// 网页搜索结果
|
||||
List<AiWebSearchResponse.WebPage> webPages = result.webPages();
|
||||
for (AiWebSearchResponse.WebPage page : webPages) {
|
||||
String title = page.title(); // 网页标题
|
||||
String url = page.url(); // 网页URL
|
||||
String snippet = page.snippet(); // 内容描述
|
||||
String summary = page.summary(); // 文本摘要(如果请求了summary)
|
||||
String siteName = page.siteName(); // 网站名称
|
||||
}
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 示例1:搜索技术文档
|
||||
|
||||
```java
|
||||
WebSearchRequest request = new WebSearchRequest(
|
||||
"Spring Boot 3.x 新特性",
|
||||
FreshnessType.ONE_MONTH.getValue(),
|
||||
true,
|
||||
"spring.io|baeldung.com|github.com",
|
||||
null,
|
||||
15
|
||||
);
|
||||
|
||||
AiWebSearchResponse result = client.search(request);
|
||||
```
|
||||
|
||||
### 示例2:搜索新闻资讯
|
||||
|
||||
```java
|
||||
WebSearchRequest request = new WebSearchRequest(
|
||||
"AI大模型发展趋势",
|
||||
FreshnessType.ONE_WEEK.getValue(),
|
||||
null,
|
||||
null,
|
||||
"advertisement.com|spam.net",
|
||||
30
|
||||
);
|
||||
|
||||
AiWebSearchResponse result = client.search(request);
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **API密钥**:需要先到博查AI开放平台(https://open.bochaai.com)获取API KEY
|
||||
2. **请求频率**:注意遵守平台的API调用频率限制
|
||||
3. **时间范围**:建议使用 `noLimit` 以获得更好的搜索效果
|
||||
4. **域名过滤**:include和exclude参数最多支持20个域名
|
||||
5. **结果数量**:单次搜索最多返回50条结果
|
||||
|
||||
## 集成建议
|
||||
|
||||
在Spring Boot项目中,建议将客户端配置为Bean:
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class AiConfiguration {
|
||||
|
||||
@Value("${ai.bocha.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
@Value("${ai.bocha.base-url:https://open.bochaai.com}")
|
||||
private String baseUrl;
|
||||
|
||||
@Bean
|
||||
public AiBoChaWebSearchClient boChaWebSearchClient() {
|
||||
return new AiBoChaWebSearchClient(baseUrl, apiKey);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
1. **网络连接问题**:检查网络连接和防火墙设置
|
||||
2. **API密钥错误**:确认API KEY正确且有效
|
||||
3. **请求参数错误**:检查必填参数是否正确填写
|
||||
4. **服务器响应错误**:查看日志中的详细错误信息
|
||||
|
||||
## 更新日志
|
||||
|
||||
- v2.0.0:重大重构,统一 Record 类型,简化 API 调用,支持新的响应结构
|
||||
- v1.3.0:统一使用 Record 类型,移除 Lombok 注解,保持代码风格一致性
|
||||
- v1.2.0:进一步简化,移除视频搜索功能,专注于网页搜索
|
||||
- v1.1.0:使用 Lombok 简化代码,移除图片搜索功能
|
||||
- v1.0.0:初始版本,实现基本的网页搜索功能
|
|
@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel;
|
|||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
|
@ -19,13 +18,14 @@ import reactor.core.publisher.Flux;
|
|||
public class DouBaoChatModel implements ChatModel {
|
||||
|
||||
public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api";
|
||||
public static final String COMPLETE_PATH = "/v3/chat/completions";
|
||||
|
||||
public static final String MODEL_DEFAULT = "doubao-1-5-lite-32k-250115";
|
||||
|
||||
/**
|
||||
* 兼容 OpenAI 接口,进行复用
|
||||
*/
|
||||
private final OpenAiChatModel openAiChatModel;
|
||||
private final ChatModel openAiChatModel;
|
||||
|
||||
@Override
|
||||
public ChatResponse call(Prompt prompt) {
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
* 谷歌 Gemini {@link ChatModel} 实现类,基于 Google AI Studio 提供的 <a href="https://ai.google.dev/gemini-api/docs/openai">OpenAI 兼容方案</a>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class GeminiChatModel implements ChatModel {
|
||||
|
||||
public static final String BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/";
|
||||
public static final String COMPLETE_PATH = "/chat/completions";
|
||||
|
||||
public static final String MODEL_DEFAULT = "gemini-2.5-flash";
|
||||
|
||||
/**
|
||||
* 兼容 OpenAI 接口,进行复用
|
||||
*/
|
||||
private final OpenAiChatModel openAiChatModel;
|
||||
|
||||
@Override
|
||||
public ChatResponse call(Prompt prompt) {
|
||||
return openAiChatModel.call(prompt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ChatResponse> stream(Prompt prompt) {
|
||||
return openAiChatModel.stream(prompt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatOptions getDefaultOptions() {
|
||||
return openAiChatModel.getDefaultOptions();
|
||||
}
|
||||
|
||||
}
|
|
@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel;
|
|||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
|
@ -22,6 +21,7 @@ import reactor.core.publisher.Flux;
|
|||
public class HunYuanChatModel implements ChatModel {
|
||||
|
||||
public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com";
|
||||
public static final String COMPLETE_PATH = "/v1/chat/completions";
|
||||
|
||||
public static final String MODEL_DEFAULT = "hunyuan-turbo";
|
||||
|
||||
|
@ -32,7 +32,7 @@ public class HunYuanChatModel implements ChatModel {
|
|||
/**
|
||||
* 兼容 OpenAI 接口,进行复用
|
||||
*/
|
||||
private final OpenAiChatModel openAiChatModel;
|
||||
private final ChatModel openAiChatModel;
|
||||
|
||||
@Override
|
||||
public ChatResponse call(Prompt prompt) {
|
||||
|
|
|
@ -23,7 +23,7 @@ public class SiliconFlowChatModel implements ChatModel {
|
|||
/**
|
||||
* 兼容 OpenAI 接口,进行复用
|
||||
*/
|
||||
private final OpenAiChatModel openAiChatModel;
|
||||
private final ChatModel openAiChatModel;
|
||||
|
||||
@Override
|
||||
public ChatResponse call(Prompt prompt) {
|
||||
|
|
|
@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel;
|
|||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
/**
|
||||
|
@ -18,28 +17,34 @@ import reactor.core.publisher.Flux;
|
|||
@RequiredArgsConstructor
|
||||
public class XingHuoChatModel implements ChatModel {
|
||||
|
||||
public static final String BASE_URL = "https://spark-api-open.xf-yun.com";
|
||||
public static final String BASE_URL_V1 = "https://spark-api-open.xf-yun.com";
|
||||
|
||||
public static final String MODEL_DEFAULT = "generalv3.5";
|
||||
public static final String BASE_URL_V2 = "https://spark-api-open.xf-yun.com";
|
||||
public static final String BASE_COMPLETIONS_PATH_V2 = "/v2/chat/completions";
|
||||
|
||||
/**
|
||||
* 兼容 OpenAI 接口,进行复用
|
||||
* 已知模型名列表:x1、4.0Ultra、generalv3.5、max-32k、generalv3、pro-128k、lite
|
||||
*/
|
||||
private final OpenAiChatModel openAiChatModel;
|
||||
public static final String MODEL_DEFAULT = "4.0Ultra";
|
||||
|
||||
/**
|
||||
* v1 兼容 OpenAI 接口,进行复用
|
||||
*/
|
||||
private final ChatModel openAiChatModelV1;
|
||||
|
||||
@Override
|
||||
public ChatResponse call(Prompt prompt) {
|
||||
return openAiChatModel.call(prompt);
|
||||
return openAiChatModelV1.call(prompt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<ChatResponse> stream(Prompt prompt) {
|
||||
return openAiChatModel.stream(prompt);
|
||||
return openAiChatModelV1.stream(prompt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatOptions getDefaultOptions() {
|
||||
return openAiChatModel.getDefaultOptions();
|
||||
return openAiChatModelV1.getDefaultOptions();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
package cn.iocoder.yudao.module.ai.framework.ai.core.webserch;
|
||||
|
||||
/**
|
||||
* 网络搜索客户端接口
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
public interface AiWebSearchClient {
|
||||
|
||||
/**
|
||||
* 网页搜索
|
||||
*
|
||||
* @param request 搜索请求
|
||||
* @return 搜索结果
|
||||
*/
|
||||
AiWebSearchResponse search(AiWebSearchRequest request);
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package cn.iocoder.yudao.module.ai.framework.ai.core.webserch;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class AiWebSearchRequest {
|
||||
|
||||
/**
|
||||
* 用户的搜索词
|
||||
*/
|
||||
@NotEmpty(message = "搜索词不能为空")
|
||||
private String query;
|
||||
|
||||
/**
|
||||
* 是否显示文本摘要
|
||||
*
|
||||
* true - 显示
|
||||
* false - 不显示(默认)
|
||||
*/
|
||||
private Boolean summary;
|
||||
|
||||
/**
|
||||
* 返回结果的条数
|
||||
*/
|
||||
@NotNull(message = "返回结果条数不能为空")
|
||||
@Min(message = "返回结果条数最小为 1", value = 1)
|
||||
@Max(message = "返回结果条数最大为 50", value = 50)
|
||||
private Integer count;
|
||||
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
package cn.iocoder.yudao.module.ai.framework.ai.core.webserch;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class AiWebSearchResponse {
|
||||
|
||||
/**
|
||||
* 总数(总共匹配的网页数)
|
||||
*/
|
||||
private Long total;
|
||||
|
||||
/**
|
||||
* 数据列表
|
||||
*/
|
||||
private List<WebPage> lists;
|
||||
|
||||
/**
|
||||
* 网页对象
|
||||
*/
|
||||
@Data
|
||||
public static class WebPage {
|
||||
|
||||
/**
|
||||
* 名称
|
||||
*
|
||||
* 例如说:搜狐网
|
||||
*/
|
||||
private String name;
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
private String icon;
|
||||
|
||||
/**
|
||||
* 标题
|
||||
*
|
||||
* 例如说:186页|阿里巴巴:2024年环境、社会和治理(ESG)报告
|
||||
*/
|
||||
private String title;
|
||||
/**
|
||||
* URL
|
||||
*
|
||||
* 例如说:https://m.sohu.com/a/815036254_121819701/?pvid=000115_3w_a
|
||||
*/
|
||||
@SuppressWarnings("JavadocLinkAsPlainText")
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 内容的简短描述
|
||||
*/
|
||||
private String snippet;
|
||||
/**
|
||||
* 内容的文本摘要
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
package cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||
|
||||
/**
|
||||
* 博查 {@link AiWebSearchClient} 实现类
|
||||
*
|
||||
* @see <a href="https://open.bochaai.com/overview">博查 AI 开放平台</a>
|
||||
*
|
||||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
public class AiBoChaWebSearchClient implements AiWebSearchClient {
|
||||
|
||||
public static final String BASE_URL = "https://api.bochaai.com";
|
||||
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
private static final String BEARER_PREFIX = "Bearer ";
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful();
|
||||
|
||||
private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION =
|
||||
reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> {
|
||||
log.error("[AiBoChaWebSearchClient] 调用失败!请求参数:[{}],响应数据: [{}]", reqParam, responseBody);
|
||||
sink.error(new IllegalStateException("[AiBoChaWebSearchClient] 调用失败!"));
|
||||
});
|
||||
|
||||
public AiBoChaWebSearchClient(String apiKey) {
|
||||
this.webClient = WebClient.builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.defaultHeaders((headers) -> {
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.add(AUTHORIZATION_HEADER, BEARER_PREFIX + apiKey);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AiWebSearchResponse search(AiWebSearchRequest request) {
|
||||
// 转换请求参数
|
||||
WebSearchRequest webSearchRequest = new WebSearchRequest(
|
||||
request.getQuery(),
|
||||
request.getSummary(),
|
||||
request.getCount()
|
||||
);
|
||||
// 调用博查 API
|
||||
CommonResult<WebSearchResponse> response = this.webClient.post()
|
||||
.uri("/v1/web-search")
|
||||
.bodyValue(webSearchRequest)
|
||||
.retrieve()
|
||||
.onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(webSearchRequest))
|
||||
.bodyToMono(new ParameterizedTypeReference<CommonResult<WebSearchResponse>>() {})
|
||||
.block();
|
||||
if (response == null) {
|
||||
throw new IllegalStateException("[search][搜索结果为空]");
|
||||
}
|
||||
if (response.getData() == null) {
|
||||
throw new IllegalStateException(String.format("[search][搜索失败,code = %s, msg = %s]",
|
||||
response.getCode(), response.getMsg()));
|
||||
}
|
||||
WebSearchResponse data = response.getData();
|
||||
|
||||
// 转换结果
|
||||
AiWebSearchResponse result = new AiWebSearchResponse();
|
||||
if (data.webPages() == null || CollUtil.isEmpty(data.webPages().value())) {
|
||||
return result.setTotal(0L).setLists(List.of());
|
||||
}
|
||||
return result.setTotal(data.webPages().totalEstimatedMatches())
|
||||
.setLists(convertList(data.webPages().value(), page -> new AiWebSearchResponse.WebPage()
|
||||
.setName(page.siteName()).setIcon(page.siteIcon())
|
||||
.setTitle(page.name()).setUrl(page.url())
|
||||
.setSnippet(page.snippet()).setSummary(page.summary())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 网页搜索请求参数
|
||||
*/
|
||||
@JsonInclude(value = JsonInclude.Include.NON_NULL)
|
||||
public record WebSearchRequest(
|
||||
String query,
|
||||
Boolean summary,
|
||||
Integer count
|
||||
) {
|
||||
public WebSearchRequest {
|
||||
Assert.notBlank(query, "query 不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网页搜索响应
|
||||
*/
|
||||
@JsonInclude(value = JsonInclude.Include.NON_NULL)
|
||||
public record WebSearchResponse(
|
||||
WebSearchWebPages webPages
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 网页搜索结果
|
||||
*/
|
||||
@JsonInclude(value = JsonInclude.Include.NON_NULL)
|
||||
public record WebSearchWebPages(
|
||||
String webSearchUrl,
|
||||
Long totalEstimatedMatches,
|
||||
List<WebPageValue> value,
|
||||
Boolean someResultsRemoved
|
||||
) {
|
||||
|
||||
/**
|
||||
* 网页结果值
|
||||
*/
|
||||
@JsonInclude(value = JsonInclude.Include.NON_NULL)
|
||||
public record WebPageValue(
|
||||
String id,
|
||||
String name,
|
||||
String url,
|
||||
String displayUrl,
|
||||
String snippet,
|
||||
String summary,
|
||||
String siteName,
|
||||
String siteIcon,
|
||||
String datePublished,
|
||||
String dateLastCrawled,
|
||||
String cachedPageUrl,
|
||||
String language,
|
||||
Boolean isFamilyFriendly,
|
||||
Boolean isNavigational
|
||||
) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.ai.framework.security.config;
|
|||
|
||||
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
|
||||
import cn.iocoder.yudao.module.infra.enums.ApiConstants;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
|
@ -13,6 +15,9 @@ import org.springframework.security.config.annotation.web.configurers.AuthorizeH
|
|||
@Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration")
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Resource
|
||||
private McpServerProperties serverProperties;
|
||||
|
||||
@Bean("aiAuthorizeRequestsCustomizer")
|
||||
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
|
||||
return new AuthorizeRequestsCustomizer() {
|
||||
|
@ -33,6 +38,10 @@ public class SecurityConfiguration {
|
|||
// TODO 芋艿:这个每个项目都需要重复配置,得捉摸有没通用的方案
|
||||
// RPC 服务的安全配置
|
||||
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
|
||||
|
||||
// MCP Server
|
||||
registry.requestMatchers(serverProperties.getSseEndpoint()).permitAll();
|
||||
registry.requestMatchers(serverProperties.getSseMessageEndpoint()).permitAll();
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package cn.iocoder.yudao.module.ai.service.chat;
|
||||
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
|
||||
import cn.iocoder.yudao.module.ai.util.AiUtils;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||
|
@ -21,6 +22,10 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
|
|||
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO;
|
||||
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
|
||||
import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
|
||||
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
|
||||
import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient;
|
||||
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.service.knowledge.AiKnowledgeDocumentService;
|
||||
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
|
||||
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
|
||||
|
@ -28,6 +33,10 @@ import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchR
|
|||
import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiModelService;
|
||||
import cn.iocoder.yudao.module.ai.service.model.AiToolService;
|
||||
import cn.iocoder.yudao.module.ai.util.AiUtils;
|
||||
import cn.iocoder.yudao.module.ai.util.FileTypeUtils;
|
||||
import com.google.common.collect.Maps;
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
|
@ -39,6 +48,11 @@ import org.springframework.ai.chat.model.ChatResponse;
|
|||
import org.springframework.ai.chat.model.StreamingChatModel;
|
||||
import org.springframework.ai.chat.prompt.ChatOptions;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.mcp.SyncMcpToolCallbackProvider;
|
||||
import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties;
|
||||
import org.springframework.ai.tool.ToolCallback;
|
||||
import org.springframework.ai.tool.resolution.ToolCallbackResolver;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
@ -64,6 +78,13 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_N
|
|||
@Slf4j
|
||||
public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||
|
||||
/**
|
||||
* 联网搜索的结束数
|
||||
*/
|
||||
private static final Integer WEB_SEARCH_COUNT = 10;
|
||||
|
||||
// TODO @芋艿:后续优化下对话的 Prompt 整体结构
|
||||
|
||||
/**
|
||||
* 知识库转 {@link UserMessage} 的内容模版
|
||||
*/
|
||||
|
@ -71,6 +92,18 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|||
"%s\n\n" + // 多个 <Reference></Reference> 的拼接
|
||||
"回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。";
|
||||
|
||||
private static final String WEB_SEARCH_USER_MESSAGE_TEMPLATE = "使用 <WebSearch></WebSearch> 标记中的内容作为本次对话的参考:\n\n" +
|
||||
"%s\n\n" + // 多个 <WebSearch></WebSearch> 的拼接
|
||||
"回答要求:\n- 避免提及你是从 <WebSearch></WebSearch> 获取的知识。";
|
||||
|
||||
/**
|
||||
* 附件转 ${@link UserMessage} 的内容模版
|
||||
*/
|
||||
@SuppressWarnings("TextBlockMigration")
|
||||
private static final String Attachment_USER_MESSAGE_TEMPLATE = "使用 <Attachment></Attachment> 标记用户对话上传的附件内容:\n\n" +
|
||||
"%s\n\n" + // 多个 <Attachment></Attachment> 的拼接
|
||||
"回答要求:\n- 避免提及 <Attachment></Attachment> 附件的编码格式。";
|
||||
|
||||
@Resource
|
||||
private AiChatMessageMapper chatMessageMapper;
|
||||
|
||||
|
@ -87,6 +120,21 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|||
@Resource
|
||||
private AiToolService toolService;
|
||||
|
||||
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
|
||||
@Autowired(required = false) // 由于 yudao.ai.web-search.enable 配置项,可以关闭 AiWebSearchClient 的功能,所以这里只能不强制注入
|
||||
private AiWebSearchClient webSearchClient;
|
||||
|
||||
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
|
||||
@Autowired(required = false) // 由于 yudao.ai.mcp.client.enable 配置项,可以关闭 McpSyncClient 的功能,所以这里只能不强制注入
|
||||
private List<McpSyncClient> mcpClients;
|
||||
|
||||
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
|
||||
@Autowired(required = false) // 由于 yudao.ai.mcp.client.enable 配置项,可以关闭 McpSyncClient 的功能,所以这里只能不强制注入
|
||||
private McpClientCommonProperties mcpClientCommonProperties;
|
||||
|
||||
@Resource
|
||||
private ToolCallbackResolver toolCallbackResolver;
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
|
||||
// 1.1 校验对话存在
|
||||
|
@ -100,27 +148,35 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|||
AiModelDO model = modalService.validateModel(conversation.getModelId());
|
||||
ChatModel chatModel = modalService.getChatModel(model.getId());
|
||||
|
||||
// 2. 知识库找回
|
||||
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), conversation);
|
||||
// 2.1 知识库召回
|
||||
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(
|
||||
sendReqVO.getContent(), conversation);
|
||||
|
||||
// 2.2 联网搜索
|
||||
AiWebSearchResponse webSearchResponse = Boolean.TRUE.equals(sendReqVO.getUseSearch()) && webSearchClient != null ?
|
||||
webSearchClient.search(new AiWebSearchRequest().setQuery(sendReqVO.getContent())
|
||||
.setSummary(true).setCount(WEB_SEARCH_COUNT)) : null;
|
||||
|
||||
// 3. 插入 user 发送消息
|
||||
AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
|
||||
userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(),
|
||||
null);
|
||||
null, sendReqVO.getAttachmentUrls(), null);
|
||||
|
||||
// 3.1 插入 assistant 接收消息
|
||||
// 4.1 插入 assistant 接收消息
|
||||
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
|
||||
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(),
|
||||
knowledgeSegments);
|
||||
knowledgeSegments, null, webSearchResponse);
|
||||
|
||||
// 3.2 创建 chat 需要的 Prompt
|
||||
Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO);
|
||||
// 4.2 创建 chat 需要的 Prompt
|
||||
Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, webSearchResponse, model, sendReqVO);
|
||||
ChatResponse chatResponse = chatModel.call(prompt);
|
||||
|
||||
// 3.3 更新响应内容
|
||||
String newContent = chatResponse.getResult().getOutput().getText();
|
||||
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent));
|
||||
// 3.4 响应结果
|
||||
// 4.3 更新响应内容
|
||||
String newContent = AiUtils.getChatResponseContent(chatResponse);
|
||||
String newReasoningContent = AiUtils.getChatResponseReasoningContent(chatResponse);
|
||||
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId())
|
||||
.setContent(newContent).setReasoningContent(newReasoningContent));
|
||||
// 4.4 响应结果
|
||||
Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap(
|
||||
convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId));
|
||||
List<AiChatMessageRespVO.KnowledgeSegment> segments = BeanUtils.toBean(knowledgeSegments,
|
||||
|
@ -131,7 +187,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|||
return new AiChatMessageSendRespVO()
|
||||
.setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
|
||||
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
|
||||
.setContent(newContent).setSegments(segments));
|
||||
.setContent(newContent).setSegments(segments)
|
||||
.setWebSearchPages(webSearchResponse != null ? webSearchResponse.getLists() : null));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -148,29 +205,36 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|||
AiModelDO model = modalService.validateModel(conversation.getModelId());
|
||||
StreamingChatModel chatModel = modalService.getChatModel(model.getId());
|
||||
|
||||
// 2. 知识库找回
|
||||
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(),
|
||||
conversation);
|
||||
// 2.1 知识库找回
|
||||
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(
|
||||
sendReqVO.getContent(), conversation);
|
||||
|
||||
// 2.2 联网搜索
|
||||
AiWebSearchResponse webSearchResponse = Boolean.TRUE.equals(sendReqVO.getUseSearch()) && webSearchClient != null ?
|
||||
webSearchClient.search(new AiWebSearchRequest().setQuery(sendReqVO.getContent())
|
||||
.setSummary(true).setCount(WEB_SEARCH_COUNT)) : null;
|
||||
|
||||
// 3. 插入 user 发送消息
|
||||
AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model,
|
||||
userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(),
|
||||
null);
|
||||
null, sendReqVO.getAttachmentUrls(), null);
|
||||
|
||||
// 4.1 插入 assistant 接收消息
|
||||
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
|
||||
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(),
|
||||
knowledgeSegments);
|
||||
knowledgeSegments, null, webSearchResponse);
|
||||
|
||||
// 4.2 构建 Prompt,并进行调用
|
||||
Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO);
|
||||
Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, webSearchResponse, model, sendReqVO);
|
||||
Flux<ChatResponse> streamResponse = chatModel.stream(prompt);
|
||||
|
||||
// 4.3 流式返回
|
||||
StringBuffer contentBuffer = new StringBuffer();
|
||||
StringBuffer reasoningContentBuffer = new StringBuffer();
|
||||
return streamResponse.map(chunk -> {
|
||||
// 处理知识库的返回,只有首次才有
|
||||
// 仅首次:返回知识库、联网搜索
|
||||
List<AiChatMessageRespVO.KnowledgeSegment> segments = null;
|
||||
List<AiWebSearchResponse.WebPage> webSearchPages = null;
|
||||
if (StrUtil.isEmpty(contentBuffer)) {
|
||||
Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() ->
|
||||
knowledgeDocumentService.getKnowledgeDocumentMap(
|
||||
|
@ -179,24 +243,56 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|||
AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
|
||||
segment.setDocumentName(document != null ? document.getName() : null);
|
||||
});
|
||||
if (webSearchResponse != null) {
|
||||
webSearchPages = webSearchResponse.getLists();
|
||||
}
|
||||
}
|
||||
// 响应结果
|
||||
String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null;
|
||||
newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况
|
||||
contentBuffer.append(newContent);
|
||||
String newContent = AiUtils.getChatResponseContent(chunk);
|
||||
String newReasoningContent = AiUtils.getChatResponseReasoningContent(chunk);
|
||||
if (StrUtil.isNotEmpty(newContent)) {
|
||||
contentBuffer.append(newContent);
|
||||
}
|
||||
if (StrUtil.isNotEmpty(newReasoningContent)) {
|
||||
reasoningContentBuffer.append(newReasoningContent);
|
||||
}
|
||||
return success(new AiChatMessageSendRespVO()
|
||||
.setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
|
||||
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
|
||||
.setContent(newContent).setSegments(segments)));
|
||||
.setContent(StrUtil.nullToDefault(newContent, "")) // 避免 null 的 情况
|
||||
.setReasoningContent(StrUtil.nullToDefault(newReasoningContent, "")) // 避免 null 的 情况
|
||||
.setSegments(segments).setWebSearchPages(webSearchPages))); // 知识库 + 联网搜索
|
||||
}).doOnComplete(() -> {
|
||||
// 忽略租户,因为 Flux 异步无法透传租户
|
||||
TenantUtils.executeIgnore(() -> chatMessageMapper.updateById(
|
||||
new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())));
|
||||
new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString())
|
||||
.setReasoningContent(reasoningContentBuffer.toString())));
|
||||
}).doOnError(throwable -> {
|
||||
log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
|
||||
// 忽略租户,因为 Flux 异步无法透传租户
|
||||
TenantUtils.executeIgnore(() -> chatMessageMapper.updateById(
|
||||
new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage())));
|
||||
TenantUtils.executeIgnore(() -> {
|
||||
// 如果有内容,则更新内容
|
||||
if (StrUtil.isNotEmpty(contentBuffer)) {
|
||||
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId())
|
||||
.setContent(contentBuffer.toString()).setReasoningContent(reasoningContentBuffer.toString()));
|
||||
} else {
|
||||
// 否则,则进行删除
|
||||
chatMessageMapper.deleteById(assistantMessage.getId());
|
||||
}
|
||||
});
|
||||
}).doOnCancel(() -> {
|
||||
log.info("[sendChatMessageStream][userId({}) sendReqVO({}) 取消请求]", userId, sendReqVO);
|
||||
// 忽略租户,因为 Flux 异步无法透传租户
|
||||
TenantUtils.executeIgnore(() -> {
|
||||
// 如果有内容,则更新内容
|
||||
if (StrUtil.isNotEmpty(contentBuffer)) {
|
||||
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId())
|
||||
.setContent(contentBuffer.toString()).setReasoningContent(reasoningContentBuffer.toString()));
|
||||
} else {
|
||||
// 否则,则进行删除
|
||||
chatMessageMapper.deleteById(assistantMessage.getId());
|
||||
}
|
||||
});
|
||||
}).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
|
||||
}
|
||||
|
||||
|
@ -211,7 +307,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 2. 遍历找回
|
||||
// 2. 遍历召回
|
||||
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = new ArrayList<>();
|
||||
for (Long knowledgeId : role.getKnowledgeIds()) {
|
||||
knowledgeSegments.addAll(knowledgeSegmentService.searchKnowledgeSegment(new AiKnowledgeSegmentSearchReqBO()
|
||||
|
@ -222,6 +318,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|||
|
||||
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
|
||||
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments,
|
||||
AiWebSearchResponse webSearchResponse,
|
||||
AiModelDO model, AiChatMessageSendReqVO sendReqVO) {
|
||||
List<Message> chatMessages = new ArrayList<>();
|
||||
// 1.1 System Context 角色设定
|
||||
|
@ -231,8 +328,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|||
|
||||
// 1.2 历史 history message 历史消息
|
||||
List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
|
||||
contextMessages
|
||||
.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent())));
|
||||
contextMessages.forEach(message -> {
|
||||
chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent()));
|
||||
UserMessage attachmentUserMessage = buildAttachmentUserMessage(message.getAttachmentUrls());
|
||||
if (attachmentUserMessage != null) {
|
||||
chatMessages.add(attachmentUserMessage);
|
||||
}
|
||||
// TODO @芋艿:历史的知识库;历史的搜索,要不要拼接?
|
||||
});
|
||||
|
||||
// 1.3 当前 user message 新发送消息
|
||||
chatMessages.add(new UserMessage(sendReqVO.getContent()));
|
||||
|
@ -245,23 +348,76 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|||
chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference)));
|
||||
}
|
||||
|
||||
// 2.1 查询 tool 工具
|
||||
Set<String> toolNames = null;
|
||||
Map<String,Object> toolContext = Map.of();
|
||||
if (conversation.getRoleId() != null) {
|
||||
AiChatRoleDO chatRole = chatRoleService.getChatRole(conversation.getRoleId());
|
||||
if (chatRole != null && CollUtil.isNotEmpty(chatRole.getToolIds())) {
|
||||
toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName);
|
||||
toolContext = AiUtils.buildCommonToolContext();
|
||||
// 1.5 联网搜索,通过 UserMessage 实现
|
||||
if (webSearchResponse != null && CollUtil.isNotEmpty(webSearchResponse.getLists())) {
|
||||
String webSearch = webSearchResponse.getLists().stream()
|
||||
.map(page -> {
|
||||
String summary = StrUtil.isNotEmpty(page.getSummary()) ?
|
||||
"\nSummary: " + page.getSummary() : "";
|
||||
return "<WebSearch title=\"" + page.getTitle() + "\" url=\"" + page.getUrl() + "\">"
|
||||
+ StrUtil.blankToDefault(page.getSummary(), page.getSnippet()) + "</WebSearch>";
|
||||
})
|
||||
.collect(Collectors.joining("\n\n"));
|
||||
chatMessages.add(new UserMessage(String.format(WEB_SEARCH_USER_MESSAGE_TEMPLATE, webSearch)));
|
||||
}
|
||||
|
||||
// 1.6 附件,通过 UserMessage 实现
|
||||
if (CollUtil.isNotEmpty(sendReqVO.getAttachmentUrls())) {
|
||||
UserMessage attachmentUserMessage = buildAttachmentUserMessage(sendReqVO.getAttachmentUrls());
|
||||
if (attachmentUserMessage != null) {
|
||||
chatMessages.add(attachmentUserMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// 2.1 查询 tool 工具
|
||||
List<ToolCallback> toolCallbacks = getToolCallbackListByRoleId(conversation.getRoleId());
|
||||
Map<String,Object> toolContext = CollUtil.isNotEmpty(toolCallbacks) ? AiUtils.buildCommonToolContext()
|
||||
: Map.of();
|
||||
// 2.2 构建 ChatOptions 对象
|
||||
AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform());
|
||||
ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(),
|
||||
conversation.getTemperature(), conversation.getMaxTokens(), toolNames, toolContext);
|
||||
conversation.getTemperature(), conversation.getMaxTokens(),
|
||||
toolCallbacks, toolContext);
|
||||
return new Prompt(chatMessages, chatOptions);
|
||||
}
|
||||
|
||||
private List<ToolCallback> getToolCallbackListByRoleId(Long roleId) {
|
||||
if (roleId == null) {
|
||||
return null;
|
||||
}
|
||||
AiChatRoleDO chatRole = chatRoleService.getChatRole(roleId);
|
||||
if (chatRole == null) {
|
||||
return null;
|
||||
}
|
||||
List<ToolCallback> toolCallbacks = new ArrayList<>();
|
||||
// 1. 通过 toolIds
|
||||
if (CollUtil.isNotEmpty(chatRole.getToolIds())) {
|
||||
Set<String> 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 组消息作为消息上下文
|
||||
* <p>
|
||||
|
@ -302,14 +458,56 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
|||
return contextMessages;
|
||||
}
|
||||
|
||||
private UserMessage buildAttachmentUserMessage(List<String> attachmentUrls) {
|
||||
if (CollUtil.isEmpty(attachmentUrls)) {
|
||||
return null;
|
||||
}
|
||||
// 读取文件内容
|
||||
Map<String, String> 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 -> "<Attachment name=\"" + entry.getKey() + "\">" + entry.getValue() + "</Attachment>")
|
||||
.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<AiKnowledgeSegmentSearchRespBO> knowledgeSegments) {
|
||||
AiModelDO model, Long userId, Long roleId,
|
||||
MessageType messageType, String content, Boolean useContext,
|
||||
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments,
|
||||
List<String> 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;
|
||||
|
|
|
@ -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<AiKnowledgeSegmentDO> 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<Document> 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<Document> documents = searchDocument(knowledge, reqBO);
|
||||
|
||||
// 3.1 段落召回
|
||||
List<AiKnowledgeSegmentDO> 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<Document> 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<Document> 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<AiKnowledgeSegmentDO> splitContent(String url, Integer segmentMaxTokens) {
|
||||
// 1. 读取 URL 内容
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* 参考 <a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Tool Calling —— Methods as Tools</a>
|
||||
*/
|
||||
package cn.iocoder.yudao.module.ai.tool.function;
|
|
@ -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
|
||||
) {
|
||||
}
|
|
@ -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<Person> getPersonById(int id);
|
||||
|
||||
/**
|
||||
* Retrieves all Person records currently stored.
|
||||
*
|
||||
* @return An unmodifiable List containing all Persons. Returns an empty list if none exist.
|
||||
*/
|
||||
List<Person> 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<Person> 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<Person> 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<Person> filterByAge(int age);
|
||||
|
||||
}
|
|
@ -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<Integer, Person> 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<String> 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<Person> 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<Person> getAllPersons() {
|
||||
// Return an unmodifiable view of the values
|
||||
List<Person> 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<Person> 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<Person> 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<Person> filterBySex(String sex) {
|
||||
if (sex == null || sex.isBlank()) {
|
||||
log.debug("Filter by sex skipped due to blank filter.");
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<Person> 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<Person> 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<Person> 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* 参考 <a href="https://docs.spring.io/spring-ai/reference/api/tools.html#_methods_as_tools">Tool Calling —— Methods as Tools</a>
|
||||
*/
|
||||
package cn.iocoder.yudao.module.ai.tool.method;
|
|
@ -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<String> toolNames, Map<String, Object> toolContext) {
|
||||
toolNames = ObjUtil.defaultIfNull(toolNames, Collections.emptySet());
|
||||
List<ToolCallback> toolCallbacks, Map<String, Object> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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/");
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
|
|
|
@ -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<Message> 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<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> 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<Message> 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<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
}
|
|
@ -60,4 +60,23 @@ public class DeepSeekChatModelTests {
|
|||
flux.doOnNext(System.out::println).then().block();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testStream_thinking() {
|
||||
// 准备参数
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
|
||||
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
|
||||
.model("deepseek-reasoner")
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
messages.add(new UserMessage("详细推理下,帮我设计一个用户中心!"));
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> 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<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
|
||||
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
|
||||
.model("doubao-seed-1-6-thinking-250715")
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Message> 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<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
|
||||
// 打印结果
|
||||
flux.doOnNext(System.out::println).then().block();
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
|
||||
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
|
||||
.model("hunyuan-a13b")
|
||||
// .model("hunyuan-turbos-latest")
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> 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<Message> 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<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
|
||||
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
|
||||
.model("deepseek-r1")
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = deepSeekChatModel.stream(new Prompt(messages, options));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Message> 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<Message> messages = new ArrayList<>();
|
||||
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
// messages.add(new UserMessage("1 + 1 = ?"));
|
||||
//
|
||||
// // 调用
|
||||
// Flux<ChatResponse> 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<Message> 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<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> 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<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
|
||||
OllamaOptions options = OllamaOptions.builder()
|
||||
.model("qwen3")
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -59,4 +59,24 @@ public class MiniMaxChatModelTests {
|
|||
}).then().block();
|
||||
}
|
||||
|
||||
// TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
|
||||
@Test
|
||||
@Disabled
|
||||
public void testStream_thinking() {
|
||||
// 准备参数
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
|
||||
MiniMaxChatOptions options = MiniMaxChatOptions.builder()
|
||||
.model("MiniMax-M1")
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -63,4 +63,25 @@ public class MoonshotChatModelTests {
|
|||
}).then().block();
|
||||
}
|
||||
|
||||
// TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复
|
||||
@Test
|
||||
@Disabled
|
||||
public void testStream_thinking() {
|
||||
// 准备参数
|
||||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
|
||||
MoonshotChatOptions options = MoonshotChatOptions.builder()
|
||||
// .model("kimi-k2-0711-preview")
|
||||
.model("kimi-thinking-preview")
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
messages.add(new UserMessage("帮我推理下,怎么实现一个用户中心!"));
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> 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<Message> 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<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
|
||||
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
|
||||
.model("deepseek-ai/DeepSeek-R1")
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Message> messages = new ArrayList<>();
|
||||
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("1 + 1 = ?"));
|
||||
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
|
||||
messages.add(new UserMessage("帮我推理下,怎么实现一个用户中心!"));
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
|
||||
|
@ -66,4 +78,52 @@ public class TongYiChatModelTests {
|
|||
}).then().block();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void testStream_thinking() {
|
||||
// 准备参数
|
||||
List<Message> 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<ChatResponse> 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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
|
||||
DeepSeekChatOptions options = DeepSeekChatOptions.builder()
|
||||
.model("x1")
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
|
||||
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder()
|
||||
.model("GLM-4.5")
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options));
|
||||
// 打印结果
|
||||
flux.doOnNext(response -> {
|
||||
// System.out.println(response);
|
||||
System.out.println(response.getResult().getOutput());
|
||||
}).then().block();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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/");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -166,6 +166,10 @@ public class CouponServiceImpl implements CouponService {
|
|||
public void invalidateCouponsByAdmin(List<Long> giveCouponIds, Long userId) {
|
||||
// 循环收回
|
||||
for (Long couponId : giveCouponIds) {
|
||||
// couponId 为空或 0 则跳过
|
||||
if (null == couponId || couponId <= 0) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
getSelf().invalidateCoupon(couponId, userId);
|
||||
} catch (Exception e) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
|
|
Loading…
Reference in New Issue