Commit 839085edadad49a65590d095bfb03cec2fea6abd
Committed by
Gitee
!1415 AI 大模型最新特性
Merge pull request !1415 from 芋道源码/feature/ai
Showing
67 changed files
with
2135 additions
and
271 deletions
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java
| @@ -12,8 +12,10 @@ import org.springframework.beans.factory.config.BeanPostProcessor; | @@ -12,8 +12,10 @@ import org.springframework.beans.factory.config.BeanPostProcessor; | ||
| 12 | public class TenantRabbitMQInitializer implements BeanPostProcessor { | 12 | public class TenantRabbitMQInitializer implements BeanPostProcessor { |
| 13 | 13 | ||
| 14 | @Override | 14 | @Override |
| 15 | + @SuppressWarnings("PatternVariableCanBeUsed") | ||
| 15 | public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { | 16 | public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { |
| 16 | - if (bean instanceof RabbitTemplate rabbitTemplate) { | 17 | + if (bean instanceof RabbitTemplate) { |
| 18 | + RabbitTemplate rabbitTemplate = (RabbitTemplate) bean; | ||
| 17 | rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor()); | 19 | rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor()); |
| 18 | } | 20 | } |
| 19 | return bean; | 21 | return bean; |
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java
| @@ -17,10 +17,13 @@ import org.springframework.beans.factory.config.BeanPostProcessor; | @@ -17,10 +17,13 @@ import org.springframework.beans.factory.config.BeanPostProcessor; | ||
| 17 | public class TenantRocketMQInitializer implements BeanPostProcessor { | 17 | public class TenantRocketMQInitializer implements BeanPostProcessor { |
| 18 | 18 | ||
| 19 | @Override | 19 | @Override |
| 20 | + @SuppressWarnings("PatternVariableCanBeUsed") | ||
| 20 | public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { | 21 | public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { |
| 21 | - if (bean instanceof DefaultRocketMQListenerContainer container) { | 22 | + if (bean instanceof DefaultRocketMQListenerContainer) { |
| 23 | + DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean; | ||
| 22 | initTenantConsumer(container.getConsumer()); | 24 | initTenantConsumer(container.getConsumer()); |
| 23 | - } else if (bean instanceof RocketMQTemplate template) { | 25 | + } else if (bean instanceof RocketMQTemplate) { |
| 26 | + RocketMQTemplate template = (RocketMQTemplate) bean; | ||
| 24 | initTenantProducer(template.getProducer()); | 27 | initTenantProducer(template.getProducer()); |
| 25 | } | 28 | } |
| 26 | return bean; | 29 | return bean; |
yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java
| @@ -21,15 +21,18 @@ public class YudaoAsyncAutoConfiguration { | @@ -21,15 +21,18 @@ public class YudaoAsyncAutoConfiguration { | ||
| 21 | return new BeanPostProcessor() { | 21 | return new BeanPostProcessor() { |
| 22 | 22 | ||
| 23 | @Override | 23 | @Override |
| 24 | + @SuppressWarnings("PatternVariableCanBeUsed") | ||
| 24 | public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { | 25 | public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { |
| 25 | // 处理 ThreadPoolTaskExecutor | 26 | // 处理 ThreadPoolTaskExecutor |
| 26 | - if (bean instanceof ThreadPoolTaskExecutor executor) { | 27 | + if (bean instanceof ThreadPoolTaskExecutor) { |
| 28 | + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; | ||
| 27 | executor.setTaskDecorator(TtlRunnable::get); | 29 | executor.setTaskDecorator(TtlRunnable::get); |
| 28 | return executor; | 30 | return executor; |
| 29 | } | 31 | } |
| 30 | // 处理 SimpleAsyncTaskExecutor | 32 | // 处理 SimpleAsyncTaskExecutor |
| 31 | // 参考 https://t.zsxq.com/CBoks 增加 | 33 | // 参考 https://t.zsxq.com/CBoks 增加 |
| 32 | - if (bean instanceof SimpleAsyncTaskExecutor executor) { | 34 | + if (bean instanceof SimpleAsyncTaskExecutor) { |
| 35 | + SimpleAsyncTaskExecutor executor = (SimpleAsyncTaskExecutor) bean; | ||
| 33 | executor.setTaskDecorator(TtlRunnable::get); | 36 | executor.setTaskDecorator(TtlRunnable::get); |
| 34 | return executor; | 37 | return executor; |
| 35 | } | 38 | } |
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/config/YudaoMybatisAutoConfiguration.java
| @@ -10,7 +10,6 @@ import com.baomidou.mybatisplus.extension.incrementer.*; | @@ -10,7 +10,6 @@ import com.baomidou.mybatisplus.extension.incrementer.*; | ||
| 10 | import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal; | 10 | import com.baomidou.mybatisplus.extension.parser.JsqlParserGlobal; |
| 11 | import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache; | 11 | import com.baomidou.mybatisplus.extension.parser.cache.JdkSerialCaffeineJsqlParseCache; |
| 12 | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; | 12 | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
| 13 | -import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor; | ||
| 14 | import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; | 13 | import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; |
| 15 | import org.apache.ibatis.annotations.Mapper; | 14 | import org.apache.ibatis.annotations.Mapper; |
| 16 | import org.mybatis.spring.annotation.MapperScan; | 15 | import org.mybatis.spring.annotation.MapperScan; |
| @@ -43,7 +42,8 @@ public class YudaoMybatisAutoConfiguration { | @@ -43,7 +42,8 @@ public class YudaoMybatisAutoConfiguration { | ||
| 43 | public MybatisPlusInterceptor mybatisPlusInterceptor() { | 42 | public MybatisPlusInterceptor mybatisPlusInterceptor() { |
| 44 | MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); | 43 | MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); |
| 45 | mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 | 44 | mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 |
| 46 | - mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 拦截没有指定条件的 update 和 delete 语句 | 45 | + // ↓↓↓ 按需开启,可能会影响到 updateBatch 的地方:例如说文件配置管理 ↓↓↓ |
| 46 | + // mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); // 拦截没有指定条件的 update 和 delete 语句 | ||
| 47 | return mybatisPlusInterceptor; | 47 | return mybatisPlusInterceptor; |
| 48 | } | 48 | } |
| 49 | 49 |
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java
| @@ -18,8 +18,10 @@ import java.util.Objects; | @@ -18,8 +18,10 @@ import java.util.Objects; | ||
| 18 | public class DefaultDBFieldHandler implements MetaObjectHandler { | 18 | public class DefaultDBFieldHandler implements MetaObjectHandler { |
| 19 | 19 | ||
| 20 | @Override | 20 | @Override |
| 21 | + @SuppressWarnings("PatternVariableCanBeUsed") | ||
| 21 | public void insertFill(MetaObject metaObject) { | 22 | public void insertFill(MetaObject metaObject) { |
| 22 | - if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO baseDO) { | 23 | + if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) { |
| 24 | + BaseDO baseDO = (BaseDO) metaObject.getOriginalObject(); | ||
| 23 | 25 | ||
| 24 | LocalDateTime current = LocalDateTime.now(); | 26 | LocalDateTime current = LocalDateTime.now(); |
| 25 | // 创建时间为空,则以当前时间为插入时间 | 27 | // 创建时间为空,则以当前时间为插入时间 |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
| @@ -182,9 +182,11 @@ public class GlobalExceptionHandler { | @@ -182,9 +182,11 @@ public class GlobalExceptionHandler { | ||
| 182 | * 例如说,接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String | 182 | * 例如说,接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String |
| 183 | */ | 183 | */ |
| 184 | @ExceptionHandler(HttpMessageNotReadableException.class) | 184 | @ExceptionHandler(HttpMessageNotReadableException.class) |
| 185 | + @SuppressWarnings("PatternVariableCanBeUsed") | ||
| 185 | public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) { | 186 | public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) { |
| 186 | log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex); | 187 | log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex); |
| 187 | - if (ex.getCause() instanceof InvalidFormatException invalidFormatException) { | 188 | + if (ex.getCause() instanceof InvalidFormatException) { |
| 189 | + InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause(); | ||
| 188 | return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue())); | 190 | return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue())); |
| 189 | } | 191 | } |
| 190 | if (StrUtil.startWith(ex.getMessage(), "Required request body is missing")) { | 192 | if (StrUtil.startWith(ex.getMessage(), "Required request body is missing")) { |
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/util/WebFrameworkUtils.java
| @@ -144,11 +144,13 @@ public class WebFrameworkUtils { | @@ -144,11 +144,13 @@ public class WebFrameworkUtils { | ||
| 144 | return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); | 144 | return (CommonResult<?>) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); |
| 145 | } | 145 | } |
| 146 | 146 | ||
| 147 | + @SuppressWarnings("PatternVariableCanBeUsed") | ||
| 147 | public static HttpServletRequest getRequest() { | 148 | public static HttpServletRequest getRequest() { |
| 148 | RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); | 149 | RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); |
| 149 | - if (!(requestAttributes instanceof ServletRequestAttributes servletRequestAttributes)) { | 150 | + if (!(requestAttributes instanceof ServletRequestAttributes)) { |
| 150 | return null; | 151 | return null; |
| 151 | } | 152 | } |
| 153 | + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; | ||
| 152 | return servletRequestAttributes.getRequest(); | 154 | return servletRequestAttributes.getRequest(); |
| 153 | } | 155 | } |
| 154 | 156 |
yudao-module-ai/pom.xml
| @@ -19,8 +19,8 @@ | @@ -19,8 +19,8 @@ | ||
| 19 | 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno | 19 | 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno |
| 20 | </description> | 20 | </description> |
| 21 | <properties> | 21 | <properties> |
| 22 | - <spring-ai.version>1.0.0</spring-ai.version> | ||
| 23 | - <alibaba-ai.version>1.0.0.2</alibaba-ai.version> | 22 | + <spring-ai.version>1.0.1</spring-ai.version> |
| 23 | + <alibaba-ai.version>1.0.0.3</alibaba-ai.version> | ||
| 24 | <tinyflow.version>1.0.2</tinyflow.version> | 24 | <tinyflow.version>1.0.2</tinyflow.version> |
| 25 | </properties> | 25 | </properties> |
| 26 | 26 | ||
| @@ -86,6 +86,11 @@ | @@ -86,6 +86,11 @@ | ||
| 86 | </dependency> | 86 | </dependency> |
| 87 | <dependency> | 87 | <dependency> |
| 88 | <groupId>org.springframework.ai</groupId> | 88 | <groupId>org.springframework.ai</groupId> |
| 89 | + <artifactId>spring-ai-starter-model-anthropic</artifactId> | ||
| 90 | + <version>${spring-ai.version}</version> | ||
| 91 | + </dependency> | ||
| 92 | + <dependency> | ||
| 93 | + <groupId>org.springframework.ai</groupId> | ||
| 89 | <artifactId>spring-ai-starter-model-deepseek</artifactId> | 94 | <artifactId>spring-ai-starter-model-deepseek</artifactId> |
| 90 | <version>${spring-ai.version}</version> | 95 | <version>${spring-ai.version}</version> |
| 91 | </dependency> | 96 | </dependency> |
| @@ -182,6 +187,24 @@ | @@ -182,6 +187,24 @@ | ||
| 182 | </exclusions> | 187 | </exclusions> |
| 183 | </dependency> | 188 | </dependency> |
| 184 | 189 | ||
| 190 | + <!-- MCP 相关 --> | ||
| 191 | + <!-- | ||
| 192 | + 特殊说明:不能使用 spring-ai-starter-mcp-server-webflux 或 spring-ai-starter-mcp-client-webflux !!! | ||
| 193 | + 原因:项目使用了 SpringMVC,而不是 WebFlux。引入上述 2 个,会导致 SSE Server 失效。 | ||
| 194 | + --> | ||
| 195 | + <dependency> | ||
| 196 | + <!-- 服务端 --> | ||
| 197 | + <groupId>org.springframework.ai</groupId> | ||
| 198 | + <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId> | ||
| 199 | + <version>${spring-ai.version}</version> | ||
| 200 | + </dependency> | ||
| 201 | + <dependency> | ||
| 202 | + <!-- 客户端 --> | ||
| 203 | + <groupId>org.springframework.ai</groupId> | ||
| 204 | + <artifactId>spring-ai-starter-mcp-client</artifactId> | ||
| 205 | + <version>${spring-ai.version}</version> | ||
| 206 | + </dependency> | ||
| 207 | + | ||
| 185 | <!-- TinyFlow:AI 工作流 --> | 208 | <!-- TinyFlow:AI 工作流 --> |
| 186 | <dependency> | 209 | <dependency> |
| 187 | <groupId>dev.tinyflow</groupId> | 210 | <groupId>dev.tinyflow</groupId> |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatMessageController.http
| @@ -20,9 +20,46 @@ tenant-id: {{adminTenantId}} | @@ -20,9 +20,46 @@ tenant-id: {{adminTenantId}} | ||
| 20 | "content": "1+1=?" | 20 | "content": "1+1=?" |
| 21 | } | 21 | } |
| 22 | 22 | ||
| 23 | +### 发送消息(流式)【带文件】 | ||
| 24 | +POST {{baseUrl}}/ai/chat/message/send-stream | ||
| 25 | +Content-Type: application/json | ||
| 26 | +Authorization: {{token}} | ||
| 27 | +tenant-id: {{adminTenantId}} | ||
| 28 | + | ||
| 29 | +{ | ||
| 30 | + "conversationId": "1781604279872581797", | ||
| 31 | + "content": "图片里有什么?", | ||
| 32 | + "attachmentUrls": ["http://test.yudao.iocoder.cn/1755531278.jpeg"] | ||
| 33 | +} | ||
| 34 | + | ||
| 35 | +### 发送消息(流式)【追问带文件】 | ||
| 36 | +POST {{baseUrl}}/ai/chat/message/send-stream | ||
| 37 | +Content-Type: application/json | ||
| 38 | +Authorization: {{token}} | ||
| 39 | +tenant-id: {{adminTenantId}} | ||
| 40 | + | ||
| 41 | +{ | ||
| 42 | + "conversationId": "1781604279872581799", | ||
| 43 | + "content": "说下图片里,有哪些字?", | ||
| 44 | + "useContext": true | ||
| 45 | +} | ||
| 46 | + | ||
| 47 | +### 发送消息(流式)【联网搜索】 | ||
| 48 | +POST {{baseUrl}}/ai/chat/message/send-stream | ||
| 49 | +Content-Type: application/json | ||
| 50 | +Authorization: {{token}} | ||
| 51 | +tenant-id: {{adminTenantId}} | ||
| 52 | + | ||
| 53 | +{ | ||
| 54 | + "conversationId": "1781604279872581799", | ||
| 55 | + "content": "今天是周几?", | ||
| 56 | + "useSearch": true | ||
| 57 | +} | ||
| 58 | + | ||
| 23 | ### 获得指定对话的消息列表 | 59 | ### 获得指定对话的消息列表 |
| 24 | -GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581649 | 60 | +GET {{baseUrl}}/ai/chat/message/list-by-conversation-id?conversationId=1781604279872581799 |
| 25 | Authorization: {{token}} | 61 | Authorization: {{token}} |
| 62 | +tenant-id: {{adminTenantId}} | ||
| 26 | 63 | ||
| 27 | ### 删除消息 | 64 | ### 删除消息 |
| 28 | DELETE {{baseUrl}}/ai/chat/message/delete?id=50 | 65 | DELETE {{baseUrl}}/ai/chat/message/delete?id=50 |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageRespVO.java
| 1 | package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; | 1 | package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse; | ||
| 3 | import io.swagger.v3.oas.annotations.media.Schema; | 4 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | import lombok.Data; | 5 | import lombok.Data; |
| 5 | 6 | ||
| @@ -37,6 +38,9 @@ public class AiChatMessageRespVO { | @@ -37,6 +38,9 @@ public class AiChatMessageRespVO { | ||
| 37 | @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊") | 38 | @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊") |
| 38 | private String content; | 39 | private String content; |
| 39 | 40 | ||
| 41 | + @Schema(description = "推理内容", example = "要达到这个目标,你需要...") | ||
| 42 | + private String reasoningContent; | ||
| 43 | + | ||
| 40 | @Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") | 44 | @Schema(description = "是否携带上下文", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") |
| 41 | private Boolean useContext; | 45 | private Boolean useContext; |
| 42 | 46 | ||
| @@ -46,6 +50,12 @@ public class AiChatMessageRespVO { | @@ -46,6 +50,12 @@ public class AiChatMessageRespVO { | ||
| 46 | @Schema(description = "知识库段落数组") | 50 | @Schema(description = "知识库段落数组") |
| 47 | private List<KnowledgeSegment> segments; | 51 | private List<KnowledgeSegment> segments; |
| 48 | 52 | ||
| 53 | + @Schema(description = "联网搜索的网页内容数组") | ||
| 54 | + private List<AiWebSearchResponse.WebPage> webSearchPages; | ||
| 55 | + | ||
| 56 | + @Schema(description = "附件 URL 数组", example = "https://www.iocoder.cn/1.png") | ||
| 57 | + private List<String> attachmentUrls; | ||
| 58 | + | ||
| 49 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51") | 59 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-05-12 12:51") |
| 50 | private LocalDateTime createTime; | 60 | private LocalDateTime createTime; |
| 51 | 61 |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendReqVO.java
| @@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; | @@ -3,9 +3,9 @@ package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; | ||
| 3 | import io.swagger.v3.oas.annotations.media.Schema; | 3 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | import jakarta.validation.constraints.NotEmpty; | 4 | import jakarta.validation.constraints.NotEmpty; |
| 5 | import jakarta.validation.constraints.NotNull; | 5 | import jakarta.validation.constraints.NotNull; |
| 6 | -import jakarta.validation.constraints.Size; | ||
| 7 | import lombok.Data; | 6 | import lombok.Data; |
| 8 | -import lombok.experimental.Accessors; | 7 | + |
| 8 | +import java.util.List; | ||
| 9 | 9 | ||
| 10 | @Schema(description = "管理后台 - AI 聊天消息发送 Request VO") | 10 | @Schema(description = "管理后台 - AI 聊天消息发送 Request VO") |
| 11 | @Data | 11 | @Data |
| @@ -22,4 +22,10 @@ public class AiChatMessageSendReqVO { | @@ -22,4 +22,10 @@ public class AiChatMessageSendReqVO { | ||
| 22 | @Schema(description = "是否携带上下文", example = "true") | 22 | @Schema(description = "是否携带上下文", example = "true") |
| 23 | private Boolean useContext; | 23 | private Boolean useContext; |
| 24 | 24 | ||
| 25 | + @Schema(description = "是否联网搜索", example = "true") | ||
| 26 | + private Boolean useSearch; | ||
| 27 | + | ||
| 28 | + @Schema(description = "附件 URL 数组", example = "https://www.iocoder.cn/1.png") | ||
| 29 | + private List<String> attachmentUrls; | ||
| 30 | + | ||
| 25 | } | 31 | } |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/vo/message/AiChatMessageSendRespVO.java
| 1 | package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; | 1 | package cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse; | ||
| 3 | import io.swagger.v3.oas.annotations.media.Schema; | 4 | import io.swagger.v3.oas.annotations.media.Schema; |
| 4 | import lombok.Data; | 5 | import lombok.Data; |
| 5 | 6 | ||
| @@ -29,12 +30,18 @@ public class AiChatMessageSendRespVO { | @@ -29,12 +30,18 @@ public class AiChatMessageSendRespVO { | ||
| 29 | @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊") | 30 | @Schema(description = "聊天内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你好啊") |
| 30 | private String content; | 31 | private String content; |
| 31 | 32 | ||
| 33 | + @Schema(description = "推理内容", example = "要达到这个目标,你需要...") | ||
| 34 | + private String reasoningContent; | ||
| 35 | + | ||
| 32 | @Schema(description = "知识库段落编号数组", example = "[1,2,3]") | 36 | @Schema(description = "知识库段落编号数组", example = "[1,2,3]") |
| 33 | private List<Long> segmentIds; | 37 | private List<Long> segmentIds; |
| 34 | 38 | ||
| 35 | @Schema(description = "知识库段落数组") | 39 | @Schema(description = "知识库段落数组") |
| 36 | private List<AiChatMessageRespVO.KnowledgeSegment> segments; | 40 | private List<AiChatMessageRespVO.KnowledgeSegment> segments; |
| 37 | 41 | ||
| 42 | + @Schema(description = "联网搜索的网页内容数组") | ||
| 43 | + private List<AiWebSearchResponse.WebPage> webSearchPages; | ||
| 44 | + | ||
| 38 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) | 45 | @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) |
| 39 | private LocalDateTime createTime; | 46 | private LocalDateTime createTime; |
| 40 | 47 |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleRespVO.java
| @@ -52,6 +52,9 @@ public class AiChatRoleRespVO implements VO { | @@ -52,6 +52,9 @@ public class AiChatRoleRespVO implements VO { | ||
| 52 | @Schema(description = "引用的工具编号列表", example = "1,2,3") | 52 | @Schema(description = "引用的工具编号列表", example = "1,2,3") |
| 53 | private List<Long> toolIds; | 53 | private List<Long> toolIds; |
| 54 | 54 | ||
| 55 | + @Schema(description = "引用的 MCP Client 名字列表", example = "filesystem") | ||
| 56 | + private List<String> mcpClientNames; | ||
| 57 | + | ||
| 55 | @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | 58 | @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
| 56 | private Boolean publicStatus; | 59 | private Boolean publicStatus; |
| 57 | 60 |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveMyReqVO.java
| @@ -37,4 +37,7 @@ public class AiChatRoleSaveMyReqVO { | @@ -37,4 +37,7 @@ public class AiChatRoleSaveMyReqVO { | ||
| 37 | @Schema(description = "引用的工具编号列表", example = "1,2,3") | 37 | @Schema(description = "引用的工具编号列表", example = "1,2,3") |
| 38 | private List<Long> toolIds; | 38 | private List<Long> toolIds; |
| 39 | 39 | ||
| 40 | + @Schema(description = "引用的 MCP Client 名字列表", example = "filesystem") | ||
| 41 | + private List<String> mcpClientNames; | ||
| 42 | + | ||
| 40 | } | 43 | } |
| 41 | \ No newline at end of file | 44 | \ No newline at end of file |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/vo/chatRole/AiChatRoleSaveReqVO.java
| @@ -50,6 +50,9 @@ public class AiChatRoleSaveReqVO { | @@ -50,6 +50,9 @@ public class AiChatRoleSaveReqVO { | ||
| 50 | @Schema(description = "引用的工具编号列表", example = "1,2,3") | 50 | @Schema(description = "引用的工具编号列表", example = "1,2,3") |
| 51 | private List<Long> toolIds; | 51 | private List<Long> toolIds; |
| 52 | 52 | ||
| 53 | + @Schema(description = "引用的 MCP Client 名字列表", example = "filesystem") | ||
| 54 | + private List<String> mcpClientNames; | ||
| 55 | + | ||
| 53 | @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") | 56 | @Schema(description = "是否公开", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") |
| 54 | @NotNull(message = "是否公开不能为空") | 57 | @NotNull(message = "是否公开不能为空") |
| 55 | private Boolean publicStatus; | 58 | private Boolean publicStatus; |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/chat/AiChatMessageDO.java
| @@ -2,14 +2,20 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat; | @@ -2,14 +2,20 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.chat; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 4 | import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; | 4 | import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; |
| 5 | +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; | ||
| 5 | import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; | 6 | import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; |
| 6 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; | 7 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; |
| 7 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; | 8 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; |
| 9 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse; | ||
| 8 | import com.baomidou.mybatisplus.annotation.KeySequence; | 10 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 9 | import com.baomidou.mybatisplus.annotation.TableField; | 11 | import com.baomidou.mybatisplus.annotation.TableField; |
| 10 | import com.baomidou.mybatisplus.annotation.TableId; | 12 | import com.baomidou.mybatisplus.annotation.TableId; |
| 11 | import com.baomidou.mybatisplus.annotation.TableName; | 13 | import com.baomidou.mybatisplus.annotation.TableName; |
| 12 | -import lombok.*; | 14 | +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; |
| 15 | +import lombok.AllArgsConstructor; | ||
| 16 | +import lombok.Builder; | ||
| 17 | +import lombok.Data; | ||
| 18 | +import lombok.NoArgsConstructor; | ||
| 13 | import org.springframework.ai.chat.messages.MessageType; | 19 | import org.springframework.ai.chat.messages.MessageType; |
| 14 | 20 | ||
| 15 | import java.util.List; | 21 | import java.util.List; |
| @@ -87,6 +93,10 @@ public class AiChatMessageDO extends BaseDO { | @@ -87,6 +93,10 @@ public class AiChatMessageDO extends BaseDO { | ||
| 87 | * 聊天内容 | 93 | * 聊天内容 |
| 88 | */ | 94 | */ |
| 89 | private String content; | 95 | private String content; |
| 96 | + /** | ||
| 97 | + * 推理内容 | ||
| 98 | + */ | ||
| 99 | + private String reasoningContent; | ||
| 90 | 100 | ||
| 91 | /** | 101 | /** |
| 92 | * 是否携带上下文 | 102 | * 是否携带上下文 |
| @@ -101,4 +111,16 @@ public class AiChatMessageDO extends BaseDO { | @@ -101,4 +111,16 @@ public class AiChatMessageDO extends BaseDO { | ||
| 101 | @TableField(typeHandler = LongListTypeHandler.class) | 111 | @TableField(typeHandler = LongListTypeHandler.class) |
| 102 | private List<Long> segmentIds; | 112 | private List<Long> segmentIds; |
| 103 | 113 | ||
| 114 | + /** | ||
| 115 | + * 联网搜索的网页内容数组 | ||
| 116 | + */ | ||
| 117 | + @TableField(typeHandler = JacksonTypeHandler.class) | ||
| 118 | + private List<AiWebSearchResponse.WebPage> webSearchPages; | ||
| 119 | + | ||
| 120 | + /** | ||
| 121 | + * 附件 URL 数组 | ||
| 122 | + */ | ||
| 123 | + @TableField(typeHandler = StringListTypeHandler.class) | ||
| 124 | + private List<String> attachmentUrls; | ||
| 125 | + | ||
| 104 | } | 126 | } |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiChatRoleDO.java
| @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model; | @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.model; | ||
| 3 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | 3 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | 4 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 5 | import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; | 5 | import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; |
| 6 | +import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; | ||
| 6 | import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; | 7 | import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; |
| 7 | import com.baomidou.mybatisplus.annotation.KeySequence; | 8 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 8 | import com.baomidou.mybatisplus.annotation.TableField; | 9 | import com.baomidou.mybatisplus.annotation.TableField; |
| @@ -80,6 +81,13 @@ public class AiChatRoleDO extends BaseDO { | @@ -80,6 +81,13 @@ public class AiChatRoleDO extends BaseDO { | ||
| 80 | */ | 81 | */ |
| 81 | @TableField(typeHandler = LongListTypeHandler.class) | 82 | @TableField(typeHandler = LongListTypeHandler.class) |
| 82 | private List<Long> toolIds; | 83 | private List<Long> toolIds; |
| 84 | + /** | ||
| 85 | + * 引用的 MCP Client 名字列表 | ||
| 86 | + * | ||
| 87 | + * 关联 spring.ai.mcp.client 下的名字 | ||
| 88 | + */ | ||
| 89 | + @TableField(typeHandler = StringListTypeHandler.class) | ||
| 90 | + private List<String> mcpClientNames; | ||
| 83 | 91 | ||
| 84 | /** | 92 | /** |
| 85 | * 是否公开 | 93 | * 是否公开 |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/dal/dataobject/model/AiToolDO.java
| 1 | package cn.iocoder.yudao.module.ai.dal.dataobject.model; | 1 | package cn.iocoder.yudao.module.ai.dal.dataobject.model; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; | 3 | import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; |
| 4 | -import cn.iocoder.yudao.module.ai.service.model.tool.DirectoryListToolFunction; | ||
| 5 | -import cn.iocoder.yudao.module.ai.service.model.tool.WeatherQueryToolFunction; | 4 | +import cn.iocoder.yudao.module.ai.tool.function.DirectoryListToolFunction; |
| 5 | +import cn.iocoder.yudao.module.ai.tool.function.WeatherQueryToolFunction; | ||
| 6 | import com.baomidou.mybatisplus.annotation.KeySequence; | 6 | import com.baomidou.mybatisplus.annotation.KeySequence; |
| 7 | import com.baomidou.mybatisplus.annotation.TableId; | 7 | import com.baomidou.mybatisplus.annotation.TableId; |
| 8 | import com.baomidou.mybatisplus.annotation.TableName; | 8 | import com.baomidou.mybatisplus.annotation.TableName; |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/enums/model/AiPlatformEnum.java
| @@ -33,6 +33,8 @@ public enum AiPlatformEnum implements ArrayValuable<String> { | @@ -33,6 +33,8 @@ public enum AiPlatformEnum implements ArrayValuable<String> { | ||
| 33 | 33 | ||
| 34 | OPENAI("OpenAI", "OpenAI"), // OpenAI 官方 | 34 | OPENAI("OpenAI", "OpenAI"), // OpenAI 官方 |
| 35 | AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软 | 35 | AZURE_OPENAI("AzureOpenAI", "AzureOpenAI"), // OpenAI 微软 |
| 36 | + ANTHROPIC("Anthropic", "Anthropic"), // Anthropic Claude | ||
| 37 | + GEMINI("Gemini", "Gemini"), // 谷歌 Gemini | ||
| 36 | OLLAMA("Ollama", "Ollama"), | 38 | OLLAMA("Ollama", "Ollama"), |
| 37 | 39 | ||
| 38 | STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI | 40 | STABLE_DIFFUSION("StableDiffusion", "StableDiffusion"), // Stability AI |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java
| @@ -2,25 +2,34 @@ package cn.iocoder.yudao.module.ai.framework.ai.config; | @@ -2,25 +2,34 @@ package cn.iocoder.yudao.module.ai.framework.ai.config; | ||
| 2 | 2 | ||
| 3 | import cn.hutool.core.util.StrUtil; | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | import cn.hutool.extra.spring.SpringUtil; | 4 | import cn.hutool.extra.spring.SpringUtil; |
| 5 | -import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory; | ||
| 6 | -import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactoryImpl; | 5 | +import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory; |
| 6 | +import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactoryImpl; | ||
| 7 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; | 7 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; |
| 8 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; | 8 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; |
| 9 | +import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel; | ||
| 9 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; | 10 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; |
| 10 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; | 11 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; |
| 11 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; | 12 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; |
| 12 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; | 13 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; |
| 13 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; | 14 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; |
| 14 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; | 15 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; |
| 16 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; | ||
| 17 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; | ||
| 18 | +import cn.iocoder.yudao.module.ai.tool.method.PersonService; | ||
| 15 | import lombok.extern.slf4j.Slf4j; | 19 | import lombok.extern.slf4j.Slf4j; |
| 20 | +import org.springframework.ai.deepseek.DeepSeekChatModel; | ||
| 21 | +import org.springframework.ai.deepseek.DeepSeekChatOptions; | ||
| 22 | +import org.springframework.ai.deepseek.api.DeepSeekApi; | ||
| 16 | import org.springframework.ai.embedding.BatchingStrategy; | 23 | import org.springframework.ai.embedding.BatchingStrategy; |
| 17 | import org.springframework.ai.embedding.TokenCountBatchingStrategy; | 24 | import org.springframework.ai.embedding.TokenCountBatchingStrategy; |
| 18 | import org.springframework.ai.model.tool.ToolCallingManager; | 25 | import org.springframework.ai.model.tool.ToolCallingManager; |
| 19 | import org.springframework.ai.openai.OpenAiChatModel; | 26 | import org.springframework.ai.openai.OpenAiChatModel; |
| 20 | import org.springframework.ai.openai.OpenAiChatOptions; | 27 | import org.springframework.ai.openai.OpenAiChatOptions; |
| 21 | import org.springframework.ai.openai.api.OpenAiApi; | 28 | import org.springframework.ai.openai.api.OpenAiApi; |
| 29 | +import org.springframework.ai.support.ToolCallbacks; | ||
| 22 | import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; | 30 | import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; |
| 23 | import org.springframework.ai.tokenizer.TokenCountEstimator; | 31 | import org.springframework.ai.tokenizer.TokenCountEstimator; |
| 32 | +import org.springframework.ai.tool.ToolCallback; | ||
| 24 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; | 33 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; |
| 25 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; | 34 | import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; |
| 26 | import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; | 35 | import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; |
| @@ -30,6 +39,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties | @@ -30,6 +39,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties | ||
| 30 | import org.springframework.context.annotation.Bean; | 39 | import org.springframework.context.annotation.Bean; |
| 31 | import org.springframework.context.annotation.Configuration; | 40 | import org.springframework.context.annotation.Configuration; |
| 32 | 41 | ||
| 42 | +import java.util.List; | ||
| 43 | + | ||
| 33 | /** | 44 | /** |
| 34 | * 芋道 AI 自动配置 | 45 | * 芋道 AI 自动配置 |
| 35 | * | 46 | * |
| @@ -52,19 +63,48 @@ public class AiAutoConfiguration { | @@ -52,19 +63,48 @@ public class AiAutoConfiguration { | ||
| 52 | // ========== 各种 AI Client 创建 ========== | 63 | // ========== 各种 AI Client 创建 ========== |
| 53 | 64 | ||
| 54 | @Bean | 65 | @Bean |
| 66 | + @ConditionalOnProperty(value = "yudao.ai.gemini.enable", havingValue = "true") | ||
| 67 | + public GeminiChatModel geminiChatModel(YudaoAiProperties yudaoAiProperties) { | ||
| 68 | + YudaoAiProperties.Gemini properties = yudaoAiProperties.getGemini(); | ||
| 69 | + return buildGeminiChatClient(properties); | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.Gemini properties) { | ||
| 73 | + if (StrUtil.isEmpty(properties.getModel())) { | ||
| 74 | + properties.setModel(GeminiChatModel.MODEL_DEFAULT); | ||
| 75 | + } | ||
| 76 | + OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | ||
| 77 | + .openAiApi(OpenAiApi.builder() | ||
| 78 | + .baseUrl(GeminiChatModel.BASE_URL) | ||
| 79 | + .completionsPath(GeminiChatModel.COMPLETE_PATH) | ||
| 80 | + .apiKey(properties.getApiKey()) | ||
| 81 | + .build()) | ||
| 82 | + .defaultOptions(OpenAiChatOptions.builder() | ||
| 83 | + .model(properties.getModel()) | ||
| 84 | + .temperature(properties.getTemperature()) | ||
| 85 | + .maxTokens(properties.getMaxTokens()) | ||
| 86 | + .topP(properties.getTopP()) | ||
| 87 | + .build()) | ||
| 88 | + .toolCallingManager(getToolCallingManager()) | ||
| 89 | + .build(); | ||
| 90 | + return new GeminiChatModel(openAiChatModel); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + @Bean | ||
| 55 | @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") | 94 | @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") |
| 56 | public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { | 95 | public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { |
| 57 | - YudaoAiProperties.DouBaoProperties properties = yudaoAiProperties.getDoubao(); | 96 | + YudaoAiProperties.DouBao properties = yudaoAiProperties.getDoubao(); |
| 58 | return buildDouBaoChatClient(properties); | 97 | return buildDouBaoChatClient(properties); |
| 59 | } | 98 | } |
| 60 | 99 | ||
| 61 | - public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBaoProperties properties) { | 100 | + public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBao properties) { |
| 62 | if (StrUtil.isEmpty(properties.getModel())) { | 101 | if (StrUtil.isEmpty(properties.getModel())) { |
| 63 | properties.setModel(DouBaoChatModel.MODEL_DEFAULT); | 102 | properties.setModel(DouBaoChatModel.MODEL_DEFAULT); |
| 64 | } | 103 | } |
| 65 | OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | 104 | OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() |
| 66 | .openAiApi(OpenAiApi.builder() | 105 | .openAiApi(OpenAiApi.builder() |
| 67 | .baseUrl(DouBaoChatModel.BASE_URL) | 106 | .baseUrl(DouBaoChatModel.BASE_URL) |
| 107 | + .completionsPath(DouBaoChatModel.COMPLETE_PATH) | ||
| 68 | .apiKey(properties.getApiKey()) | 108 | .apiKey(properties.getApiKey()) |
| 69 | .build()) | 109 | .build()) |
| 70 | .defaultOptions(OpenAiChatOptions.builder() | 110 | .defaultOptions(OpenAiChatOptions.builder() |
| @@ -81,20 +121,20 @@ public class AiAutoConfiguration { | @@ -81,20 +121,20 @@ public class AiAutoConfiguration { | ||
| 81 | @Bean | 121 | @Bean |
| 82 | @ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true") | 122 | @ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true") |
| 83 | public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) { | 123 | public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) { |
| 84 | - YudaoAiProperties.SiliconFlowProperties properties = yudaoAiProperties.getSiliconflow(); | 124 | + YudaoAiProperties.SiliconFlow properties = yudaoAiProperties.getSiliconflow(); |
| 85 | return buildSiliconFlowChatClient(properties); | 125 | return buildSiliconFlowChatClient(properties); |
| 86 | } | 126 | } |
| 87 | 127 | ||
| 88 | - public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlowProperties properties) { | 128 | + public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlow properties) { |
| 89 | if (StrUtil.isEmpty(properties.getModel())) { | 129 | if (StrUtil.isEmpty(properties.getModel())) { |
| 90 | properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT); | 130 | properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT); |
| 91 | } | 131 | } |
| 92 | - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | ||
| 93 | - .openAiApi(OpenAiApi.builder() | 132 | + DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() |
| 133 | + .deepSeekApi(DeepSeekApi.builder() | ||
| 94 | .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL) | 134 | .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL) |
| 95 | .apiKey(properties.getApiKey()) | 135 | .apiKey(properties.getApiKey()) |
| 96 | .build()) | 136 | .build()) |
| 97 | - .defaultOptions(OpenAiChatOptions.builder() | 137 | + .defaultOptions(DeepSeekChatOptions.builder() |
| 98 | .model(properties.getModel()) | 138 | .model(properties.getModel()) |
| 99 | .temperature(properties.getTemperature()) | 139 | .temperature(properties.getTemperature()) |
| 100 | .maxTokens(properties.getMaxTokens()) | 140 | .maxTokens(properties.getMaxTokens()) |
| @@ -108,11 +148,11 @@ public class AiAutoConfiguration { | @@ -108,11 +148,11 @@ public class AiAutoConfiguration { | ||
| 108 | @Bean | 148 | @Bean |
| 109 | @ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true") | 149 | @ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true") |
| 110 | public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) { | 150 | public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) { |
| 111 | - YudaoAiProperties.HunYuanProperties properties = yudaoAiProperties.getHunyuan(); | 151 | + YudaoAiProperties.HunYuan properties = yudaoAiProperties.getHunyuan(); |
| 112 | return buildHunYuanChatClient(properties); | 152 | return buildHunYuanChatClient(properties); |
| 113 | } | 153 | } |
| 114 | 154 | ||
| 115 | - public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuanProperties properties) { | 155 | + public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuan properties) { |
| 116 | if (StrUtil.isEmpty(properties.getModel())) { | 156 | if (StrUtil.isEmpty(properties.getModel())) { |
| 117 | properties.setModel(HunYuanChatModel.MODEL_DEFAULT); | 157 | properties.setModel(HunYuanChatModel.MODEL_DEFAULT); |
| 118 | } | 158 | } |
| @@ -122,13 +162,14 @@ public class AiAutoConfiguration { | @@ -122,13 +162,14 @@ public class AiAutoConfiguration { | ||
| 122 | StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL | 162 | StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL |
| 123 | : HunYuanChatModel.BASE_URL); | 163 | : HunYuanChatModel.BASE_URL); |
| 124 | } | 164 | } |
| 125 | - // 创建 OpenAiChatModel、HunYuanChatModel 对象 | ||
| 126 | - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | ||
| 127 | - .openAiApi(OpenAiApi.builder() | 165 | + // 创建 DeepSeekChatModel、HunYuanChatModel 对象 |
| 166 | + DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() | ||
| 167 | + .deepSeekApi(DeepSeekApi.builder() | ||
| 128 | .baseUrl(properties.getBaseUrl()) | 168 | .baseUrl(properties.getBaseUrl()) |
| 169 | + .completionsPath(HunYuanChatModel.COMPLETE_PATH) | ||
| 129 | .apiKey(properties.getApiKey()) | 170 | .apiKey(properties.getApiKey()) |
| 130 | .build()) | 171 | .build()) |
| 131 | - .defaultOptions(OpenAiChatOptions.builder() | 172 | + .defaultOptions(DeepSeekChatOptions.builder() |
| 132 | .model(properties.getModel()) | 173 | .model(properties.getModel()) |
| 133 | .temperature(properties.getTemperature()) | 174 | .temperature(properties.getTemperature()) |
| 134 | .maxTokens(properties.getMaxTokens()) | 175 | .maxTokens(properties.getMaxTokens()) |
| @@ -142,25 +183,30 @@ public class AiAutoConfiguration { | @@ -142,25 +183,30 @@ public class AiAutoConfiguration { | ||
| 142 | @Bean | 183 | @Bean |
| 143 | @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") | 184 | @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") |
| 144 | public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { | 185 | public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { |
| 145 | - YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo(); | 186 | + YudaoAiProperties.XingHuo properties = yudaoAiProperties.getXinghuo(); |
| 146 | return buildXingHuoChatClient(properties); | 187 | return buildXingHuoChatClient(properties); |
| 147 | } | 188 | } |
| 148 | 189 | ||
| 149 | - public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuoProperties properties) { | 190 | + public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuo properties) { |
| 150 | if (StrUtil.isEmpty(properties.getModel())) { | 191 | if (StrUtil.isEmpty(properties.getModel())) { |
| 151 | properties.setModel(XingHuoChatModel.MODEL_DEFAULT); | 192 | properties.setModel(XingHuoChatModel.MODEL_DEFAULT); |
| 152 | } | 193 | } |
| 194 | + OpenAiApi.Builder builder = OpenAiApi.builder() | ||
| 195 | + .baseUrl(XingHuoChatModel.BASE_URL_V1) | ||
| 196 | + .apiKey(properties.getAppKey() + ":" + properties.getSecretKey()); | ||
| 197 | + if ("x1".equals(properties.getModel())) { | ||
| 198 | + builder.baseUrl(XingHuoChatModel.BASE_URL_V2) | ||
| 199 | + .completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2); | ||
| 200 | + } | ||
| 153 | OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | 201 | OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() |
| 154 | - .openAiApi(OpenAiApi.builder() | ||
| 155 | - .baseUrl(XingHuoChatModel.BASE_URL) | ||
| 156 | - .apiKey(properties.getAppKey() + ":" + properties.getSecretKey()) | ||
| 157 | - .build()) | 202 | + .openAiApi(builder.build()) |
| 158 | .defaultOptions(OpenAiChatOptions.builder() | 203 | .defaultOptions(OpenAiChatOptions.builder() |
| 159 | .model(properties.getModel()) | 204 | .model(properties.getModel()) |
| 160 | .temperature(properties.getTemperature()) | 205 | .temperature(properties.getTemperature()) |
| 161 | .maxTokens(properties.getMaxTokens()) | 206 | .maxTokens(properties.getMaxTokens()) |
| 162 | .topP(properties.getTopP()) | 207 | .topP(properties.getTopP()) |
| 163 | .build()) | 208 | .build()) |
| 209 | + // TODO @芋艿:星火的 function call 有 bug,会报 ToolResponseMessage must have an id 错误!!! | ||
| 164 | .toolCallingManager(getToolCallingManager()) | 210 | .toolCallingManager(getToolCallingManager()) |
| 165 | .build(); | 211 | .build(); |
| 166 | return new XingHuoChatModel(openAiChatModel); | 212 | return new XingHuoChatModel(openAiChatModel); |
| @@ -169,11 +215,11 @@ public class AiAutoConfiguration { | @@ -169,11 +215,11 @@ public class AiAutoConfiguration { | ||
| 169 | @Bean | 215 | @Bean |
| 170 | @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true") | 216 | @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true") |
| 171 | public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) { | 217 | public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) { |
| 172 | - YudaoAiProperties.BaiChuanProperties properties = yudaoAiProperties.getBaichuan(); | 218 | + YudaoAiProperties.BaiChuan properties = yudaoAiProperties.getBaichuan(); |
| 173 | return buildBaiChuanChatClient(properties); | 219 | return buildBaiChuanChatClient(properties); |
| 174 | } | 220 | } |
| 175 | 221 | ||
| 176 | - public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuanProperties properties) { | 222 | + public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuan properties) { |
| 177 | if (StrUtil.isEmpty(properties.getModel())) { | 223 | if (StrUtil.isEmpty(properties.getModel())) { |
| 178 | properties.setModel(BaiChuanChatModel.MODEL_DEFAULT); | 224 | properties.setModel(BaiChuanChatModel.MODEL_DEFAULT); |
| 179 | } | 225 | } |
| @@ -196,7 +242,7 @@ public class AiAutoConfiguration { | @@ -196,7 +242,7 @@ public class AiAutoConfiguration { | ||
| 196 | @Bean | 242 | @Bean |
| 197 | @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true") | 243 | @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true") |
| 198 | public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) { | 244 | public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) { |
| 199 | - YudaoAiProperties.MidjourneyProperties config = yudaoAiProperties.getMidjourney(); | 245 | + YudaoAiProperties.Midjourney config = yudaoAiProperties.getMidjourney(); |
| 200 | return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl()); | 246 | return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl()); |
| 201 | } | 247 | } |
| 202 | 248 | ||
| @@ -222,4 +268,22 @@ public class AiAutoConfiguration { | @@ -222,4 +268,22 @@ public class AiAutoConfiguration { | ||
| 222 | return SpringUtil.getBean(ToolCallingManager.class); | 268 | return SpringUtil.getBean(ToolCallingManager.class); |
| 223 | } | 269 | } |
| 224 | 270 | ||
| 271 | + // ========== Web Search 相关 ========== | ||
| 272 | + | ||
| 273 | + @Bean | ||
| 274 | + @ConditionalOnProperty(value = "yudao.ai.web-search.enable", havingValue = "true") | ||
| 275 | + public AiWebSearchClient webSearchClient(YudaoAiProperties yudaoAiProperties) { | ||
| 276 | + return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey()); | ||
| 277 | + } | ||
| 278 | + | ||
| 279 | + // ========== MCP 相关 ========== | ||
| 280 | + | ||
| 281 | + /** | ||
| 282 | + * 参考自 <a href="https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html">MCP Server Boot Starter</> | ||
| 283 | + */ | ||
| 284 | + @Bean | ||
| 285 | + public List<ToolCallback> toolCallbacks(PersonService personService) { | ||
| 286 | + return List.of(ToolCallbacks.from(personService)); | ||
| 287 | + } | ||
| 288 | + | ||
| 225 | } | 289 | } |
| 226 | \ No newline at end of file | 290 | \ No newline at end of file |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java
| @@ -14,48 +14,66 @@ import org.springframework.boot.context.properties.ConfigurationProperties; | @@ -14,48 +14,66 @@ import org.springframework.boot.context.properties.ConfigurationProperties; | ||
| 14 | public class YudaoAiProperties { | 14 | public class YudaoAiProperties { |
| 15 | 15 | ||
| 16 | /** | 16 | /** |
| 17 | + * 谷歌 Gemini | ||
| 18 | + */ | ||
| 19 | + private Gemini gemini; | ||
| 20 | + | ||
| 21 | + /** | ||
| 17 | * 字节豆包 | 22 | * 字节豆包 |
| 18 | */ | 23 | */ |
| 19 | - @SuppressWarnings("SpellCheckingInspection") | ||
| 20 | - private DouBaoProperties doubao; | 24 | + private DouBao doubao; |
| 21 | 25 | ||
| 22 | /** | 26 | /** |
| 23 | * 腾讯混元 | 27 | * 腾讯混元 |
| 24 | */ | 28 | */ |
| 25 | - @SuppressWarnings("SpellCheckingInspection") | ||
| 26 | - private HunYuanProperties hunyuan; | 29 | + private HunYuan hunyuan; |
| 27 | 30 | ||
| 28 | /** | 31 | /** |
| 29 | * 硅基流动 | 32 | * 硅基流动 |
| 30 | */ | 33 | */ |
| 31 | - @SuppressWarnings("SpellCheckingInspection") | ||
| 32 | - private SiliconFlowProperties siliconflow; | 34 | + private SiliconFlow siliconflow; |
| 33 | 35 | ||
| 34 | /** | 36 | /** |
| 35 | * 讯飞星火 | 37 | * 讯飞星火 |
| 36 | */ | 38 | */ |
| 37 | - @SuppressWarnings("SpellCheckingInspection") | ||
| 38 | - private XingHuoProperties xinghuo; | 39 | + private XingHuo xinghuo; |
| 39 | 40 | ||
| 40 | /** | 41 | /** |
| 41 | * 百川 | 42 | * 百川 |
| 42 | */ | 43 | */ |
| 43 | - @SuppressWarnings("SpellCheckingInspection") | ||
| 44 | - private BaiChuanProperties baichuan; | 44 | + private BaiChuan baichuan; |
| 45 | 45 | ||
| 46 | /** | 46 | /** |
| 47 | * Midjourney 绘图 | 47 | * Midjourney 绘图 |
| 48 | */ | 48 | */ |
| 49 | - private MidjourneyProperties midjourney; | 49 | + private Midjourney midjourney; |
| 50 | 50 | ||
| 51 | /** | 51 | /** |
| 52 | * Suno 音乐 | 52 | * Suno 音乐 |
| 53 | */ | 53 | */ |
| 54 | @SuppressWarnings("SpellCheckingInspection") | 54 | @SuppressWarnings("SpellCheckingInspection") |
| 55 | - private SunoProperties suno; | 55 | + private Suno suno; |
| 56 | + | ||
| 57 | + /** | ||
| 58 | + * 网络搜索 | ||
| 59 | + */ | ||
| 60 | + private WebSearch webSearch; | ||
| 61 | + | ||
| 62 | + @Data | ||
| 63 | + public static class Gemini { | ||
| 64 | + | ||
| 65 | + private String enable; | ||
| 66 | + private String apiKey; | ||
| 67 | + | ||
| 68 | + private String model; | ||
| 69 | + private Double temperature; | ||
| 70 | + private Integer maxTokens; | ||
| 71 | + private Double topP; | ||
| 72 | + | ||
| 73 | + } | ||
| 56 | 74 | ||
| 57 | @Data | 75 | @Data |
| 58 | - public static class DouBaoProperties { | 76 | + public static class DouBao { |
| 59 | 77 | ||
| 60 | private String enable; | 78 | private String enable; |
| 61 | private String apiKey; | 79 | private String apiKey; |
| @@ -68,7 +86,7 @@ public class YudaoAiProperties { | @@ -68,7 +86,7 @@ public class YudaoAiProperties { | ||
| 68 | } | 86 | } |
| 69 | 87 | ||
| 70 | @Data | 88 | @Data |
| 71 | - public static class HunYuanProperties { | 89 | + public static class HunYuan { |
| 72 | 90 | ||
| 73 | private String enable; | 91 | private String enable; |
| 74 | private String baseUrl; | 92 | private String baseUrl; |
| @@ -82,7 +100,7 @@ public class YudaoAiProperties { | @@ -82,7 +100,7 @@ public class YudaoAiProperties { | ||
| 82 | } | 100 | } |
| 83 | 101 | ||
| 84 | @Data | 102 | @Data |
| 85 | - public static class SiliconFlowProperties { | 103 | + public static class SiliconFlow { |
| 86 | 104 | ||
| 87 | private String enable; | 105 | private String enable; |
| 88 | private String apiKey; | 106 | private String apiKey; |
| @@ -95,7 +113,7 @@ public class YudaoAiProperties { | @@ -95,7 +113,7 @@ public class YudaoAiProperties { | ||
| 95 | } | 113 | } |
| 96 | 114 | ||
| 97 | @Data | 115 | @Data |
| 98 | - public static class XingHuoProperties { | 116 | + public static class XingHuo { |
| 99 | 117 | ||
| 100 | private String enable; | 118 | private String enable; |
| 101 | private String appId; | 119 | private String appId; |
| @@ -110,7 +128,7 @@ public class YudaoAiProperties { | @@ -110,7 +128,7 @@ public class YudaoAiProperties { | ||
| 110 | } | 128 | } |
| 111 | 129 | ||
| 112 | @Data | 130 | @Data |
| 113 | - public static class BaiChuanProperties { | 131 | + public static class BaiChuan { |
| 114 | 132 | ||
| 115 | private String enable; | 133 | private String enable; |
| 116 | private String apiKey; | 134 | private String apiKey; |
| @@ -123,7 +141,7 @@ public class YudaoAiProperties { | @@ -123,7 +141,7 @@ public class YudaoAiProperties { | ||
| 123 | } | 141 | } |
| 124 | 142 | ||
| 125 | @Data | 143 | @Data |
| 126 | - public static class MidjourneyProperties { | 144 | + public static class Midjourney { |
| 127 | 145 | ||
| 128 | private String enable; | 146 | private String enable; |
| 129 | private String baseUrl; | 147 | private String baseUrl; |
| @@ -134,12 +152,21 @@ public class YudaoAiProperties { | @@ -134,12 +152,21 @@ public class YudaoAiProperties { | ||
| 134 | } | 152 | } |
| 135 | 153 | ||
| 136 | @Data | 154 | @Data |
| 137 | - public static class SunoProperties { | 155 | + public static class Suno { |
| 138 | 156 | ||
| 139 | - private boolean enable = false; | 157 | + private boolean enable; |
| 140 | 158 | ||
| 141 | private String baseUrl; | 159 | private String baseUrl; |
| 142 | 160 | ||
| 143 | } | 161 | } |
| 144 | 162 | ||
| 163 | + @Data | ||
| 164 | + public static class WebSearch { | ||
| 165 | + | ||
| 166 | + private boolean enable; | ||
| 167 | + | ||
| 168 | + private String apiKey; | ||
| 169 | + | ||
| 170 | + } | ||
| 171 | + | ||
| 145 | } | 172 | } |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactory.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactory.java
| 1 | -package cn.iocoder.yudao.module.ai.framework.ai.core; | 1 | +package cn.iocoder.yudao.module.ai.framework.ai.core.model; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; | 3 | import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; |
| 4 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; | 4 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java
| 1 | -package cn.iocoder.yudao.module.ai.framework.ai.core; | 1 | +package cn.iocoder.yudao.module.ai.framework.ai.core.model; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.io.FileUtil; | 3 | import cn.hutool.core.io.FileUtil; |
| 4 | import cn.hutool.core.lang.Assert; | 4 | import cn.hutool.core.lang.Assert; |
| @@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration; | @@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration; | ||
| 14 | import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties; | 14 | import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties; |
| 15 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; | 15 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; |
| 16 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; | 16 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; |
| 17 | +import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel; | ||
| 17 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; | 18 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; |
| 18 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; | 19 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; |
| 19 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; | 20 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowApiConstants; |
| @@ -67,6 +68,7 @@ import org.springframework.ai.minimax.MiniMaxChatOptions; | @@ -67,6 +68,7 @@ import org.springframework.ai.minimax.MiniMaxChatOptions; | ||
| 67 | import org.springframework.ai.minimax.MiniMaxEmbeddingModel; | 68 | import org.springframework.ai.minimax.MiniMaxEmbeddingModel; |
| 68 | import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; | 69 | import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; |
| 69 | import org.springframework.ai.minimax.api.MiniMaxApi; | 70 | import org.springframework.ai.minimax.api.MiniMaxApi; |
| 71 | +import org.springframework.ai.model.anthropic.autoconfigure.AnthropicChatAutoConfiguration; | ||
| 70 | import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration; | 72 | import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration; |
| 71 | import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration; | 73 | import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration; |
| 72 | import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties; | 74 | import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties; |
| @@ -93,6 +95,8 @@ import org.springframework.ai.openai.OpenAiImageModel; | @@ -93,6 +95,8 @@ import org.springframework.ai.openai.OpenAiImageModel; | ||
| 93 | import org.springframework.ai.openai.api.OpenAiApi; | 95 | import org.springframework.ai.openai.api.OpenAiApi; |
| 94 | import org.springframework.ai.openai.api.OpenAiImageApi; | 96 | import org.springframework.ai.openai.api.OpenAiImageApi; |
| 95 | import org.springframework.ai.openai.api.common.OpenAiApiConstants; | 97 | import org.springframework.ai.openai.api.common.OpenAiApiConstants; |
| 98 | +import org.springframework.ai.anthropic.AnthropicChatModel; | ||
| 99 | +import org.springframework.ai.anthropic.api.AnthropicApi; | ||
| 96 | import org.springframework.ai.stabilityai.StabilityAiImageModel; | 100 | import org.springframework.ai.stabilityai.StabilityAiImageModel; |
| 97 | import org.springframework.ai.stabilityai.api.StabilityAiApi; | 101 | import org.springframework.ai.stabilityai.api.StabilityAiApi; |
| 98 | import org.springframework.ai.vectorstore.SimpleVectorStore; | 102 | import org.springframework.ai.vectorstore.SimpleVectorStore; |
| @@ -168,6 +172,10 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -168,6 +172,10 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 168 | return buildOpenAiChatModel(apiKey, url); | 172 | return buildOpenAiChatModel(apiKey, url); |
| 169 | case AZURE_OPENAI: | 173 | case AZURE_OPENAI: |
| 170 | return buildAzureOpenAiChatModel(apiKey, url); | 174 | return buildAzureOpenAiChatModel(apiKey, url); |
| 175 | + case ANTHROPIC: | ||
| 176 | + return buildAnthropicChatModel(apiKey, url); | ||
| 177 | + case GEMINI: | ||
| 178 | + return buildGeminiChatModel(apiKey); | ||
| 171 | case OLLAMA: | 179 | case OLLAMA: |
| 172 | return buildOllamaChatModel(url); | 180 | return buildOllamaChatModel(url); |
| 173 | default: | 181 | default: |
| @@ -206,6 +214,10 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -206,6 +214,10 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 206 | return SpringUtil.getBean(OpenAiChatModel.class); | 214 | return SpringUtil.getBean(OpenAiChatModel.class); |
| 207 | case AZURE_OPENAI: | 215 | case AZURE_OPENAI: |
| 208 | return SpringUtil.getBean(AzureOpenAiChatModel.class); | 216 | return SpringUtil.getBean(AzureOpenAiChatModel.class); |
| 217 | + case ANTHROPIC: | ||
| 218 | + return SpringUtil.getBean(AnthropicChatModel.class); | ||
| 219 | + case GEMINI: | ||
| 220 | + return SpringUtil.getBean(GeminiChatModel.class); | ||
| 209 | case OLLAMA: | 221 | case OLLAMA: |
| 210 | return SpringUtil.getBean(OllamaChatModel.class); | 222 | return SpringUtil.getBean(OllamaChatModel.class); |
| 211 | default: | 223 | default: |
| @@ -260,7 +272,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -260,7 +272,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 260 | String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, | 272 | String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, |
| 261 | url); | 273 | url); |
| 262 | return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> { | 274 | return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> { |
| 263 | - YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class) | 275 | + YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class) |
| 264 | .getMidjourney(); | 276 | .getMidjourney(); |
| 265 | return new MidjourneyApi(url, apiKey, properties.getNotifyUrl()); | 277 | return new MidjourneyApi(url, apiKey, properties.getNotifyUrl()); |
| 266 | }); | 278 | }); |
| @@ -347,7 +359,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -347,7 +359,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 347 | * 可参考 {@link DashScopeImageAutoConfiguration} 的 dashScopeImageModel 方法 | 359 | * 可参考 {@link DashScopeImageAutoConfiguration} 的 dashScopeImageModel 方法 |
| 348 | */ | 360 | */ |
| 349 | private static DashScopeImageModel buildTongYiImagesModel(String key) { | 361 | private static DashScopeImageModel buildTongYiImagesModel(String key) { |
| 350 | - DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key); | 362 | + DashScopeImageApi dashScopeImageApi = DashScopeImageApi.builder().apiKey(key).build(); |
| 351 | return DashScopeImageModel.builder() | 363 | return DashScopeImageModel.builder() |
| 352 | .dashScopeApi(dashScopeImageApi) | 364 | .dashScopeApi(dashScopeImageApi) |
| 353 | .build(); | 365 | .build(); |
| @@ -397,7 +409,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -397,7 +409,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 397 | * 可参考 {@link AiAutoConfiguration#douBaoChatClient(YudaoAiProperties)} | 409 | * 可参考 {@link AiAutoConfiguration#douBaoChatClient(YudaoAiProperties)} |
| 398 | */ | 410 | */ |
| 399 | private ChatModel buildDouBaoChatModel(String apiKey) { | 411 | private ChatModel buildDouBaoChatModel(String apiKey) { |
| 400 | - YudaoAiProperties.DouBaoProperties properties = new YudaoAiProperties.DouBaoProperties() | 412 | + YudaoAiProperties.DouBao properties = new YudaoAiProperties.DouBao() |
| 401 | .setApiKey(apiKey); | 413 | .setApiKey(apiKey); |
| 402 | return new AiAutoConfiguration().buildDouBaoChatClient(properties); | 414 | return new AiAutoConfiguration().buildDouBaoChatClient(properties); |
| 403 | } | 415 | } |
| @@ -406,7 +418,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -406,7 +418,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 406 | * 可参考 {@link AiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)} | 418 | * 可参考 {@link AiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)} |
| 407 | */ | 419 | */ |
| 408 | private ChatModel buildHunYuanChatModel(String apiKey, String url) { | 420 | private ChatModel buildHunYuanChatModel(String apiKey, String url) { |
| 409 | - YudaoAiProperties.HunYuanProperties properties = new YudaoAiProperties.HunYuanProperties() | 421 | + YudaoAiProperties.HunYuan properties = new YudaoAiProperties.HunYuan() |
| 410 | .setBaseUrl(url).setApiKey(apiKey); | 422 | .setBaseUrl(url).setApiKey(apiKey); |
| 411 | return new AiAutoConfiguration().buildHunYuanChatClient(properties); | 423 | return new AiAutoConfiguration().buildHunYuanChatClient(properties); |
| 412 | } | 424 | } |
| @@ -415,7 +427,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -415,7 +427,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 415 | * 可参考 {@link AiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)} | 427 | * 可参考 {@link AiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)} |
| 416 | */ | 428 | */ |
| 417 | private ChatModel buildSiliconFlowChatModel(String apiKey) { | 429 | private ChatModel buildSiliconFlowChatModel(String apiKey) { |
| 418 | - YudaoAiProperties.SiliconFlowProperties properties = new YudaoAiProperties.SiliconFlowProperties() | 430 | + YudaoAiProperties.SiliconFlow properties = new YudaoAiProperties.SiliconFlow() |
| 419 | .setApiKey(apiKey); | 431 | .setApiKey(apiKey); |
| 420 | return new AiAutoConfiguration().buildSiliconFlowChatClient(properties); | 432 | return new AiAutoConfiguration().buildSiliconFlowChatClient(properties); |
| 421 | } | 433 | } |
| @@ -473,7 +485,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -473,7 +485,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 473 | private static XingHuoChatModel buildXingHuoChatModel(String key) { | 485 | private static XingHuoChatModel buildXingHuoChatModel(String key) { |
| 474 | List<String> keys = StrUtil.split(key, '|'); | 486 | List<String> keys = StrUtil.split(key, '|'); |
| 475 | Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式"); | 487 | Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式"); |
| 476 | - YudaoAiProperties.XingHuoProperties properties = new YudaoAiProperties.XingHuoProperties() | 488 | + YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo() |
| 477 | .setAppKey(keys.get(0)).setSecretKey(keys.get(1)); | 489 | .setAppKey(keys.get(0)).setSecretKey(keys.get(1)); |
| 478 | return new AiAutoConfiguration().buildXingHuoChatClient(properties); | 490 | return new AiAutoConfiguration().buildXingHuoChatClient(properties); |
| 479 | } | 491 | } |
| @@ -482,7 +494,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -482,7 +494,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 482 | * 可参考 {@link AiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)} | 494 | * 可参考 {@link AiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)} |
| 483 | */ | 495 | */ |
| 484 | private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) { | 496 | private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) { |
| 485 | - YudaoAiProperties.BaiChuanProperties properties = new YudaoAiProperties.BaiChuanProperties() | 497 | + YudaoAiProperties.BaiChuan properties = new YudaoAiProperties.BaiChuan() |
| 486 | .setApiKey(apiKey); | 498 | .setApiKey(apiKey); |
| 487 | return new AiAutoConfiguration().buildBaiChuanChatClient(properties); | 499 | return new AiAutoConfiguration().buildBaiChuanChatClient(properties); |
| 488 | } | 500 | } |
| @@ -513,6 +525,30 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -513,6 +525,30 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 513 | } | 525 | } |
| 514 | 526 | ||
| 515 | /** | 527 | /** |
| 528 | + * 可参考 {@link AnthropicChatAutoConfiguration} 的 anthropicApi 方法 | ||
| 529 | + */ | ||
| 530 | + private static AnthropicChatModel buildAnthropicChatModel(String apiKey, String url) { | ||
| 531 | + AnthropicApi.Builder builder = AnthropicApi.builder().apiKey(apiKey); | ||
| 532 | + if (StrUtil.isNotEmpty(url)) { | ||
| 533 | + builder.baseUrl(url); | ||
| 534 | + } | ||
| 535 | + AnthropicApi anthropicApi = builder.build(); | ||
| 536 | + return AnthropicChatModel.builder() | ||
| 537 | + .anthropicApi(anthropicApi) | ||
| 538 | + .toolCallingManager(getToolCallingManager()) | ||
| 539 | + .build(); | ||
| 540 | + } | ||
| 541 | + | ||
| 542 | + /** | ||
| 543 | + * 可参考 {@link AiAutoConfiguration#buildGeminiChatClient(YudaoAiProperties.Gemini)} | ||
| 544 | + */ | ||
| 545 | + private static GeminiChatModel buildGeminiChatModel(String apiKey) { | ||
| 546 | + YudaoAiProperties.Gemini properties = SpringUtil.getBean(YudaoAiProperties.class) | ||
| 547 | + .getGemini().setApiKey(apiKey); | ||
| 548 | + return new AiAutoConfiguration().buildGeminiChatClient(properties); | ||
| 549 | + } | ||
| 550 | + | ||
| 551 | + /** | ||
| 516 | * 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法 | 552 | * 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法 |
| 517 | */ | 553 | */ |
| 518 | private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { | 554 | private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/doubao/DouBaoChatModel.java
| @@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel; | @@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel; | ||
| 6 | import org.springframework.ai.chat.model.ChatResponse; | 6 | import org.springframework.ai.chat.model.ChatResponse; |
| 7 | import org.springframework.ai.chat.prompt.ChatOptions; | 7 | import org.springframework.ai.chat.prompt.ChatOptions; |
| 8 | import org.springframework.ai.chat.prompt.Prompt; | 8 | import org.springframework.ai.chat.prompt.Prompt; |
| 9 | -import org.springframework.ai.openai.OpenAiChatModel; | ||
| 10 | import reactor.core.publisher.Flux; | 9 | import reactor.core.publisher.Flux; |
| 11 | 10 | ||
| 12 | /** | 11 | /** |
| @@ -19,13 +18,14 @@ import reactor.core.publisher.Flux; | @@ -19,13 +18,14 @@ import reactor.core.publisher.Flux; | ||
| 19 | public class DouBaoChatModel implements ChatModel { | 18 | public class DouBaoChatModel implements ChatModel { |
| 20 | 19 | ||
| 21 | public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api"; | 20 | public static final String BASE_URL = "https://ark.cn-beijing.volces.com/api"; |
| 21 | + public static final String COMPLETE_PATH = "/v3/chat/completions"; | ||
| 22 | 22 | ||
| 23 | public static final String MODEL_DEFAULT = "doubao-1-5-lite-32k-250115"; | 23 | public static final String MODEL_DEFAULT = "doubao-1-5-lite-32k-250115"; |
| 24 | 24 | ||
| 25 | /** | 25 | /** |
| 26 | * 兼容 OpenAI 接口,进行复用 | 26 | * 兼容 OpenAI 接口,进行复用 |
| 27 | */ | 27 | */ |
| 28 | - private final OpenAiChatModel openAiChatModel; | 28 | + private final ChatModel openAiChatModel; |
| 29 | 29 | ||
| 30 | @Override | 30 | @Override |
| 31 | public ChatResponse call(Prompt prompt) { | 31 | public ChatResponse call(Prompt prompt) { |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/gemini/GeminiChatModel.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini; | ||
| 2 | + | ||
| 3 | +import lombok.RequiredArgsConstructor; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | +import org.springframework.ai.chat.model.ChatModel; | ||
| 6 | +import org.springframework.ai.chat.model.ChatResponse; | ||
| 7 | +import org.springframework.ai.chat.prompt.ChatOptions; | ||
| 8 | +import org.springframework.ai.chat.prompt.Prompt; | ||
| 9 | +import org.springframework.ai.openai.OpenAiChatModel; | ||
| 10 | +import reactor.core.publisher.Flux; | ||
| 11 | + | ||
| 12 | +/** | ||
| 13 | + * 谷歌 Gemini {@link ChatModel} 实现类,基于 Google AI Studio 提供的 <a href="https://ai.google.dev/gemini-api/docs/openai">OpenAI 兼容方案</a> | ||
| 14 | + * | ||
| 15 | + * @author 芋道源码 | ||
| 16 | + */ | ||
| 17 | +@Slf4j | ||
| 18 | +@RequiredArgsConstructor | ||
| 19 | +public class GeminiChatModel implements ChatModel { | ||
| 20 | + | ||
| 21 | + public static final String BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"; | ||
| 22 | + public static final String COMPLETE_PATH = "/chat/completions"; | ||
| 23 | + | ||
| 24 | + public static final String MODEL_DEFAULT = "gemini-2.5-flash"; | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 兼容 OpenAI 接口,进行复用 | ||
| 28 | + */ | ||
| 29 | + private final OpenAiChatModel openAiChatModel; | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public ChatResponse call(Prompt prompt) { | ||
| 33 | + return openAiChatModel.call(prompt); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Override | ||
| 37 | + public Flux<ChatResponse> stream(Prompt prompt) { | ||
| 38 | + return openAiChatModel.stream(prompt); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public ChatOptions getDefaultOptions() { | ||
| 43 | + return openAiChatModel.getDefaultOptions(); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | +} |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/hunyuan/HunYuanChatModel.java
| @@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel; | @@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel; | ||
| 6 | import org.springframework.ai.chat.model.ChatResponse; | 6 | import org.springframework.ai.chat.model.ChatResponse; |
| 7 | import org.springframework.ai.chat.prompt.ChatOptions; | 7 | import org.springframework.ai.chat.prompt.ChatOptions; |
| 8 | import org.springframework.ai.chat.prompt.Prompt; | 8 | import org.springframework.ai.chat.prompt.Prompt; |
| 9 | -import org.springframework.ai.openai.OpenAiChatModel; | ||
| 10 | import reactor.core.publisher.Flux; | 9 | import reactor.core.publisher.Flux; |
| 11 | 10 | ||
| 12 | /** | 11 | /** |
| @@ -22,6 +21,7 @@ import reactor.core.publisher.Flux; | @@ -22,6 +21,7 @@ import reactor.core.publisher.Flux; | ||
| 22 | public class HunYuanChatModel implements ChatModel { | 21 | public class HunYuanChatModel implements ChatModel { |
| 23 | 22 | ||
| 24 | public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com"; | 23 | public static final String BASE_URL = "https://api.hunyuan.cloud.tencent.com"; |
| 24 | + public static final String COMPLETE_PATH = "/v1/chat/completions"; | ||
| 25 | 25 | ||
| 26 | public static final String MODEL_DEFAULT = "hunyuan-turbo"; | 26 | public static final String MODEL_DEFAULT = "hunyuan-turbo"; |
| 27 | 27 | ||
| @@ -32,7 +32,7 @@ public class HunYuanChatModel implements ChatModel { | @@ -32,7 +32,7 @@ public class HunYuanChatModel implements ChatModel { | ||
| 32 | /** | 32 | /** |
| 33 | * 兼容 OpenAI 接口,进行复用 | 33 | * 兼容 OpenAI 接口,进行复用 |
| 34 | */ | 34 | */ |
| 35 | - private final OpenAiChatModel openAiChatModel; | 35 | + private final ChatModel openAiChatModel; |
| 36 | 36 | ||
| 37 | @Override | 37 | @Override |
| 38 | public ChatResponse call(Prompt prompt) { | 38 | public ChatResponse call(Prompt prompt) { |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowChatModel.java
| @@ -23,7 +23,7 @@ public class SiliconFlowChatModel implements ChatModel { | @@ -23,7 +23,7 @@ public class SiliconFlowChatModel implements ChatModel { | ||
| 23 | /** | 23 | /** |
| 24 | * 兼容 OpenAI 接口,进行复用 | 24 | * 兼容 OpenAI 接口,进行复用 |
| 25 | */ | 25 | */ |
| 26 | - private final OpenAiChatModel openAiChatModel; | 26 | + private final ChatModel openAiChatModel; |
| 27 | 27 | ||
| 28 | @Override | 28 | @Override |
| 29 | public ChatResponse call(Prompt prompt) { | 29 | public ChatResponse call(Prompt prompt) { |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/xinghuo/XingHuoChatModel.java
| @@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel; | @@ -6,7 +6,6 @@ import org.springframework.ai.chat.model.ChatModel; | ||
| 6 | import org.springframework.ai.chat.model.ChatResponse; | 6 | import org.springframework.ai.chat.model.ChatResponse; |
| 7 | import org.springframework.ai.chat.prompt.ChatOptions; | 7 | import org.springframework.ai.chat.prompt.ChatOptions; |
| 8 | import org.springframework.ai.chat.prompt.Prompt; | 8 | import org.springframework.ai.chat.prompt.Prompt; |
| 9 | -import org.springframework.ai.openai.OpenAiChatModel; | ||
| 10 | import reactor.core.publisher.Flux; | 9 | import reactor.core.publisher.Flux; |
| 11 | 10 | ||
| 12 | /** | 11 | /** |
| @@ -18,28 +17,34 @@ import reactor.core.publisher.Flux; | @@ -18,28 +17,34 @@ import reactor.core.publisher.Flux; | ||
| 18 | @RequiredArgsConstructor | 17 | @RequiredArgsConstructor |
| 19 | public class XingHuoChatModel implements ChatModel { | 18 | public class XingHuoChatModel implements ChatModel { |
| 20 | 19 | ||
| 21 | - public static final String BASE_URL = "https://spark-api-open.xf-yun.com"; | 20 | + public static final String BASE_URL_V1 = "https://spark-api-open.xf-yun.com"; |
| 22 | 21 | ||
| 23 | - public static final String MODEL_DEFAULT = "generalv3.5"; | 22 | + public static final String BASE_URL_V2 = "https://spark-api-open.xf-yun.com"; |
| 23 | + public static final String BASE_COMPLETIONS_PATH_V2 = "/v2/chat/completions"; | ||
| 24 | 24 | ||
| 25 | /** | 25 | /** |
| 26 | - * 兼容 OpenAI 接口,进行复用 | 26 | + * 已知模型名列表:x1、4.0Ultra、generalv3.5、max-32k、generalv3、pro-128k、lite |
| 27 | */ | 27 | */ |
| 28 | - private final OpenAiChatModel openAiChatModel; | 28 | + public static final String MODEL_DEFAULT = "4.0Ultra"; |
| 29 | + | ||
| 30 | + /** | ||
| 31 | + * v1 兼容 OpenAI 接口,进行复用 | ||
| 32 | + */ | ||
| 33 | + private final ChatModel openAiChatModelV1; | ||
| 29 | 34 | ||
| 30 | @Override | 35 | @Override |
| 31 | public ChatResponse call(Prompt prompt) { | 36 | public ChatResponse call(Prompt prompt) { |
| 32 | - return openAiChatModel.call(prompt); | 37 | + return openAiChatModelV1.call(prompt); |
| 33 | } | 38 | } |
| 34 | 39 | ||
| 35 | @Override | 40 | @Override |
| 36 | public Flux<ChatResponse> stream(Prompt prompt) { | 41 | public Flux<ChatResponse> stream(Prompt prompt) { |
| 37 | - return openAiChatModel.stream(prompt); | 42 | + return openAiChatModelV1.stream(prompt); |
| 38 | } | 43 | } |
| 39 | 44 | ||
| 40 | @Override | 45 | @Override |
| 41 | public ChatOptions getDefaultOptions() { | 46 | public ChatOptions getDefaultOptions() { |
| 42 | - return openAiChatModel.getDefaultOptions(); | 47 | + return openAiChatModelV1.getDefaultOptions(); |
| 43 | } | 48 | } |
| 44 | 49 | ||
| 45 | } | 50 | } |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchClient.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.framework.ai.core.webserch; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * 网络搜索客户端接口 | ||
| 5 | + * | ||
| 6 | + * @author 芋道源码 | ||
| 7 | + */ | ||
| 8 | +public interface AiWebSearchClient { | ||
| 9 | + | ||
| 10 | + /** | ||
| 11 | + * 网页搜索 | ||
| 12 | + * | ||
| 13 | + * @param request 搜索请求 | ||
| 14 | + * @return 搜索结果 | ||
| 15 | + */ | ||
| 16 | + AiWebSearchResponse search(AiWebSearchRequest request); | ||
| 17 | + | ||
| 18 | +} |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchRequest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.framework.ai.core.webserch; | ||
| 2 | + | ||
| 3 | +import jakarta.validation.constraints.Max; | ||
| 4 | +import jakarta.validation.constraints.Min; | ||
| 5 | +import jakarta.validation.constraints.NotEmpty; | ||
| 6 | +import jakarta.validation.constraints.NotNull; | ||
| 7 | +import lombok.Data; | ||
| 8 | + | ||
| 9 | +@Data | ||
| 10 | +public class AiWebSearchRequest { | ||
| 11 | + | ||
| 12 | + /** | ||
| 13 | + * 用户的搜索词 | ||
| 14 | + */ | ||
| 15 | + @NotEmpty(message = "搜索词不能为空") | ||
| 16 | + private String query; | ||
| 17 | + | ||
| 18 | + /** | ||
| 19 | + * 是否显示文本摘要 | ||
| 20 | + * | ||
| 21 | + * true - 显示 | ||
| 22 | + * false - 不显示(默认) | ||
| 23 | + */ | ||
| 24 | + private Boolean summary; | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 返回结果的条数 | ||
| 28 | + */ | ||
| 29 | + @NotNull(message = "返回结果条数不能为空") | ||
| 30 | + @Min(message = "返回结果条数最小为 1", value = 1) | ||
| 31 | + @Max(message = "返回结果条数最大为 50", value = 50) | ||
| 32 | + private Integer count; | ||
| 33 | + | ||
| 34 | +} |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/AiWebSearchResponse.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.framework.ai.core.webserch; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +import java.util.List; | ||
| 6 | + | ||
| 7 | +@Data | ||
| 8 | +public class AiWebSearchResponse { | ||
| 9 | + | ||
| 10 | + /** | ||
| 11 | + * 总数(总共匹配的网页数) | ||
| 12 | + */ | ||
| 13 | + private Long total; | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * 数据列表 | ||
| 17 | + */ | ||
| 18 | + private List<WebPage> lists; | ||
| 19 | + | ||
| 20 | + /** | ||
| 21 | + * 网页对象 | ||
| 22 | + */ | ||
| 23 | + @Data | ||
| 24 | + public static class WebPage { | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 名称 | ||
| 28 | + * | ||
| 29 | + * 例如说:搜狐网 | ||
| 30 | + */ | ||
| 31 | + private String name; | ||
| 32 | + /** | ||
| 33 | + * 图标 | ||
| 34 | + */ | ||
| 35 | + private String icon; | ||
| 36 | + | ||
| 37 | + /** | ||
| 38 | + * 标题 | ||
| 39 | + * | ||
| 40 | + * 例如说:186页|阿里巴巴:2024年环境、社会和治理(ESG)报告 | ||
| 41 | + */ | ||
| 42 | + private String title; | ||
| 43 | + /** | ||
| 44 | + * URL | ||
| 45 | + * | ||
| 46 | + * 例如说:https://m.sohu.com/a/815036254_121819701/?pvid=000115_3w_a | ||
| 47 | + */ | ||
| 48 | + @SuppressWarnings("JavadocLinkAsPlainText") | ||
| 49 | + private String url; | ||
| 50 | + | ||
| 51 | + /** | ||
| 52 | + * 内容的简短描述 | ||
| 53 | + */ | ||
| 54 | + private String snippet; | ||
| 55 | + /** | ||
| 56 | + * 内容的文本摘要 | ||
| 57 | + */ | ||
| 58 | + private String summary; | ||
| 59 | + | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | +} | ||
| 0 | \ No newline at end of file | 63 | \ No newline at end of file |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/webserch/bocha/AiBoChaWebSearchClient.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 4 | +import cn.hutool.core.lang.Assert; | ||
| 5 | +import cn.iocoder.yudao.framework.common.pojo.CommonResult; | ||
| 6 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; | ||
| 7 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest; | ||
| 8 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse; | ||
| 9 | +import com.fasterxml.jackson.annotation.JsonInclude; | ||
| 10 | +import lombok.extern.slf4j.Slf4j; | ||
| 11 | +import org.springframework.core.ParameterizedTypeReference; | ||
| 12 | +import org.springframework.http.HttpStatusCode; | ||
| 13 | +import org.springframework.http.MediaType; | ||
| 14 | +import org.springframework.web.reactive.function.client.ClientResponse; | ||
| 15 | +import org.springframework.web.reactive.function.client.WebClient; | ||
| 16 | +import reactor.core.publisher.Mono; | ||
| 17 | + | ||
| 18 | +import java.util.List; | ||
| 19 | +import java.util.function.Function; | ||
| 20 | +import java.util.function.Predicate; | ||
| 21 | + | ||
| 22 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | ||
| 23 | + | ||
| 24 | +/** | ||
| 25 | + * 博查 {@link AiWebSearchClient} 实现类 | ||
| 26 | + * | ||
| 27 | + * @see <a href="https://open.bochaai.com/overview">博查 AI 开放平台</a> | ||
| 28 | + * | ||
| 29 | + * @author 芋道源码 | ||
| 30 | + */ | ||
| 31 | +@Slf4j | ||
| 32 | +public class AiBoChaWebSearchClient implements AiWebSearchClient { | ||
| 33 | + | ||
| 34 | + public static final String BASE_URL = "https://api.bochaai.com"; | ||
| 35 | + private static final String AUTHORIZATION_HEADER = "Authorization"; | ||
| 36 | + private static final String BEARER_PREFIX = "Bearer "; | ||
| 37 | + | ||
| 38 | + private final WebClient webClient; | ||
| 39 | + | ||
| 40 | + private final Predicate<HttpStatusCode> STATUS_PREDICATE = status -> !status.is2xxSuccessful(); | ||
| 41 | + | ||
| 42 | + private final Function<Object, Function<ClientResponse, Mono<? extends Throwable>>> EXCEPTION_FUNCTION = | ||
| 43 | + reqParam -> response -> response.bodyToMono(String.class).handle((responseBody, sink) -> { | ||
| 44 | + log.error("[AiBoChaWebSearchClient] 调用失败!请求参数:[{}],响应数据: [{}]", reqParam, responseBody); | ||
| 45 | + sink.error(new IllegalStateException("[AiBoChaWebSearchClient] 调用失败!")); | ||
| 46 | + }); | ||
| 47 | + | ||
| 48 | + public AiBoChaWebSearchClient(String apiKey) { | ||
| 49 | + this.webClient = WebClient.builder() | ||
| 50 | + .baseUrl(BASE_URL) | ||
| 51 | + .defaultHeaders((headers) -> { | ||
| 52 | + headers.setContentType(MediaType.APPLICATION_JSON); | ||
| 53 | + headers.add(AUTHORIZATION_HEADER, BEARER_PREFIX + apiKey); | ||
| 54 | + }) | ||
| 55 | + .build(); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + @Override | ||
| 59 | + public AiWebSearchResponse search(AiWebSearchRequest request) { | ||
| 60 | + // 转换请求参数 | ||
| 61 | + WebSearchRequest webSearchRequest = new WebSearchRequest( | ||
| 62 | + request.getQuery(), | ||
| 63 | + request.getSummary(), | ||
| 64 | + request.getCount() | ||
| 65 | + ); | ||
| 66 | + // 调用博查 API | ||
| 67 | + CommonResult<WebSearchResponse> response = this.webClient.post() | ||
| 68 | + .uri("/v1/web-search") | ||
| 69 | + .bodyValue(webSearchRequest) | ||
| 70 | + .retrieve() | ||
| 71 | + .onStatus(STATUS_PREDICATE, EXCEPTION_FUNCTION.apply(webSearchRequest)) | ||
| 72 | + .bodyToMono(new ParameterizedTypeReference<CommonResult<WebSearchResponse>>() {}) | ||
| 73 | + .block(); | ||
| 74 | + if (response == null) { | ||
| 75 | + throw new IllegalStateException("[search][搜索结果为空]"); | ||
| 76 | + } | ||
| 77 | + if (response.getData() == null) { | ||
| 78 | + throw new IllegalStateException(String.format("[search][搜索失败,code = %s, msg = %s]", | ||
| 79 | + response.getCode(), response.getMsg())); | ||
| 80 | + } | ||
| 81 | + WebSearchResponse data = response.getData(); | ||
| 82 | + | ||
| 83 | + // 转换结果 | ||
| 84 | + AiWebSearchResponse result = new AiWebSearchResponse(); | ||
| 85 | + if (data.webPages() == null || CollUtil.isEmpty(data.webPages().value())) { | ||
| 86 | + return result.setTotal(0L).setLists(List.of()); | ||
| 87 | + } | ||
| 88 | + return result.setTotal(data.webPages().totalEstimatedMatches()) | ||
| 89 | + .setLists(convertList(data.webPages().value(), page -> new AiWebSearchResponse.WebPage() | ||
| 90 | + .setName(page.siteName()).setIcon(page.siteIcon()) | ||
| 91 | + .setTitle(page.name()).setUrl(page.url()) | ||
| 92 | + .setSnippet(page.snippet()).setSummary(page.summary()))); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + /** | ||
| 96 | + * 网页搜索请求参数 | ||
| 97 | + */ | ||
| 98 | + @JsonInclude(value = JsonInclude.Include.NON_NULL) | ||
| 99 | + public record WebSearchRequest( | ||
| 100 | + String query, | ||
| 101 | + Boolean summary, | ||
| 102 | + Integer count | ||
| 103 | + ) { | ||
| 104 | + public WebSearchRequest { | ||
| 105 | + Assert.notBlank(query, "query 不能为空"); | ||
| 106 | + } | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + /** | ||
| 110 | + * 网页搜索响应 | ||
| 111 | + */ | ||
| 112 | + @JsonInclude(value = JsonInclude.Include.NON_NULL) | ||
| 113 | + public record WebSearchResponse( | ||
| 114 | + WebSearchWebPages webPages | ||
| 115 | + ) { | ||
| 116 | + } | ||
| 117 | + | ||
| 118 | + /** | ||
| 119 | + * 网页搜索结果 | ||
| 120 | + */ | ||
| 121 | + @JsonInclude(value = JsonInclude.Include.NON_NULL) | ||
| 122 | + public record WebSearchWebPages( | ||
| 123 | + String webSearchUrl, | ||
| 124 | + Long totalEstimatedMatches, | ||
| 125 | + List<WebPageValue> value, | ||
| 126 | + Boolean someResultsRemoved | ||
| 127 | + ) { | ||
| 128 | + | ||
| 129 | + /** | ||
| 130 | + * 网页结果值 | ||
| 131 | + */ | ||
| 132 | + @JsonInclude(value = JsonInclude.Include.NON_NULL) | ||
| 133 | + public record WebPageValue( | ||
| 134 | + String id, | ||
| 135 | + String name, | ||
| 136 | + String url, | ||
| 137 | + String displayUrl, | ||
| 138 | + String snippet, | ||
| 139 | + String summary, | ||
| 140 | + String siteName, | ||
| 141 | + String siteIcon, | ||
| 142 | + String datePublished, | ||
| 143 | + String dateLastCrawled, | ||
| 144 | + String cachedPageUrl, | ||
| 145 | + String language, | ||
| 146 | + Boolean isFamilyFriendly, | ||
| 147 | + Boolean isNavigational | ||
| 148 | + ) { | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | +} |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/config/SecurityConfiguration.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.framework.security.config; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; | ||
| 4 | +import jakarta.annotation.Resource; | ||
| 5 | +import org.springframework.ai.mcp.server.autoconfigure.McpServerProperties; | ||
| 6 | +import org.springframework.context.annotation.Bean; | ||
| 7 | +import org.springframework.context.annotation.Configuration; | ||
| 8 | +import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
| 9 | +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * AI 模块的 Security 配置 | ||
| 13 | + */ | ||
| 14 | +@Configuration(proxyBeanMethods = false, value = "aiSecurityConfiguration") | ||
| 15 | +public class SecurityConfiguration { | ||
| 16 | + | ||
| 17 | + @Resource | ||
| 18 | + private McpServerProperties serverProperties; | ||
| 19 | + | ||
| 20 | + @Bean("aiAuthorizeRequestsCustomizer") | ||
| 21 | + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { | ||
| 22 | + return new AuthorizeRequestsCustomizer() { | ||
| 23 | + | ||
| 24 | + @Override | ||
| 25 | + public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) { | ||
| 26 | + // MCP Server | ||
| 27 | + registry.requestMatchers(serverProperties.getSseEndpoint()).permitAll(); | ||
| 28 | + registry.requestMatchers(serverProperties.getSseMessageEndpoint()).permitAll(); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + }; | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | +} |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/security/core/package-info.java
0 → 100644
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/chat/AiChatMessageServiceImpl.java
| 1 | package cn.iocoder.yudao.module.ai.service.chat; | 1 | package cn.iocoder.yudao.module.ai.service.chat; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.codec.Base64; | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 4 | import cn.hutool.core.collection.CollUtil; |
| 5 | +import cn.hutool.core.io.file.FileNameUtil; | ||
| 4 | import cn.hutool.core.util.ObjUtil; | 6 | import cn.hutool.core.util.ObjUtil; |
| 5 | import cn.hutool.core.util.StrUtil; | 7 | import cn.hutool.core.util.StrUtil; |
| 6 | -import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; | ||
| 7 | -import cn.iocoder.yudao.module.ai.util.AiUtils; | 8 | +import cn.hutool.http.HttpUtil; |
| 8 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 9 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 9 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 10 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 10 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 11 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| @@ -21,6 +22,10 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; | @@ -21,6 +22,10 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; | ||
| 21 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; | 22 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; |
| 22 | import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; | 23 | import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; |
| 23 | import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; | 24 | import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; |
| 25 | +import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; | ||
| 26 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchClient; | ||
| 27 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest; | ||
| 28 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse; | ||
| 24 | import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; | 29 | import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeDocumentService; |
| 25 | import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; | 30 | import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService; |
| 26 | import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; | 31 | import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; |
| @@ -28,6 +33,10 @@ import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchR | @@ -28,6 +33,10 @@ import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchR | ||
| 28 | import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; | 33 | import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; |
| 29 | import cn.iocoder.yudao.module.ai.service.model.AiModelService; | 34 | import cn.iocoder.yudao.module.ai.service.model.AiModelService; |
| 30 | import cn.iocoder.yudao.module.ai.service.model.AiToolService; | 35 | import cn.iocoder.yudao.module.ai.service.model.AiToolService; |
| 36 | +import cn.iocoder.yudao.module.ai.util.AiUtils; | ||
| 37 | +import cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils; | ||
| 38 | +import com.google.common.collect.Maps; | ||
| 39 | +import io.modelcontextprotocol.client.McpSyncClient; | ||
| 31 | import jakarta.annotation.Resource; | 40 | import jakarta.annotation.Resource; |
| 32 | import lombok.extern.slf4j.Slf4j; | 41 | import lombok.extern.slf4j.Slf4j; |
| 33 | import org.springframework.ai.chat.messages.Message; | 42 | import org.springframework.ai.chat.messages.Message; |
| @@ -39,6 +48,11 @@ import org.springframework.ai.chat.model.ChatResponse; | @@ -39,6 +48,11 @@ import org.springframework.ai.chat.model.ChatResponse; | ||
| 39 | import org.springframework.ai.chat.model.StreamingChatModel; | 48 | import org.springframework.ai.chat.model.StreamingChatModel; |
| 40 | import org.springframework.ai.chat.prompt.ChatOptions; | 49 | import org.springframework.ai.chat.prompt.ChatOptions; |
| 41 | import org.springframework.ai.chat.prompt.Prompt; | 50 | import org.springframework.ai.chat.prompt.Prompt; |
| 51 | +import org.springframework.ai.mcp.SyncMcpToolCallbackProvider; | ||
| 52 | +import org.springframework.ai.mcp.client.autoconfigure.properties.McpClientCommonProperties; | ||
| 53 | +import org.springframework.ai.tool.ToolCallback; | ||
| 54 | +import org.springframework.ai.tool.resolution.ToolCallbackResolver; | ||
| 55 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 42 | import org.springframework.stereotype.Service; | 56 | import org.springframework.stereotype.Service; |
| 43 | import org.springframework.transaction.annotation.Transactional; | 57 | import org.springframework.transaction.annotation.Transactional; |
| 44 | import reactor.core.publisher.Flux; | 58 | import reactor.core.publisher.Flux; |
| @@ -65,12 +79,31 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_N | @@ -65,12 +79,31 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_N | ||
| 65 | public class AiChatMessageServiceImpl implements AiChatMessageService { | 79 | public class AiChatMessageServiceImpl implements AiChatMessageService { |
| 66 | 80 | ||
| 67 | /** | 81 | /** |
| 82 | + * 联网搜索的结束数 | ||
| 83 | + */ | ||
| 84 | + private static final Integer WEB_SEARCH_COUNT = 10; | ||
| 85 | + | ||
| 86 | + // TODO @芋艿:后续优化下对话的 Prompt 整体结构 | ||
| 87 | + | ||
| 88 | + /** | ||
| 68 | * 知识库转 {@link UserMessage} 的内容模版 | 89 | * 知识库转 {@link UserMessage} 的内容模版 |
| 69 | */ | 90 | */ |
| 70 | private static final String KNOWLEDGE_USER_MESSAGE_TEMPLATE = "使用 <Reference></Reference> 标记中的内容作为本次对话的参考:\n\n" + | 91 | private static final String KNOWLEDGE_USER_MESSAGE_TEMPLATE = "使用 <Reference></Reference> 标记中的内容作为本次对话的参考:\n\n" + |
| 71 | "%s\n\n" + // 多个 <Reference></Reference> 的拼接 | 92 | "%s\n\n" + // 多个 <Reference></Reference> 的拼接 |
| 72 | "回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。"; | 93 | "回答要求:\n- 避免提及你是从 <Reference></Reference> 获取的知识。"; |
| 73 | 94 | ||
| 95 | + private static final String WEB_SEARCH_USER_MESSAGE_TEMPLATE = "使用 <WebSearch></WebSearch> 标记中的内容作为本次对话的参考:\n\n" + | ||
| 96 | + "%s\n\n" + // 多个 <WebSearch></WebSearch> 的拼接 | ||
| 97 | + "回答要求:\n- 避免提及你是从 <WebSearch></WebSearch> 获取的知识。"; | ||
| 98 | + | ||
| 99 | + /** | ||
| 100 | + * 附件转 ${@link UserMessage} 的内容模版 | ||
| 101 | + */ | ||
| 102 | + @SuppressWarnings("TextBlockMigration") | ||
| 103 | + private static final String Attachment_USER_MESSAGE_TEMPLATE = "使用 <Attachment></Attachment> 标记用户对话上传的附件内容:\n\n" + | ||
| 104 | + "%s\n\n" + // 多个 <Attachment></Attachment> 的拼接 | ||
| 105 | + "回答要求:\n- 避免提及 <Attachment></Attachment> 附件的编码格式。"; | ||
| 106 | + | ||
| 74 | @Resource | 107 | @Resource |
| 75 | private AiChatMessageMapper chatMessageMapper; | 108 | private AiChatMessageMapper chatMessageMapper; |
| 76 | 109 | ||
| @@ -87,6 +120,21 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -87,6 +120,21 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 87 | @Resource | 120 | @Resource |
| 88 | private AiToolService toolService; | 121 | private AiToolService toolService; |
| 89 | 122 | ||
| 123 | + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") | ||
| 124 | + @Autowired(required = false) // 由于 yudao.ai.web-search.enable 配置项,可以关闭 AiWebSearchClient 的功能,所以这里只能不强制注入 | ||
| 125 | + private AiWebSearchClient webSearchClient; | ||
| 126 | + | ||
| 127 | + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") | ||
| 128 | + @Autowired(required = false) // 由于 yudao.ai.mcp.client.enable 配置项,可以关闭 McpSyncClient 的功能,所以这里只能不强制注入 | ||
| 129 | + private List<McpSyncClient> mcpClients; | ||
| 130 | + | ||
| 131 | + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") | ||
| 132 | + @Autowired(required = false) // 由于 yudao.ai.mcp.client.enable 配置项,可以关闭 McpSyncClient 的功能,所以这里只能不强制注入 | ||
| 133 | + private McpClientCommonProperties mcpClientCommonProperties; | ||
| 134 | + | ||
| 135 | + @Resource | ||
| 136 | + private ToolCallbackResolver toolCallbackResolver; | ||
| 137 | + | ||
| 90 | @Transactional(rollbackFor = Exception.class) | 138 | @Transactional(rollbackFor = Exception.class) |
| 91 | public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { | 139 | public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { |
| 92 | // 1.1 校验对话存在 | 140 | // 1.1 校验对话存在 |
| @@ -100,27 +148,35 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -100,27 +148,35 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 100 | AiModelDO model = modalService.validateModel(conversation.getModelId()); | 148 | AiModelDO model = modalService.validateModel(conversation.getModelId()); |
| 101 | ChatModel chatModel = modalService.getChatModel(model.getId()); | 149 | ChatModel chatModel = modalService.getChatModel(model.getId()); |
| 102 | 150 | ||
| 103 | - // 2. 知识库找回 | ||
| 104 | - List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), conversation); | 151 | + // 2.1 知识库召回 |
| 152 | + List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment( | ||
| 153 | + sendReqVO.getContent(), conversation); | ||
| 154 | + | ||
| 155 | + // 2.2 联网搜索 | ||
| 156 | + AiWebSearchResponse webSearchResponse = Boolean.TRUE.equals(sendReqVO.getUseSearch()) && webSearchClient != null ? | ||
| 157 | + webSearchClient.search(new AiWebSearchRequest().setQuery(sendReqVO.getContent()) | ||
| 158 | + .setSummary(true).setCount(WEB_SEARCH_COUNT)) : null; | ||
| 105 | 159 | ||
| 106 | // 3. 插入 user 发送消息 | 160 | // 3. 插入 user 发送消息 |
| 107 | AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, | 161 | AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, |
| 108 | userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), | 162 | userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), |
| 109 | - null); | 163 | + null, sendReqVO.getAttachmentUrls(), null); |
| 110 | 164 | ||
| 111 | - // 3.1 插入 assistant 接收消息 | 165 | + // 4.1 插入 assistant 接收消息 |
| 112 | AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, | 166 | AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, |
| 113 | userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), | 167 | userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), |
| 114 | - knowledgeSegments); | 168 | + knowledgeSegments, null, webSearchResponse); |
| 115 | 169 | ||
| 116 | - // 3.2 创建 chat 需要的 Prompt | ||
| 117 | - Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); | 170 | + // 4.2 创建 chat 需要的 Prompt |
| 171 | + Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, webSearchResponse, model, sendReqVO); | ||
| 118 | ChatResponse chatResponse = chatModel.call(prompt); | 172 | ChatResponse chatResponse = chatModel.call(prompt); |
| 119 | 173 | ||
| 120 | - // 3.3 更新响应内容 | ||
| 121 | - String newContent = chatResponse.getResult().getOutput().getText(); | ||
| 122 | - chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent)); | ||
| 123 | - // 3.4 响应结果 | 174 | + // 4.3 更新响应内容 |
| 175 | + String newContent = AiUtils.getChatResponseContent(chatResponse); | ||
| 176 | + String newReasoningContent = AiUtils.getChatResponseReasoningContent(chatResponse); | ||
| 177 | + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()) | ||
| 178 | + .setContent(newContent).setReasoningContent(newReasoningContent)); | ||
| 179 | + // 4.4 响应结果 | ||
| 124 | Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( | 180 | Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap( |
| 125 | convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); | 181 | convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)); |
| 126 | List<AiChatMessageRespVO.KnowledgeSegment> segments = BeanUtils.toBean(knowledgeSegments, | 182 | List<AiChatMessageRespVO.KnowledgeSegment> segments = BeanUtils.toBean(knowledgeSegments, |
| @@ -131,7 +187,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -131,7 +187,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 131 | return new AiChatMessageSendRespVO() | 187 | return new AiChatMessageSendRespVO() |
| 132 | .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) | 188 | .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) |
| 133 | .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) | 189 | .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) |
| 134 | - .setContent(newContent).setSegments(segments)); | 190 | + .setContent(newContent).setSegments(segments) |
| 191 | + .setWebSearchPages(webSearchResponse != null ? webSearchResponse.getLists() : null)); | ||
| 135 | } | 192 | } |
| 136 | 193 | ||
| 137 | @Override | 194 | @Override |
| @@ -148,29 +205,36 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -148,29 +205,36 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 148 | AiModelDO model = modalService.validateModel(conversation.getModelId()); | 205 | AiModelDO model = modalService.validateModel(conversation.getModelId()); |
| 149 | StreamingChatModel chatModel = modalService.getChatModel(model.getId()); | 206 | StreamingChatModel chatModel = modalService.getChatModel(model.getId()); |
| 150 | 207 | ||
| 151 | - // 2. 知识库找回 | ||
| 152 | - List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(), | ||
| 153 | - conversation); | 208 | + // 2.1 知识库找回 |
| 209 | + List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment( | ||
| 210 | + sendReqVO.getContent(), conversation); | ||
| 211 | + | ||
| 212 | + // 2.2 联网搜索 | ||
| 213 | + AiWebSearchResponse webSearchResponse = Boolean.TRUE.equals(sendReqVO.getUseSearch()) && webSearchClient != null ? | ||
| 214 | + webSearchClient.search(new AiWebSearchRequest().setQuery(sendReqVO.getContent()) | ||
| 215 | + .setSummary(true).setCount(WEB_SEARCH_COUNT)) : null; | ||
| 154 | 216 | ||
| 155 | // 3. 插入 user 发送消息 | 217 | // 3. 插入 user 发送消息 |
| 156 | AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, | 218 | AiChatMessageDO userMessage = createChatMessage(conversation.getId(), null, model, |
| 157 | userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), | 219 | userId, conversation.getRoleId(), MessageType.USER, sendReqVO.getContent(), sendReqVO.getUseContext(), |
| 158 | - null); | 220 | + null, sendReqVO.getAttachmentUrls(), null); |
| 159 | 221 | ||
| 160 | // 4.1 插入 assistant 接收消息 | 222 | // 4.1 插入 assistant 接收消息 |
| 161 | AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, | 223 | AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, |
| 162 | userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), | 224 | userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext(), |
| 163 | - knowledgeSegments); | 225 | + knowledgeSegments, null, webSearchResponse); |
| 164 | 226 | ||
| 165 | // 4.2 构建 Prompt,并进行调用 | 227 | // 4.2 构建 Prompt,并进行调用 |
| 166 | - Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, model, sendReqVO); | 228 | + Prompt prompt = buildPrompt(conversation, historyMessages, knowledgeSegments, webSearchResponse, model, sendReqVO); |
| 167 | Flux<ChatResponse> streamResponse = chatModel.stream(prompt); | 229 | Flux<ChatResponse> streamResponse = chatModel.stream(prompt); |
| 168 | 230 | ||
| 169 | // 4.3 流式返回 | 231 | // 4.3 流式返回 |
| 170 | StringBuffer contentBuffer = new StringBuffer(); | 232 | StringBuffer contentBuffer = new StringBuffer(); |
| 233 | + StringBuffer reasoningContentBuffer = new StringBuffer(); | ||
| 171 | return streamResponse.map(chunk -> { | 234 | return streamResponse.map(chunk -> { |
| 172 | - // 处理知识库的返回,只有首次才有 | 235 | + // 仅首次:返回知识库、联网搜索 |
| 173 | List<AiChatMessageRespVO.KnowledgeSegment> segments = null; | 236 | List<AiChatMessageRespVO.KnowledgeSegment> segments = null; |
| 237 | + List<AiWebSearchResponse.WebPage> webSearchPages = null; | ||
| 174 | if (StrUtil.isEmpty(contentBuffer)) { | 238 | if (StrUtil.isEmpty(contentBuffer)) { |
| 175 | Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() -> | 239 | Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() -> |
| 176 | knowledgeDocumentService.getKnowledgeDocumentMap( | 240 | knowledgeDocumentService.getKnowledgeDocumentMap( |
| @@ -179,24 +243,56 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -179,24 +243,56 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 179 | AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); | 243 | AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId()); |
| 180 | segment.setDocumentName(document != null ? document.getName() : null); | 244 | segment.setDocumentName(document != null ? document.getName() : null); |
| 181 | }); | 245 | }); |
| 246 | + if (webSearchResponse != null) { | ||
| 247 | + webSearchPages = webSearchResponse.getLists(); | ||
| 248 | + } | ||
| 182 | } | 249 | } |
| 183 | // 响应结果 | 250 | // 响应结果 |
| 184 | - String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null; | ||
| 185 | - newContent = StrUtil.nullToDefault(newContent, ""); // 避免 null 的 情况 | ||
| 186 | - contentBuffer.append(newContent); | 251 | + String newContent = AiUtils.getChatResponseContent(chunk); |
| 252 | + String newReasoningContent = AiUtils.getChatResponseReasoningContent(chunk); | ||
| 253 | + if (StrUtil.isNotEmpty(newContent)) { | ||
| 254 | + contentBuffer.append(newContent); | ||
| 255 | + } | ||
| 256 | + if (StrUtil.isNotEmpty(newReasoningContent)) { | ||
| 257 | + reasoningContentBuffer.append(newReasoningContent); | ||
| 258 | + } | ||
| 187 | return success(new AiChatMessageSendRespVO() | 259 | return success(new AiChatMessageSendRespVO() |
| 188 | .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) | 260 | .setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) |
| 189 | .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) | 261 | .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class) |
| 190 | - .setContent(newContent).setSegments(segments))); | 262 | + .setContent(StrUtil.nullToDefault(newContent, "")) // 避免 null 的 情况 |
| 263 | + .setReasoningContent(StrUtil.nullToDefault(newReasoningContent, "")) // 避免 null 的 情况 | ||
| 264 | + .setSegments(segments).setWebSearchPages(webSearchPages))); // 知识库 + 联网搜索 | ||
| 191 | }).doOnComplete(() -> { | 265 | }).doOnComplete(() -> { |
| 192 | // 忽略租户,因为 Flux 异步无法透传租户 | 266 | // 忽略租户,因为 Flux 异步无法透传租户 |
| 193 | TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( | 267 | TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( |
| 194 | - new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString()))); | 268 | + new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString()) |
| 269 | + .setReasoningContent(reasoningContentBuffer.toString()))); | ||
| 195 | }).doOnError(throwable -> { | 270 | }).doOnError(throwable -> { |
| 196 | log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable); | 271 | log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable); |
| 197 | // 忽略租户,因为 Flux 异步无法透传租户 | 272 | // 忽略租户,因为 Flux 异步无法透传租户 |
| 198 | - TenantUtils.executeIgnore(() -> chatMessageMapper.updateById( | ||
| 199 | - new AiChatMessageDO().setId(assistantMessage.getId()).setContent(throwable.getMessage()))); | 273 | + TenantUtils.executeIgnore(() -> { |
| 274 | + // 如果有内容,则更新内容 | ||
| 275 | + if (StrUtil.isNotEmpty(contentBuffer)) { | ||
| 276 | + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()) | ||
| 277 | + .setContent(contentBuffer.toString()).setReasoningContent(reasoningContentBuffer.toString())); | ||
| 278 | + } else { | ||
| 279 | + // 否则,则进行删除 | ||
| 280 | + chatMessageMapper.deleteById(assistantMessage.getId()); | ||
| 281 | + } | ||
| 282 | + }); | ||
| 283 | + }).doOnCancel(() -> { | ||
| 284 | + log.info("[sendChatMessageStream][userId({}) sendReqVO({}) 取消请求]", userId, sendReqVO); | ||
| 285 | + // 忽略租户,因为 Flux 异步无法透传租户 | ||
| 286 | + TenantUtils.executeIgnore(() -> { | ||
| 287 | + // 如果有内容,则更新内容 | ||
| 288 | + if (StrUtil.isNotEmpty(contentBuffer)) { | ||
| 289 | + chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()) | ||
| 290 | + .setContent(contentBuffer.toString()).setReasoningContent(reasoningContentBuffer.toString())); | ||
| 291 | + } else { | ||
| 292 | + // 否则,则进行删除 | ||
| 293 | + chatMessageMapper.deleteById(assistantMessage.getId()); | ||
| 294 | + } | ||
| 295 | + }); | ||
| 200 | }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR))); | 296 | }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR))); |
| 201 | } | 297 | } |
| 202 | 298 | ||
| @@ -211,7 +307,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -211,7 +307,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 211 | return Collections.emptyList(); | 307 | return Collections.emptyList(); |
| 212 | } | 308 | } |
| 213 | 309 | ||
| 214 | - // 2. 遍历找回 | 310 | + // 2. 遍历召回 |
| 215 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = new ArrayList<>(); | 311 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = new ArrayList<>(); |
| 216 | for (Long knowledgeId : role.getKnowledgeIds()) { | 312 | for (Long knowledgeId : role.getKnowledgeIds()) { |
| 217 | knowledgeSegments.addAll(knowledgeSegmentService.searchKnowledgeSegment(new AiKnowledgeSegmentSearchReqBO() | 313 | knowledgeSegments.addAll(knowledgeSegmentService.searchKnowledgeSegment(new AiKnowledgeSegmentSearchReqBO() |
| @@ -222,6 +318,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -222,6 +318,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 222 | 318 | ||
| 223 | private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages, | 319 | private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages, |
| 224 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments, | 320 | List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments, |
| 321 | + AiWebSearchResponse webSearchResponse, | ||
| 225 | AiModelDO model, AiChatMessageSendReqVO sendReqVO) { | 322 | AiModelDO model, AiChatMessageSendReqVO sendReqVO) { |
| 226 | List<Message> chatMessages = new ArrayList<>(); | 323 | List<Message> chatMessages = new ArrayList<>(); |
| 227 | // 1.1 System Context 角色设定 | 324 | // 1.1 System Context 角色设定 |
| @@ -231,8 +328,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -231,8 +328,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 231 | 328 | ||
| 232 | // 1.2 历史 history message 历史消息 | 329 | // 1.2 历史 history message 历史消息 |
| 233 | List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO); | 330 | List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO); |
| 234 | - contextMessages | ||
| 235 | - .forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent()))); | 331 | + contextMessages.forEach(message -> { |
| 332 | + chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent())); | ||
| 333 | + UserMessage attachmentUserMessage = buildAttachmentUserMessage(message.getAttachmentUrls()); | ||
| 334 | + if (attachmentUserMessage != null) { | ||
| 335 | + chatMessages.add(attachmentUserMessage); | ||
| 336 | + } | ||
| 337 | + // TODO @芋艿:历史的知识库;历史的搜索,要不要拼接? | ||
| 338 | + }); | ||
| 236 | 339 | ||
| 237 | // 1.3 当前 user message 新发送消息 | 340 | // 1.3 当前 user message 新发送消息 |
| 238 | chatMessages.add(new UserMessage(sendReqVO.getContent())); | 341 | chatMessages.add(new UserMessage(sendReqVO.getContent())); |
| @@ -245,23 +348,76 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -245,23 +348,76 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 245 | chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference))); | 348 | chatMessages.add(new UserMessage(String.format(KNOWLEDGE_USER_MESSAGE_TEMPLATE, reference))); |
| 246 | } | 349 | } |
| 247 | 350 | ||
| 248 | - // 2.1 查询 tool 工具 | ||
| 249 | - Set<String> toolNames = null; | ||
| 250 | - Map<String,Object> toolContext = Map.of(); | ||
| 251 | - if (conversation.getRoleId() != null) { | ||
| 252 | - AiChatRoleDO chatRole = chatRoleService.getChatRole(conversation.getRoleId()); | ||
| 253 | - if (chatRole != null && CollUtil.isNotEmpty(chatRole.getToolIds())) { | ||
| 254 | - toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName); | ||
| 255 | - toolContext = AiUtils.buildCommonToolContext(); | 351 | + // 1.5 联网搜索,通过 UserMessage 实现 |
| 352 | + if (webSearchResponse != null && CollUtil.isNotEmpty(webSearchResponse.getLists())) { | ||
| 353 | + String webSearch = webSearchResponse.getLists().stream() | ||
| 354 | + .map(page -> { | ||
| 355 | + String summary = StrUtil.isNotEmpty(page.getSummary()) ? | ||
| 356 | + "\nSummary: " + page.getSummary() : ""; | ||
| 357 | + return "<WebSearch title=\"" + page.getTitle() + "\" url=\"" + page.getUrl() + "\">" | ||
| 358 | + + StrUtil.blankToDefault(page.getSummary(), page.getSnippet()) + "</WebSearch>"; | ||
| 359 | + }) | ||
| 360 | + .collect(Collectors.joining("\n\n")); | ||
| 361 | + chatMessages.add(new UserMessage(String.format(WEB_SEARCH_USER_MESSAGE_TEMPLATE, webSearch))); | ||
| 362 | + } | ||
| 363 | + | ||
| 364 | + // 1.6 附件,通过 UserMessage 实现 | ||
| 365 | + if (CollUtil.isNotEmpty(sendReqVO.getAttachmentUrls())) { | ||
| 366 | + UserMessage attachmentUserMessage = buildAttachmentUserMessage(sendReqVO.getAttachmentUrls()); | ||
| 367 | + if (attachmentUserMessage != null) { | ||
| 368 | + chatMessages.add(attachmentUserMessage); | ||
| 256 | } | 369 | } |
| 257 | } | 370 | } |
| 371 | + | ||
| 372 | + // 2.1 查询 tool 工具 | ||
| 373 | + List<ToolCallback> toolCallbacks = getToolCallbackListByRoleId(conversation.getRoleId()); | ||
| 374 | + Map<String,Object> toolContext = CollUtil.isNotEmpty(toolCallbacks) ? AiUtils.buildCommonToolContext() | ||
| 375 | + : Map.of(); | ||
| 258 | // 2.2 构建 ChatOptions 对象 | 376 | // 2.2 构建 ChatOptions 对象 |
| 259 | AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); | 377 | AiPlatformEnum platform = AiPlatformEnum.validatePlatform(model.getPlatform()); |
| 260 | ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), | 378 | ChatOptions chatOptions = AiUtils.buildChatOptions(platform, model.getModel(), |
| 261 | - conversation.getTemperature(), conversation.getMaxTokens(), toolNames, toolContext); | 379 | + conversation.getTemperature(), conversation.getMaxTokens(), |
| 380 | + toolCallbacks, toolContext); | ||
| 262 | return new Prompt(chatMessages, chatOptions); | 381 | return new Prompt(chatMessages, chatOptions); |
| 263 | } | 382 | } |
| 264 | 383 | ||
| 384 | + private List<ToolCallback> getToolCallbackListByRoleId(Long roleId) { | ||
| 385 | + if (roleId == null) { | ||
| 386 | + return null; | ||
| 387 | + } | ||
| 388 | + AiChatRoleDO chatRole = chatRoleService.getChatRole(roleId); | ||
| 389 | + if (chatRole == null) { | ||
| 390 | + return null; | ||
| 391 | + } | ||
| 392 | + List<ToolCallback> toolCallbacks = new ArrayList<>(); | ||
| 393 | + // 1. 通过 toolIds | ||
| 394 | + if (CollUtil.isNotEmpty(chatRole.getToolIds())) { | ||
| 395 | + Set<String> toolNames = convertSet(toolService.getToolList(chatRole.getToolIds()), AiToolDO::getName); | ||
| 396 | + toolNames.forEach(toolName -> { | ||
| 397 | + ToolCallback toolCallback = toolCallbackResolver.resolve(toolName); | ||
| 398 | + if (toolCallback != null) { | ||
| 399 | + toolCallbacks.add(toolCallback); | ||
| 400 | + } | ||
| 401 | + }); | ||
| 402 | + } | ||
| 403 | + // 2. 通过 mcpClients | ||
| 404 | + if (CollUtil.isNotEmpty(mcpClients) && CollUtil.isNotEmpty(chatRole.getMcpClientNames())) { | ||
| 405 | + chatRole.getMcpClientNames().forEach(mcpClientName -> { | ||
| 406 | + // 2.1 标准化名字,参考 McpClientAutoConfiguration 的 connectedClientName 方法 | ||
| 407 | + String finalMcpClientName = mcpClientCommonProperties.getName() + " - " + mcpClientName; | ||
| 408 | + // 2.2 匹配对应的 McpSyncClient | ||
| 409 | + mcpClients.forEach(mcpClient -> { | ||
| 410 | + if (ObjUtil.notEqual(mcpClient.getClientInfo().name(), finalMcpClientName)) { | ||
| 411 | + return; | ||
| 412 | + } | ||
| 413 | + ToolCallback[] mcpToolCallBacks = new SyncMcpToolCallbackProvider(mcpClient).getToolCallbacks(); | ||
| 414 | + CollUtil.addAll(toolCallbacks, mcpToolCallBacks); | ||
| 415 | + }); | ||
| 416 | + }); | ||
| 417 | + } | ||
| 418 | + return toolCallbacks; | ||
| 419 | + } | ||
| 420 | + | ||
| 265 | /** | 421 | /** |
| 266 | * 从历史消息中,获得倒序的 n 组消息作为消息上下文 | 422 | * 从历史消息中,获得倒序的 n 组消息作为消息上下文 |
| 267 | * <p> | 423 | * <p> |
| @@ -302,14 +458,56 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | @@ -302,14 +458,56 @@ public class AiChatMessageServiceImpl implements AiChatMessageService { | ||
| 302 | return contextMessages; | 458 | return contextMessages; |
| 303 | } | 459 | } |
| 304 | 460 | ||
| 461 | + private UserMessage buildAttachmentUserMessage(List<String> attachmentUrls) { | ||
| 462 | + if (CollUtil.isEmpty(attachmentUrls)) { | ||
| 463 | + return null; | ||
| 464 | + } | ||
| 465 | + // 读取文件内容 | ||
| 466 | + Map<String, String> attachmentContents = Maps.newLinkedHashMapWithExpectedSize(attachmentUrls.size()); | ||
| 467 | + for (String attachmentUrl : attachmentUrls) { | ||
| 468 | + try { | ||
| 469 | + String name = FileNameUtil.getName(attachmentUrl); | ||
| 470 | + String mineType = FileTypeUtils.getMineType(name); | ||
| 471 | + String content; | ||
| 472 | + if (FileTypeUtils.isImage(mineType)) { | ||
| 473 | + // 特殊:图片则转为 Base64 | ||
| 474 | + byte[] bytes = HttpUtil.downloadBytes(attachmentUrl); | ||
| 475 | + content = Base64.encode(bytes); | ||
| 476 | + } else { | ||
| 477 | + content = knowledgeDocumentService.readUrl(attachmentUrl); | ||
| 478 | + } | ||
| 479 | + if (StrUtil.isNotEmpty(content)) { | ||
| 480 | + attachmentContents.put(name, content); | ||
| 481 | + } | ||
| 482 | + } catch (Exception e) { | ||
| 483 | + log.error("[buildAttachmentUserMessage][读取附件({}) 发生异常]", attachmentUrl, e); | ||
| 484 | + } | ||
| 485 | + } | ||
| 486 | + if (CollUtil.isEmpty(attachmentContents)) { | ||
| 487 | + return null; | ||
| 488 | + } | ||
| 489 | + | ||
| 490 | + // 拼接 UserMessage 消息 | ||
| 491 | + String attachment = attachmentContents.entrySet().stream() | ||
| 492 | + .map(entry -> "<Attachment name=\"" + entry.getKey() + "\">" + entry.getValue() + "</Attachment>") | ||
| 493 | + .collect(Collectors.joining("\n\n")); | ||
| 494 | + return new UserMessage(String.format(Attachment_USER_MESSAGE_TEMPLATE, attachment)); | ||
| 495 | + } | ||
| 496 | + | ||
| 305 | private AiChatMessageDO createChatMessage(Long conversationId, Long replyId, | 497 | private AiChatMessageDO createChatMessage(Long conversationId, Long replyId, |
| 306 | - AiModelDO model, Long userId, Long roleId, | ||
| 307 | - MessageType messageType, String content, Boolean useContext, | ||
| 308 | - List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments) { | 498 | + AiModelDO model, Long userId, Long roleId, |
| 499 | + MessageType messageType, String content, Boolean useContext, | ||
| 500 | + List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments, | ||
| 501 | + List<String> attachmentUrls, | ||
| 502 | + AiWebSearchResponse webSearchResponse) { | ||
| 309 | AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) | 503 | AiChatMessageDO message = new AiChatMessageDO().setConversationId(conversationId).setReplyId(replyId) |
| 310 | .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) | 504 | .setModel(model.getModel()).setModelId(model.getId()).setUserId(userId).setRoleId(roleId) |
| 311 | .setType(messageType.getValue()).setContent(content).setUseContext(useContext) | 505 | .setType(messageType.getValue()).setContent(content).setUseContext(useContext) |
| 312 | - .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId)); | 506 | + .setSegmentIds(convertList(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getId)) |
| 507 | + .setAttachmentUrls(attachmentUrls); | ||
| 508 | + if (webSearchResponse != null) { | ||
| 509 | + message.setWebSearchPages(webSearchResponse.getLists()); | ||
| 510 | + } | ||
| 313 | message.setCreateTime(LocalDateTime.now()); | 511 | message.setCreateTime(LocalDateTime.now()); |
| 314 | chatMessageMapper.insert(message); | 512 | chatMessageMapper.insert(message); |
| 315 | return message; | 513 | return message; |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/knowledge/AiKnowledgeSegmentServiceImpl.java
| @@ -18,6 +18,10 @@ import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; | @@ -18,6 +18,10 @@ import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; | ||
| 18 | import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; | 18 | import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO; |
| 19 | import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; | 19 | import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO; |
| 20 | import cn.iocoder.yudao.module.ai.service.model.AiModelService; | 20 | import cn.iocoder.yudao.module.ai.service.model.AiModelService; |
| 21 | +import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions; | ||
| 22 | +import com.alibaba.cloud.ai.model.RerankModel; | ||
| 23 | +import com.alibaba.cloud.ai.model.RerankRequest; | ||
| 24 | +import com.alibaba.cloud.ai.model.RerankResponse; | ||
| 21 | import jakarta.annotation.Resource; | 25 | import jakarta.annotation.Resource; |
| 22 | import lombok.extern.slf4j.Slf4j; | 26 | import lombok.extern.slf4j.Slf4j; |
| 23 | import org.springframework.ai.document.Document; | 27 | import org.springframework.ai.document.Document; |
| @@ -27,6 +31,7 @@ import org.springframework.ai.transformer.splitter.TokenTextSplitter; | @@ -27,6 +31,7 @@ import org.springframework.ai.transformer.splitter.TokenTextSplitter; | ||
| 27 | import org.springframework.ai.vectorstore.SearchRequest; | 31 | import org.springframework.ai.vectorstore.SearchRequest; |
| 28 | import org.springframework.ai.vectorstore.VectorStore; | 32 | import org.springframework.ai.vectorstore.VectorStore; |
| 29 | import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder; | 33 | import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder; |
| 34 | +import org.springframework.beans.factory.annotation.Autowired; | ||
| 30 | import org.springframework.context.annotation.Lazy; | 35 | import org.springframework.context.annotation.Lazy; |
| 31 | import org.springframework.stereotype.Service; | 36 | import org.springframework.stereotype.Service; |
| 32 | 37 | ||
| @@ -36,6 +41,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU | @@ -36,6 +41,7 @@ import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionU | ||
| 36 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | 41 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; |
| 37 | import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG; | 42 | import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG; |
| 38 | import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS; | 43 | import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS; |
| 44 | +import static org.springframework.ai.vectorstore.SearchRequest.SIMILARITY_THRESHOLD_ACCEPT_ALL; | ||
| 39 | 45 | ||
| 40 | /** | 46 | /** |
| 41 | * AI 知识库分片 Service 实现类 | 47 | * AI 知识库分片 Service 实现类 |
| @@ -55,6 +61,11 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService | @@ -55,6 +61,11 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService | ||
| 55 | VECTOR_STORE_METADATA_DOCUMENT_ID, String.class, | 61 | VECTOR_STORE_METADATA_DOCUMENT_ID, String.class, |
| 56 | VECTOR_STORE_METADATA_SEGMENT_ID, String.class); | 62 | VECTOR_STORE_METADATA_SEGMENT_ID, String.class); |
| 57 | 63 | ||
| 64 | + /** | ||
| 65 | + * Rerank 在向量检索时,检索数量 * 该系数,目的是为了提升 Rerank 的效果 | ||
| 66 | + */ | ||
| 67 | + private static final Integer RERANK_RETRIEVAL_FACTOR = 4; | ||
| 68 | + | ||
| 58 | @Resource | 69 | @Resource |
| 59 | private AiKnowledgeSegmentMapper segmentMapper; | 70 | private AiKnowledgeSegmentMapper segmentMapper; |
| 60 | 71 | ||
| @@ -69,6 +80,9 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService | @@ -69,6 +80,9 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService | ||
| 69 | @Resource | 80 | @Resource |
| 70 | private TokenCountEstimator tokenCountEstimator; | 81 | private TokenCountEstimator tokenCountEstimator; |
| 71 | 82 | ||
| 83 | + @Autowired(required = false) // 由于 spring.ai.model.rerank 配置项,可以关闭 RerankModel 的功能,所以这里只能不强制注入 | ||
| 84 | + private RerankModel rerankModel; | ||
| 85 | + | ||
| 72 | @Override | 86 | @Override |
| 73 | public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) { | 87 | public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) { |
| 74 | return segmentMapper.selectPage(pageReqVO); | 88 | return segmentMapper.selectPage(pageReqVO); |
| @@ -211,28 +225,16 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService | @@ -211,28 +225,16 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService | ||
| 211 | // 1. 校验 | 225 | // 1. 校验 |
| 212 | AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqBO.getKnowledgeId()); | 226 | AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqBO.getKnowledgeId()); |
| 213 | 227 | ||
| 214 | - // 2.1 向量检索 | ||
| 215 | - VectorStore vectorStore = getVectorStoreById(knowledge); | ||
| 216 | - List<Document> documents = vectorStore.similaritySearch(SearchRequest.builder() | ||
| 217 | - .query(reqBO.getContent()) | ||
| 218 | - .topK(ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK())) | ||
| 219 | - .similarityThreshold( | ||
| 220 | - ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold())) | ||
| 221 | - .filterExpression(new FilterExpressionBuilder() | ||
| 222 | - .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId().toString()) | ||
| 223 | - .build()) | ||
| 224 | - .build()); | ||
| 225 | - if (CollUtil.isEmpty(documents)) { | ||
| 226 | - return ListUtil.empty(); | ||
| 227 | - } | ||
| 228 | - // 2.2 段落召回 | 228 | + // 2. 检索 |
| 229 | + List<Document> documents = searchDocument(knowledge, reqBO); | ||
| 230 | + | ||
| 231 | + // 3.1 段落召回 | ||
| 229 | List<AiKnowledgeSegmentDO> segments = segmentMapper | 232 | List<AiKnowledgeSegmentDO> segments = segmentMapper |
| 230 | .selectListByVectorIds(convertList(documents, Document::getId)); | 233 | .selectListByVectorIds(convertList(documents, Document::getId)); |
| 231 | if (CollUtil.isEmpty(segments)) { | 234 | if (CollUtil.isEmpty(segments)) { |
| 232 | return ListUtil.empty(); | 235 | return ListUtil.empty(); |
| 233 | } | 236 | } |
| 234 | - | ||
| 235 | - // 3. 增加召回次数 | 237 | + // 3.2 增加召回次数 |
| 236 | segmentMapper.updateRetrievalCountIncrByIds(convertList(segments, AiKnowledgeSegmentDO::getId)); | 238 | segmentMapper.updateRetrievalCountIncrByIds(convertList(segments, AiKnowledgeSegmentDO::getId)); |
| 237 | 239 | ||
| 238 | // 4. 构建结果 | 240 | // 4. 构建结果 |
| @@ -249,6 +251,42 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService | @@ -249,6 +251,42 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService | ||
| 249 | return result; | 251 | return result; |
| 250 | } | 252 | } |
| 251 | 253 | ||
| 254 | + /** | ||
| 255 | + * 基于 Embedding + Rerank Model,检索知识库中的文档 | ||
| 256 | + * | ||
| 257 | + * @param knowledge 知识库 | ||
| 258 | + * @param reqBO 检索请求 | ||
| 259 | + * @return 文档列表 | ||
| 260 | + */ | ||
| 261 | + private List<Document> searchDocument(AiKnowledgeDO knowledge, AiKnowledgeSegmentSearchReqBO reqBO) { | ||
| 262 | + VectorStore vectorStore = getVectorStoreById(knowledge); | ||
| 263 | + Integer topK = ObjUtil.defaultIfNull(reqBO.getTopK(), knowledge.getTopK()); | ||
| 264 | + Double similarityThreshold = ObjUtil.defaultIfNull(reqBO.getSimilarityThreshold(), knowledge.getSimilarityThreshold()); | ||
| 265 | + | ||
| 266 | + // 1. 向量检索 | ||
| 267 | + int searchTopK = rerankModel != null ? topK * RERANK_RETRIEVAL_FACTOR : topK; | ||
| 268 | + double searchSimilarityThreshold = rerankModel != null ? SIMILARITY_THRESHOLD_ACCEPT_ALL : similarityThreshold; | ||
| 269 | + SearchRequest.Builder searchRequestBuilder = SearchRequest.builder() | ||
| 270 | + .query(reqBO.getContent()) | ||
| 271 | + .topK(searchTopK).similarityThreshold(searchSimilarityThreshold) | ||
| 272 | + .filterExpression(new FilterExpressionBuilder() | ||
| 273 | + .eq(VECTOR_STORE_METADATA_KNOWLEDGE_ID, reqBO.getKnowledgeId().toString()).build()); | ||
| 274 | + List<Document> documents = vectorStore.similaritySearch(searchRequestBuilder.build()); | ||
| 275 | + if (CollUtil.isEmpty(documents)) { | ||
| 276 | + return documents; | ||
| 277 | + } | ||
| 278 | + | ||
| 279 | + // 2. Rerank 重排序 | ||
| 280 | + if (rerankModel != null) { | ||
| 281 | + RerankResponse rerankResponse = rerankModel.call(new RerankRequest(reqBO.getContent(), documents, | ||
| 282 | + DashScopeRerankOptions.builder().withTopN(topK).build())); | ||
| 283 | + documents = convertList(rerankResponse.getResults(), | ||
| 284 | + documentWithScore -> documentWithScore.getScore() >= similarityThreshold | ||
| 285 | + ? documentWithScore.getOutput() : null); | ||
| 286 | + } | ||
| 287 | + return documents; | ||
| 288 | + } | ||
| 289 | + | ||
| 252 | @Override | 290 | @Override |
| 253 | public List<AiKnowledgeSegmentDO> splitContent(String url, Integer segmentMaxTokens) { | 291 | public List<AiKnowledgeSegmentDO> splitContent(String url, Integer segmentMaxTokens) { |
| 254 | // 1. 读取 URL 内容 | 292 | // 1. 读取 URL 内容 |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiModelServiceImpl.java
| 1 | package cn.iocoder.yudao.module.ai.service.model; | 1 | package cn.iocoder.yudao.module.ai.service.model; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; | 3 | import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; |
| 4 | -import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory; | 4 | +import cn.iocoder.yudao.module.ai.framework.ai.core.model.AiModelFactory; |
| 5 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; | 5 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; |
| 6 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; | 6 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; |
| 7 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | 7 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/AiToolServiceImpl.java
| 1 | package cn.iocoder.yudao.module.ai.service.model; | 1 | package cn.iocoder.yudao.module.ai.service.model; |
| 2 | 2 | ||
| 3 | -import cn.hutool.extra.spring.SpringUtil; | ||
| 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 3 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 5 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 4 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
| 6 | import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolPageReqVO; | 5 | 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 | @@ -8,7 +7,8 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.tool.AiToolSaveReqVO | ||
| 8 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; | 7 | import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiToolDO; |
| 9 | import cn.iocoder.yudao.module.ai.dal.mysql.model.AiToolMapper; | 8 | import cn.iocoder.yudao.module.ai.dal.mysql.model.AiToolMapper; |
| 10 | import jakarta.annotation.Resource; | 9 | import jakarta.annotation.Resource; |
| 11 | -import org.springframework.beans.factory.NoSuchBeanDefinitionException; | 10 | +import org.springframework.ai.tool.ToolCallback; |
| 11 | +import org.springframework.ai.tool.resolution.ToolCallbackResolver; | ||
| 12 | import org.springframework.stereotype.Service; | 12 | import org.springframework.stereotype.Service; |
| 13 | import org.springframework.validation.annotation.Validated; | 13 | import org.springframework.validation.annotation.Validated; |
| 14 | 14 | ||
| @@ -31,6 +31,9 @@ public class AiToolServiceImpl implements AiToolService { | @@ -31,6 +31,9 @@ public class AiToolServiceImpl implements AiToolService { | ||
| 31 | @Resource | 31 | @Resource |
| 32 | private AiToolMapper toolMapper; | 32 | private AiToolMapper toolMapper; |
| 33 | 33 | ||
| 34 | + @Resource | ||
| 35 | + private ToolCallbackResolver toolCallbackResolver; | ||
| 36 | + | ||
| 34 | @Override | 37 | @Override |
| 35 | public Long createTool(AiToolSaveReqVO createReqVO) { | 38 | public Long createTool(AiToolSaveReqVO createReqVO) { |
| 36 | // 校验名称是否存在 | 39 | // 校验名称是否存在 |
| @@ -70,9 +73,8 @@ public class AiToolServiceImpl implements AiToolService { | @@ -70,9 +73,8 @@ public class AiToolServiceImpl implements AiToolService { | ||
| 70 | } | 73 | } |
| 71 | 74 | ||
| 72 | private void validateToolNameExists(String name) { | 75 | private void validateToolNameExists(String name) { |
| 73 | - try { | ||
| 74 | - SpringUtil.getBean(name); | ||
| 75 | - } catch (NoSuchBeanDefinitionException e) { | 76 | + ToolCallback toolCallback = toolCallbackResolver.resolve(name); |
| 77 | + if (toolCallback == null) { | ||
| 76 | throw exception(TOOL_NAME_NOT_EXISTS, name); | 78 | throw exception(TOOL_NAME_NOT_EXISTS, name); |
| 77 | } | 79 | } |
| 78 | } | 80 | } |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/DirectoryListToolFunction.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/DirectoryListToolFunction.java
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/UserProfileQueryToolFunction.java
| 1 | -package cn.iocoder.yudao.module.ai.service.model.tool; | 1 | +package cn.iocoder.yudao.module.ai.tool.function; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.ai.util.AiUtils; | 3 | import cn.iocoder.yudao.module.ai.util.AiUtils; |
| 4 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; | 4 | import cn.iocoder.yudao.framework.common.util.object.BeanUtils; |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/WeatherQueryToolFunction.java renamed to yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/WeatherQueryToolFunction.java
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/function/package-info.java
0 → 100644
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/Person.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.tool.method; | ||
| 2 | + | ||
| 3 | +/** | ||
| 4 | + * 来自 Spring AI 官方文档 | ||
| 5 | + * | ||
| 6 | + * Represents a person with basic information. | ||
| 7 | + * This is an immutable record. | ||
| 8 | + */ | ||
| 9 | +public record Person( | ||
| 10 | + int id, | ||
| 11 | + String firstName, | ||
| 12 | + String lastName, | ||
| 13 | + String email, | ||
| 14 | + String sex, | ||
| 15 | + String ipAddress, | ||
| 16 | + String jobTitle, | ||
| 17 | + int age | ||
| 18 | +) { | ||
| 19 | +} | ||
| 0 | \ No newline at end of file | 20 | \ No newline at end of file |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonService.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.tool.method; | ||
| 2 | + | ||
| 3 | +import java.util.List; | ||
| 4 | +import java.util.Optional; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * 来自 Spring AI 官方文档 | ||
| 8 | + * | ||
| 9 | + * Service interface for managing Person data. | ||
| 10 | + * Defines the contract for CRUD operations and search/filter functionalities. | ||
| 11 | + */ | ||
| 12 | +public interface PersonService { | ||
| 13 | + | ||
| 14 | + /** | ||
| 15 | + * Creates a new Person record. | ||
| 16 | + * Assigns a unique ID to the person and stores it. | ||
| 17 | + * | ||
| 18 | + * @param personData The data for the new person (ID field is ignored). Must not be null. | ||
| 19 | + * @return The created Person record, including the generated ID. | ||
| 20 | + */ | ||
| 21 | + Person createPerson(Person personData); | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * Retrieves a Person by their unique ID. | ||
| 25 | + * | ||
| 26 | + * @param id The ID of the person to retrieve. | ||
| 27 | + * @return An Optional containing the found Person, or an empty Optional if not found. | ||
| 28 | + */ | ||
| 29 | + Optional<Person> getPersonById(int id); | ||
| 30 | + | ||
| 31 | + /** | ||
| 32 | + * Retrieves all Person records currently stored. | ||
| 33 | + * | ||
| 34 | + * @return An unmodifiable List containing all Persons. Returns an empty list if none exist. | ||
| 35 | + */ | ||
| 36 | + List<Person> getAllPersons(); | ||
| 37 | + | ||
| 38 | + /** | ||
| 39 | + * Updates an existing Person record identified by ID. | ||
| 40 | + * Replaces the existing data with the provided data, keeping the original ID. | ||
| 41 | + * | ||
| 42 | + * @param id The ID of the person to update. | ||
| 43 | + * @param updatedPersonData The new data for the person (ID field is ignored). Must not be null. | ||
| 44 | + * @return true if the person was found and updated, false otherwise. | ||
| 45 | + */ | ||
| 46 | + boolean updatePerson(int id, Person updatedPersonData); | ||
| 47 | + | ||
| 48 | + /** | ||
| 49 | + * Deletes a Person record identified by ID. | ||
| 50 | + * | ||
| 51 | + * @param id The ID of the person to delete. | ||
| 52 | + * @return true if the person was found and deleted, false otherwise. | ||
| 53 | + */ | ||
| 54 | + boolean deletePerson(int id); | ||
| 55 | + | ||
| 56 | + /** | ||
| 57 | + * Searches for Persons whose job title contains the given query string (case-insensitive). | ||
| 58 | + * | ||
| 59 | + * @param jobTitleQuery The string to search for within job titles. Can be null or blank. | ||
| 60 | + * @return An unmodifiable List of matching Persons. Returns an empty list if no matches or query is invalid. | ||
| 61 | + */ | ||
| 62 | + List<Person> searchByJobTitle(String jobTitleQuery); | ||
| 63 | + | ||
| 64 | + /** | ||
| 65 | + * Filters Persons by their exact sex (case-insensitive). | ||
| 66 | + * | ||
| 67 | + * @param sex The sex to filter by (e.g., "Male", "Female"). Can be null or blank. | ||
| 68 | + * @return An unmodifiable List of matching Persons. Returns an empty list if no matches or filter is invalid. | ||
| 69 | + */ | ||
| 70 | + List<Person> filterBySex(String sex); | ||
| 71 | + | ||
| 72 | + /** | ||
| 73 | + * Filters Persons by their exact age. | ||
| 74 | + * | ||
| 75 | + * @param age The age to filter by. | ||
| 76 | + * @return An unmodifiable List of matching Persons. Returns an empty list if no matches. | ||
| 77 | + */ | ||
| 78 | + List<Person> filterByAge(int age); | ||
| 79 | + | ||
| 80 | +} | ||
| 0 | \ No newline at end of file | 81 | \ No newline at end of file |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/PersonServiceImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.tool.method; | ||
| 2 | + | ||
| 3 | +import jakarta.annotation.PostConstruct; | ||
| 4 | +import lombok.extern.slf4j.Slf4j; | ||
| 5 | +import org.springframework.ai.tool.annotation.Tool; | ||
| 6 | +import org.springframework.stereotype.Service; | ||
| 7 | + | ||
| 8 | +import java.util.Collections; | ||
| 9 | +import java.util.List; | ||
| 10 | +import java.util.Map; | ||
| 11 | +import java.util.Optional; | ||
| 12 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 13 | +import java.util.concurrent.atomic.AtomicInteger; | ||
| 14 | +import java.util.stream.Collectors; | ||
| 15 | +import java.util.stream.Stream; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * 来自 Spring AI 官方文档 | ||
| 19 | + * | ||
| 20 | + * Implementation of the PersonService interface using an in-memory data store. | ||
| 21 | + * Manages a collection of Person objects loaded from embedded CSV data. | ||
| 22 | + * This class is thread-safe due to the use of ConcurrentHashMap and AtomicInteger. | ||
| 23 | + */ | ||
| 24 | +@Service | ||
| 25 | +@Slf4j | ||
| 26 | +public class PersonServiceImpl implements PersonService { | ||
| 27 | + | ||
| 28 | + private final Map<Integer, Person> personStore = new ConcurrentHashMap<>(); | ||
| 29 | + | ||
| 30 | + private AtomicInteger idGenerator; | ||
| 31 | + | ||
| 32 | + /** | ||
| 33 | + * Embedded CSV data for initial population | ||
| 34 | + */ | ||
| 35 | + private static final String CSV_DATA = """ | ||
| 36 | + Id,FirstName,LastName,Email,Sex,IpAddress,JobTitle,Age | ||
| 37 | + 1,Fons,Tollfree,ftollfree0@senate.gov,Male,55.1 Tollfree Lane,Research Associate,31 | ||
| 38 | + 2,Emlynne,Tabourier,etabourier1@networksolutions.com,Female,18 Tabourier Way,Associate Professor,38 | ||
| 39 | + 3,Shae,Johncey,sjohncey2@yellowpages.com,Male,1 Johncey Circle,Structural Analysis Engineer,30 | ||
| 40 | + 4,Sebastien,Bradly,sbradly3@mapquest.com,Male,2 Bradly Hill,Chief Executive Officer,40 | ||
| 41 | + 5,Harriott,Kitteringham,hkitteringham4@typepad.com,Female,3 Kitteringham Drive,VP Sales,47 | ||
| 42 | + 6,Anallise,Parradine,aparradine5@miibeian.gov.cn,Female,4 Parradine Street,Analog Circuit Design manager,44 | ||
| 43 | + 7,Gorden,Kirkbright,gkirkbright6@reuters.com,Male,5 Kirkbright Plaza,Senior Editor,40 | ||
| 44 | + 8,Veradis,Ledwitch,vledwitch7@google.com.au,Female,6 Ledwitch Avenue,Computer Systems Analyst IV,44 | ||
| 45 | + 9,Agnesse,Penhalurick,apenhalurick8@google.it,Female,7 Penhalurick Terrace,Automation Specialist IV,41 | ||
| 46 | + 10,Bibby,Hutable,bhutable9@craigslist.org,Female,8 Hutable Place,Account Representative I,43 | ||
| 47 | + 11,Karoly,Lightoller,klightollera@rakuten.co.jp,Female,9 Lightoller Parkway,Senior Developer,46 | ||
| 48 | + 12,Cristine,Durrad,cdurradb@aol.com,Female,10 Durrad Center,Senior Developer,48 | ||
| 49 | + 13,Aggy,Napier,anapierc@hostgator.com,Female,11 Napier Court,VP Product Management,44 | ||
| 50 | + 14,Prisca,Caddens,pcaddensd@vinaora.com,Female,12 Caddens Alley,Business Systems Development Analyst,41 | ||
| 51 | + 15,Khalil,McKernan,kmckernane@google.fr,Male,13 McKernan Pass,Engineer IV,44 | ||
| 52 | + 16,Lorry,MacTrusty,lmactrustyf@eventbrite.com,Male,14 MacTrusty Junction,Design Engineer,42 | ||
| 53 | + 17,Casandra,Worsell,cworsellg@goo.gl,Female,15 Worsell Point,Systems Administrator IV,45 | ||
| 54 | + 18,Ulrikaumeko,Haveline,uhavelineh@usgs.gov,Female,16 Haveline Trail,Financial Advisor,42 | ||
| 55 | + 19,Shurlocke,Albany,salbanyi@artisteer.com,Male,17 Albany Plaza,Software Test Engineer III,46 | ||
| 56 | + 20,Myrilla,Brimilcombe,mbrimilcombej@accuweather.com,Female,18 Brimilcombe Road,Programmer Analyst I,48 | ||
| 57 | + 21,Carlina,Scimonelli,cscimonellik@va.gov,Female,19 Scimonelli Pass,Help Desk Technician,45 | ||
| 58 | + 22,Tina,Goullee,tgoulleel@miibeian.gov.cn,Female,20 Goullee Crossing,Accountant IV,43 | ||
| 59 | + 23,Adriaens,Storek,astorekm@devhub.com,Female,21 Storek Avenue,Recruiting Manager,40 | ||
| 60 | + 24,Tedra,Giraudot,tgiraudotn@wiley.com,Female,22 Giraudot Terrace,Speech Pathologist,47 | ||
| 61 | + 25,Josiah,Soares,jsoareso@google.nl,Male,23 Soares Street,Tax Accountant,45 | ||
| 62 | + 26,Kayle,Gaukrodge,kgaukrodgep@wikispaces.com,Female,24 Gaukrodge Parkway,Accountant II,43 | ||
| 63 | + 27,Ardys,Chuter,achuterq@ustream.tv,Female,25 Chuter Drive,Engineer IV,41 | ||
| 64 | + 28,Francyne,Baudinet,fbaudinetr@newyorker.com,Female,26 Baudinet Center,VP Accounting,48 | ||
| 65 | + 29,Gerick,Bullan,gbullans@seesaa.net,Male,27 Bullan Way,Senior Financial Analyst,43 | ||
| 66 | + 30,Northrup,Grivori,ngrivorit@unc.edu,Male,28 Grivori Plaza,Systems Administrator I,45 | ||
| 67 | + 31,Town,Duguid,tduguidu@squarespace.com,Male,29 Duguid Pass,Safety Technician IV,46 | ||
| 68 | + 32,Pierette,Kopisch,pkopischv@google.com.br,Female,30 Kopisch Lane,Director of Sales,41 | ||
| 69 | + 33,Jacquenetta,Le Prevost,jleprevostw@netlog.com,Female,31 Le Prevost Trail,Senior Developer,47 | ||
| 70 | + 34,Garvy,Rusted,grustedx@aboutads.info,Male,32 Rusted Junction,Senior Developer,42 | ||
| 71 | + 35,Clarice,Aysh,cayshy@merriam-webster.com,Female,33 Aysh Avenue,VP Quality Control,40 | ||
| 72 | + 36,Tracie,Fedorski,tfedorskiz@bloglines.com,Male,34 Fedorski Terrace,Design Engineer,44 | ||
| 73 | + 37,Noelyn,Matushenko,nmatushenko10@globo.com,Female,35 Matushenko Place,VP Sales,48 | ||
| 74 | + 38,Rudiger,Klaesson,rklaesson11@usnews.com,Male,36 Klaesson Road,Database Administrator IV,43 | ||
| 75 | + 39,Mirella,Syddie,msyddie12@geocities.jp,Female,37 Syddie Circle,Geological Engineer,46 | ||
| 76 | + 40,Donalt,O'Lunny,dolunny13@elpais.com,Male,38 O'Lunny Center,Analog Circuit Design manager,41 | ||
| 77 | + 41,Guntar,Deniskevich,gdeniskevich14@google.com.hk,Male,39 Deniskevich Way,Structural Engineer,47 | ||
| 78 | + 42,Hort,Shufflebotham,hshufflebotham15@about.me,Male,40 Shufflebotham Court,Structural Analysis Engineer,45 | ||
| 79 | + 43,Dominique,Thickett,dthickett16@slashdot.org,Male,41 Thickett Crossing,Safety Technician I,42 | ||
| 80 | + 44,Zebulen,Piscopello,zpiscopello17@umich.edu,Male,42 Piscopello Parkway,Web Developer II,40 | ||
| 81 | + 45,Mellicent,Mac Giany,mmacgiany18@state.tx.us,Female,43 Mac Giany Pass,Assistant Manager,44 | ||
| 82 | + 46,Merle,Bounds,mbounds19@amazon.co.jp,Female,44 Bounds Alley,Systems Administrator III,41 | ||
| 83 | + 47,Madelle,Farbrace,mfarbrace1a@xinhuanet.com,Female,45 Farbrace Terrace,Quality Engineer,48 | ||
| 84 | + 48,Galvin,O'Sheeryne,gosheeryne1b@addtoany.com,Male,46 O'Sheeryne Way,Environmental Specialist,43 | ||
| 85 | + 49,Guillemette,Bootherstone,gbootherstone1c@nationalgeographic.com,Female,47 Bootherstone Plaza,Professor,46 | ||
| 86 | + 50,Letti,Aylmore,laylmore1d@vinaora.com,Female,48 Aylmore Circle,Automation Specialist I,40 | ||
| 87 | + 51,Nonie,Rivalland,nrivalland1e@weather.com,Female,49 Rivalland Avenue,Software Test Engineer IV,45 | ||
| 88 | + 52,Jacquelynn,Halfacre,jhalfacre1f@surveymonkey.com,Female,50 Halfacre Pass,Geologist II,42 | ||
| 89 | + 53,Anderea,MacKibbon,amackibbon1g@weibo.com,Female,51 MacKibbon Parkway,Automation Specialist II,47 | ||
| 90 | + 54,Wash,Klimko,wklimko1h@slashdot.org,Male,52 Klimko Alley,Database Administrator I,40 | ||
| 91 | + 55,Flori,Kynett,fkynett1i@auda.org.au,Female,53 Kynett Trail,Quality Control Specialist,46 | ||
| 92 | + 56,Libbey,Penswick,lpenswick1j@google.co.uk,Female,54 Penswick Point,VP Accounting,43 | ||
| 93 | + 57,Silvanus,Skellorne,sskellorne1k@booking.com,Male,55 Skellorne Drive,Account Executive,48 | ||
| 94 | + 58,Carmine,Mateos,cmateos1l@plala.or.jp,Male,56 Mateos Terrace,Systems Administrator I,41 | ||
| 95 | + 59,Sheffie,Blazewicz,sblazewicz1m@google.com.au,Male,57 Blazewicz Center,VP Sales,44 | ||
| 96 | + 60,Leanor,Worsnop,lworsnop1n@uol.com.br,Female,58 Worsnop Plaza,Systems Administrator III,45 | ||
| 97 | + 61,Caspar,Pamment,cpamment1o@google.co.jp,Male,59 Pamment Court,Senior Financial Analyst,42 | ||
| 98 | + 62,Justinian,Pentycost,jpentycost1p@sciencedaily.com,Male,60 Pentycost Way,Senior Quality Engineer,47 | ||
| 99 | + 63,Gerianne,Jarnell,gjarnell1q@bing.com,Female,61 Jarnell Avenue,Help Desk Operator,40 | ||
| 100 | + 64,Boycie,Zanetto,bzanetto1r@about.com,Male,62 Zanetto Place,Quality Engineer,46 | ||
| 101 | + 65,Camilla,Mac Giany,cmacgiany1s@state.gov,Female,63 Mac Giany Parkway,Senior Cost Accountant,43 | ||
| 102 | + 66,Hadlee,Piscopiello,hpiscopiello1t@artisteer.com,Male,64 Piscopiello Street,Account Representative III,48 | ||
| 103 | + 67,Bobbie,Penvarden,bpenvarden1u@google.cn,Male,65 Penvarden Lane,Help Desk Operator,41 | ||
| 104 | + 68,Ali,Gowlett,agowlett1v@parallels.com,Male,66 Gowlett Pass,VP Marketing,44 | ||
| 105 | + 69,Olivette,Acome,oacome1w@qq.com,Female,67 Acome Hill,VP Product Management,45 | ||
| 106 | + 70,Jehanna,Brotherheed,jbrotherheed1x@google.nl,Female,68 Brotherheed Junction,Database Administrator III,42 | ||
| 107 | + 71,Morgan,Berthomieu,mberthomieu1y@artisteer.com,Male,69 Berthomieu Alley,Systems Administrator II,47 | ||
| 108 | + 72,Linzy,Shilladay,lshilladay1z@icq.com,Female,70 Shilladay Trail,Research Assistant IV,40 | ||
| 109 | + 73,Faydra,Brimner,fbrimner20@mozilla.org,Female,71 Brimner Road,Senior Editor,46 | ||
| 110 | + 74,Gwenore,Oxlee,goxlee21@devhub.com,Female,72 Oxlee Terrace,Systems Administrator II,43 | ||
| 111 | + 75,Evangelin,Beinke,ebeinke22@mozilla.com,Female,73 Beinke Circle,Accountant I,48 | ||
| 112 | + 76,Missy,Cockling,mcockling23@si.edu,Female,74 Cockling Way,Software Engineer I,41 | ||
| 113 | + 77,Suzanne,Klimschak,sklimschak24@etsy.com,Female,75 Klimschak Plaza,Tax Accountant,44 | ||
| 114 | + 78,Candide,Goricke,cgoricke25@weebly.com,Female,76 Goricke Pass,Sales Associate,45 | ||
| 115 | + 79,Gerome,Pinsent,gpinsent26@google.com.au,Male,77 Pinsent Junction,Software Consultant,42 | ||
| 116 | + 80,Lezley,Mac Giany,lmacgiany27@scribd.com,Male,78 Mac Giany Alley,Operator,47 | ||
| 117 | + 81,Tobiah,Durn,tdurn28@state.tx.us,Male,79 Durn Court,VP Sales,40 | ||
| 118 | + 82,Sherlocke,Cockshoot,scockshoot29@yelp.com,Male,80 Cockshoot Street,Senior Financial Analyst,46 | ||
| 119 | + 83,Myrle,Speenden,mspeenden2a@utexas.edu,Female,81 Speenden Center,Senior Developer,43 | ||
| 120 | + 84,Isidore,Gorries,igorries2b@flavors.me,Male,82 Gorries Parkway,Sales Representative,48 | ||
| 121 | + 85,Isac,Kitchingman,ikitchingman2c@businessinsider.com,Male,83 Kitchingman Drive,VP Accounting,41 | ||
| 122 | + 86,Benedetta,Purrier,bpurrier2d@admin.ch,Female,84 Purrier Trail,VP Accounting,44 | ||
| 123 | + 87,Tera,Fitchell,tfitchell2e@fotki.com,Female,85 Fitchell Place,Software Engineer IV,45 | ||
| 124 | + 88,Abbe,Pamment,apamment2f@about.com,Male,86 Pamment Avenue,VP Sales,42 | ||
| 125 | + 89,Jandy,Gommowe,jgommowe2g@angelfire.com,Female,87 Gommowe Road,Financial Analyst,47 | ||
| 126 | + 90,Karena,Fussey,kfussey2h@google.com.au,Female,88 Fussey Point,Assistant Professor,40 | ||
| 127 | + 91,Gaspar,Pammenter,gpammenter2i@google.com.br,Male,89 Pammenter Hill,Help Desk Operator,46 | ||
| 128 | + 92,Stanwood,Mac Giany,smacgiany2j@prlog.org,Male,90 Mac Giany Terrace,Research Associate,43 | ||
| 129 | + 93,Byrom,Beedell,bbeedell2k@google.co.jp,Male,91 Beedell Way,VP Sales,48 | ||
| 130 | + 94,Annabella,Rowbottom,arowbottom2l@google.com.au,Female,92 Rowbottom Plaza,Help Desk Operator,41 | ||
| 131 | + 95,Rodolphe,Debell,rdebell2m@imageshack.us,Male,93 Debell Pass,Design Engineer,44 | ||
| 132 | + 96,Tyne,Gommey,tgommey2n@joomla.org,Female,94 Gommey Junction,VP Marketing,45 | ||
| 133 | + 97,Christoper,Pincked,cpincked2o@icq.com,Male,95 Pincked Alley,Human Resources Manager,42 | ||
| 134 | + 98,Kore,Le Prevost,kleprevost2p@tripadvisor.com,Female,96 Le Prevost Street,VP Quality Control,47 | ||
| 135 | + 99,Ceciley,Petrolli,cpetrolli2q@oaic.gov.au,Female,97 Petrolli Court,Senior Developer,40 | ||
| 136 | + 100,Elspeth,Mac Giany,emacgiany2r@icio.us,Female,98 Mac Giany Parkway,Internal Auditor,46 | ||
| 137 | + """; | ||
| 138 | + | ||
| 139 | + /** | ||
| 140 | + * Initializes the service after dependency injection by loading data from the CSV string. | ||
| 141 | + * Uses @PostConstruct to ensure this runs after the bean is created. | ||
| 142 | + */ | ||
| 143 | + @PostConstruct | ||
| 144 | + private void initializeData() { | ||
| 145 | + log.info("Initializing PersonService data store..."); | ||
| 146 | + int maxId = loadDataFromCsv(); | ||
| 147 | + idGenerator = new AtomicInteger(maxId); | ||
| 148 | + log.info("PersonService initialized with {} records. Next ID: {}", personStore.size(), idGenerator.get() + 1); | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + /** | ||
| 152 | + * Parses the embedded CSV data and populates the in-memory store. | ||
| 153 | + * Calculates the maximum ID found in the data to initialize the ID generator. | ||
| 154 | + * | ||
| 155 | + * @return The maximum ID found in the loaded CSV data. | ||
| 156 | + */ | ||
| 157 | + private int loadDataFromCsv() { | ||
| 158 | + final AtomicInteger currentMaxId = new AtomicInteger(0); | ||
| 159 | + // Clear existing data before loading (important for tests or re-initialization scenarios) | ||
| 160 | + personStore.clear(); | ||
| 161 | + try (Stream<String> lines = CSV_DATA.lines().skip(1)) { // Skip header row | ||
| 162 | + lines.forEach(line -> { | ||
| 163 | + try { | ||
| 164 | + // Split carefully, handling potential commas within quoted fields if necessary (simple split here) | ||
| 165 | + String[] fields = line.split(",", 8); // Limit split to handle potential commas in job title | ||
| 166 | + if (fields.length == 8) { | ||
| 167 | + int id = Integer.parseInt(fields[0].trim()); | ||
| 168 | + String firstName = fields[1].trim(); | ||
| 169 | + String lastName = fields[2].trim(); | ||
| 170 | + String email = fields[3].trim(); | ||
| 171 | + String sex = fields[4].trim(); | ||
| 172 | + String ipAddress = fields[5].trim(); | ||
| 173 | + String jobTitle = fields[6].trim(); | ||
| 174 | + int age = Integer.parseInt(fields[7].trim()); | ||
| 175 | + | ||
| 176 | + Person person = new Person(id, firstName, lastName, email, sex, ipAddress, jobTitle, age); | ||
| 177 | + personStore.put(id, person); | ||
| 178 | + currentMaxId.updateAndGet(max -> Math.max(max, id)); | ||
| 179 | + } else { | ||
| 180 | + log.warn("Skipping malformed CSV line (expected 8 fields, found {}): {}", fields.length, line); | ||
| 181 | + } | ||
| 182 | + } catch (NumberFormatException e) { | ||
| 183 | + log.warn("Skipping line due to parsing error (ID or Age): {} - Error: {}", line, e.getMessage()); | ||
| 184 | + } catch (Exception e) { | ||
| 185 | + log.error("Skipping line due to unexpected error: {} - Error: {}", line, e.getMessage(), e); | ||
| 186 | + } | ||
| 187 | + }); | ||
| 188 | + } catch (Exception e) { | ||
| 189 | + log.error("Fatal error reading embedded CSV data: {}", e.getMessage(), e); | ||
| 190 | + // In a real application, might throw a specific initialization exception | ||
| 191 | + } | ||
| 192 | + return currentMaxId.get(); | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + @Override | ||
| 196 | + @Tool( | ||
| 197 | + name = "ps_create_person", | ||
| 198 | + description = "Create a new person record in the in-memory store." | ||
| 199 | + ) | ||
| 200 | + public Person createPerson(Person personData) { | ||
| 201 | + if (personData == null) { | ||
| 202 | + throw new IllegalArgumentException("Person data cannot be null"); | ||
| 203 | + } | ||
| 204 | + int newId = idGenerator.incrementAndGet(); | ||
| 205 | + // Create a new Person record using data from the input, but with the generated ID | ||
| 206 | + Person newPerson = new Person( | ||
| 207 | + newId, | ||
| 208 | + personData.firstName(), | ||
| 209 | + personData.lastName(), | ||
| 210 | + personData.email(), | ||
| 211 | + personData.sex(), | ||
| 212 | + personData.ipAddress(), | ||
| 213 | + personData.jobTitle(), | ||
| 214 | + personData.age() | ||
| 215 | + ); | ||
| 216 | + personStore.put(newId, newPerson); | ||
| 217 | + log.debug("Created person: {}", newPerson); | ||
| 218 | + return newPerson; | ||
| 219 | + } | ||
| 220 | + | ||
| 221 | + @Override | ||
| 222 | + @Tool( | ||
| 223 | + name = "ps_get_person_by_id", | ||
| 224 | + description = "Retrieve a person record by ID from the in-memory store." | ||
| 225 | + ) | ||
| 226 | + public Optional<Person> getPersonById(int id) { | ||
| 227 | + Person person = personStore.get(id); | ||
| 228 | + log.debug("Retrieved person by ID {}: {}", id, person); | ||
| 229 | + return Optional.ofNullable(person); | ||
| 230 | + } | ||
| 231 | + | ||
| 232 | + @Override | ||
| 233 | + @Tool( | ||
| 234 | + name = "ps_get_all_persons", | ||
| 235 | + description = "Retrieve all person records from the in-memory store." | ||
| 236 | + ) | ||
| 237 | + public List<Person> getAllPersons() { | ||
| 238 | + // Return an unmodifiable view of the values | ||
| 239 | + List<Person> allPersons = personStore.values().stream().toList(); | ||
| 240 | + log.debug("Retrieved all persons (count: {})", allPersons.size()); | ||
| 241 | + return allPersons; | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + @Override | ||
| 245 | + @Tool( | ||
| 246 | + name = "ps_update_person", | ||
| 247 | + description = "Update an existing person record by ID in the in-memory store." | ||
| 248 | + ) | ||
| 249 | + public boolean updatePerson(int id, Person updatedPersonData) { | ||
| 250 | + if (updatedPersonData == null) { | ||
| 251 | + throw new IllegalArgumentException("Updated person data cannot be null"); | ||
| 252 | + } | ||
| 253 | + // Use computeIfPresent for atomic update if the key exists | ||
| 254 | + Person result = personStore.computeIfPresent(id, (key, existingPerson) -> | ||
| 255 | + // Create a new Person record with the original ID but updated data | ||
| 256 | + new Person( | ||
| 257 | + id, // Keep original ID | ||
| 258 | + updatedPersonData.firstName(), | ||
| 259 | + updatedPersonData.lastName(), | ||
| 260 | + updatedPersonData.email(), | ||
| 261 | + updatedPersonData.sex(), | ||
| 262 | + updatedPersonData.ipAddress(), | ||
| 263 | + updatedPersonData.jobTitle(), | ||
| 264 | + updatedPersonData.age() | ||
| 265 | + ) | ||
| 266 | + ); | ||
| 267 | + boolean updated = result != null; | ||
| 268 | + log.debug("Update attempt for ID {}: {}", id, updated ? "Successful" : "Failed (Not Found)"); | ||
| 269 | + if(updated) log.trace("Updated person data for ID {}: {}", id, result); | ||
| 270 | + return updated; | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + @Override | ||
| 274 | + @Tool( | ||
| 275 | + name = "ps_delete_person", | ||
| 276 | + description = "Delete a person record by ID from the in-memory store." | ||
| 277 | + ) | ||
| 278 | + public boolean deletePerson(int id) { | ||
| 279 | + boolean removed = personStore.remove(id) != null; | ||
| 280 | + log.debug("Delete attempt for ID {}: {}", id, removed ? "Successful" : "Failed (Not Found)"); | ||
| 281 | + return removed; | ||
| 282 | + } | ||
| 283 | + | ||
| 284 | + @Override | ||
| 285 | + @Tool( | ||
| 286 | + name = "ps_search_by_job_title", | ||
| 287 | + description = "Search for persons by job title in the in-memory store." | ||
| 288 | + ) | ||
| 289 | + public List<Person> searchByJobTitle(String jobTitleQuery) { | ||
| 290 | + if (jobTitleQuery == null || jobTitleQuery.isBlank()) { | ||
| 291 | + log.debug("Search by job title skipped due to blank query."); | ||
| 292 | + return Collections.emptyList(); | ||
| 293 | + } | ||
| 294 | + String lowerCaseQuery = jobTitleQuery.toLowerCase(); | ||
| 295 | + List<Person> results = personStore.values().stream() | ||
| 296 | + .filter(person -> person.jobTitle() != null && person.jobTitle().toLowerCase().contains(lowerCaseQuery)) | ||
| 297 | + .collect(Collectors.toList()); | ||
| 298 | + log.debug("Search by job title '{}' found {} results.", jobTitleQuery, results.size()); | ||
| 299 | + return Collections.unmodifiableList(results); | ||
| 300 | + } | ||
| 301 | + | ||
| 302 | + @Override | ||
| 303 | + @Tool( | ||
| 304 | + name = "ps_filter_by_sex", | ||
| 305 | + description = "Filters Persons by sex (case-insensitive)." | ||
| 306 | + ) | ||
| 307 | + public List<Person> filterBySex(String sex) { | ||
| 308 | + if (sex == null || sex.isBlank()) { | ||
| 309 | + log.debug("Filter by sex skipped due to blank filter."); | ||
| 310 | + return Collections.emptyList(); | ||
| 311 | + } | ||
| 312 | + List<Person> results = personStore.values().stream() | ||
| 313 | + .filter(person -> person.sex() != null && person.sex().equalsIgnoreCase(sex)) | ||
| 314 | + .collect(Collectors.toList()); | ||
| 315 | + log.debug("Filter by sex '{}' found {} results.", sex, results.size()); | ||
| 316 | + return Collections.unmodifiableList(results); | ||
| 317 | + } | ||
| 318 | + | ||
| 319 | + @Override | ||
| 320 | + @Tool( | ||
| 321 | + name = "ps_filter_by_age", | ||
| 322 | + description = "Filters Persons by age." | ||
| 323 | + ) | ||
| 324 | + public List<Person> filterByAge(int age) { | ||
| 325 | + if (age < 0) { | ||
| 326 | + log.debug("Filter by age skipped due to negative age: {}", age); | ||
| 327 | + return Collections.emptyList(); // Or throw IllegalArgumentException based on requirements | ||
| 328 | + } | ||
| 329 | + List<Person> results = personStore.values().stream() | ||
| 330 | + .filter(person -> person.age() == age) | ||
| 331 | + .collect(Collectors.toList()); | ||
| 332 | + log.debug("Filter by age {} found {} results.", age, results.size()); | ||
| 333 | + return Collections.unmodifiableList(results); | ||
| 334 | + } | ||
| 335 | + | ||
| 336 | +} | ||
| 0 | \ No newline at end of file | 337 | \ No newline at end of file |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/tool/method/package-info.java
0 → 100644
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java
| @@ -8,18 +8,20 @@ import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; | @@ -8,18 +8,20 @@ import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; | ||
| 8 | import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; | 8 | import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; |
| 9 | import org.springaicommunity.moonshot.MoonshotChatOptions; | 9 | import org.springaicommunity.moonshot.MoonshotChatOptions; |
| 10 | import org.springaicommunity.qianfan.QianFanChatOptions; | 10 | import org.springaicommunity.qianfan.QianFanChatOptions; |
| 11 | +import org.springframework.ai.anthropic.AnthropicChatOptions; | ||
| 11 | import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; | 12 | import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; |
| 12 | import org.springframework.ai.chat.messages.*; | 13 | import org.springframework.ai.chat.messages.*; |
| 14 | +import org.springframework.ai.chat.model.ChatResponse; | ||
| 13 | import org.springframework.ai.chat.prompt.ChatOptions; | 15 | import org.springframework.ai.chat.prompt.ChatOptions; |
| 16 | +import org.springframework.ai.deepseek.DeepSeekAssistantMessage; | ||
| 17 | +import org.springframework.ai.deepseek.DeepSeekChatOptions; | ||
| 14 | import org.springframework.ai.minimax.MiniMaxChatOptions; | 18 | import org.springframework.ai.minimax.MiniMaxChatOptions; |
| 15 | import org.springframework.ai.ollama.api.OllamaOptions; | 19 | import org.springframework.ai.ollama.api.OllamaOptions; |
| 16 | import org.springframework.ai.openai.OpenAiChatOptions; | 20 | import org.springframework.ai.openai.OpenAiChatOptions; |
| 21 | +import org.springframework.ai.tool.ToolCallback; | ||
| 17 | import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; | 22 | import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; |
| 18 | 23 | ||
| 19 | -import java.util.Collections; | ||
| 20 | -import java.util.HashMap; | ||
| 21 | -import java.util.Map; | ||
| 22 | -import java.util.Set; | 24 | +import java.util.*; |
| 23 | 25 | ||
| 24 | /** | 26 | /** |
| 25 | * Spring AI 工具类 | 27 | * Spring AI 工具类 |
| @@ -36,40 +38,47 @@ public class AiUtils { | @@ -36,40 +38,47 @@ public class AiUtils { | ||
| 36 | } | 38 | } |
| 37 | 39 | ||
| 38 | public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens, | 40 | public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens, |
| 39 | - Set<String> toolNames, Map<String, Object> toolContext) { | ||
| 40 | - toolNames = ObjUtil.defaultIfNull(toolNames, Collections.emptySet()); | 41 | + List<ToolCallback> toolCallbacks, Map<String, Object> toolContext) { |
| 42 | + toolCallbacks = ObjUtil.defaultIfNull(toolCallbacks, Collections.emptyList()); | ||
| 41 | toolContext = ObjUtil.defaultIfNull(toolContext, Collections.emptyMap()); | 43 | toolContext = ObjUtil.defaultIfNull(toolContext, Collections.emptyMap()); |
| 42 | // noinspection EnhancedSwitchMigration | 44 | // noinspection EnhancedSwitchMigration |
| 43 | switch (platform) { | 45 | switch (platform) { |
| 44 | case TONG_YI: | 46 | case TONG_YI: |
| 45 | return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) | 47 | return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) |
| 46 | - .withToolNames(toolNames).withToolContext(toolContext).build(); | 48 | + .withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置 |
| 49 | + .withToolCallbacks(toolCallbacks).withToolContext(toolContext).build(); | ||
| 47 | case YI_YAN: | 50 | case YI_YAN: |
| 48 | return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); | 51 | return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); |
| 52 | + case DEEP_SEEK: | ||
| 53 | + case DOU_BAO: // 复用 DeepSeek 客户端 | ||
| 54 | + case HUN_YUAN: // 复用 DeepSeek 客户端 | ||
| 55 | + case SILICON_FLOW: // 复用 DeepSeek 客户端 | ||
| 56 | + case XING_HUO: // 复用 DeepSeek 客户端 | ||
| 57 | + return DeepSeekChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) | ||
| 58 | + .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); | ||
| 49 | case ZHI_PU: | 59 | case ZHI_PU: |
| 50 | return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) | 60 | return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) |
| 51 | - .toolNames(toolNames).toolContext(toolContext).build(); | 61 | + .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); |
| 52 | case MINI_MAX: | 62 | case MINI_MAX: |
| 53 | return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) | 63 | return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) |
| 54 | - .toolNames(toolNames).toolContext(toolContext).build(); | 64 | + .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); |
| 55 | case MOONSHOT: | 65 | case MOONSHOT: |
| 56 | return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) | 66 | return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) |
| 57 | - .toolNames(toolNames).toolContext(toolContext).build(); | 67 | + .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); |
| 58 | case OPENAI: | 68 | case OPENAI: |
| 59 | - case DEEP_SEEK: // 复用 OpenAI 客户端 | ||
| 60 | - case DOU_BAO: // 复用 OpenAI 客户端 | ||
| 61 | - case HUN_YUAN: // 复用 OpenAI 客户端 | ||
| 62 | - case XING_HUO: // 复用 OpenAI 客户端 | ||
| 63 | - case SILICON_FLOW: // 复用 OpenAI 客户端 | 69 | + case GEMINI: // 复用 OpenAI 客户端 |
| 64 | case BAI_CHUAN: // 复用 OpenAI 客户端 | 70 | case BAI_CHUAN: // 复用 OpenAI 客户端 |
| 65 | return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) | 71 | return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) |
| 66 | - .toolNames(toolNames).toolContext(toolContext).build(); | 72 | + .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); |
| 67 | case AZURE_OPENAI: | 73 | case AZURE_OPENAI: |
| 68 | return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens) | 74 | return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens) |
| 69 | - .toolNames(toolNames).toolContext(toolContext).build(); | 75 | + .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); |
| 76 | + case ANTHROPIC: | ||
| 77 | + return AnthropicChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) | ||
| 78 | + .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); | ||
| 70 | case OLLAMA: | 79 | case OLLAMA: |
| 71 | return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens) | 80 | return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens) |
| 72 | - .toolNames(toolNames).toolContext(toolContext).build(); | 81 | + .toolCallbacks(toolCallbacks).toolContext(toolContext).build(); |
| 73 | default: | 82 | default: |
| 74 | throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); | 83 | throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); |
| 75 | } | 84 | } |
| @@ -98,4 +107,27 @@ public class AiUtils { | @@ -98,4 +107,27 @@ public class AiUtils { | ||
| 98 | return context; | 107 | return context; |
| 99 | } | 108 | } |
| 100 | 109 | ||
| 110 | + @SuppressWarnings("ConstantValue") | ||
| 111 | + public static String getChatResponseContent(ChatResponse response) { | ||
| 112 | + if (response == null | ||
| 113 | + || response.getResult() == null | ||
| 114 | + || response.getResult().getOutput() == null) { | ||
| 115 | + return null; | ||
| 116 | + } | ||
| 117 | + return response.getResult().getOutput().getText(); | ||
| 118 | + } | ||
| 119 | + | ||
| 120 | + @SuppressWarnings("ConstantValue") | ||
| 121 | + public static String getChatResponseReasoningContent(ChatResponse response) { | ||
| 122 | + if (response == null | ||
| 123 | + || response.getResult() == null | ||
| 124 | + || response.getResult().getOutput() == null) { | ||
| 125 | + return null; | ||
| 126 | + } | ||
| 127 | + if (response.getResult().getOutput() instanceof DeepSeekAssistantMessage) { | ||
| 128 | + return ((DeepSeekAssistantMessage) (response.getResult().getOutput())).getReasoningContent(); | ||
| 129 | + } | ||
| 130 | + return null; | ||
| 131 | + } | ||
| 132 | + | ||
| 101 | } | 133 | } |
| 102 | \ No newline at end of file | 134 | \ No newline at end of file |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AnthropicChatModelTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; | ||
| 2 | + | ||
| 3 | +import org.junit.jupiter.api.Disabled; | ||
| 4 | +import org.junit.jupiter.api.Test; | ||
| 5 | +import org.springframework.ai.anthropic.AnthropicChatModel; | ||
| 6 | +import org.springframework.ai.anthropic.AnthropicChatOptions; | ||
| 7 | +import org.springframework.ai.anthropic.api.AnthropicApi; | ||
| 8 | +import org.springframework.ai.chat.messages.Message; | ||
| 9 | +import org.springframework.ai.chat.messages.SystemMessage; | ||
| 10 | +import org.springframework.ai.chat.messages.UserMessage; | ||
| 11 | +import org.springframework.ai.chat.model.ChatResponse; | ||
| 12 | +import org.springframework.ai.chat.prompt.Prompt; | ||
| 13 | +import reactor.core.publisher.Flux; | ||
| 14 | + | ||
| 15 | +import java.util.ArrayList; | ||
| 16 | +import java.util.List; | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * {@link AnthropicChatModel} 集成测试类 | ||
| 20 | + * | ||
| 21 | + * @author 芋道源码 | ||
| 22 | + */ | ||
| 23 | +public class AnthropicChatModelTest { | ||
| 24 | + | ||
| 25 | + private final AnthropicChatModel chatModel = AnthropicChatModel.builder() | ||
| 26 | + .anthropicApi(AnthropicApi.builder() | ||
| 27 | + .apiKey("sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942") | ||
| 28 | + .baseUrl("https://aihubmix.com") | ||
| 29 | + .build()) | ||
| 30 | + .defaultOptions(AnthropicChatOptions.builder() | ||
| 31 | + .model(AnthropicApi.ChatModel.CLAUDE_SONNET_4) | ||
| 32 | + .temperature(0.7) | ||
| 33 | + .maxTokens(4096) | ||
| 34 | + .build()) | ||
| 35 | + .build(); | ||
| 36 | + | ||
| 37 | + @Test | ||
| 38 | + @Disabled | ||
| 39 | + public void testCall() { | ||
| 40 | + // 准备参数 | ||
| 41 | + List<Message> messages = new ArrayList<>(); | ||
| 42 | + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | ||
| 43 | + messages.add(new UserMessage("1 + 1 = ?")); | ||
| 44 | + | ||
| 45 | + // 调用 | ||
| 46 | + ChatResponse response = chatModel.call(new Prompt(messages)); | ||
| 47 | + // 打印结果 | ||
| 48 | + System.out.println(response); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @Test | ||
| 52 | + @Disabled | ||
| 53 | + public void testStream() { | ||
| 54 | + // 准备参数 | ||
| 55 | + List<Message> messages = new ArrayList<>(); | ||
| 56 | + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | ||
| 57 | + messages.add(new UserMessage("1 + 1 = ?")); | ||
| 58 | + | ||
| 59 | + // 调用 | ||
| 60 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); | ||
| 61 | + // 打印结果 | ||
| 62 | + flux.doOnNext(System.out::println).then().block(); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + // TODO @芋艿:需要等 spring ai 升级:https://github.com/spring-projects/spring-ai/pull/2800 | ||
| 66 | + @Test | ||
| 67 | + @Disabled | ||
| 68 | + public void testStream_thinking() { | ||
| 69 | + // 准备参数 | ||
| 70 | + List<Message> messages = new ArrayList<>(); | ||
| 71 | + messages.add(new UserMessage("thkinking 下,1+1 为什么等于 2 ")); | ||
| 72 | + AnthropicChatOptions options = AnthropicChatOptions.builder() | ||
| 73 | + .model(AnthropicApi.ChatModel.CLAUDE_SONNET_4) | ||
| 74 | + .thinking(AnthropicApi.ThinkingType.ENABLED, 3096) | ||
| 75 | + .temperature(1D) | ||
| 76 | + .build(); | ||
| 77 | + | ||
| 78 | + // 调用 | ||
| 79 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); | ||
| 80 | + // 打印结果 | ||
| 81 | + flux.doOnNext(response -> { | ||
| 82 | +// System.out.println(response); | ||
| 83 | + System.out.println(response.getResult()); | ||
| 84 | + }).then().block(); | ||
| 85 | + } | ||
| 86 | + | ||
| 87 | +} |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java
| @@ -60,4 +60,23 @@ public class DeepSeekChatModelTests { | @@ -60,4 +60,23 @@ public class DeepSeekChatModelTests { | ||
| 60 | flux.doOnNext(System.out::println).then().block(); | 60 | flux.doOnNext(System.out::println).then().block(); |
| 61 | } | 61 | } |
| 62 | 62 | ||
| 63 | + @Test | ||
| 64 | + @Disabled | ||
| 65 | + public void testStream_thinking() { | ||
| 66 | + // 准备参数 | ||
| 67 | + List<Message> messages = new ArrayList<>(); | ||
| 68 | + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); | ||
| 69 | + DeepSeekChatOptions options = DeepSeekChatOptions.builder() | ||
| 70 | + .model("deepseek-reasoner") | ||
| 71 | + .build(); | ||
| 72 | + | ||
| 73 | + // 调用 | ||
| 74 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); | ||
| 75 | + // 打印结果 | ||
| 76 | + flux.doOnNext(response -> { | ||
| 77 | +// System.out.println(response); | ||
| 78 | + System.out.println(response.getResult().getOutput()); | ||
| 79 | + }).then().block(); | ||
| 80 | + } | ||
| 81 | + | ||
| 63 | } | 82 | } |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DouBaoChatModelTests.java
| @@ -8,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage; | @@ -8,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage; | ||
| 8 | import org.springframework.ai.chat.messages.UserMessage; | 8 | import org.springframework.ai.chat.messages.UserMessage; |
| 9 | import org.springframework.ai.chat.model.ChatResponse; | 9 | import org.springframework.ai.chat.model.ChatResponse; |
| 10 | import org.springframework.ai.chat.prompt.Prompt; | 10 | import org.springframework.ai.chat.prompt.Prompt; |
| 11 | -import org.springframework.ai.openai.OpenAiChatModel; | ||
| 12 | -import org.springframework.ai.openai.OpenAiChatOptions; | ||
| 13 | -import org.springframework.ai.openai.api.OpenAiApi; | 11 | +import org.springframework.ai.deepseek.DeepSeekChatModel; |
| 12 | +import org.springframework.ai.deepseek.DeepSeekChatOptions; | ||
| 13 | +import org.springframework.ai.deepseek.api.DeepSeekApi; | ||
| 14 | import reactor.core.publisher.Flux; | 14 | import reactor.core.publisher.Flux; |
| 15 | 15 | ||
| 16 | import java.util.ArrayList; | 16 | import java.util.ArrayList; |
| @@ -23,13 +23,18 @@ import java.util.List; | @@ -23,13 +23,18 @@ import java.util.List; | ||
| 23 | */ | 23 | */ |
| 24 | public class DouBaoChatModelTests { | 24 | public class DouBaoChatModelTests { |
| 25 | 25 | ||
| 26 | - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | ||
| 27 | - .openAiApi(OpenAiApi.builder() | 26 | + /** |
| 27 | + * 相比 OpenAIChatModel 来说,DeepSeekChatModel 可以兼容豆包的 thinking 能力! | ||
| 28 | + */ | ||
| 29 | + private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() | ||
| 30 | + .deepSeekApi(DeepSeekApi.builder() | ||
| 28 | .baseUrl(DouBaoChatModel.BASE_URL) | 31 | .baseUrl(DouBaoChatModel.BASE_URL) |
| 32 | + .completionsPath(DouBaoChatModel.COMPLETE_PATH) | ||
| 29 | .apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey | 33 | .apiKey("5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272") // apiKey |
| 30 | .build()) | 34 | .build()) |
| 31 | - .defaultOptions(OpenAiChatOptions.builder() | 35 | + .defaultOptions(DeepSeekChatOptions.builder() |
| 32 | .model("doubao-1-5-lite-32k-250115") // 模型(doubao) | 36 | .model("doubao-1-5-lite-32k-250115") // 模型(doubao) |
| 37 | +// .model("doubao-seed-1-6-thinking-250715") // 模型(doubao) | ||
| 33 | // .model("deepseek-r1-250120") // 模型(deepseek) | 38 | // .model("deepseek-r1-250120") // 模型(deepseek) |
| 34 | .temperature(0.7) | 39 | .temperature(0.7) |
| 35 | .build()) | 40 | .build()) |
| @@ -51,14 +56,13 @@ public class DouBaoChatModelTests { | @@ -51,14 +56,13 @@ public class DouBaoChatModelTests { | ||
| 51 | System.out.println(response); | 56 | System.out.println(response); |
| 52 | } | 57 | } |
| 53 | 58 | ||
| 54 | - // TODO @芋艿:因为使用的是 v1 api,导致 deepseek-r1-250120 不返回 think 过程,后续需要优化 | ||
| 55 | @Test | 59 | @Test |
| 56 | @Disabled | 60 | @Disabled |
| 57 | public void testStream() { | 61 | public void testStream() { |
| 58 | // 准备参数 | 62 | // 准备参数 |
| 59 | List<Message> messages = new ArrayList<>(); | 63 | List<Message> messages = new ArrayList<>(); |
| 60 | messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | 64 | messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); |
| 61 | - messages.add(new UserMessage("1 + 1 = ?")); | 65 | + messages.add(new UserMessage("详细推理下,帮我设计一个用户中心!")); |
| 62 | 66 | ||
| 63 | // 调用 | 67 | // 调用 |
| 64 | Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); | 68 | Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); |
| @@ -66,4 +70,23 @@ public class DouBaoChatModelTests { | @@ -66,4 +70,23 @@ public class DouBaoChatModelTests { | ||
| 66 | flux.doOnNext(System.out::println).then().block(); | 70 | flux.doOnNext(System.out::println).then().block(); |
| 67 | } | 71 | } |
| 68 | 72 | ||
| 73 | + @Test | ||
| 74 | + @Disabled | ||
| 75 | + public void testStream_thinking() { | ||
| 76 | + // 准备参数 | ||
| 77 | + List<Message> messages = new ArrayList<>(); | ||
| 78 | + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); | ||
| 79 | + DeepSeekChatOptions options = DeepSeekChatOptions.builder() | ||
| 80 | + .model("doubao-seed-1-6-thinking-250715") | ||
| 81 | + .build(); | ||
| 82 | + | ||
| 83 | + // 调用 | ||
| 84 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); | ||
| 85 | + // 打印结果 | ||
| 86 | + flux.doOnNext(response -> { | ||
| 87 | +// System.out.println(response); | ||
| 88 | + System.out.println(response.getResult().getOutput()); | ||
| 89 | + }).then().block(); | ||
| 90 | + } | ||
| 91 | + | ||
| 69 | } | 92 | } |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/GeminiChatModelTests.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.module.ai.framework.ai.core.model.gemini.GeminiChatModel; | ||
| 4 | +import org.junit.jupiter.api.Disabled; | ||
| 5 | +import org.junit.jupiter.api.Test; | ||
| 6 | +import org.springframework.ai.chat.messages.Message; | ||
| 7 | +import org.springframework.ai.chat.messages.SystemMessage; | ||
| 8 | +import org.springframework.ai.chat.messages.UserMessage; | ||
| 9 | +import org.springframework.ai.chat.model.ChatResponse; | ||
| 10 | +import org.springframework.ai.chat.prompt.Prompt; | ||
| 11 | +import org.springframework.ai.openai.OpenAiChatModel; | ||
| 12 | +import org.springframework.ai.openai.OpenAiChatOptions; | ||
| 13 | +import org.springframework.ai.openai.api.OpenAiApi; | ||
| 14 | +import reactor.core.publisher.Flux; | ||
| 15 | + | ||
| 16 | +import java.util.ArrayList; | ||
| 17 | +import java.util.List; | ||
| 18 | + | ||
| 19 | +/** | ||
| 20 | + * {@link GeminiChatModel} 集成测试 | ||
| 21 | + * | ||
| 22 | + * @author 芋道源码 | ||
| 23 | + */ | ||
| 24 | +public class GeminiChatModelTests { | ||
| 25 | + | ||
| 26 | + private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | ||
| 27 | + .openAiApi(OpenAiApi.builder() | ||
| 28 | + .baseUrl(GeminiChatModel.BASE_URL) | ||
| 29 | + .completionsPath(GeminiChatModel.COMPLETE_PATH) | ||
| 30 | + .apiKey("AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ") | ||
| 31 | + .build()) | ||
| 32 | + .defaultOptions(OpenAiChatOptions.builder() | ||
| 33 | + .model(GeminiChatModel.MODEL_DEFAULT) // 模型 | ||
| 34 | + .temperature(0.7) | ||
| 35 | + .build()) | ||
| 36 | + .build(); | ||
| 37 | + | ||
| 38 | + private final GeminiChatModel chatModel = new GeminiChatModel(openAiChatModel); | ||
| 39 | + | ||
| 40 | + @Test | ||
| 41 | + @Disabled | ||
| 42 | + public void testCall() { | ||
| 43 | + // 准备参数 | ||
| 44 | + List<Message> messages = new ArrayList<>(); | ||
| 45 | + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | ||
| 46 | + messages.add(new UserMessage("1 + 1 = ?")); | ||
| 47 | + | ||
| 48 | + // 调用 | ||
| 49 | + ChatResponse response = chatModel.call(new Prompt(messages)); | ||
| 50 | + // 打印结果 | ||
| 51 | + System.out.println(response); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + @Test | ||
| 55 | + @Disabled | ||
| 56 | + public void testStream() { | ||
| 57 | + // 准备参数 | ||
| 58 | + List<Message> messages = new ArrayList<>(); | ||
| 59 | + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | ||
| 60 | + messages.add(new UserMessage("1 + 1 = ?")); | ||
| 61 | + | ||
| 62 | + // 调用 | ||
| 63 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); | ||
| 64 | + // 打印结果 | ||
| 65 | + flux.doOnNext(System.out::println).then().block(); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | +} |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/HunYuanChatModelTests.java
| @@ -8,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage; | @@ -8,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage; | ||
| 8 | import org.springframework.ai.chat.messages.UserMessage; | 8 | import org.springframework.ai.chat.messages.UserMessage; |
| 9 | import org.springframework.ai.chat.model.ChatResponse; | 9 | import org.springframework.ai.chat.model.ChatResponse; |
| 10 | import org.springframework.ai.chat.prompt.Prompt; | 10 | import org.springframework.ai.chat.prompt.Prompt; |
| 11 | -import org.springframework.ai.openai.OpenAiChatModel; | ||
| 12 | -import org.springframework.ai.openai.OpenAiChatOptions; | ||
| 13 | -import org.springframework.ai.openai.api.OpenAiApi; | 11 | +import org.springframework.ai.deepseek.DeepSeekChatModel; |
| 12 | +import org.springframework.ai.deepseek.DeepSeekChatOptions; | ||
| 13 | +import org.springframework.ai.deepseek.api.DeepSeekApi; | ||
| 14 | import reactor.core.publisher.Flux; | 14 | import reactor.core.publisher.Flux; |
| 15 | 15 | ||
| 16 | import java.util.ArrayList; | 16 | import java.util.ArrayList; |
| @@ -23,12 +23,13 @@ import java.util.List; | @@ -23,12 +23,13 @@ import java.util.List; | ||
| 23 | */ | 23 | */ |
| 24 | public class HunYuanChatModelTests { | 24 | public class HunYuanChatModelTests { |
| 25 | 25 | ||
| 26 | - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | ||
| 27 | - .openAiApi(OpenAiApi.builder() | 26 | + private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() |
| 27 | + .deepSeekApi(DeepSeekApi.builder() | ||
| 28 | .baseUrl(HunYuanChatModel.BASE_URL) | 28 | .baseUrl(HunYuanChatModel.BASE_URL) |
| 29 | - .apiKey("sk-bcd") // apiKey | 29 | + .completionsPath(HunYuanChatModel.COMPLETE_PATH) |
| 30 | + .apiKey("sk-abc") // apiKey | ||
| 30 | .build()) | 31 | .build()) |
| 31 | - .defaultOptions(OpenAiChatOptions.builder() | 32 | + .defaultOptions(DeepSeekChatOptions.builder() |
| 32 | .model(HunYuanChatModel.MODEL_DEFAULT) // 模型 | 33 | .model(HunYuanChatModel.MODEL_DEFAULT) // 模型 |
| 33 | .temperature(0.7) | 34 | .temperature(0.7) |
| 34 | .build()) | 35 | .build()) |
| @@ -64,12 +65,33 @@ public class HunYuanChatModelTests { | @@ -64,12 +65,33 @@ public class HunYuanChatModelTests { | ||
| 64 | flux.doOnNext(System.out::println).then().block(); | 65 | flux.doOnNext(System.out::println).then().block(); |
| 65 | } | 66 | } |
| 66 | 67 | ||
| 67 | - private final OpenAiChatModel deepSeekOpenAiChatModel = OpenAiChatModel.builder() | ||
| 68 | - .openAiApi(OpenAiApi.builder() | 68 | + @Test |
| 69 | + @Disabled | ||
| 70 | + public void testStream_thinking() { | ||
| 71 | + // 准备参数 | ||
| 72 | + List<Message> messages = new ArrayList<>(); | ||
| 73 | + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); | ||
| 74 | + DeepSeekChatOptions options = DeepSeekChatOptions.builder() | ||
| 75 | + .model("hunyuan-a13b") | ||
| 76 | +// .model("hunyuan-turbos-latest") | ||
| 77 | + .build(); | ||
| 78 | + | ||
| 79 | + // 调用 | ||
| 80 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); | ||
| 81 | + // 打印结果 | ||
| 82 | + flux.doOnNext(response -> { | ||
| 83 | +// System.out.println(response); | ||
| 84 | + System.out.println(response.getResult().getOutput()); | ||
| 85 | + }).then().block(); | ||
| 86 | + } | ||
| 87 | + | ||
| 88 | + private final DeepSeekChatModel deepSeekOpenAiChatModel = DeepSeekChatModel.builder() | ||
| 89 | + .deepSeekApi(DeepSeekApi.builder() | ||
| 69 | .baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL) | 90 | .baseUrl(HunYuanChatModel.DEEP_SEEK_BASE_URL) |
| 91 | + .completionsPath(HunYuanChatModel.COMPLETE_PATH) | ||
| 70 | .apiKey("sk-abc") // apiKey | 92 | .apiKey("sk-abc") // apiKey |
| 71 | .build()) | 93 | .build()) |
| 72 | - .defaultOptions(OpenAiChatOptions.builder() | 94 | + .defaultOptions(DeepSeekChatOptions.builder() |
| 73 | // .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型("deepseek-v3") | 95 | // .model(HunYuanChatModel.DEEP_SEEK_MODEL_DEFAULT) // 模型("deepseek-v3") |
| 74 | .model("deepseek-r1") // 模型("deepseek-r1") | 96 | .model("deepseek-r1") // 模型("deepseek-r1") |
| 75 | .temperature(0.7) | 97 | .temperature(0.7) |
| @@ -94,7 +116,7 @@ public class HunYuanChatModelTests { | @@ -94,7 +116,7 @@ public class HunYuanChatModelTests { | ||
| 94 | 116 | ||
| 95 | @Test | 117 | @Test |
| 96 | @Disabled | 118 | @Disabled |
| 97 | - public void testStream_deekseek() { | 119 | + public void testStream_deepseek() { |
| 98 | // 准备参数 | 120 | // 准备参数 |
| 99 | List<Message> messages = new ArrayList<>(); | 121 | List<Message> messages = new ArrayList<>(); |
| 100 | messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | 122 | messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); |
| @@ -106,5 +128,23 @@ public class HunYuanChatModelTests { | @@ -106,5 +128,23 @@ public class HunYuanChatModelTests { | ||
| 106 | flux.doOnNext(System.out::println).then().block(); | 128 | flux.doOnNext(System.out::println).then().block(); |
| 107 | } | 129 | } |
| 108 | 130 | ||
| 131 | + @Test | ||
| 132 | + @Disabled | ||
| 133 | + public void testStream_deepseek_thinking() { | ||
| 134 | + // 准备参数 | ||
| 135 | + List<Message> messages = new ArrayList<>(); | ||
| 136 | + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); | ||
| 137 | + DeepSeekChatOptions options = DeepSeekChatOptions.builder() | ||
| 138 | + .model("deepseek-r1") | ||
| 139 | + .build(); | ||
| 140 | + | ||
| 141 | + // 调用 | ||
| 142 | + Flux<ChatResponse> flux = deepSeekChatModel.stream(new Prompt(messages, options)); | ||
| 143 | + // 打印结果 | ||
| 144 | + flux.doOnNext(response -> { | ||
| 145 | +// System.out.println(response); | ||
| 146 | + System.out.println(response.getResult().getOutput()); | ||
| 147 | + }).then().block(); | ||
| 148 | + } | ||
| 109 | 149 | ||
| 110 | } | 150 | } |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java
| 1 | package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; | 1 | package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; |
| 2 | 2 | ||
| 3 | +import org.junit.jupiter.api.Disabled; | ||
| 4 | +import org.junit.jupiter.api.Test; | ||
| 5 | +import org.springframework.ai.chat.messages.Message; | ||
| 6 | +import org.springframework.ai.chat.messages.SystemMessage; | ||
| 7 | +import org.springframework.ai.chat.messages.UserMessage; | ||
| 8 | +import org.springframework.ai.chat.model.ChatResponse; | ||
| 9 | +import org.springframework.ai.chat.prompt.Prompt; | ||
| 3 | import org.springframework.ai.ollama.OllamaChatModel; | 10 | import org.springframework.ai.ollama.OllamaChatModel; |
| 11 | +import org.springframework.ai.ollama.api.OllamaApi; | ||
| 12 | +import org.springframework.ai.ollama.api.OllamaModel; | ||
| 13 | +import org.springframework.ai.ollama.api.OllamaOptions; | ||
| 14 | +import reactor.core.publisher.Flux; | ||
| 15 | + | ||
| 16 | +import java.util.ArrayList; | ||
| 17 | +import java.util.List; | ||
| 4 | 18 | ||
| 5 | /** | 19 | /** |
| 6 | * {@link OllamaChatModel} 集成测试 | 20 | * {@link OllamaChatModel} 集成测试 |
| @@ -9,43 +23,65 @@ import org.springframework.ai.ollama.OllamaChatModel; | @@ -9,43 +23,65 @@ import org.springframework.ai.ollama.OllamaChatModel; | ||
| 9 | */ | 23 | */ |
| 10 | public class LlamaChatModelTests { | 24 | public class LlamaChatModelTests { |
| 11 | 25 | ||
| 12 | -// private final OllamaChatModel chatModel = OllamaChatModel.builder() | ||
| 13 | -// .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 | ||
| 14 | -// .defaultOptions(OllamaOptions.builder() | ||
| 15 | -// .model(OllamaModel.LLAMA3.getName()) // 模型 | ||
| 16 | -// .build()) | ||
| 17 | -// .build(); | ||
| 18 | -// | ||
| 19 | -// @Test | ||
| 20 | -// @Disabled | ||
| 21 | -// public void testCall() { | ||
| 22 | -// // 准备参数 | ||
| 23 | -// List<Message> messages = new ArrayList<>(); | ||
| 24 | -// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | ||
| 25 | -// messages.add(new UserMessage("1 + 1 = ?")); | ||
| 26 | -// | ||
| 27 | -// // 调用 | ||
| 28 | -// ChatResponse response = chatModel.call(new Prompt(messages)); | ||
| 29 | -// // 打印结果 | ||
| 30 | -// System.out.println(response); | ||
| 31 | -// System.out.println(response.getResult().getOutput()); | ||
| 32 | -// } | ||
| 33 | -// | ||
| 34 | -// @Test | ||
| 35 | -// @Disabled | ||
| 36 | -// public void testStream() { | ||
| 37 | -// // 准备参数 | ||
| 38 | -// List<Message> messages = new ArrayList<>(); | ||
| 39 | -// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | ||
| 40 | -// messages.add(new UserMessage("1 + 1 = ?")); | ||
| 41 | -// | ||
| 42 | -// // 调用 | ||
| 43 | -// Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); | ||
| 44 | -// // 打印结果 | ||
| 45 | -// flux.doOnNext(response -> { | ||
| 46 | -//// System.out.println(response); | ||
| 47 | -// System.out.println(response.getResult().getOutput()); | ||
| 48 | -// }).then().block(); | ||
| 49 | -// } | 26 | + private final OllamaChatModel chatModel = OllamaChatModel.builder() |
| 27 | + .ollamaApi(OllamaApi.builder() | ||
| 28 | + .baseUrl("http://127.0.0.1:11434") // Ollama 服务地址 | ||
| 29 | + .build()) | ||
| 30 | + .defaultOptions(OllamaOptions.builder() | ||
| 31 | + .model(OllamaModel.LLAMA3.getName()) // 模型 | ||
| 32 | + .build()) | ||
| 33 | + .build(); | ||
| 34 | + | ||
| 35 | + @Test | ||
| 36 | + @Disabled | ||
| 37 | + public void testCall() { | ||
| 38 | + // 准备参数 | ||
| 39 | + List<Message> messages = new ArrayList<>(); | ||
| 40 | + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | ||
| 41 | + messages.add(new UserMessage("1 + 1 = ?")); | ||
| 42 | + | ||
| 43 | + // 调用 | ||
| 44 | + ChatResponse response = chatModel.call(new Prompt(messages)); | ||
| 45 | + // 打印结果 | ||
| 46 | + System.out.println(response); | ||
| 47 | + System.out.println(response.getResult().getOutput()); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + @Test | ||
| 51 | + @Disabled | ||
| 52 | + public void testStream() { | ||
| 53 | + // 准备参数 | ||
| 54 | + List<Message> messages = new ArrayList<>(); | ||
| 55 | + messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | ||
| 56 | + messages.add(new UserMessage("1 + 1 = ?")); | ||
| 57 | + | ||
| 58 | + // 调用 | ||
| 59 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); | ||
| 60 | + // 打印结果 | ||
| 61 | + flux.doOnNext(response -> { | ||
| 62 | +// System.out.println(response); | ||
| 63 | + System.out.println(response.getResult().getOutput()); | ||
| 64 | + }).then().block(); | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | + @Test | ||
| 68 | + @Disabled | ||
| 69 | + public void testStream_thinking() { | ||
| 70 | + // 准备参数 | ||
| 71 | + List<Message> messages = new ArrayList<>(); | ||
| 72 | + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); | ||
| 73 | + OllamaOptions options = OllamaOptions.builder() | ||
| 74 | + .model("qwen3") | ||
| 75 | + .build(); | ||
| 76 | + | ||
| 77 | + // 调用 | ||
| 78 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); | ||
| 79 | + // 打印结果 | ||
| 80 | + flux.doOnNext(response -> { | ||
| 81 | +// System.out.println(response); | ||
| 82 | + System.out.println(response.getResult().getOutput()); | ||
| 83 | + }).then().block(); | ||
| 84 | + } | ||
| 85 | + | ||
| 50 | 86 | ||
| 51 | } | 87 | } |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MiniMaxChatModelTests.java
| @@ -59,4 +59,24 @@ public class MiniMaxChatModelTests { | @@ -59,4 +59,24 @@ public class MiniMaxChatModelTests { | ||
| 59 | }).then().block(); | 59 | }).then().block(); |
| 60 | } | 60 | } |
| 61 | 61 | ||
| 62 | + // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复 | ||
| 63 | + @Test | ||
| 64 | + @Disabled | ||
| 65 | + public void testStream_thinking() { | ||
| 66 | + // 准备参数 | ||
| 67 | + List<Message> messages = new ArrayList<>(); | ||
| 68 | + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); | ||
| 69 | + MiniMaxChatOptions options = MiniMaxChatOptions.builder() | ||
| 70 | + .model("MiniMax-M1") | ||
| 71 | + .build(); | ||
| 72 | + | ||
| 73 | + // 调用 | ||
| 74 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); | ||
| 75 | + // 打印结果 | ||
| 76 | + flux.doOnNext(response -> { | ||
| 77 | +// System.out.println(response); | ||
| 78 | + System.out.println(response.getResult().getOutput()); | ||
| 79 | + }).then().block(); | ||
| 80 | + } | ||
| 81 | + | ||
| 62 | } | 82 | } |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java
| @@ -63,4 +63,25 @@ public class MoonshotChatModelTests { | @@ -63,4 +63,25 @@ public class MoonshotChatModelTests { | ||
| 63 | }).then().block(); | 63 | }).then().block(); |
| 64 | } | 64 | } |
| 65 | 65 | ||
| 66 | + // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复 | ||
| 67 | + @Test | ||
| 68 | + @Disabled | ||
| 69 | + public void testStream_thinking() { | ||
| 70 | + // 准备参数 | ||
| 71 | + List<Message> messages = new ArrayList<>(); | ||
| 72 | + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); | ||
| 73 | + MoonshotChatOptions options = MoonshotChatOptions.builder() | ||
| 74 | +// .model("kimi-k2-0711-preview") | ||
| 75 | + .model("kimi-thinking-preview") | ||
| 76 | + .build(); | ||
| 77 | + | ||
| 78 | + // 调用 | ||
| 79 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); | ||
| 80 | + // 打印结果 | ||
| 81 | + flux.doOnNext(response -> { | ||
| 82 | +// System.out.println(response); | ||
| 83 | + System.out.println(response.getResult().getOutput()); | ||
| 84 | + }).then().block(); | ||
| 85 | + } | ||
| 86 | + | ||
| 66 | } | 87 | } |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java
| 1 | package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; | 1 | package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; |
| 2 | 2 | ||
| 3 | +import com.azure.ai.openai.models.ReasoningEffortValue; | ||
| 3 | import org.junit.jupiter.api.Disabled; | 4 | import org.junit.jupiter.api.Disabled; |
| 4 | import org.junit.jupiter.api.Test; | 5 | import org.junit.jupiter.api.Test; |
| 5 | import org.springframework.ai.chat.messages.Message; | 6 | import org.springframework.ai.chat.messages.Message; |
| @@ -25,10 +26,11 @@ public class OpenAIChatModelTests { | @@ -25,10 +26,11 @@ public class OpenAIChatModelTests { | ||
| 25 | private final OpenAiChatModel chatModel = OpenAiChatModel.builder() | 26 | private final OpenAiChatModel chatModel = OpenAiChatModel.builder() |
| 26 | .openAiApi(OpenAiApi.builder() | 27 | .openAiApi(OpenAiApi.builder() |
| 27 | .baseUrl("https://api.holdai.top") | 28 | .baseUrl("https://api.holdai.top") |
| 28 | - .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") // apiKey | 29 | + .apiKey("sk-z5joyRoV1iFEnh2SAi8QPNrIZTXyQSyxTmD5CoNDQbFixK2l") // apiKey |
| 29 | .build()) | 30 | .build()) |
| 30 | .defaultOptions(OpenAiChatOptions.builder() | 31 | .defaultOptions(OpenAiChatOptions.builder() |
| 31 | - .model(OpenAiApi.ChatModel.GPT_4_1_NANO) // 模型 | 32 | + .model("gpt-5-nano-2025-08-07") // 模型 |
| 33 | +// .model(OpenAiApi.ChatModel.O1) // 模型 | ||
| 32 | .temperature(0.7) | 34 | .temperature(0.7) |
| 33 | .build()) | 35 | .build()) |
| 34 | .build(); | 36 | .build(); |
| @@ -54,7 +56,7 @@ public class OpenAIChatModelTests { | @@ -54,7 +56,7 @@ public class OpenAIChatModelTests { | ||
| 54 | // 准备参数 | 56 | // 准备参数 |
| 55 | List<Message> messages = new ArrayList<>(); | 57 | List<Message> messages = new ArrayList<>(); |
| 56 | messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | 58 | messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); |
| 57 | - messages.add(new UserMessage("1 + 1 = ?")); | 59 | + messages.add(new UserMessage("帮我推理下,怎么实现一个用户中心!")); |
| 58 | 60 | ||
| 59 | // 调用 | 61 | // 调用 |
| 60 | Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); | 62 | Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); |
| @@ -65,4 +67,29 @@ public class OpenAIChatModelTests { | @@ -65,4 +67,29 @@ public class OpenAIChatModelTests { | ||
| 65 | }).then().block(); | 67 | }).then().block(); |
| 66 | } | 68 | } |
| 67 | 69 | ||
| 70 | + // TODO @芋艿:无法触发思考的字段返回,需要 response api:https://github.com/spring-projects/spring-ai/issues/2962 | ||
| 71 | + @Test | ||
| 72 | + @Disabled | ||
| 73 | + public void testStream_thinking() { | ||
| 74 | + // 准备参数 | ||
| 75 | + List<Message> messages = new ArrayList<>(); | ||
| 76 | + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); | ||
| 77 | + OpenAiChatOptions options = OpenAiChatOptions.builder() | ||
| 78 | + .model("gpt-5") | ||
| 79 | +// .model(OpenAiApi.ChatModel.O4_MINI) | ||
| 80 | +// .model("o3-pro") | ||
| 81 | + .reasoningEffort(ReasoningEffortValue.LOW.getValue()) | ||
| 82 | + .build(); | ||
| 83 | + | ||
| 84 | + // 调用 | ||
| 85 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); | ||
| 86 | + // 打印结果 | ||
| 87 | + flux.doOnNext(response -> { | ||
| 88 | +// System.out.println(response); | ||
| 89 | + System.out.println(response.getResult().getOutput()); | ||
| 90 | + }).then().block(); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + | ||
| 94 | + | ||
| 68 | } | 95 | } |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/SiliconFlowChatModelTests.java
| @@ -9,9 +9,9 @@ import org.springframework.ai.chat.messages.SystemMessage; | @@ -9,9 +9,9 @@ import org.springframework.ai.chat.messages.SystemMessage; | ||
| 9 | import org.springframework.ai.chat.messages.UserMessage; | 9 | import org.springframework.ai.chat.messages.UserMessage; |
| 10 | import org.springframework.ai.chat.model.ChatResponse; | 10 | import org.springframework.ai.chat.model.ChatResponse; |
| 11 | import org.springframework.ai.chat.prompt.Prompt; | 11 | import org.springframework.ai.chat.prompt.Prompt; |
| 12 | -import org.springframework.ai.openai.OpenAiChatModel; | ||
| 13 | -import org.springframework.ai.openai.OpenAiChatOptions; | ||
| 14 | -import org.springframework.ai.openai.api.OpenAiApi; | 12 | +import org.springframework.ai.deepseek.DeepSeekChatModel; |
| 13 | +import org.springframework.ai.deepseek.DeepSeekChatOptions; | ||
| 14 | +import org.springframework.ai.deepseek.api.DeepSeekApi; | ||
| 15 | import reactor.core.publisher.Flux; | 15 | import reactor.core.publisher.Flux; |
| 16 | 16 | ||
| 17 | import java.util.ArrayList; | 17 | import java.util.ArrayList; |
| @@ -24,12 +24,12 @@ import java.util.List; | @@ -24,12 +24,12 @@ import java.util.List; | ||
| 24 | */ | 24 | */ |
| 25 | public class SiliconFlowChatModelTests { | 25 | public class SiliconFlowChatModelTests { |
| 26 | 26 | ||
| 27 | - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | ||
| 28 | - .openAiApi(OpenAiApi.builder() | 27 | + private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() |
| 28 | + .deepSeekApi(DeepSeekApi.builder() | ||
| 29 | .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL) | 29 | .baseUrl(SiliconFlowApiConstants.DEFAULT_BASE_URL) |
| 30 | .apiKey("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // apiKey | 30 | .apiKey("sk-epsakfenqnyzoxhmbucsxlhkdqlcbnimslqoivkshalvdozz") // apiKey |
| 31 | .build()) | 31 | .build()) |
| 32 | - .defaultOptions(OpenAiChatOptions.builder() | 32 | + .defaultOptions(DeepSeekChatOptions.builder() |
| 33 | .model(SiliconFlowApiConstants.MODEL_DEFAULT) // 模型 | 33 | .model(SiliconFlowApiConstants.MODEL_DEFAULT) // 模型 |
| 34 | // .model("deepseek-ai/DeepSeek-R1") // 模型(deepseek-ai/DeepSeek-R1)可用赠费 | 34 | // .model("deepseek-ai/DeepSeek-R1") // 模型(deepseek-ai/DeepSeek-R1)可用赠费 |
| 35 | // .model("Pro/deepseek-ai/DeepSeek-R1") // 模型(Pro/deepseek-ai/DeepSeek-R1)需要付费 | 35 | // .model("Pro/deepseek-ai/DeepSeek-R1") // 模型(Pro/deepseek-ai/DeepSeek-R1)需要付费 |
| @@ -67,4 +67,23 @@ public class SiliconFlowChatModelTests { | @@ -67,4 +67,23 @@ public class SiliconFlowChatModelTests { | ||
| 67 | flux.doOnNext(System.out::println).then().block(); | 67 | flux.doOnNext(System.out::println).then().block(); |
| 68 | } | 68 | } |
| 69 | 69 | ||
| 70 | + @Test | ||
| 71 | + @Disabled | ||
| 72 | + public void testStream_thinking() { | ||
| 73 | + // 准备参数 | ||
| 74 | + List<Message> messages = new ArrayList<>(); | ||
| 75 | + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); | ||
| 76 | + DeepSeekChatOptions options = DeepSeekChatOptions.builder() | ||
| 77 | + .model("deepseek-ai/DeepSeek-R1") | ||
| 78 | + .build(); | ||
| 79 | + | ||
| 80 | + // 调用 | ||
| 81 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); | ||
| 82 | + // 打印结果 | ||
| 83 | + flux.doOnNext(response -> { | ||
| 84 | +// System.out.println(response); | ||
| 85 | + System.out.println(response.getResult().getOutput()); | ||
| 86 | + }).then().block(); | ||
| 87 | + } | ||
| 88 | + | ||
| 70 | } | 89 | } |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java
| 1 | package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; | 1 | package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 3 | import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; | 4 | import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; |
| 4 | import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; | 5 | import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; |
| 5 | import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; | 6 | import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; |
| 7 | +import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankModel; | ||
| 8 | +import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions; | ||
| 9 | +import com.alibaba.cloud.ai.model.RerankModel; | ||
| 10 | +import com.alibaba.cloud.ai.model.RerankOptions; | ||
| 11 | +import com.alibaba.cloud.ai.model.RerankRequest; | ||
| 12 | +import com.alibaba.cloud.ai.model.RerankResponse; | ||
| 6 | import org.junit.jupiter.api.Disabled; | 13 | import org.junit.jupiter.api.Disabled; |
| 7 | import org.junit.jupiter.api.Test; | 14 | import org.junit.jupiter.api.Test; |
| 8 | import org.springframework.ai.chat.messages.Message; | 15 | import org.springframework.ai.chat.messages.Message; |
| @@ -10,11 +17,14 @@ import org.springframework.ai.chat.messages.SystemMessage; | @@ -10,11 +17,14 @@ import org.springframework.ai.chat.messages.SystemMessage; | ||
| 10 | import org.springframework.ai.chat.messages.UserMessage; | 17 | import org.springframework.ai.chat.messages.UserMessage; |
| 11 | import org.springframework.ai.chat.model.ChatResponse; | 18 | import org.springframework.ai.chat.model.ChatResponse; |
| 12 | import org.springframework.ai.chat.prompt.Prompt; | 19 | import org.springframework.ai.chat.prompt.Prompt; |
| 20 | +import org.springframework.ai.document.Document; | ||
| 13 | import reactor.core.publisher.Flux; | 21 | import reactor.core.publisher.Flux; |
| 14 | 22 | ||
| 15 | import java.util.ArrayList; | 23 | import java.util.ArrayList; |
| 16 | import java.util.List; | 24 | import java.util.List; |
| 17 | 25 | ||
| 26 | +import static java.util.Arrays.asList; | ||
| 27 | + | ||
| 18 | /** | 28 | /** |
| 19 | * {@link DashScopeChatModel} 集成测试类 | 29 | * {@link DashScopeChatModel} 集成测试类 |
| 20 | * | 30 | * |
| @@ -26,11 +36,13 @@ public class TongYiChatModelTests { | @@ -26,11 +36,13 @@ public class TongYiChatModelTests { | ||
| 26 | .dashScopeApi(DashScopeApi.builder() | 36 | .dashScopeApi(DashScopeApi.builder() |
| 27 | .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0") | 37 | .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0") |
| 28 | .build()) | 38 | .build()) |
| 29 | - .defaultOptions( DashScopeChatOptions.builder() | ||
| 30 | - .withModel("qwen1.5-72b-chat") // 模型 | 39 | + .defaultOptions(DashScopeChatOptions.builder() |
| 40 | +// .withModel("qwen1.5-72b-chat") // 模型 | ||
| 41 | + .withModel("qwen3-235b-a22b-thinking-2507") // 模型 | ||
| 31 | // .withModel("deepseek-r1") // 模型(deepseek-r1) | 42 | // .withModel("deepseek-r1") // 模型(deepseek-r1) |
| 32 | // .withModel("deepseek-v3") // 模型(deepseek-v3) | 43 | // .withModel("deepseek-v3") // 模型(deepseek-v3) |
| 33 | // .withModel("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b) | 44 | // .withModel("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b) |
| 45 | +// .withEnableThinking(true) | ||
| 34 | .build()) | 46 | .build()) |
| 35 | .build(); | 47 | .build(); |
| 36 | 48 | ||
| @@ -54,8 +66,8 @@ public class TongYiChatModelTests { | @@ -54,8 +66,8 @@ public class TongYiChatModelTests { | ||
| 54 | public void testStream() { | 66 | public void testStream() { |
| 55 | // 准备参数 | 67 | // 准备参数 |
| 56 | List<Message> messages = new ArrayList<>(); | 68 | List<Message> messages = new ArrayList<>(); |
| 57 | - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); | ||
| 58 | - messages.add(new UserMessage("1 + 1 = ?")); | 69 | +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); |
| 70 | + messages.add(new UserMessage("帮我推理下,怎么实现一个用户中心!")); | ||
| 59 | 71 | ||
| 60 | // 调用 | 72 | // 调用 |
| 61 | Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); | 73 | Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages)); |
| @@ -66,4 +78,52 @@ public class TongYiChatModelTests { | @@ -66,4 +78,52 @@ public class TongYiChatModelTests { | ||
| 66 | }).then().block(); | 78 | }).then().block(); |
| 67 | } | 79 | } |
| 68 | 80 | ||
| 81 | + @Test | ||
| 82 | + @Disabled | ||
| 83 | + public void testStream_thinking() { | ||
| 84 | + // 准备参数 | ||
| 85 | + List<Message> messages = new ArrayList<>(); | ||
| 86 | + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); | ||
| 87 | + DashScopeChatOptions options = DashScopeChatOptions.builder() | ||
| 88 | + .withModel("qwen3-235b-a22b-thinking-2507") | ||
| 89 | +// .withModel("qwen-max-2025-01-25") | ||
| 90 | + .withEnableThinking(true) // 必须设置,否则会报错 | ||
| 91 | + .build(); | ||
| 92 | + | ||
| 93 | + // 调用 | ||
| 94 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); | ||
| 95 | + // 打印结果 | ||
| 96 | + flux.doOnNext(response -> { | ||
| 97 | +// System.out.println(response); | ||
| 98 | + System.out.println(response.getResult().getOutput()); | ||
| 99 | + }).then().block(); | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + @Test | ||
| 103 | + @Disabled | ||
| 104 | + public void testRerank() { | ||
| 105 | + // 准备环境 | ||
| 106 | + RerankModel rerankModel = new DashScopeRerankModel( | ||
| 107 | + DashScopeApi.builder() | ||
| 108 | + .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0") | ||
| 109 | + .build()); | ||
| 110 | + // 准备参数 | ||
| 111 | + String query = "spring"; | ||
| 112 | + Document document01 = new Document("abc"); | ||
| 113 | + Document document02 = new Document("sapring"); | ||
| 114 | + RerankOptions options = DashScopeRerankOptions.builder() | ||
| 115 | + .withTopN(1) | ||
| 116 | + .withModel("gte-rerank-v2") | ||
| 117 | + .build(); | ||
| 118 | + RerankRequest rerankRequest = new RerankRequest( | ||
| 119 | + query, | ||
| 120 | + asList(document01, document02), | ||
| 121 | + options); | ||
| 122 | + | ||
| 123 | + // 调用 | ||
| 124 | + RerankResponse call = rerankModel.call(rerankRequest); | ||
| 125 | + // 打印结果 | ||
| 126 | + System.out.println(JsonUtils.toJsonPrettyString(call)); | ||
| 127 | + } | ||
| 128 | + | ||
| 69 | } | 129 | } |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/XingHuoChatModelTests.java
| @@ -8,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage; | @@ -8,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage; | ||
| 8 | import org.springframework.ai.chat.messages.UserMessage; | 8 | import org.springframework.ai.chat.messages.UserMessage; |
| 9 | import org.springframework.ai.chat.model.ChatResponse; | 9 | import org.springframework.ai.chat.model.ChatResponse; |
| 10 | import org.springframework.ai.chat.prompt.Prompt; | 10 | import org.springframework.ai.chat.prompt.Prompt; |
| 11 | -import org.springframework.ai.openai.OpenAiChatModel; | ||
| 12 | -import org.springframework.ai.openai.OpenAiChatOptions; | ||
| 13 | -import org.springframework.ai.openai.api.OpenAiApi; | 11 | +import org.springframework.ai.deepseek.DeepSeekChatModel; |
| 12 | +import org.springframework.ai.deepseek.DeepSeekChatOptions; | ||
| 13 | +import org.springframework.ai.deepseek.api.DeepSeekApi; | ||
| 14 | import reactor.core.publisher.Flux; | 14 | import reactor.core.publisher.Flux; |
| 15 | 15 | ||
| 16 | import java.util.ArrayList; | 16 | import java.util.ArrayList; |
| @@ -23,13 +23,15 @@ import java.util.List; | @@ -23,13 +23,15 @@ import java.util.List; | ||
| 23 | */ | 23 | */ |
| 24 | public class XingHuoChatModelTests { | 24 | public class XingHuoChatModelTests { |
| 25 | 25 | ||
| 26 | - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | ||
| 27 | - .openAiApi(OpenAiApi.builder() | ||
| 28 | - .baseUrl(XingHuoChatModel.BASE_URL) | 26 | + private final DeepSeekChatModel openAiChatModel = DeepSeekChatModel.builder() |
| 27 | + .deepSeekApi(DeepSeekApi.builder() | ||
| 28 | + .baseUrl(XingHuoChatModel.BASE_URL_V2) | ||
| 29 | + .completionsPath(XingHuoChatModel.BASE_COMPLETIONS_PATH_V2) | ||
| 29 | .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey | 30 | .apiKey("75b161ed2aef4719b275d6e7f2a4d4cd:YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz") // appKey:secretKey |
| 30 | .build()) | 31 | .build()) |
| 31 | - .defaultOptions(OpenAiChatOptions.builder() | ||
| 32 | - .model("generalv3.5") // 模型 | 32 | + .defaultOptions(DeepSeekChatOptions.builder() |
| 33 | +// .model("generalv3.5") // 模型 | ||
| 34 | + .model("x1") // 模型 | ||
| 33 | .temperature(0.7) | 35 | .temperature(0.7) |
| 34 | .build()) | 36 | .build()) |
| 35 | .build(); | 37 | .build(); |
| @@ -64,4 +66,23 @@ public class XingHuoChatModelTests { | @@ -64,4 +66,23 @@ public class XingHuoChatModelTests { | ||
| 64 | flux.doOnNext(System.out::println).then().block(); | 66 | flux.doOnNext(System.out::println).then().block(); |
| 65 | } | 67 | } |
| 66 | 68 | ||
| 69 | + @Test | ||
| 70 | + @Disabled | ||
| 71 | + public void testStream_thinking() { | ||
| 72 | + // 准备参数 | ||
| 73 | + List<Message> messages = new ArrayList<>(); | ||
| 74 | + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); | ||
| 75 | + DeepSeekChatOptions options = DeepSeekChatOptions.builder() | ||
| 76 | + .model("x1") | ||
| 77 | + .build(); | ||
| 78 | + | ||
| 79 | + // 调用 | ||
| 80 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); | ||
| 81 | + // 打印结果 | ||
| 82 | + flux.doOnNext(response -> { | ||
| 83 | +// System.out.println(response); | ||
| 84 | + System.out.println(response.getResult().getOutput()); | ||
| 85 | + }).then().block(); | ||
| 86 | + } | ||
| 87 | + | ||
| 67 | } | 88 | } |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/ZhiPuAiChatModelTests.java
| @@ -23,7 +23,7 @@ import java.util.List; | @@ -23,7 +23,7 @@ import java.util.List; | ||
| 23 | public class ZhiPuAiChatModelTests { | 23 | public class ZhiPuAiChatModelTests { |
| 24 | 24 | ||
| 25 | private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel( | 25 | private final ZhiPuAiChatModel chatModel = new ZhiPuAiChatModel( |
| 26 | - new ZhiPuAiApi("32f84543e54eee31f8d56b2bd6020573.3vh9idLJZ2ZhxDEs"), // 密钥 | 26 | + new ZhiPuAiApi("2f35fb6ca4ea41fab898729b7fac086c.6ESSfPcCkxaKEUlR"), // 密钥 |
| 27 | ZhiPuAiChatOptions.builder() | 27 | ZhiPuAiChatOptions.builder() |
| 28 | .model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型 | 28 | .model(ZhiPuAiApi.ChatModel.GLM_4.getName()) // 模型 |
| 29 | .build() | 29 | .build() |
| @@ -61,4 +61,24 @@ public class ZhiPuAiChatModelTests { | @@ -61,4 +61,24 @@ public class ZhiPuAiChatModelTests { | ||
| 61 | }).then().block(); | 61 | }).then().block(); |
| 62 | } | 62 | } |
| 63 | 63 | ||
| 64 | + // TODO @芋艿:暂时没解析 reasoning_content 结果,需要等官方修复 | ||
| 65 | + @Test | ||
| 66 | + @Disabled | ||
| 67 | + public void testStream_thinking() { | ||
| 68 | + // 准备参数 | ||
| 69 | + List<Message> messages = new ArrayList<>(); | ||
| 70 | + messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); | ||
| 71 | + ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder() | ||
| 72 | + .model("GLM-4.5") | ||
| 73 | + .build(); | ||
| 74 | + | ||
| 75 | + // 调用 | ||
| 76 | + Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages, options)); | ||
| 77 | + // 打印结果 | ||
| 78 | + flux.doOnNext(response -> { | ||
| 79 | +// System.out.println(response); | ||
| 80 | + System.out.println(response.getResult().getOutput()); | ||
| 81 | + }).then().block(); | ||
| 82 | + } | ||
| 83 | + | ||
| 64 | } | 84 | } |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/TongYiImagesModelTest.java
| @@ -16,8 +16,11 @@ import org.springframework.ai.image.ImageResponse; | @@ -16,8 +16,11 @@ import org.springframework.ai.image.ImageResponse; | ||
| 16 | */ | 16 | */ |
| 17 | public class TongYiImagesModelTest { | 17 | public class TongYiImagesModelTest { |
| 18 | 18 | ||
| 19 | - private final DashScopeImageModel imageModel = new DashScopeImageModel( | ||
| 20 | - new DashScopeImageApi("sk-7d903764249848cfa912733146da12d1")); | 19 | + private final DashScopeImageModel imageModel = DashScopeImageModel.builder() |
| 20 | + .dashScopeApi(DashScopeImageApi.builder() | ||
| 21 | + .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0") | ||
| 22 | + .build()) | ||
| 23 | + .build(); | ||
| 21 | 24 | ||
| 22 | @Test | 25 | @Test |
| 23 | @Disabled | 26 | @Disabled |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/mcp/DouBaoMcpTests.java
| @@ -36,44 +36,47 @@ public class DouBaoMcpTests { | @@ -36,44 +36,47 @@ public class DouBaoMcpTests { | ||
| 36 | 36 | ||
| 37 | @Test | 37 | @Test |
| 38 | public void testMcpGetUserInfo() { | 38 | public void testMcpGetUserInfo() { |
| 39 | - | ||
| 40 | // 打印结果 | 39 | // 打印结果 |
| 41 | System.out.println(chatClient.prompt() | 40 | System.out.println(chatClient.prompt() |
| 42 | .user("目前有哪些工具可以使用") | 41 | .user("目前有哪些工具可以使用") |
| 43 | .call() | 42 | .call() |
| 44 | .content()); | 43 | .content()); |
| 45 | System.out.println("===================================="); | 44 | System.out.println("===================================="); |
| 45 | + | ||
| 46 | // 打印结果 | 46 | // 打印结果 |
| 47 | System.out.println(chatClient.prompt() | 47 | System.out.println(chatClient.prompt() |
| 48 | .user("小新的年龄是多少") | 48 | .user("小新的年龄是多少") |
| 49 | .call() | 49 | .call() |
| 50 | .content()); | 50 | .content()); |
| 51 | System.out.println("===================================="); | 51 | System.out.println("===================================="); |
| 52 | + | ||
| 52 | // 打印结果 | 53 | // 打印结果 |
| 53 | System.out.println(chatClient.prompt() | 54 | System.out.println(chatClient.prompt() |
| 54 | .user("获取小新的基本信息") | 55 | .user("获取小新的基本信息") |
| 55 | .call() | 56 | .call() |
| 56 | .content()); | 57 | .content()); |
| 57 | System.out.println("===================================="); | 58 | System.out.println("===================================="); |
| 59 | + | ||
| 58 | // 打印结果 | 60 | // 打印结果 |
| 59 | System.out.println(chatClient.prompt() | 61 | System.out.println(chatClient.prompt() |
| 60 | .user("小新是什么职业的") | 62 | .user("小新是什么职业的") |
| 61 | .call() | 63 | .call() |
| 62 | .content()); | 64 | .content()); |
| 63 | System.out.println("===================================="); | 65 | System.out.println("===================================="); |
| 66 | + | ||
| 64 | // 打印结果 | 67 | // 打印结果 |
| 65 | System.out.println(chatClient.prompt() | 68 | System.out.println(chatClient.prompt() |
| 66 | .user("小新的教育背景") | 69 | .user("小新的教育背景") |
| 67 | .call() | 70 | .call() |
| 68 | .content()); | 71 | .content()); |
| 69 | System.out.println("===================================="); | 72 | System.out.println("===================================="); |
| 73 | + | ||
| 70 | // 打印结果 | 74 | // 打印结果 |
| 71 | System.out.println(chatClient.prompt() | 75 | System.out.println(chatClient.prompt() |
| 72 | .user("小新的兴趣爱好是什么") | 76 | .user("小新的兴趣爱好是什么") |
| 73 | .call() | 77 | .call() |
| 74 | .content()); | 78 | .content()); |
| 75 | System.out.println("===================================="); | 79 | System.out.println("===================================="); |
| 76 | - | ||
| 77 | } | 80 | } |
| 78 | 81 | ||
| 79 | 82 |
yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/websearch/AiBoChaWebSearchClientTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.framework.ai.core.websearch; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 4 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchRequest; | ||
| 5 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.AiWebSearchResponse; | ||
| 6 | +import cn.iocoder.yudao.module.ai.framework.ai.core.webserch.bocha.AiBoChaWebSearchClient; | ||
| 7 | +import org.junit.jupiter.api.Test; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * {@link AiBoChaWebSearchClient} 集成测试类 | ||
| 11 | + * | ||
| 12 | + * @author 芋道源码 | ||
| 13 | + */ | ||
| 14 | +public class AiBoChaWebSearchClientTest { | ||
| 15 | + | ||
| 16 | + private final AiBoChaWebSearchClient webSearchClient = new AiBoChaWebSearchClient( | ||
| 17 | + "sk-40500e52840f4d24b956d0b1d80d9abe"); | ||
| 18 | + | ||
| 19 | + @Test | ||
| 20 | + public void testSearch() { | ||
| 21 | + AiWebSearchRequest request = new AiWebSearchRequest() | ||
| 22 | + .setQuery("阿里巴巴") | ||
| 23 | + .setCount(3); | ||
| 24 | + AiWebSearchResponse response = webSearchClient.search(request); | ||
| 25 | + System.out.println(JsonUtils.toJsonPrettyString(response)); | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | +} | ||
| 0 | \ No newline at end of file | 29 | \ No newline at end of file |
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java
| @@ -80,17 +80,17 @@ public class FileTypeUtils { | @@ -80,17 +80,17 @@ public class FileTypeUtils { | ||
| 80 | */ | 80 | */ |
| 81 | public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { | 81 | public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { |
| 82 | // 设置 header 和 contentType | 82 | // 设置 header 和 contentType |
| 83 | - String contentType = getMineType(content, filename); | ||
| 84 | - response.setContentType(contentType); | 83 | + String mineType = getMineType(content, filename); |
| 84 | + response.setContentType(mineType); | ||
| 85 | // 设置内容显示、下载文件名:https://www.cnblogs.com/wq-9/articles/12165056.html | 85 | // 设置内容显示、下载文件名:https://www.cnblogs.com/wq-9/articles/12165056.html |
| 86 | - if (StrUtil.containsIgnoreCase(contentType, "image/")) { | 86 | + if (isImage(mineType)) { |
| 87 | // 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论 | 87 | // 参见 https://github.com/YunaiV/ruoyi-vue-pro/issues/692 讨论 |
| 88 | response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename)); | 88 | response.setHeader("Content-Disposition", "inline;filename=" + HttpUtils.encodeUtf8(filename)); |
| 89 | } else { | 89 | } else { |
| 90 | response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); | 90 | response.setHeader("Content-Disposition", "attachment;filename=" + HttpUtils.encodeUtf8(filename)); |
| 91 | } | 91 | } |
| 92 | // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 | 92 | // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 |
| 93 | - if (StrUtil.containsIgnoreCase(contentType, "video")) { | 93 | + if (StrUtil.containsIgnoreCase(mineType, "video")) { |
| 94 | response.setHeader("Content-Length", String.valueOf(content.length)); | 94 | response.setHeader("Content-Length", String.valueOf(content.length)); |
| 95 | response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length); | 95 | response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length); |
| 96 | response.setHeader("Accept-Ranges", "bytes"); | 96 | response.setHeader("Accept-Ranges", "bytes"); |
| @@ -99,4 +99,14 @@ public class FileTypeUtils { | @@ -99,4 +99,14 @@ public class FileTypeUtils { | ||
| 99 | IoUtil.write(response.getOutputStream(), false, content); | 99 | IoUtil.write(response.getOutputStream(), false, content); |
| 100 | } | 100 | } |
| 101 | 101 | ||
| 102 | + /** | ||
| 103 | + * 判断是否是图片 | ||
| 104 | + * | ||
| 105 | + * @param mineType 类型 | ||
| 106 | + * @return 是否是图片 | ||
| 107 | + */ | ||
| 108 | + public static boolean isImage(String mineType) { | ||
| 109 | + return StrUtil.startWith(mineType, "image/"); | ||
| 110 | + } | ||
| 111 | + | ||
| 102 | } | 112 | } |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/justauth/core/AuthRequestFactory.java
| @@ -77,8 +77,7 @@ public class AuthRequestFactory { | @@ -77,8 +77,7 @@ public class AuthRequestFactory { | ||
| 77 | extendList = extend.getConfig() | 77 | extendList = extend.getConfig() |
| 78 | .keySet() | 78 | .keySet() |
| 79 | .stream() | 79 | .stream() |
| 80 | - .map(String::toUpperCase) | ||
| 81 | - .filter(names::contains) | 80 | + .filter(x -> names.contains(x.toUpperCase())) |
| 82 | .collect(Collectors.toList()); | 81 | .collect(Collectors.toList()); |
| 83 | } | 82 | } |
| 84 | 83 |
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/sms/core/client/impl/AliyunSmsClient.java
| @@ -113,13 +113,14 @@ public class AliyunSmsClient extends AbstractSmsClient { | @@ -113,13 +113,14 @@ public class AliyunSmsClient extends AbstractSmsClient { | ||
| 113 | } | 113 | } |
| 114 | 114 | ||
| 115 | @VisibleForTesting | 115 | @VisibleForTesting |
| 116 | + @SuppressWarnings("EnhancedSwitchMigration") | ||
| 116 | Integer convertSmsTemplateAuditStatus(Integer templateStatus) { | 117 | Integer convertSmsTemplateAuditStatus(Integer templateStatus) { |
| 117 | - return switch (templateStatus) { | ||
| 118 | - case 0 -> SmsTemplateAuditStatusEnum.CHECKING.getStatus(); | ||
| 119 | - case 1 -> SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); | ||
| 120 | - case 2 -> SmsTemplateAuditStatusEnum.FAIL.getStatus(); | ||
| 121 | - default -> throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus)); | ||
| 122 | - }; | 118 | + switch (templateStatus) { |
| 119 | + case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); | ||
| 120 | + case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); | ||
| 121 | + case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus(); | ||
| 122 | + default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus)); | ||
| 123 | + } | ||
| 123 | } | 124 | } |
| 124 | 125 | ||
| 125 | /** | 126 | /** |
| @@ -189,7 +190,7 @@ public class AliyunSmsClient extends AbstractSmsClient { | @@ -189,7 +190,7 @@ public class AliyunSmsClient extends AbstractSmsClient { | ||
| 189 | @SneakyThrows | 190 | @SneakyThrows |
| 190 | private static String percentCode(String str) { | 191 | private static String percentCode(String str) { |
| 191 | Assert.notNull(str, "str 不能为空"); | 192 | Assert.notNull(str, "str 不能为空"); |
| 192 | - return URLEncoder.encode(str, StandardCharsets.UTF_8) | 193 | + return HttpUtils.encodeUtf8(str) |
| 193 | .replace("+", "%20") // 加号 "+" 被替换为 "%20" | 194 | .replace("+", "%20") // 加号 "+" 被替换为 "%20" |
| 194 | .replace("*", "%2A") // 星号 "*" 被替换为 "%2A" | 195 | .replace("*", "%2A") // 星号 "*" 被替换为 "%2A" |
| 195 | .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~" | 196 | .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~" |
yudao-server/src/main/resources/application.yaml
| @@ -175,7 +175,8 @@ spring: | @@ -175,7 +175,8 @@ spring: | ||
| 175 | azure: # OpenAI 微软 | 175 | azure: # OpenAI 微软 |
| 176 | openai: | 176 | openai: |
| 177 | endpoint: https://eastusprejade.openai.azure.com | 177 | endpoint: https://eastusprejade.openai.azure.com |
| 178 | - api-key: xxx | 178 | + anthropic: # Anthropic Claude |
| 179 | + api-key: sk-muubv7cXeLw0Etgs743f365cD5Ea44429946Fa7e672d8942 | ||
| 179 | ollama: | 180 | ollama: |
| 180 | base-url: http://127.0.0.1:11434 | 181 | base-url: http://127.0.0.1:11434 |
| 181 | chat: | 182 | chat: |
| @@ -183,7 +184,7 @@ spring: | @@ -183,7 +184,7 @@ spring: | ||
| 183 | stabilityai: | 184 | stabilityai: |
| 184 | api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx | 185 | api-key: sk-e53UqbboF8QJCscYvzJscJxJXoFcFg4iJjl1oqgE7baJETmx |
| 185 | dashscope: # 通义千问 | 186 | dashscope: # 通义千问 |
| 186 | - api-key: sk-71800982914041848008480000000000 | 187 | + api-key: sk-47aa124781be4bfb95244cc62f6xxxx |
| 187 | minimax: # Minimax:https://www.minimaxi.com/ | 188 | minimax: # Minimax:https://www.minimaxi.com/ |
| 188 | api-key: xxxx | 189 | api-key: xxxx |
| 189 | moonshot: # 月之暗灭(KIMI) | 190 | moonshot: # 月之暗灭(KIMI) |
| @@ -193,9 +194,30 @@ spring: | @@ -193,9 +194,30 @@ spring: | ||
| 193 | chat: | 194 | chat: |
| 194 | options: | 195 | options: |
| 195 | model: deepseek-chat | 196 | model: deepseek-chat |
| 197 | + model: | ||
| 198 | + rerank: false # 是否开启“通义千问”的 Rerank 模型,填写 dashscope 开启 | ||
| 199 | + mcp: | ||
| 200 | + server: | ||
| 201 | + enabled: true | ||
| 202 | + name: yudao-mcp-server | ||
| 203 | + version: 1.0.0 | ||
| 204 | + instructions: 一个 MCP 示例服务 | ||
| 205 | + sse-endpoint: /sse | ||
| 206 | + client: | ||
| 207 | + enabled: true | ||
| 208 | + name: mcp | ||
| 209 | + sse: | ||
| 210 | + connections: | ||
| 211 | + filesystem: | ||
| 212 | + url: http://127.0.0.1:8089 | ||
| 213 | + sse-endpoint: /sse | ||
| 196 | 214 | ||
| 197 | yudao: | 215 | yudao: |
| 198 | ai: | 216 | ai: |
| 217 | + gemini: # 谷歌 Gemini | ||
| 218 | + enable: true | ||
| 219 | + api-key: AIzaSyAVoBxgoFvvte820vEQMma2LKBnC98bqMQ | ||
| 220 | + model: gemini-2.5-flash | ||
| 199 | doubao: # 字节豆包 | 221 | doubao: # 字节豆包 |
| 200 | enable: true | 222 | enable: true |
| 201 | api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 | 223 | api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 |
| @@ -212,7 +234,7 @@ yudao: | @@ -212,7 +234,7 @@ yudao: | ||
| 212 | enable: true | 234 | enable: true |
| 213 | appKey: 75b161ed2aef4719b275d6e7f2a4d4cd | 235 | appKey: 75b161ed2aef4719b275d6e7f2a4d4cd |
| 214 | secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz | 236 | secretKey: YWYxYWI2MTA4ODI2NGZlYTQyNjAzZTcz |
| 215 | - model: generalv3.5 | 237 | + model: x1 |
| 216 | baichuan: # 百川智能 | 238 | baichuan: # 百川智能 |
| 217 | enable: true | 239 | enable: true |
| 218 | api-key: sk-abc | 240 | api-key: sk-abc |
| @@ -227,6 +249,9 @@ yudao: | @@ -227,6 +249,9 @@ yudao: | ||
| 227 | enable: true | 249 | enable: true |
| 228 | # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app | 250 | # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app |
| 229 | base-url: http://127.0.0.1:3001 | 251 | base-url: http://127.0.0.1:3001 |
| 252 | + web-search: | ||
| 253 | + enable: true | ||
| 254 | + api-key: sk-40500e52840f4d24b956d0b1d80d9abe | ||
| 230 | 255 | ||
| 231 | --- #################### 芋道相关配置 #################### | 256 | --- #################### 芋道相关配置 #################### |
| 232 | 257 |