Commit 25a0fe908a6a9a272fb76b5ef43058b421354c98
1 parent
f7ab30c5
【功能新增】AI:新增 function call 示例。会继续完善!
Showing
6 changed files
with
226 additions
and
12 deletions
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirTool.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.service.tool; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.date.LocalDateTimeUtil; | ||
| 4 | +import cn.hutool.core.io.FileUtil; | ||
| 5 | +import cn.hutool.core.util.ArrayUtil; | ||
| 6 | +import cn.hutool.core.util.StrUtil; | ||
| 7 | +import lombok.AllArgsConstructor; | ||
| 8 | +import lombok.Data; | ||
| 9 | +import lombok.NoArgsConstructor; | ||
| 10 | +import org.springframework.ai.tool.annotation.Tool; | ||
| 11 | +import org.springframework.ai.tool.annotation.ToolParam; | ||
| 12 | +import org.springframework.stereotype.Component; | ||
| 13 | + | ||
| 14 | +import java.io.File; | ||
| 15 | +import java.nio.file.Path; | ||
| 16 | +import java.nio.file.Paths; | ||
| 17 | +import java.util.Arrays; | ||
| 18 | +import java.util.Collections; | ||
| 19 | +import java.util.List; | ||
| 20 | + | ||
| 21 | +import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN; | ||
| 22 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | ||
| 23 | + | ||
| 24 | +/** | ||
| 25 | + * 目录内容列表工具:列出指定目录的内容 | ||
| 26 | + * | ||
| 27 | + * @author 芋道源码 | ||
| 28 | + */ | ||
| 29 | +@Component | ||
| 30 | +public class ListDirTool { | ||
| 31 | + | ||
| 32 | + /** | ||
| 33 | + * 列出指定目录的内容 | ||
| 34 | + * | ||
| 35 | + * @param relativePath 要列出内容的目录路径,相对于工作区根目录 | ||
| 36 | + * @return 目录内容列表 | ||
| 37 | + */ | ||
| 38 | + @Tool(name = "listDir", description = "列出指定目录的内容") | ||
| 39 | + public Response listDir(@ToolParam(description = "要列出内容的目录路径,相对于工作区根目录") String relativePath) { | ||
| 40 | + // 校验目录存在 | ||
| 41 | + String path = StrUtil.blankToDefault(relativePath, "."); | ||
| 42 | + Path dirPath = Paths.get(path); | ||
| 43 | + if (!FileUtil.exist(dirPath.toString()) || !FileUtil.isDirectory(dirPath.toString())) { | ||
| 44 | + return new Response(Collections.emptyList()); | ||
| 45 | + } | ||
| 46 | + // 列出目录内容 | ||
| 47 | + File[] files = dirPath.toFile().listFiles(); | ||
| 48 | + if (ArrayUtil.isEmpty(files)) { | ||
| 49 | + return new Response(Collections.emptyList()); | ||
| 50 | + } | ||
| 51 | + return new Response(convertList(Arrays.asList(files), file -> new Response.File() | ||
| 52 | + .setDirectory(file.isDirectory()).setName(file.getName()) | ||
| 53 | + .setSize(file.isFile() ? FileUtil.readableFileSize(file.length()) : null) | ||
| 54 | + .setLastModified( | ||
| 55 | + LocalDateTimeUtil.format(LocalDateTimeUtil.of(file.lastModified()), NORM_DATETIME_PATTERN)))); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + @Data | ||
| 59 | + @AllArgsConstructor | ||
| 60 | + @NoArgsConstructor | ||
| 61 | + public static class Response { | ||
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 目录内容列表 | ||
| 65 | + */ | ||
| 66 | + private List<File> files; | ||
| 67 | + | ||
| 68 | + @Data | ||
| 69 | + public static class File { | ||
| 70 | + | ||
| 71 | + /** | ||
| 72 | + * 是否为目录 | ||
| 73 | + */ | ||
| 74 | + private Boolean directory; | ||
| 75 | + | ||
| 76 | + /** | ||
| 77 | + * 名称 | ||
| 78 | + */ | ||
| 79 | + private String name; | ||
| 80 | + | ||
| 81 | + /** | ||
| 82 | + * 大小,仅对文件有效 | ||
| 83 | + */ | ||
| 84 | + private String size; | ||
| 85 | + | ||
| 86 | + /** | ||
| 87 | + * 最后修改时间 | ||
| 88 | + */ | ||
| 89 | + private String lastModified; | ||
| 90 | + | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | +} | ||
| 0 | \ No newline at end of file | 96 | \ No newline at end of file |
yudao-module-ai/yudao-module-ai-biz/src/main/java/cn/iocoder/yudao/module/ai/service/tool/ListDirToolB.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.ai.service.tool; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.date.LocalDateTimeUtil; | ||
| 4 | +import cn.hutool.core.io.FileUtil; | ||
| 5 | +import cn.hutool.core.util.ArrayUtil; | ||
| 6 | +import cn.hutool.core.util.StrUtil; | ||
| 7 | +import com.fasterxml.jackson.annotation.JsonClassDescription; | ||
| 8 | +import com.fasterxml.jackson.annotation.JsonPropertyDescription; | ||
| 9 | +import lombok.AllArgsConstructor; | ||
| 10 | +import lombok.Data; | ||
| 11 | +import lombok.NoArgsConstructor; | ||
| 12 | +import org.springframework.stereotype.Component; | ||
| 13 | + | ||
| 14 | +import java.io.File; | ||
| 15 | +import java.nio.file.Path; | ||
| 16 | +import java.nio.file.Paths; | ||
| 17 | +import java.util.Arrays; | ||
| 18 | +import java.util.Collections; | ||
| 19 | +import java.util.List; | ||
| 20 | +import java.util.function.Function; | ||
| 21 | + | ||
| 22 | +import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN; | ||
| 23 | +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; | ||
| 24 | + | ||
| 25 | +/** | ||
| 26 | + * 目录内容列表工具:列出指定目录的内容 | ||
| 27 | + * | ||
| 28 | + * @author 芋道源码 | ||
| 29 | + */ | ||
| 30 | +@Component("listDir") | ||
| 31 | +public class ListDirToolB implements Function<ListDirToolB.Request, ListDirToolB.Response> { | ||
| 32 | + | ||
| 33 | + @Data | ||
| 34 | + @JsonClassDescription("列出指定目录的内容") | ||
| 35 | + public static class Request { | ||
| 36 | + | ||
| 37 | + /** | ||
| 38 | + * 要列出内容的目录路径 | ||
| 39 | + */ | ||
| 40 | + @JsonPropertyDescription("要列出内容的目录路径,例如说:/Users/yunai") | ||
| 41 | + private String path; | ||
| 42 | + | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + @Data | ||
| 46 | + @AllArgsConstructor | ||
| 47 | + @NoArgsConstructor | ||
| 48 | + public static class Response { | ||
| 49 | + | ||
| 50 | + /** | ||
| 51 | + * 目录内容列表 | ||
| 52 | + */ | ||
| 53 | + private List<File> files; | ||
| 54 | + | ||
| 55 | + @Data | ||
| 56 | + public static class File { | ||
| 57 | + | ||
| 58 | + /** | ||
| 59 | + * 是否为目录 | ||
| 60 | + */ | ||
| 61 | + private Boolean directory; | ||
| 62 | + | ||
| 63 | + /** | ||
| 64 | + * 名称 | ||
| 65 | + */ | ||
| 66 | + private String name; | ||
| 67 | + | ||
| 68 | + /** | ||
| 69 | + * 大小,仅对文件有效 | ||
| 70 | + */ | ||
| 71 | + private String size; | ||
| 72 | + | ||
| 73 | + /** | ||
| 74 | + * 最后修改时间 | ||
| 75 | + */ | ||
| 76 | + private String lastModified; | ||
| 77 | + | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + @Override | ||
| 83 | + public Response apply(Request request) { | ||
| 84 | + // 校验目录存在 | ||
| 85 | + String path = StrUtil.blankToDefault(request.getPath(), "."); | ||
| 86 | + Path dirPath = Paths.get(path); | ||
| 87 | + if (!FileUtil.exist(dirPath.toString()) || !FileUtil.isDirectory(dirPath.toString())) { | ||
| 88 | + return new Response(Collections.emptyList()); | ||
| 89 | + } | ||
| 90 | + // 列出目录内容 | ||
| 91 | + File[] files = dirPath.toFile().listFiles(); | ||
| 92 | + if (ArrayUtil.isEmpty(files)) { | ||
| 93 | + return new Response(Collections.emptyList()); | ||
| 94 | + } | ||
| 95 | + return new Response(convertList(Arrays.asList(files), file -> | ||
| 96 | + new Response.File().setDirectory(file.isDirectory()).setName(file.getName()) | ||
| 97 | + .setLastModified(LocalDateTimeUtil.format(LocalDateTimeUtil.of(file.lastModified()), NORM_DATETIME_PATTERN)))); | ||
| 98 | + } | ||
| 99 | + | ||
| 100 | +} | ||
| 0 | \ No newline at end of file | 101 | \ No newline at end of file |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/config/YudaoAiAutoConfiguration.java
| @@ -33,7 +33,7 @@ import org.springframework.context.annotation.Bean; | @@ -33,7 +33,7 @@ import org.springframework.context.annotation.Bean; | ||
| 33 | * @author fansili | 33 | * @author fansili |
| 34 | */ | 34 | */ |
| 35 | @AutoConfiguration | 35 | @AutoConfiguration |
| 36 | -@EnableConfigurationProperties({YudaoAiProperties.class, | 36 | +@EnableConfigurationProperties({ YudaoAiProperties.class, |
| 37 | QdrantVectorStoreProperties.class, // 解析 Qdrant 配置 | 37 | QdrantVectorStoreProperties.class, // 解析 Qdrant 配置 |
| 38 | RedisVectorStoreProperties.class, // 解析 Redis 配置 | 38 | RedisVectorStoreProperties.class, // 解析 Redis 配置 |
| 39 | MilvusVectorStoreProperties.class, MilvusServiceClientProperties.class // 解析 Milvus 配置 | 39 | MilvusVectorStoreProperties.class, MilvusServiceClientProperties.class // 解析 Milvus 配置 |
| @@ -139,15 +139,16 @@ public class YudaoAiAutoConfiguration { | @@ -139,15 +139,16 @@ public class YudaoAiAutoConfiguration { | ||
| 139 | } | 139 | } |
| 140 | // 特殊:由于混元大模型不提供 deepseek,而是通过知识引擎,所以需要区分下 URL | 140 | // 特殊:由于混元大模型不提供 deepseek,而是通过知识引擎,所以需要区分下 URL |
| 141 | if (StrUtil.isEmpty(properties.getBaseUrl())) { | 141 | if (StrUtil.isEmpty(properties.getBaseUrl())) { |
| 142 | - properties.setBaseUrl(StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? | ||
| 143 | - HunYuanChatModel.DEEP_SEEK_BASE_URL : HunYuanChatModel.BASE_URL); | 142 | + properties.setBaseUrl( |
| 143 | + StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL | ||
| 144 | + : HunYuanChatModel.BASE_URL); | ||
| 144 | } | 145 | } |
| 145 | // 创建 OpenAiChatModel、HunYuanChatModel 对象 | 146 | // 创建 OpenAiChatModel、HunYuanChatModel 对象 |
| 146 | OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() | 147 | OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() |
| 147 | .openAiApi(OpenAiApi.builder() | 148 | .openAiApi(OpenAiApi.builder() |
| 148 | - .baseUrl(properties.getBaseUrl()) | ||
| 149 | - .apiKey(properties.getApiKey()) | ||
| 150 | - .build()) | 149 | + .baseUrl(properties.getBaseUrl()) |
| 150 | + .apiKey(properties.getApiKey()) | ||
| 151 | + .build()) | ||
| 151 | .defaultOptions(OpenAiChatOptions.builder() | 152 | .defaultOptions(OpenAiChatOptions.builder() |
| 152 | .model(properties.getModel()) | 153 | .model(properties.getModel()) |
| 153 | .temperature(properties.getTemperature()) | 154 | .temperature(properties.getTemperature()) |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/factory/AiModelFactoryImpl.java
| @@ -61,6 +61,7 @@ import org.springframework.ai.minimax.MiniMaxChatModel; | @@ -61,6 +61,7 @@ import org.springframework.ai.minimax.MiniMaxChatModel; | ||
| 61 | import org.springframework.ai.minimax.MiniMaxEmbeddingModel; | 61 | import org.springframework.ai.minimax.MiniMaxEmbeddingModel; |
| 62 | import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; | 62 | import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; |
| 63 | import org.springframework.ai.minimax.api.MiniMaxApi; | 63 | import org.springframework.ai.minimax.api.MiniMaxApi; |
| 64 | +import org.springframework.ai.model.tool.ToolCallingManager; | ||
| 64 | import org.springframework.ai.moonshot.MoonshotChatModel; | 65 | import org.springframework.ai.moonshot.MoonshotChatModel; |
| 65 | import org.springframework.ai.moonshot.api.MoonshotApi; | 66 | import org.springframework.ai.moonshot.api.MoonshotApi; |
| 66 | import org.springframework.ai.ollama.OllamaChatModel; | 67 | import org.springframework.ai.ollama.OllamaChatModel; |
| @@ -431,7 +432,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -431,7 +432,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 431 | private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { | 432 | private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { |
| 432 | url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); | 433 | url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); |
| 433 | OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); | 434 | OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); |
| 434 | - return OpenAiChatModel.builder().openAiApi(openAiApi).build(); | 435 | + return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build(); |
| 435 | } | 436 | } |
| 436 | 437 | ||
| 437 | // TODO @芋艿:手头暂时没密钥,使用建议再测试下 | 438 | // TODO @芋艿:手头暂时没密钥,使用建议再测试下 |
| @@ -465,7 +466,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -465,7 +466,7 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 465 | */ | 466 | */ |
| 466 | private static OllamaChatModel buildOllamaChatModel(String url) { | 467 | private static OllamaChatModel buildOllamaChatModel(String url) { |
| 467 | OllamaApi ollamaApi = new OllamaApi(url); | 468 | OllamaApi ollamaApi = new OllamaApi(url); |
| 468 | - return OllamaChatModel.builder().ollamaApi(ollamaApi).build(); | 469 | + return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build(); |
| 469 | } | 470 | } |
| 470 | 471 | ||
| 471 | private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) { | 472 | private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) { |
| @@ -699,4 +700,8 @@ public class AiModelFactoryImpl implements AiModelFactory { | @@ -699,4 +700,8 @@ public class AiModelFactoryImpl implements AiModelFactory { | ||
| 699 | return SpringUtil.getBean(BatchingStrategy.class); | 700 | return SpringUtil.getBean(BatchingStrategy.class); |
| 700 | } | 701 | } |
| 701 | 702 | ||
| 703 | + private static ToolCallingManager getToolCallingManager() { | ||
| 704 | + return SpringUtil.getBean(ToolCallingManager.class); | ||
| 705 | + } | ||
| 706 | + | ||
| 702 | } | 707 | } |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/core/util/AiUtils.java
| @@ -24,14 +24,18 @@ public class AiUtils { | @@ -24,14 +24,18 @@ public class AiUtils { | ||
| 24 | // noinspection EnhancedSwitchMigration | 24 | // noinspection EnhancedSwitchMigration |
| 25 | switch (platform) { | 25 | switch (platform) { |
| 26 | case TONG_YI: | 26 | case TONG_YI: |
| 27 | + // TODO functions | ||
| 27 | return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens).build(); | 28 | return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens).build(); |
| 28 | case YI_YAN: | 29 | case YI_YAN: |
| 29 | return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); | 30 | return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); |
| 30 | case ZHI_PU: | 31 | case ZHI_PU: |
| 32 | + // TODO functions | ||
| 31 | return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); | 33 | return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); |
| 32 | case MINI_MAX: | 34 | case MINI_MAX: |
| 35 | + // TODO functions | ||
| 33 | return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); | 36 | return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); |
| 34 | case MOONSHOT: | 37 | case MOONSHOT: |
| 38 | + // TODO functions | ||
| 35 | return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); | 39 | return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); |
| 36 | case OPENAI: | 40 | case OPENAI: |
| 37 | case DEEP_SEEK: // 复用 OpenAI 客户端 | 41 | case DEEP_SEEK: // 复用 OpenAI 客户端 |
| @@ -39,12 +43,18 @@ public class AiUtils { | @@ -39,12 +43,18 @@ public class AiUtils { | ||
| 39 | case HUN_YUAN: // 复用 OpenAI 客户端 | 43 | case HUN_YUAN: // 复用 OpenAI 客户端 |
| 40 | case XING_HUO: // 复用 OpenAI 客户端 | 44 | case XING_HUO: // 复用 OpenAI 客户端 |
| 41 | case SILICON_FLOW: // 复用 OpenAI 客户端 | 45 | case SILICON_FLOW: // 复用 OpenAI 客户端 |
| 42 | - return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); | 46 | + return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) |
| 47 | +// .toolNames() TODO | ||
| 48 | + .toolNames("listDir") | ||
| 49 | + .build(); | ||
| 43 | case AZURE_OPENAI: | 50 | case AZURE_OPENAI: |
| 44 | // TODO 芋艿:貌似没 model 字段???! | 51 | // TODO 芋艿:貌似没 model 字段???! |
| 52 | + // TODO 芋艿:.toolNames() TODO | ||
| 45 | return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens).build(); | 53 | return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens).build(); |
| 46 | case OLLAMA: | 54 | case OLLAMA: |
| 47 | - return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens).build(); | 55 | + // TODO 芋艿:.toolNames() TODO |
| 56 | + return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens) | ||
| 57 | + .toolNames("listDir").build(); | ||
| 48 | default: | 58 | default: |
| 49 | throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); | 59 | throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); |
| 50 | } | 60 | } |
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/package-info.java
| @@ -4,7 +4,10 @@ | @@ -4,7 +4,10 @@ | ||
| 4 | * models 包路径: | 4 | * models 包路径: |
| 5 | * 1. xinghuo 包:【讯飞】星火,自己实现 | 5 | * 1. xinghuo 包:【讯飞】星火,自己实现 |
| 6 | * 2. deepseek 包:【深度求索】DeepSeek,自己实现 | 6 | * 2. deepseek 包:【深度求索】DeepSeek,自己实现 |
| 7 | - * 3. midjourney 包:Midjourney API,对接 https://github.com/novicezk/midjourney-proxy 实现 | ||
| 8 | - * 4. suno 包:Suno API,对接 https://github.com/gcui-art/suno-api 实现 | 7 | + * 3. doubao 包:【字节豆包】DouBao,自己实现 |
| 8 | + * 4. hunyuan 包:【腾讯混元】HunYuan,自己实现 | ||
| 9 | + * 5. siliconflow 包:【硅基硅流】SiliconFlow,自己实现 | ||
| 10 | + * 6. midjourney 包:Midjourney API,对接 https://github.com/novicezk/midjourney-proxy 实现 | ||
| 11 | + * 7. suno 包:Suno API,对接 https://github.com/gcui-art/suno-api 实现 | ||
| 9 | */ | 12 | */ |
| 10 | package cn.iocoder.yudao.framework.ai; | 13 | package cn.iocoder.yudao.framework.ai; |
| 11 | \ No newline at end of file | 14 | \ No newline at end of file |