Commit ca34d1650e2e3d9d96370455eb6af3d1c212f48a
1 parent
6f795186
feat:【ai 大模型】联网搜索 AiWebSearchClient 封装
Showing
9 changed files
with
360 additions
and
43 deletions
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java
| ... | ... | @@ -13,6 +13,8 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo |
| 13 | 13 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowChatModel; |
| 14 | 14 | import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; |
| 15 | 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; | |
| 16 | 18 | import lombok.extern.slf4j.Slf4j; |
| 17 | 19 | import org.springframework.ai.deepseek.DeepSeekChatModel; |
| 18 | 20 | import org.springframework.ai.deepseek.DeepSeekChatOptions; |
| ... | ... | @@ -58,11 +60,11 @@ public class AiAutoConfiguration { |
| 58 | 60 | @Bean |
| 59 | 61 | @ConditionalOnProperty(value = "yudao.ai.gemini.enable", havingValue = "true") |
| 60 | 62 | public GeminiChatModel geminiChatModel(YudaoAiProperties yudaoAiProperties) { |
| 61 | - YudaoAiProperties.GeminiProperties properties = yudaoAiProperties.getGemini(); | |
| 63 | + YudaoAiProperties.Gemini properties = yudaoAiProperties.getGemini(); | |
| 62 | 64 | return buildGeminiChatClient(properties); |
| 63 | 65 | } |
| 64 | 66 | |
| 65 | - public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.GeminiProperties properties) { | |
| 67 | + public GeminiChatModel buildGeminiChatClient(YudaoAiProperties.Gemini properties) { | |
| 66 | 68 | if (StrUtil.isEmpty(properties.getModel())) { |
| 67 | 69 | properties.setModel(GeminiChatModel.MODEL_DEFAULT); |
| 68 | 70 | } |
| ... | ... | @@ -86,11 +88,11 @@ public class AiAutoConfiguration { |
| 86 | 88 | @Bean |
| 87 | 89 | @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") |
| 88 | 90 | public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { |
| 89 | - YudaoAiProperties.DouBaoProperties properties = yudaoAiProperties.getDoubao(); | |
| 91 | + YudaoAiProperties.DouBao properties = yudaoAiProperties.getDoubao(); | |
| 90 | 92 | return buildDouBaoChatClient(properties); |
| 91 | 93 | } |
| 92 | 94 | |
| 93 | - public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBaoProperties properties) { | |
| 95 | + public DouBaoChatModel buildDouBaoChatClient(YudaoAiProperties.DouBao properties) { | |
| 94 | 96 | if (StrUtil.isEmpty(properties.getModel())) { |
| 95 | 97 | properties.setModel(DouBaoChatModel.MODEL_DEFAULT); |
| 96 | 98 | } |
| ... | ... | @@ -114,11 +116,11 @@ public class AiAutoConfiguration { |
| 114 | 116 | @Bean |
| 115 | 117 | @ConditionalOnProperty(value = "yudao.ai.siliconflow.enable", havingValue = "true") |
| 116 | 118 | public SiliconFlowChatModel siliconFlowChatClient(YudaoAiProperties yudaoAiProperties) { |
| 117 | - YudaoAiProperties.SiliconFlowProperties properties = yudaoAiProperties.getSiliconflow(); | |
| 119 | + YudaoAiProperties.SiliconFlow properties = yudaoAiProperties.getSiliconflow(); | |
| 118 | 120 | return buildSiliconFlowChatClient(properties); |
| 119 | 121 | } |
| 120 | 122 | |
| 121 | - public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlowProperties properties) { | |
| 123 | + public SiliconFlowChatModel buildSiliconFlowChatClient(YudaoAiProperties.SiliconFlow properties) { | |
| 122 | 124 | if (StrUtil.isEmpty(properties.getModel())) { |
| 123 | 125 | properties.setModel(SiliconFlowApiConstants.MODEL_DEFAULT); |
| 124 | 126 | } |
| ... | ... | @@ -141,11 +143,11 @@ public class AiAutoConfiguration { |
| 141 | 143 | @Bean |
| 142 | 144 | @ConditionalOnProperty(value = "yudao.ai.hunyuan.enable", havingValue = "true") |
| 143 | 145 | public HunYuanChatModel hunYuanChatClient(YudaoAiProperties yudaoAiProperties) { |
| 144 | - YudaoAiProperties.HunYuanProperties properties = yudaoAiProperties.getHunyuan(); | |
| 146 | + YudaoAiProperties.HunYuan properties = yudaoAiProperties.getHunyuan(); | |
| 145 | 147 | return buildHunYuanChatClient(properties); |
| 146 | 148 | } |
| 147 | 149 | |
| 148 | - public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuanProperties properties) { | |
| 150 | + public HunYuanChatModel buildHunYuanChatClient(YudaoAiProperties.HunYuan properties) { | |
| 149 | 151 | if (StrUtil.isEmpty(properties.getModel())) { |
| 150 | 152 | properties.setModel(HunYuanChatModel.MODEL_DEFAULT); |
| 151 | 153 | } |
| ... | ... | @@ -176,11 +178,11 @@ public class AiAutoConfiguration { |
| 176 | 178 | @Bean |
| 177 | 179 | @ConditionalOnProperty(value = "yudao.ai.xinghuo.enable", havingValue = "true") |
| 178 | 180 | public XingHuoChatModel xingHuoChatClient(YudaoAiProperties yudaoAiProperties) { |
| 179 | - YudaoAiProperties.XingHuoProperties properties = yudaoAiProperties.getXinghuo(); | |
| 181 | + YudaoAiProperties.XingHuo properties = yudaoAiProperties.getXinghuo(); | |
| 180 | 182 | return buildXingHuoChatClient(properties); |
| 181 | 183 | } |
| 182 | 184 | |
| 183 | - public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuoProperties properties) { | |
| 185 | + public XingHuoChatModel buildXingHuoChatClient(YudaoAiProperties.XingHuo properties) { | |
| 184 | 186 | if (StrUtil.isEmpty(properties.getModel())) { |
| 185 | 187 | properties.setModel(XingHuoChatModel.MODEL_DEFAULT); |
| 186 | 188 | } |
| ... | ... | @@ -208,11 +210,11 @@ public class AiAutoConfiguration { |
| 208 | 210 | @Bean |
| 209 | 211 | @ConditionalOnProperty(value = "yudao.ai.baichuan.enable", havingValue = "true") |
| 210 | 212 | public BaiChuanChatModel baiChuanChatClient(YudaoAiProperties yudaoAiProperties) { |
| 211 | - YudaoAiProperties.BaiChuanProperties properties = yudaoAiProperties.getBaichuan(); | |
| 213 | + YudaoAiProperties.BaiChuan properties = yudaoAiProperties.getBaichuan(); | |
| 212 | 214 | return buildBaiChuanChatClient(properties); |
| 213 | 215 | } |
| 214 | 216 | |
| 215 | - public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuanProperties properties) { | |
| 217 | + public BaiChuanChatModel buildBaiChuanChatClient(YudaoAiProperties.BaiChuan properties) { | |
| 216 | 218 | if (StrUtil.isEmpty(properties.getModel())) { |
| 217 | 219 | properties.setModel(BaiChuanChatModel.MODEL_DEFAULT); |
| 218 | 220 | } |
| ... | ... | @@ -235,7 +237,7 @@ public class AiAutoConfiguration { |
| 235 | 237 | @Bean |
| 236 | 238 | @ConditionalOnProperty(value = "yudao.ai.midjourney.enable", havingValue = "true") |
| 237 | 239 | public MidjourneyApi midjourneyApi(YudaoAiProperties yudaoAiProperties) { |
| 238 | - YudaoAiProperties.MidjourneyProperties config = yudaoAiProperties.getMidjourney(); | |
| 240 | + YudaoAiProperties.Midjourney config = yudaoAiProperties.getMidjourney(); | |
| 239 | 241 | return new MidjourneyApi(config.getBaseUrl(), config.getApiKey(), config.getNotifyUrl()); |
| 240 | 242 | } |
| 241 | 243 | |
| ... | ... | @@ -261,4 +263,12 @@ public class AiAutoConfiguration { |
| 261 | 263 | return SpringUtil.getBean(ToolCallingManager.class); |
| 262 | 264 | } |
| 263 | 265 | |
| 266 | + // ========== Web Search 相关 ========== | |
| 267 | + | |
| 268 | + @Bean | |
| 269 | + @ConditionalOnProperty(value = "yudao.ai.web-search.enable", havingValue = "true") | |
| 270 | + public AiWebSearchClient webSearchClient(YudaoAiProperties yudaoAiProperties) { | |
| 271 | + return new AiBoChaWebSearchClient(yudaoAiProperties.getWebSearch().getApiKey()); | |
| 272 | + } | |
| 273 | + | |
| 264 | 274 | } |
| 265 | 275 | \ No newline at end of file | ... | ... |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java
| ... | ... | @@ -16,51 +16,51 @@ public class YudaoAiProperties { |
| 16 | 16 | /** |
| 17 | 17 | * 谷歌 Gemini |
| 18 | 18 | */ |
| 19 | - private GeminiProperties gemini; | |
| 19 | + private Gemini gemini; | |
| 20 | 20 | |
| 21 | 21 | /** |
| 22 | 22 | * 字节豆包 |
| 23 | 23 | */ |
| 24 | - @SuppressWarnings("SpellCheckingInspection") | |
| 25 | - private DouBaoProperties doubao; | |
| 24 | + private DouBao doubao; | |
| 26 | 25 | |
| 27 | 26 | /** |
| 28 | 27 | * 腾讯混元 |
| 29 | 28 | */ |
| 30 | - @SuppressWarnings("SpellCheckingInspection") | |
| 31 | - private HunYuanProperties hunyuan; | |
| 29 | + private HunYuan hunyuan; | |
| 32 | 30 | |
| 33 | 31 | /** |
| 34 | 32 | * 硅基流动 |
| 35 | 33 | */ |
| 36 | - @SuppressWarnings("SpellCheckingInspection") | |
| 37 | - private SiliconFlowProperties siliconflow; | |
| 34 | + private SiliconFlow siliconflow; | |
| 38 | 35 | |
| 39 | 36 | /** |
| 40 | 37 | * 讯飞星火 |
| 41 | 38 | */ |
| 42 | - @SuppressWarnings("SpellCheckingInspection") | |
| 43 | - private XingHuoProperties xinghuo; | |
| 39 | + private XingHuo xinghuo; | |
| 44 | 40 | |
| 45 | 41 | /** |
| 46 | 42 | * 百川 |
| 47 | 43 | */ |
| 48 | - @SuppressWarnings("SpellCheckingInspection") | |
| 49 | - private BaiChuanProperties baichuan; | |
| 44 | + private BaiChuan baichuan; | |
| 50 | 45 | |
| 51 | 46 | /** |
| 52 | 47 | * Midjourney 绘图 |
| 53 | 48 | */ |
| 54 | - private MidjourneyProperties midjourney; | |
| 49 | + private Midjourney midjourney; | |
| 55 | 50 | |
| 56 | 51 | /** |
| 57 | 52 | * Suno 音乐 |
| 58 | 53 | */ |
| 59 | 54 | @SuppressWarnings("SpellCheckingInspection") |
| 60 | - private SunoProperties suno; | |
| 55 | + private Suno suno; | |
| 56 | + | |
| 57 | + /** | |
| 58 | + * 网络搜索 | |
| 59 | + */ | |
| 60 | + private WebSearch webSearch; | |
| 61 | 61 | |
| 62 | 62 | @Data |
| 63 | - public static class GeminiProperties { | |
| 63 | + public static class Gemini { | |
| 64 | 64 | |
| 65 | 65 | private String enable; |
| 66 | 66 | private String apiKey; |
| ... | ... | @@ -73,7 +73,7 @@ public class YudaoAiProperties { |
| 73 | 73 | } |
| 74 | 74 | |
| 75 | 75 | @Data |
| 76 | - public static class DouBaoProperties { | |
| 76 | + public static class DouBao { | |
| 77 | 77 | |
| 78 | 78 | private String enable; |
| 79 | 79 | private String apiKey; |
| ... | ... | @@ -86,7 +86,7 @@ public class YudaoAiProperties { |
| 86 | 86 | } |
| 87 | 87 | |
| 88 | 88 | @Data |
| 89 | - public static class HunYuanProperties { | |
| 89 | + public static class HunYuan { | |
| 90 | 90 | |
| 91 | 91 | private String enable; |
| 92 | 92 | private String baseUrl; |
| ... | ... | @@ -100,7 +100,7 @@ public class YudaoAiProperties { |
| 100 | 100 | } |
| 101 | 101 | |
| 102 | 102 | @Data |
| 103 | - public static class SiliconFlowProperties { | |
| 103 | + public static class SiliconFlow { | |
| 104 | 104 | |
| 105 | 105 | private String enable; |
| 106 | 106 | private String apiKey; |
| ... | ... | @@ -113,7 +113,7 @@ public class YudaoAiProperties { |
| 113 | 113 | } |
| 114 | 114 | |
| 115 | 115 | @Data |
| 116 | - public static class XingHuoProperties { | |
| 116 | + public static class XingHuo { | |
| 117 | 117 | |
| 118 | 118 | private String enable; |
| 119 | 119 | private String appId; |
| ... | ... | @@ -128,7 +128,7 @@ public class YudaoAiProperties { |
| 128 | 128 | } |
| 129 | 129 | |
| 130 | 130 | @Data |
| 131 | - public static class BaiChuanProperties { | |
| 131 | + public static class BaiChuan { | |
| 132 | 132 | |
| 133 | 133 | private String enable; |
| 134 | 134 | private String apiKey; |
| ... | ... | @@ -141,7 +141,7 @@ public class YudaoAiProperties { |
| 141 | 141 | } |
| 142 | 142 | |
| 143 | 143 | @Data |
| 144 | - public static class MidjourneyProperties { | |
| 144 | + public static class Midjourney { | |
| 145 | 145 | |
| 146 | 146 | private String enable; |
| 147 | 147 | private String baseUrl; |
| ... | ... | @@ -152,12 +152,21 @@ public class YudaoAiProperties { |
| 152 | 152 | } |
| 153 | 153 | |
| 154 | 154 | @Data |
| 155 | - public static class SunoProperties { | |
| 155 | + public static class Suno { | |
| 156 | 156 | |
| 157 | - private boolean enable = false; | |
| 157 | + private boolean enable; | |
| 158 | 158 | |
| 159 | 159 | private String baseUrl; |
| 160 | 160 | |
| 161 | 161 | } |
| 162 | 162 | |
| 163 | + @Data | |
| 164 | + public static class WebSearch { | |
| 165 | + | |
| 166 | + private boolean enable; | |
| 167 | + | |
| 168 | + private String apiKey; | |
| 169 | + | |
| 170 | + } | |
| 171 | + | |
| 163 | 172 | } | ... | ... |
yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/AiModelFactoryImpl.java
| ... | ... | @@ -272,7 +272,7 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 272 | 272 | String cacheKey = buildClientCacheKey(MidjourneyApi.class, AiPlatformEnum.MIDJOURNEY.getPlatform(), apiKey, |
| 273 | 273 | url); |
| 274 | 274 | return Singleton.get(cacheKey, (Func0<MidjourneyApi>) () -> { |
| 275 | - YudaoAiProperties.MidjourneyProperties properties = SpringUtil.getBean(YudaoAiProperties.class) | |
| 275 | + YudaoAiProperties.Midjourney properties = SpringUtil.getBean(YudaoAiProperties.class) | |
| 276 | 276 | .getMidjourney(); |
| 277 | 277 | return new MidjourneyApi(url, apiKey, properties.getNotifyUrl()); |
| 278 | 278 | }); |
| ... | ... | @@ -409,7 +409,7 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 409 | 409 | * 可参考 {@link AiAutoConfiguration#douBaoChatClient(YudaoAiProperties)} |
| 410 | 410 | */ |
| 411 | 411 | private ChatModel buildDouBaoChatModel(String apiKey) { |
| 412 | - YudaoAiProperties.DouBaoProperties properties = new YudaoAiProperties.DouBaoProperties() | |
| 412 | + YudaoAiProperties.DouBao properties = new YudaoAiProperties.DouBao() | |
| 413 | 413 | .setApiKey(apiKey); |
| 414 | 414 | return new AiAutoConfiguration().buildDouBaoChatClient(properties); |
| 415 | 415 | } |
| ... | ... | @@ -418,7 +418,7 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 418 | 418 | * 可参考 {@link AiAutoConfiguration#hunYuanChatClient(YudaoAiProperties)} |
| 419 | 419 | */ |
| 420 | 420 | private ChatModel buildHunYuanChatModel(String apiKey, String url) { |
| 421 | - YudaoAiProperties.HunYuanProperties properties = new YudaoAiProperties.HunYuanProperties() | |
| 421 | + YudaoAiProperties.HunYuan properties = new YudaoAiProperties.HunYuan() | |
| 422 | 422 | .setBaseUrl(url).setApiKey(apiKey); |
| 423 | 423 | return new AiAutoConfiguration().buildHunYuanChatClient(properties); |
| 424 | 424 | } |
| ... | ... | @@ -427,7 +427,7 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 427 | 427 | * 可参考 {@link AiAutoConfiguration#siliconFlowChatClient(YudaoAiProperties)} |
| 428 | 428 | */ |
| 429 | 429 | private ChatModel buildSiliconFlowChatModel(String apiKey) { |
| 430 | - YudaoAiProperties.SiliconFlowProperties properties = new YudaoAiProperties.SiliconFlowProperties() | |
| 430 | + YudaoAiProperties.SiliconFlow properties = new YudaoAiProperties.SiliconFlow() | |
| 431 | 431 | .setApiKey(apiKey); |
| 432 | 432 | return new AiAutoConfiguration().buildSiliconFlowChatClient(properties); |
| 433 | 433 | } |
| ... | ... | @@ -485,7 +485,7 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 485 | 485 | private static XingHuoChatModel buildXingHuoChatModel(String key) { |
| 486 | 486 | List<String> keys = StrUtil.split(key, '|'); |
| 487 | 487 | Assert.equals(keys.size(), 2, "XingHuoChatClient 的密钥需要 (appKey|secretKey) 格式"); |
| 488 | - YudaoAiProperties.XingHuoProperties properties = new YudaoAiProperties.XingHuoProperties() | |
| 488 | + YudaoAiProperties.XingHuo properties = new YudaoAiProperties.XingHuo() | |
| 489 | 489 | .setAppKey(keys.get(0)).setSecretKey(keys.get(1)); |
| 490 | 490 | return new AiAutoConfiguration().buildXingHuoChatClient(properties); |
| 491 | 491 | } |
| ... | ... | @@ -494,7 +494,7 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 494 | 494 | * 可参考 {@link AiAutoConfiguration#baiChuanChatClient(YudaoAiProperties)} |
| 495 | 495 | */ |
| 496 | 496 | private BaiChuanChatModel buildBaiChuanChatModel(String apiKey) { |
| 497 | - YudaoAiProperties.BaiChuanProperties properties = new YudaoAiProperties.BaiChuanProperties() | |
| 497 | + YudaoAiProperties.BaiChuan properties = new YudaoAiProperties.BaiChuan() | |
| 498 | 498 | .setApiKey(apiKey); |
| 499 | 499 | return new AiAutoConfiguration().buildBaiChuanChatClient(properties); |
| 500 | 500 | } |
| ... | ... | @@ -540,10 +540,10 @@ public class AiModelFactoryImpl implements AiModelFactory { |
| 540 | 540 | } |
| 541 | 541 | |
| 542 | 542 | /** |
| 543 | - * 可参考 {@link AiAutoConfiguration#buildGeminiChatClient(YudaoAiProperties.GeminiProperties)} | |
| 543 | + * 可参考 {@link AiAutoConfiguration#buildGeminiChatClient(YudaoAiProperties.Gemini)} | |
| 544 | 544 | */ |
| 545 | 545 | private static GeminiChatModel buildGeminiChatModel(String apiKey) { |
| 546 | - YudaoAiProperties.GeminiProperties properties = SpringUtil.getBean(YudaoAiProperties.class) | |
| 546 | + YudaoAiProperties.Gemini properties = SpringUtil.getBean(YudaoAiProperties.class) | |
| 547 | 547 | .getGemini().setApiKey(apiKey); |
| 548 | 548 | return new AiAutoConfiguration().buildGeminiChatClient(properties); |
| 549 | 549 | } | ... | ... |
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 | 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/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 | 29 | \ No newline at end of file | ... | ... |
yudao-server/src/main/resources/application.yaml
| ... | ... | @@ -234,6 +234,9 @@ yudao: |
| 234 | 234 | enable: true |
| 235 | 235 | # base-url: https://suno-55ishh05u-status2xxs-projects.vercel.app |
| 236 | 236 | base-url: http://127.0.0.1:3001 |
| 237 | + web-search: | |
| 238 | + enable: true | |
| 239 | + api-key: sk-40500e52840f4d24b956d0b1d80d9abe | |
| 237 | 240 | |
| 238 | 241 | --- #################### 芋道相关配置 #################### |
| 239 | 242 | ... | ... |