【功能新增】AI:新增 function call 示例。会继续完善!

This commit is contained in:
YunaiV 2025-03-13 12:51:50 +08:00
parent f7ab30c50a
commit 25a0fe908a
6 changed files with 226 additions and 12 deletions

View File

@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.ai.service.tool;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.stereotype.Component;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* 目录内容列表工具列出指定目录的内容
*
* @author 芋道源码
*/
@Component
public class ListDirTool {
/**
* 列出指定目录的内容
*
* @param relativePath 要列出内容的目录路径相对于工作区根目录
* @return 目录内容列表
*/
@Tool(name = "listDir", description = "列出指定目录的内容")
public Response listDir(@ToolParam(description = "要列出内容的目录路径,相对于工作区根目录") String relativePath) {
// 校验目录存在
String path = StrUtil.blankToDefault(relativePath, ".");
Path dirPath = Paths.get(path);
if (!FileUtil.exist(dirPath.toString()) || !FileUtil.isDirectory(dirPath.toString())) {
return new Response(Collections.emptyList());
}
// 列出目录内容
File[] files = dirPath.toFile().listFiles();
if (ArrayUtil.isEmpty(files)) {
return new Response(Collections.emptyList());
}
return new Response(convertList(Arrays.asList(files), file -> new Response.File()
.setDirectory(file.isDirectory()).setName(file.getName())
.setSize(file.isFile() ? FileUtil.readableFileSize(file.length()) : null)
.setLastModified(
LocalDateTimeUtil.format(LocalDateTimeUtil.of(file.lastModified()), NORM_DATETIME_PATTERN))));
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Response {
/**
* 目录内容列表
*/
private List<File> files;
@Data
public static class File {
/**
* 是否为目录
*/
private Boolean directory;
/**
* 名称
*/
private String name;
/**
* 大小仅对文件有效
*/
private String size;
/**
* 最后修改时间
*/
private String lastModified;
}
}
}

View File

@ -0,0 +1,100 @@
package cn.iocoder.yudao.module.ai.service.tool;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_PATTERN;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* 目录内容列表工具列出指定目录的内容
*
* @author 芋道源码
*/
@Component("listDir")
public class ListDirToolB implements Function<ListDirToolB.Request, ListDirToolB.Response> {
@Data
@JsonClassDescription("列出指定目录的内容")
public static class Request {
/**
* 要列出内容的目录路径
*/
@JsonPropertyDescription("要列出内容的目录路径,例如说:/Users/yunai")
private String path;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Response {
/**
* 目录内容列表
*/
private List<File> files;
@Data
public static class File {
/**
* 是否为目录
*/
private Boolean directory;
/**
* 名称
*/
private String name;
/**
* 大小仅对文件有效
*/
private String size;
/**
* 最后修改时间
*/
private String lastModified;
}
}
@Override
public Response apply(Request request) {
// 校验目录存在
String path = StrUtil.blankToDefault(request.getPath(), ".");
Path dirPath = Paths.get(path);
if (!FileUtil.exist(dirPath.toString()) || !FileUtil.isDirectory(dirPath.toString())) {
return new Response(Collections.emptyList());
}
// 列出目录内容
File[] files = dirPath.toFile().listFiles();
if (ArrayUtil.isEmpty(files)) {
return new Response(Collections.emptyList());
}
return new Response(convertList(Arrays.asList(files), file ->
new Response.File().setDirectory(file.isDirectory()).setName(file.getName())
.setLastModified(LocalDateTimeUtil.format(LocalDateTimeUtil.of(file.lastModified()), NORM_DATETIME_PATTERN))));
}
}

View File

@ -33,7 +33,7 @@ import org.springframework.context.annotation.Bean;
* @author fansili * @author fansili
*/ */
@AutoConfiguration @AutoConfiguration
@EnableConfigurationProperties({YudaoAiProperties.class, @EnableConfigurationProperties({ YudaoAiProperties.class,
QdrantVectorStoreProperties.class, // 解析 Qdrant 配置 QdrantVectorStoreProperties.class, // 解析 Qdrant 配置
RedisVectorStoreProperties.class, // 解析 Redis 配置 RedisVectorStoreProperties.class, // 解析 Redis 配置
MilvusVectorStoreProperties.class, MilvusServiceClientProperties.class // 解析 Milvus 配置 MilvusVectorStoreProperties.class, MilvusServiceClientProperties.class // 解析 Milvus 配置
@ -139,15 +139,16 @@ public class YudaoAiAutoConfiguration {
} }
// 特殊由于混元大模型不提供 deepseek而是通过知识引擎所以需要区分下 URL // 特殊由于混元大模型不提供 deepseek而是通过知识引擎所以需要区分下 URL
if (StrUtil.isEmpty(properties.getBaseUrl())) { if (StrUtil.isEmpty(properties.getBaseUrl())) {
properties.setBaseUrl(StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? properties.setBaseUrl(
HunYuanChatModel.DEEP_SEEK_BASE_URL : HunYuanChatModel.BASE_URL); StrUtil.startWithIgnoreCase(properties.getModel(), "deepseek") ? HunYuanChatModel.DEEP_SEEK_BASE_URL
: HunYuanChatModel.BASE_URL);
} }
// 创建 OpenAiChatModelHunYuanChatModel 对象 // 创建 OpenAiChatModelHunYuanChatModel 对象
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder() .openAiApi(OpenAiApi.builder()
.baseUrl(properties.getBaseUrl()) .baseUrl(properties.getBaseUrl())
.apiKey(properties.getApiKey()) .apiKey(properties.getApiKey())
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel()) .model(properties.getModel())
.temperature(properties.getTemperature()) .temperature(properties.getTemperature())

View File

@ -61,6 +61,7 @@ import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxEmbeddingModel; import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
import org.springframework.ai.minimax.api.MiniMaxApi; import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.moonshot.MoonshotChatModel; import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.moonshot.api.MoonshotApi; import org.springframework.ai.moonshot.api.MoonshotApi;
import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.OllamaChatModel;
@ -431,7 +432,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build();
return OpenAiChatModel.builder().openAiApi(openAiApi).build(); return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build();
} }
// TODO @芋艿手头暂时没密钥使用建议再测试下 // TODO @芋艿手头暂时没密钥使用建议再测试下
@ -465,7 +466,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
*/ */
private static OllamaChatModel buildOllamaChatModel(String url) { private static OllamaChatModel buildOllamaChatModel(String url) {
OllamaApi ollamaApi = new OllamaApi(url); OllamaApi ollamaApi = new OllamaApi(url);
return OllamaChatModel.builder().ollamaApi(ollamaApi).build(); return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build();
} }
private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) { private StabilityAiImageModel buildStabilityAiImageModel(String apiKey, String url) {
@ -699,4 +700,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
return SpringUtil.getBean(BatchingStrategy.class); return SpringUtil.getBean(BatchingStrategy.class);
} }
private static ToolCallingManager getToolCallingManager() {
return SpringUtil.getBean(ToolCallingManager.class);
}
} }

View File

@ -24,14 +24,18 @@ public class AiUtils {
// noinspection EnhancedSwitchMigration // noinspection EnhancedSwitchMigration
switch (platform) { switch (platform) {
case TONG_YI: case TONG_YI:
// TODO functions
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens).build(); return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens).build();
case YI_YAN: case YI_YAN:
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case ZHI_PU: case ZHI_PU:
// TODO functions
return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case MINI_MAX: case MINI_MAX:
// TODO functions
return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case MOONSHOT: case MOONSHOT:
// TODO functions
return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case OPENAI: case OPENAI:
case DEEP_SEEK: // 复用 OpenAI 客户端 case DEEP_SEEK: // 复用 OpenAI 客户端
@ -39,12 +43,18 @@ public class AiUtils {
case HUN_YUAN: // 复用 OpenAI 客户端 case HUN_YUAN: // 复用 OpenAI 客户端
case XING_HUO: // 复用 OpenAI 客户端 case XING_HUO: // 复用 OpenAI 客户端
case SILICON_FLOW: // 复用 OpenAI 客户端 case SILICON_FLOW: // 复用 OpenAI 客户端
return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
// .toolNames() TODO
.toolNames("listDir")
.build();
case AZURE_OPENAI: case AZURE_OPENAI:
// TODO 芋艿貌似没 model 字段 // TODO 芋艿貌似没 model 字段
// TODO 芋艿.toolNames() TODO
return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens).build(); return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens).build();
case OLLAMA: case OLLAMA:
return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens).build(); // TODO 芋艿.toolNames() TODO
return OllamaOptions.builder().model(model).temperature(temperature).numPredict(maxTokens)
.toolNames("listDir").build();
default: default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform)); throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
} }

View File

@ -4,7 +4,10 @@
* models 包路径 * models 包路径
* 1. xinghuo 讯飞星火自己实现 * 1. xinghuo 讯飞星火自己实现
* 2. deepseek 深度求索DeepSeek自己实现 * 2. deepseek 深度求索DeepSeek自己实现
* 3. midjourney Midjourney API对接 https://github.com/novicezk/midjourney-proxy 实现 * 3. doubao 字节豆包DouBao自己实现
* 4. suno Suno API对接 https://github.com/gcui-art/suno-api 实现 * 4. hunyuan 腾讯混元HunYuan自己实现
* 5. siliconflow 硅基硅流SiliconFlow自己实现
* 6. midjourney Midjourney API对接 https://github.com/novicezk/midjourney-proxy 实现
* 7. suno Suno API对接 https://github.com/gcui-art/suno-api 实现
*/ */
package cn.iocoder.yudao.framework.ai; package cn.iocoder.yudao.framework.ai;