From b0b44e2ec63c943a3b421fc346f268ead8ce5571 Mon Sep 17 00:00:00 2001 From: choweli <1030848819@qq.com> Date: Wed, 4 Jun 2025 14:11:41 +0800 Subject: [PATCH] =?UTF-8?q?hutool-ai=E6=96=B0=E5=A2=9ESSE=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E5=93=8D=E5=BA=94=EF=BC=8C=E5=A2=9E=E5=8A=A0Hutool-AI?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E6=9C=8D=E5=8A=A1=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E8=B6=85=E6=97=B6=E6=97=B6=E9=97=B4=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/org/dromara/hutool/ai/AIUtil.java | 12 + .../java/org/dromara/hutool/ai/ModelName.java | 4 + .../java/org/dromara/hutool/ai/Models.java | 39 +- .../org/dromara/hutool/ai/core/AIConfig.java | 32 ++ .../hutool/ai/core/AIConfigBuilder.java | 28 ++ .../org/dromara/hutool/ai/core/AIService.java | 30 +- .../dromara/hutool/ai/core/BaseAIService.java | 61 ++- .../dromara/hutool/ai/core/BaseConfig.java | 23 ++ .../ai/model/deepseek/DeepSeekService.java | 10 + .../model/deepseek/DeepSeekServiceImpl.java | 50 ++- .../hutool/ai/model/doubao/DoubaoService.java | 106 ++++- .../ai/model/doubao/DoubaoServiceImpl.java | 139 +++++-- .../hutool/ai/model/grok/GrokService.java | 79 +++- .../hutool/ai/model/grok/GrokServiceImpl.java | 110 ++++- .../hutool/ai/model/hutool/HutoolCommon.java | 119 ++++++ .../hutool/ai/model/hutool/HutoolConfig.java | 49 +++ .../ai/model/hutool/HutoolProvider.java | 39 ++ .../hutool/ai/model/hutool/HutoolService.java | 170 ++++++++ .../ai/model/hutool/HutoolServiceImpl.java | 380 ++++++++++++++++++ .../hutool/ai/model/hutool/package-info.java | 24 ++ .../hutool/ai/model/openai/OpenaiService.java | 83 +++- .../ai/model/openai/OpenaiServiceImpl.java | 93 ++++- .../org.dromara.hutool.ai.core.AIConfig | 1 + ...g.dromara.hutool.ai.core.AIServiceProvider | 1 + .../org/dromara/hutool/ai/AIUtilTest.java | 7 + .../model/deepseek/DeepSeekServiceTest.java | 60 ++- .../ai/model/doubao/DoubaoServiceTest.java | 156 ++++++- .../hutool/ai/model/grok/GrokServiceTest.java | 92 ++++- .../ai/model/hutool/HutoolServiceTest.java | 193 +++++++++ .../ai/model/openai/OpenaiServiceTest.java | 101 ++++- 30 files changed, 2159 insertions(+), 132 deletions(-) create mode 100644 hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolCommon.java create mode 100644 hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolConfig.java create mode 100644 hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolProvider.java create mode 100644 hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolService.java create mode 100644 hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolServiceImpl.java create mode 100644 hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/package-info.java create mode 100644 hutool-ai/src/test/java/org/dromara/hutool/ai/model/hutool/HutoolServiceTest.java diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/AIUtil.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/AIUtil.java index 45f22eeaf..3ba1ce3fa 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/AIUtil.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/AIUtil.java @@ -22,6 +22,7 @@ import org.dromara.hutool.ai.core.Message; import org.dromara.hutool.ai.model.deepseek.DeepSeekService; import org.dromara.hutool.ai.model.doubao.DoubaoService; import org.dromara.hutool.ai.model.grok.GrokService; +import org.dromara.hutool.ai.model.hutool.HutoolService; import org.dromara.hutool.ai.model.openai.OpenaiService; import java.util.List; @@ -58,6 +59,17 @@ public class AIUtil { return getAIService(config, AIService.class); } + /** + * 获取Hutool-AI服务 + * + * @param config 创建的AI服务模型的配置 + * @return HutoolService + * @since 6.0.0 + */ + public static HutoolService getHutoolService(final AIConfig config) { + return getAIService(config, HutoolService.class); + } + /** * 获取DeepSeek模型服务 * diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/ModelName.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/ModelName.java index 3ae1718e5..d362d2a85 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/ModelName.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/ModelName.java @@ -23,6 +23,10 @@ package org.dromara.hutool.ai; * @since 6.0.0 */ public enum ModelName { + /** + * hutool + */ + HUTOOL("hutool"), /** * deepSeek */ diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/Models.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/Models.java index ed2ab92c4..118b7e12d 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/Models.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/Models.java @@ -24,6 +24,21 @@ package org.dromara.hutool.ai; */ public class Models { + // Hutool的模型 + public enum Hutool { + HUTOOL("hutool"); + + private final String model; + + Hutool(String model) { + this.model = model; + } + + public String getModel() { + return model; + } + } + // DeepSeek的模型 public enum DeepSeek { DEEPSEEK_CHAT("deepseek-chat"), @@ -123,7 +138,12 @@ public class Models { DOUBAO_VISION_LITE_32K("doubao-vision-lite-32k-241015"), DOUBAO_EMBEDDING_LARGE("doubao-embedding-large-text-240915"), DOUBAO_EMBEDDING_TEXT_240715("doubao-embedding-text-240715"), - DOUBAO_EMBEDDING_VISION("doubao-embedding-vision-241215"); + DOUBAO_EMBEDDING_VISION("doubao-embedding-vision-241215"), + DOUBAO_SEEDREAM_3_0_T2I("doubao-seedream-3-0-t2i-250415"), + Doubao_Seedance_1_0_lite_t2v("doubao-seedance-1-0-lite-t2v-250428"), + Doubao_Seedance_1_0_lite_i2v("doubao-seedance-1-0-lite-i2v-250428"), + Wan2_1_14B_t2v("wan2-1-14b-t2v-250225"), + Wan2_1_14B_i2v("wan2-1-14b-i2v-250225"); private final String model; @@ -138,6 +158,23 @@ public class Models { // Grok的模型 public enum Grok { + GROK_3_BETA_LATEST("grok-3-beta"), + GROK_3_BETA("grok-3-beta"), + GROK_3("grok-3-beta"), + GROK_3_MINI_FAST_LATEST("grok-3-mini-fast-beta"), + GROK_3_MINI_FAST_BETA("grok-3-mini-fast-beta"), + GROK_3_MINI_FAST("grok-3-mini-fast-beta"), + GROK_3_FAST_LATEST("grok-3-fast-beta"), + GROK_3_FAST_BETA("grok-3-fast-beta"), + GROK_3_FAST("grok-3-fast-beta"), + GROK_3_MINI_LATEST("grok-3-mini-beta"), + GROK_3_MINI_BETA("grok-3-mini-beta"), + GROK_3_MINI("grok-3-mini-beta"), + GROK_2_IMAGE_LATEST("grok-2-image-1212"), + GROK_2_IMAGE("grok-2-image-1212"), + GROK_2_IMAGE_1212("grok-2-image-1212"), + grok_2_latest("grok-2-1212"), + GROK_2("grok-2-1212"), GROK_2_1212("grok-2-1212"), GROK_2_VISION_1212("grok-2-vision-1212"), GROK_BETA("grok-beta"), diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/core/AIConfig.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/core/AIConfig.java index 1980b76f6..dd38bfe7a 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/core/AIConfig.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/core/AIConfig.java @@ -110,4 +110,36 @@ public interface AIConfig { */ Map getAdditionalConfigMap(); + /** + * 设置连接超时时间 + * + * @param timeout 连接超时时间 + * @since 6.0.0 + */ + void setTimeout(int timeout); + + /** + * 获取连接超时时间 + * + * @return timeout + * @since 6.0.0 + */ + int getTimeout(); + + /** + * 设置读取超时时间 + * + * @param readTimeout 连接超时时间 + * @since 6.0.0 + */ + void setReadTimeout(int readTimeout); + + /** + * 获取读取超时时间 + * + * @return readTimeout + * @since 6.0.0 + */ + int getReadTimeout(); + } diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/core/AIConfigBuilder.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/core/AIConfigBuilder.java index 060fa8a3e..71b3a031b 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/core/AIConfigBuilder.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/core/AIConfigBuilder.java @@ -106,6 +106,34 @@ public class AIConfigBuilder { return this; } + /** + * 设置连接超时时间,不设置为默认值 + * + * @param timeout 超时时间 + * @return config + * @since 6.0.0 + */ + public synchronized AIConfigBuilder setTimout(final int timeout) { + if (timeout > 0) { + config.setTimeout(timeout); + } + return this; + } + + /** + * 设置读取超时时间,不设置为默认值 + * + * @param readTimout 取超时时间 + * @return config + * @since 6.0.0 + */ + public synchronized AIConfigBuilder setReadTimout(final int readTimout) { + if (readTimout > 0) { + config.setReadTimeout(readTimout); + } + return this; + } + /** * 返回config实例 * diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/core/AIService.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/core/AIService.java index fd049ed5f..717adc601 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/core/AIService.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/core/AIService.java @@ -16,7 +16,9 @@ package org.dromara.hutool.ai.core; +import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; /** * 模型公共的API功能,特有的功能在model.xx.XXService下定义 @@ -33,7 +35,25 @@ public interface AIService { * @return AI回答 * @since 6.0.0 */ - String chat(String prompt); + default String chat(String prompt){ + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + return chat(messages); + } + + /** + * 对话-SSE流式输出 + * @param prompt user题词 + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + default void chat(String prompt, final Consumer callback){ + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + chat(messages, callback); + } /** * 对话 @@ -44,4 +64,12 @@ public interface AIService { */ String chat(final List messages); + /** + * 对话-SSE流式输出 + * @param messages 由目前为止的对话组成的消息列表,可以设置role,content。详细参考官方文档 + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + void chat(final List messages, final Consumer callback); + } diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/core/BaseAIService.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/core/BaseAIService.java index 5a0a18a9a..bda6e2409 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/core/BaseAIService.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/core/BaseAIService.java @@ -22,8 +22,15 @@ import org.dromara.hutool.http.HttpUtil; import org.dromara.hutool.http.client.Response; import org.dromara.hutool.http.meta.HeaderName; import org.dromara.hutool.http.meta.Method; +import org.dromara.hutool.json.JSONUtil; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; import java.util.Map; +import java.util.function.Consumer; /** * 基础AIService,包含基公共参数和公共方法 @@ -52,8 +59,8 @@ public class BaseAIService { protected Response sendGet(final String endpoint) { //链式构建请求 try { - //设置超时3分钟 - HttpGlobalConfig.setTimeout(180000); + //设置超时 + HttpGlobalConfig.setTimeout(config.getTimeout()); return HttpUtil.createRequest(config.getApiUrl() + endpoint, Method.GET) .header(HeaderName.ACCEPT, "application/json") .header(HeaderName.AUTHORIZATION, "Bearer " + config.getApiKey()) @@ -73,7 +80,7 @@ public class BaseAIService { //链式构建请求 try { //设置超时3分钟 - HttpGlobalConfig.setTimeout(180000); + HttpGlobalConfig.setTimeout(config.getTimeout()); return HttpUtil.createRequest(config.getApiUrl() + endpoint, Method.POST) .header(HeaderName.CONTENT_TYPE, "application/json") .header(HeaderName.ACCEPT, "application/json") @@ -96,7 +103,7 @@ public class BaseAIService { //链式构建请求 try { //设置超时3分钟 - HttpGlobalConfig.setTimeout(180000); + HttpGlobalConfig.setTimeout(config.getTimeout()); return HttpUtil.createPost(config.getApiUrl() + endpoint) //form表单中有file对象会自动将文件编码为 multipart/form-data 格式。不需要设置 // .header(HeaderName.CONTENT_TYPE, "multipart/form-data") @@ -107,4 +114,50 @@ public class BaseAIService { throw new AIException("Failed to send POST request:" + e.getMessage(), e); } } + + /** + * 支持流式返回的 POST 请求 + * + * @param endpoint 请求地址 + * @param paramMap 请求参数 + * @param callback 流式数据回调函数 + */ + protected void sendPostStream(final String endpoint, final Map paramMap, Consumer callback) { + HttpURLConnection connection = null; + try { + // 创建连接 + URL apiUrl = new URL(config.getApiUrl() + endpoint); + connection = (HttpURLConnection) apiUrl.openConnection(); + connection.setRequestMethod(Method.POST.name()); + connection.setRequestProperty(HeaderName.CONTENT_TYPE.getValue(), "application/json"); + connection.setRequestProperty(HeaderName.AUTHORIZATION.getValue(), "Bearer " + config.getApiKey()); + connection.setDoOutput(true); + //设置读取超时 + connection.setReadTimeout(config.getReadTimeout()); + //设置连接超时 + connection.setConnectTimeout(config.getTimeout()); + // 发送请求体 + try (OutputStream os = connection.getOutputStream()) { + String jsonInputString = JSONUtil.toJsonStr(paramMap); + os.write(jsonInputString.getBytes()); + os.flush(); + } + + // 读取流式响应 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + // 调用回调函数处理每一行数据 + callback.accept(line); + } + } + } catch (Exception e) { + callback.accept("{\"error\": \"" + e.getMessage() + "\"}"); + } finally { + // 关闭连接 + if (connection != null) { + connection.disconnect(); + } + } + } } diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/core/BaseConfig.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/core/BaseConfig.java index 20f195b77..e5f6d56e9 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/core/BaseConfig.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/core/BaseConfig.java @@ -36,6 +36,10 @@ public class BaseConfig implements AIConfig { protected volatile String model; //动态扩展字段 protected Map additionalConfig = new SafeConcurrentHashMap<>(); + //连接超时时间 + protected volatile int timeout = 180000; + //读取超时时间 + protected volatile int readTimeout = 300000; @Override public void setApiKey(final String apiKey) { @@ -82,4 +86,23 @@ public class BaseConfig implements AIConfig { return new SafeConcurrentHashMap<>(additionalConfig); } + @Override + public int getTimeout() { + return timeout; + } + + @Override + public void setTimeout(final int timeout) { + this.timeout = timeout; + } + + @Override + public int getReadTimeout() { + return readTimeout; + } + + @Override + public void setReadTimeout(final int readTimeout) { + this.readTimeout = readTimeout; + } } diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/deepseek/DeepSeekService.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/deepseek/DeepSeekService.java index f82b58d1f..5f5c6593e 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/deepseek/DeepSeekService.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/deepseek/DeepSeekService.java @@ -18,6 +18,8 @@ package org.dromara.hutool.ai.model.deepseek; import org.dromara.hutool.ai.core.AIService; +import java.util.function.Consumer; + /** * deepSeek支持的扩展接口 * @@ -35,6 +37,14 @@ public interface DeepSeekService extends AIService { */ String beta(String prompt); + /** + * 模型beta功能-SSE流式输出 + * @param prompt 题词 + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + void beta(String prompt, final Consumer callback); + /** * 列出所有模型列表 * diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/deepseek/DeepSeekServiceImpl.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/deepseek/DeepSeekServiceImpl.java index b9897d36d..a2a2d9723 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/deepseek/DeepSeekServiceImpl.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/deepseek/DeepSeekServiceImpl.java @@ -19,13 +19,14 @@ package org.dromara.hutool.ai.model.deepseek; import org.dromara.hutool.ai.core.AIConfig; import org.dromara.hutool.ai.core.BaseAIService; import org.dromara.hutool.ai.core.Message; +import org.dromara.hutool.core.thread.ThreadUtil; import org.dromara.hutool.http.client.Response; import org.dromara.hutool.json.JSONUtil; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; /** * DeepSeek服务,AI具体功能的实现 @@ -54,15 +55,6 @@ public class DeepSeekServiceImpl extends BaseAIService implements DeepSeekServic super(config); } - @Override - public String chat(final String prompt) { - // 定义消息结构 - final List messages = new ArrayList<>(); - messages.add(new Message("system", "You are a helpful assistant")); - messages.add(new Message("user", prompt)); - return chat(messages); - } - @Override public String chat(final List messages) { final String paramJson = buildChatRequestBody(messages); @@ -70,6 +62,12 @@ public class DeepSeekServiceImpl extends BaseAIService implements DeepSeekServic return response.bodyStr(); } + @Override + public void chat(List messages, Consumer callback) { + Map paramMap = buildChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "deepseek-chat-sse").start(); + } + @Override public String beta(final String prompt) { final String paramJson = buildBetaRequestBody(prompt); @@ -77,6 +75,12 @@ public class DeepSeekServiceImpl extends BaseAIService implements DeepSeekServic return response.bodyStr(); } + @Override + public void beta(String prompt, Consumer callback) { + Map paramMap = buildBetaStreamRequestBody(prompt); + ThreadUtil.newThread(() -> sendPostStream(BETA_ENDPOINT, paramMap, callback::accept), "deepseek-beta-sse").start(); + } + @Override public String models() { final Response response = sendGet(MODELS_ENDPOINT); @@ -101,6 +105,19 @@ public class DeepSeekServiceImpl extends BaseAIService implements DeepSeekServic return JSONUtil.toJsonStr(paramMap); } + // 构建chatStream请求体 + private Map buildChatStreamRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + // 构建beta请求体 private String buildBetaRequestBody(final String prompt) { // 定义消息结构 @@ -114,4 +131,17 @@ public class DeepSeekServiceImpl extends BaseAIService implements DeepSeekServic return JSONUtil.toJsonStr(paramMap); } + // 构建betaStream请求体 + private Map buildBetaStreamRequestBody(final String prompt) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + } diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/doubao/DoubaoService.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/doubao/DoubaoService.java index 37d86b01e..2d2e8cf7e 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/doubao/DoubaoService.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/doubao/DoubaoService.java @@ -19,7 +19,9 @@ package org.dromara.hutool.ai.model.doubao; import org.dromara.hutool.ai.core.AIService; import org.dromara.hutool.ai.core.Message; +import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; /** * doubao支持的扩展接口 @@ -29,17 +31,6 @@ import java.util.List; */ public interface DoubaoService extends AIService { - /** - * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 - * - * @param prompt 提问 - * @param images 图片列表/或者图片Base64编码图片列表(URI形式) - * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto - * @return AI回答 - * @since 6.0.0 - */ - String chatVision(String prompt, final List images, String detail); - /** * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 * @@ -52,9 +43,43 @@ public interface DoubaoService extends AIService { return chatVision(prompt, images, DoubaoCommon.DoubaoVision.AUTO.getDetail()); } + /** + * 图像理解-SSE流式输出 + * + * @param prompt 提问 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + default void chatVision(String prompt, final List images, final Consumer callback) { + chatVision(prompt, images, DoubaoCommon.DoubaoVision.AUTO.getDetail(), callback); + } + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 提问 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @return AI回答 + * @since 6.0.0 + */ + String chatVision(String prompt, final List images, String detail); + + /** + * 图像理解-SSE流式输出 + * + * @param prompt 提问 + * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + void chatVision(String prompt, final List images, String detail, final Consumer callback); + /** * 创建视频生成任务 - * 注意:调用该方法时,配置config中的model为您创建的推理接入点(Endpoint)ID。详细参考官方文档 + * 注意:调用该方法时,配置config中的model为生成视频的模型或者您创建的推理接入点(Endpoint)ID。详细参考官方文档 * * @param text 文本提示词 * @param image 图片/或者图片Base64编码图片(URI形式) @@ -114,6 +139,15 @@ public interface DoubaoService extends AIService { */ String botsChat(final List messages); + /** + * 应用(Bot)-SSE流式输出 config中model设置为您创建的应用ID + * + * @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息 + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + void botsChat(final List messages, final Consumer callback); + /** * 分词:可以将文本转换为模型可理解的 token id,并返回文本的 tokens 数量、token id、 token 在原始文本中的偏移量等信息 * @@ -132,7 +166,12 @@ public interface DoubaoService extends AIService { * @return AI回答 * @since 6.0.0 */ - String batchChat(String prompt); + default String batchChat(String prompt){ + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + return batchChat(messages); + } /** * 批量推理 Chat @@ -179,7 +218,26 @@ public interface DoubaoService extends AIService { * @return AI的回答 * @since 6.0.0 */ - String chatContext(String prompt, String contextId); + default String chatContext(String prompt, String contextId){ + final List messages = new ArrayList<>(); + messages.add(new Message("user", prompt)); + return chatContext(messages, contextId); + } + + /** + * 上下文缓存对话-SSE流式输出 + * 注意:配置config中的model可以为您创建的推理接入点(Endpoint)ID,也可以是支持chat的model + * + * @param prompt 对话的内容题词 + * @param contextId 创建上下文缓存后获取的缓存id + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + default void chatContext(String prompt, String contextId, final Consumer callback){ + final List messages = new ArrayList<>(); + messages.add(new Message("user", prompt)); + chatContext(messages, contextId, callback); + } /** * 上下文缓存对话: 向大模型发起带上下文缓存的请求 @@ -192,4 +250,24 @@ public interface DoubaoService extends AIService { */ String chatContext(final List messages, String contextId); + /** + * 上下文缓存对话-SSE流式输出 + * 注意:配置config中的model可以为您创建的推理接入点(Endpoint)ID,也可以是支持chat的model + * + * @param messages 对话的信息 不支持最后一个元素的role设置为assistant。如使用session 缓存(mode设置为session)传入最新一轮对话的信息,无需传入历史信息 + * @param contextId 创建上下文缓存后获取的缓存id + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + void chatContext(final List messages, String contextId, final Consumer callback); + + /** + * 文生图 + * 请设置config中model为支持图片功能的模型,目前支持Doubao-Seedream-3.0-t2i + * + * @param prompt 题词 + * @return 包含生成图片的url + * @since 6.0.0 + */ + String imagesGenerations(String prompt); } diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/doubao/DoubaoServiceImpl.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/doubao/DoubaoServiceImpl.java index 60daf124d..99acae556 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/doubao/DoubaoServiceImpl.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/doubao/DoubaoServiceImpl.java @@ -20,6 +20,7 @@ import org.dromara.hutool.ai.core.AIConfig; import org.dromara.hutool.ai.core.BaseAIService; import org.dromara.hutool.ai.core.Message; import org.dromara.hutool.core.text.StrUtil; +import org.dromara.hutool.core.thread.ThreadUtil; import org.dromara.hutool.http.client.Response; import org.dromara.hutool.json.JSONUtil; @@ -27,6 +28,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; /** * Doubao服务,AI具体功能的实现 @@ -54,21 +56,14 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { private final String CHAT_CONTEXT = "/context/chat/completions"; //创建视频生成任务 private final String CREATE_VIDEO = "/contents/generations/tasks"; + //文生图 + private final String IMAGES_GENERATIONS = "/images/generations"; public DoubaoServiceImpl(final AIConfig config) { //初始化doubao客户端 super(config); } - @Override - public String chat(String prompt) { - // 定义消息结构 - final List messages = new ArrayList<>(); - messages.add(new Message("system", "You are a helpful assistant")); - messages.add(new Message("user", prompt)); - return chat(messages); - } - @Override public String chat(final List messages) { String paramJson = buildChatRequestBody(messages); @@ -76,6 +71,12 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { return response.bodyStr(); } + @Override + public void chat(List messages, Consumer callback) { + Map paramMap = buildChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "doubao-chat-sse").start(); + } + @Override public String chatVision(String prompt, final List images, String detail) { String paramJson = buildChatVisionRequestBody(prompt, images, detail); @@ -83,6 +84,12 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { return response.bodyStr(); } + @Override + public void chatVision(String prompt, List images, String detail, Consumer callback) { + Map paramMap = buildChatVisionStreamRequestBody(prompt, images, detail); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "doubao-chatVision-sse").start(); + } + @Override public String videoTasks(String text, String image, final List videoParams) { String paramJson = buildGenerationsTasksRequestBody(text, image, videoParams); @@ -118,6 +125,12 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { return response.bodyStr(); } + @Override + public void botsChat(List messages, Consumer callback) { + Map paramMap = buildBotsChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(BOTS_CHAT, paramMap, callback::accept), "doubao-botsChat-sse").start(); + } + @Override public String tokenization(String[] text) { String paramJson = buildTokenizationRequestBody(text); @@ -125,15 +138,6 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { return response.bodyStr(); } - @Override - public String batchChat(String prompt) { - // 定义消息结构 - final List messages = new ArrayList<>(); - messages.add(new Message("system", "You are a helpful assistant")); - messages.add(new Message("user", prompt)); - return batchChat(messages); - } - @Override public String batchChat(final List messages) { String paramJson = buildBatchChatRequestBody(messages); @@ -148,14 +152,6 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { return response.bodyStr(); } - @Override - public String chatContext(String prompt, String contextId) { - // 定义消息结构 - final List messages = new ArrayList<>(); - messages.add(new Message("user", prompt)); - return chatContext(messages, contextId); - } - @Override public String chatContext(final List messages, String contextId) { String paramJson = buildChatContentRequestBody(messages, contextId); @@ -163,6 +159,19 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { return response.bodyStr(); } + @Override + public void chatContext(List messages, String contextId, Consumer callback) { + Map paramMap = buildChatContentStreamRequestBody(messages, contextId); + ThreadUtil.newThread(() -> sendPostStream(CHAT_CONTEXT, paramMap, callback::accept), "doubao-chatContext-sse").start(); + } + + @Override + public String imagesGenerations(String prompt) { + String paramJson = buildImagesGenerationsRequestBody(prompt); + Response response = sendPost(IMAGES_GENERATIONS, paramJson); + return response.bodyStr(); + } + // 构建chat请求体 private String buildChatRequestBody(final List messages) { //使用JSON工具 @@ -175,6 +184,19 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { return JSONUtil.toJsonStr(paramMap); } + // 构建chatStream请求体 + private Map buildChatStreamRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + //构建chatVision请求体 private String buildChatVisionRequestBody(String prompt, final List images, String detail) { // 定义消息结构 @@ -206,6 +228,37 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { return JSONUtil.toJsonStr(paramMap); } + private Map buildChatVisionStreamRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return paramMap; + } + //构建文本向量化请求体 private String buildEmbeddingTextRequestBody(String[] input) { //使用JSON工具 @@ -253,6 +306,10 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { return buildChatRequestBody(messages); } + private Map buildBotsChatStreamRequestBody(final List messages) { + return buildChatStreamRequestBody(messages); + } + //构建分词请求体 private String buildTokenizationRequestBody(String[] text) { final Map paramMap = new HashMap<>(); @@ -266,6 +323,10 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { return buildChatRequestBody(messages); } + private Map buildBatchChatStreamRequestBody(final List messages) { + return buildChatStreamRequestBody(messages); + } + //构建创建上下文缓存请求体 private String buildCreateContextRequest(final List messages, String mode) { final Map paramMap = new HashMap<>(); @@ -291,6 +352,19 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { return JSONUtil.toJsonStr(paramMap); } + private Map buildChatContentStreamRequestBody(final List messages, String contextId) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + paramMap.put("context_id", contextId); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + //构建创建视频任务请求体 private String buildGenerationsTasksRequestBody(String text, String image, final List videoParams) { //使用JSON工具 @@ -306,7 +380,7 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { content.add(textMap); } //添加图片参数 - if (!StrUtil.isNotBlank(image)) { + if (!StrUtil.isBlank(image)) { final Map imgUrlMap = new HashMap<>(); imgUrlMap.put("type", "image_url"); final Map urlMap = new HashMap<>(); @@ -351,4 +425,15 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService { return JSONUtil.toJsonStr(paramMap); } + //构建文生图请求体 + private String buildImagesGenerationsRequestBody(String prompt) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + } diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/grok/GrokService.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/grok/GrokService.java index b4bcbc0e9..712a5268e 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/grok/GrokService.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/grok/GrokService.java @@ -17,8 +17,11 @@ package org.dromara.hutool.ai.model.grok; import org.dromara.hutool.ai.core.AIService; +import org.dromara.hutool.ai.core.Message; +import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; /** * grok支持的扩展接口 @@ -36,7 +39,48 @@ public interface GrokService extends AIService { * @return AI回答 * @since 6.0.0 */ - String message(String prompt, int maxToken); + default String message(String prompt, int maxToken){ + // 定义消息结构 + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + return message(messages, maxToken); + } + + /** + * 创建消息回复-SSE流式输出 + * + * @param prompt 题词 + * @param maxToken 最大token + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + default void message(String prompt, int maxToken, final Consumer callback){ + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + message(messages, maxToken, callback); + } + + /** + * 创建消息回复 + * + * @param messages messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息 + * @param maxToken 最大token + * @return AI回答 + * @since 6.0.0 + */ + String message(List messages, int maxToken); + + /** + * 创建消息回复-SSE流式输出 + * + * @param messages messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息 + * @param maxToken 最大token + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + void message(List messages, int maxToken, final Consumer callback); /** * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 @@ -49,6 +93,17 @@ public interface GrokService extends AIService { */ String chatVision(String prompt, final List images, String detail); + /** + * 图像理解-SSE流式输出 + * + * @param prompt 题词 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + void chatVision(String prompt, final List images, String detail,final Consumer callback); + /** * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 * @@ -61,6 +116,18 @@ public interface GrokService extends AIService { return chatVision(prompt, images, GrokCommon.GrokVision.AUTO.getDetail()); } + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 传入|的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + default void chatVision(String prompt, final List images, final Consumer callback){ + chatVision(prompt, images, GrokCommon.GrokVision.AUTO.getDetail(), callback); + } + /** * 列出所有model列表 * @@ -112,4 +179,14 @@ public interface GrokService extends AIService { * @since 6.0.0 */ String deferredCompletion(String requestId); + + /** + * 文生图 + * 请设置config中model为支持图片功能的模型,目前支持GROK_2_IMAGE + * + * @param prompt 题词 + * @return 包含生成图片的url + * @since 6.0.0 + */ + String imagesGenerations(String prompt); } diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/grok/GrokServiceImpl.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/grok/GrokServiceImpl.java index 9353628f6..f127e0c30 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/grok/GrokServiceImpl.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/grok/GrokServiceImpl.java @@ -19,6 +19,7 @@ package org.dromara.hutool.ai.model.grok; import org.dromara.hutool.ai.core.AIConfig; import org.dromara.hutool.ai.core.BaseAIService; import org.dromara.hutool.ai.core.Message; +import org.dromara.hutool.core.thread.ThreadUtil; import org.dromara.hutool.http.client.Response; import org.dromara.hutool.json.JSONUtil; @@ -26,6 +27,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; /** * Grok服务,AI具体功能的实现 @@ -47,21 +49,14 @@ public class GrokServiceImpl extends BaseAIService implements GrokService { private final String TOKENIZE_TEXT = "/tokenize-text"; //获取延迟对话 private final String DEFERRED_COMPLETION = "/chat/deferred-completion"; + //文生图 + private final String IMAGES_GENERATIONS = "/images/generations"; public GrokServiceImpl(final AIConfig config) { //初始化grok客户端 super(config); } - @Override - public String chat(String prompt) { - // 定义消息结构 - final List messages = new ArrayList<>(); - messages.add(new Message("system", "You are a helpful assistant")); - messages.add(new Message("user", prompt)); - return chat(messages); - } - @Override public String chat(final List messages) { String paramJson = buildChatRequestBody(messages); @@ -70,16 +65,24 @@ public class GrokServiceImpl extends BaseAIService implements GrokService { } @Override - public String message(String prompt, int maxToken) { - // 定义消息结构 - final List messages = new ArrayList<>(); - messages.add(new Message("system", "You are a helpful assistant")); - messages.add(new Message("user", prompt)); + public void chat(List messages, Consumer callback) { + Map paramMap = buildChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "grok-chat-sse").start(); + } + + @Override + public String message(final List messages, int maxToken) { String paramJson = buildMessageRequestBody(messages, maxToken); Response response = sendPost(MESSAGES, paramJson); return response.bodyStr(); } + @Override + public void message(List messages, int maxToken, final Consumer callback) { + Map paramMap = buildMessageStreamRequestBody(messages, maxToken); + ThreadUtil.newThread(() -> sendPostStream(MESSAGES, paramMap, callback::accept), "grok-message-sse").start(); + } + @Override public String chatVision(String prompt, final List images, String detail) { String paramJson = buildChatVisionRequestBody(prompt, images, detail); @@ -87,6 +90,12 @@ public class GrokServiceImpl extends BaseAIService implements GrokService { return response.bodyStr(); } + @Override + public void chatVision(String prompt, List images, String detail, Consumer callback) { + Map paramMap = buildChatVisionStreamRequestBody(prompt, images, detail); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "grok-chatVision-sse").start(); + } + @Override public String models() { Response response = sendGet(MODELS_ENDPOINT); @@ -124,6 +133,13 @@ public class GrokServiceImpl extends BaseAIService implements GrokService { return response.bodyStr(); } + @Override + public String imagesGenerations(String prompt) { + String paramJson = buildImagesGenerationsRequestBody(prompt); + Response response = sendPost(IMAGES_GENERATIONS, paramJson); + return response.bodyStr(); + } + // 构建chat请求体 private String buildChatRequestBody(final List messages) { //使用JSON工具 @@ -136,6 +152,18 @@ public class GrokServiceImpl extends BaseAIService implements GrokService { return JSONUtil.toJsonStr(paramMap); } + private Map buildChatStreamRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + //构建chatVision请求体 private String buildChatVisionRequestBody(String prompt, final List images, String detail) { // 定义消息结构 @@ -167,6 +195,37 @@ public class GrokServiceImpl extends BaseAIService implements GrokService { return JSONUtil.toJsonStr(paramMap); } + private Map buildChatVisionStreamRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return paramMap; + } + //构建消息回复请求体 private String buildMessageRequestBody(final List messages, int maxToken) { final Map paramMap = new HashMap<>(); @@ -179,6 +238,18 @@ public class GrokServiceImpl extends BaseAIService implements GrokService { return JSONUtil.toJsonStr(paramMap); } + private Map buildMessageStreamRequestBody(final List messages, int maxToken) { + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + paramMap.put("max_tokens", maxToken); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + //构建分词请求体 private String buildTokenizeRequestBody(String text) { //使用JSON工具 @@ -190,4 +261,15 @@ public class GrokServiceImpl extends BaseAIService implements GrokService { return JSONUtil.toJsonStr(paramMap); } + + //构建文生图请求体 + private String buildImagesGenerationsRequestBody(String prompt) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } } diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolCommon.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolCommon.java new file mode 100644 index 000000000..bb33e63ce --- /dev/null +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolCommon.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.ai.model.hutool; + +/** + * hutool公共类 + * + * @author elichow + * @since 6.0.0 + */ +public class HutoolCommon { + + //hutool视觉参数 + public enum HutoolVision { + + AUTO("auto"), + LOW("low"), + HIGH("high"); + + private final String detail; + + HutoolVision(String detail) { + this.detail = detail; + } + + public String getDetail() { + return detail; + } + } + + //hutool音频参数 + public enum HutoolSpeech { + + ALLOY("alloy"), + ASH("ash"), + CORAL("coral"), + ECHO("echo"), + FABLE("fable"), + ONYX("onyx"), + NOVA("nova"), + SAGE("sage"), + SHIMMER("shimmer"); + + private final String voice; + + HutoolSpeech(String voice) { + this.voice = voice; + } + + public String getVoice() { + return voice; + } + } + + //hutool视频生成参数 + public enum HutoolVideo { + + //宽高比例 + RATIO_16_9("--rt", "16:9"),//[1280, 720] + RATIO_4_3("--rt", "4:3"),//[960, 720] + RATIO_1_1("--rt", "1:1"),//[720, 720] + RATIO_3_4("--rt", "3:4"),//[720, 960] + RATIO_9_16("--rt", "9:16"),//[720, 1280] + RATIO_21_9("--rt", "21:9"),//[1280, 544] + + //生成视频时长 + DURATION_5("--dur", 5),//文生视频,图生视频 + DURATION_10("--dur", 10),//文生视频 + + //帧率,即一秒时间内视频画面数量 + FPS_5("--fps", 24), + + //视频分辨率 + RESOLUTION_5("--rs", "720p"), + + //生成视频是否包含水印 + WATERMARK_TRUE("--wm", true), + WATERMARK_FALSE("--wm", false); + + private final String type; + private final Object value; + + HutoolVideo(String type, Object value) { + this.type = type; + this.value = value; + } + + public String getType() { + return type; + } + + public Object getValue() { + if (value instanceof String) { + return (String) value; + } else if (value instanceof Integer) { + return (Integer) value; + } else if (value instanceof Boolean) { + return (Boolean) value; + } + return value; + } + + } + +} diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolConfig.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolConfig.java new file mode 100644 index 000000000..5a61d4738 --- /dev/null +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.ai.model.hutool; + +import org.dromara.hutool.ai.Models; +import org.dromara.hutool.ai.core.BaseConfig; + +/** + * Hutool配置类,初始化API接口地址,设置默认的模型 + * + * @author elichow + * @since 6.0.0 + */ +public class HutoolConfig extends BaseConfig { + + private final String API_URL = "https://api.hutool.cn/ai/api"; + + private final String DEFAULT_MODEL = Models.Hutool.HUTOOL.getModel(); + + public HutoolConfig() { + setApiUrl(API_URL); + setModel(DEFAULT_MODEL); + } + + public HutoolConfig(String apiKey) { + this(); + setApiKey(apiKey); + } + + @Override + public String getModelName() { + return "hutool"; + } + +} diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolProvider.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolProvider.java new file mode 100644 index 000000000..c2bff59f3 --- /dev/null +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.ai.model.hutool; + +import org.dromara.hutool.ai.core.AIConfig; +import org.dromara.hutool.ai.core.AIServiceProvider; + +/**r + * 创建Hutool服务实现类 + * + * @author elichow + * @since 6.0.0 + */ +public class HutoolProvider implements AIServiceProvider { + + @Override + public String getServiceName() { + return "hutool"; + } + + @Override + public HutoolService create(final AIConfig config) { + return new HutoolServiceImpl(config); + } +} diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolService.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolService.java new file mode 100644 index 000000000..96ad5f179 --- /dev/null +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolService.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.ai.model.hutool; + +import org.dromara.hutool.ai.core.AIService; + +import java.io.File; +import java.io.InputStream; +import java.util.List; +import java.util.function.Consumer; + +/** + * hutool支持的扩展接口 + * + * @author elichow + * @since 6.0.0 + */ +public interface HutoolService extends AIService { + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @return AI回答 + * @since 6.0.0 + */ + String chatVision(String prompt, final List images, String detail); + + /** + * 图像理解-SSE流式输出 + * + * @param prompt 题词 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + void chatVision(String prompt, final List images, String detail,final Consumer callback); + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @return AI回答 + * @since 6.0.0 + */ + default String chatVision(String prompt, final List images) { + return chatVision(prompt, images, HutoolCommon.HutoolVision.AUTO.getDetail()); + } + + /** + * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 + * + * @param prompt 题词 + * @param images 传入|的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + default void chatVision(String prompt, final List images, final Consumer callback){ + chatVision(prompt, images, HutoolCommon.HutoolVision.AUTO.getDetail(), callback); + } + + /** + * 分词:可以将文本转换为模型可理解的 token 信息 + * + * @param text 需要分词的内容 + * @return 分词结果 + * @since 6.0.0 + */ + String tokenizeText(String text); + + /** + * 文生图 + * + * @param prompt 题词 + * @return 包含生成图片的url + * @since 6.0.0 + */ + String imagesGenerations(String prompt); + + /** + * 图文向量化:仅支持单一文本、单张图片或文本与图片的组合输入(即一段文本 + 一张图片),暂不支持批量文本 / 图片的同时处理 + * + * @param text 需要向量化的内容 + * @param image 需要向量化的图片地址/或者图片Base64编码图片(URI形式) + * @return 处理后的向量信息 + * @since 6.0.0 + */ + String embeddingVision(String text, String image); + + /** + * TTS文本转语音 + * + * @param input 需要转成语音的文本 + * @param voice AI的音色 + * @return 返回的音频mp3文件流 + * @since 6.0.0 + */ + InputStream tts(String input, final HutoolCommon.HutoolSpeech voice); + + /** + * TTS文本转语音 + * + * @param input 需要转成语音的文本 + * @return 返回的音频mp3文件流 + * @since 6.0.0 + */ + default InputStream tts(String input) { + return tts(input, HutoolCommon.HutoolSpeech.ALLOY); + } + + /** + * STT音频转文本 + * + * @param file 需要转成文本的音频文件 + * @return 返回的文本内容 + * @since 6.0.0 + */ + String stt(final File file); + + /** + * 创建视频生成任务 + * + * @param text 文本提示词 + * @param image 图片/或者图片Base64编码图片(URI形式) + * @param videoParams 视频参数列表 + * @return 生成任务id + * @since 6.0.0 + */ + String videoTasks(String text, String image, final List videoParams); + + /** + * 创建视频生成任务 + * + * @param text 文本提示词 + * @param image 图片/或者图片Base64编码图片(URI形式) + * @return 生成任务id + * @since 6.0.0 + */ + default String videoTasks(String text, String image) { + return videoTasks(text, image, null); + } + + /** + * 查询视频生成任务信息 + * + * @param taskId 通过创建生成视频任务返回的生成任务id + * @return 生成任务信息 + * @since 6.0.0 + */ + String getVideoTasksInfo(String taskId); + +} diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolServiceImpl.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolServiceImpl.java new file mode 100644 index 000000000..0ad646f96 --- /dev/null +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/HutoolServiceImpl.java @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.ai.model.hutool; + +import org.dromara.hutool.ai.AIException; +import org.dromara.hutool.ai.core.AIConfig; +import org.dromara.hutool.ai.core.BaseAIService; +import org.dromara.hutool.ai.core.Message; +import org.dromara.hutool.core.text.StrUtil; +import org.dromara.hutool.core.thread.ThreadUtil; +import org.dromara.hutool.http.client.Response; +import org.dromara.hutool.json.JSONUtil; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Hutool服务,AI具体功能的实现 + * + * @author elichow + * @since 6.0.0 + */ +public class HutoolServiceImpl extends BaseAIService implements HutoolService { + + //对话补全 + private final String CHAT_ENDPOINT = "/chat/completions"; + //分词 + private final String TOKENIZE_TEXT = "/tokenize/text"; + //文生图 + private final String IMAGES_GENERATIONS = "/images/generations"; + //图文向量化 + private final String EMBEDDING_VISION = "/embeddings/multimodal"; + //文本转语音 + private final String TTS = "/audio/tts"; + //语音转文本 + private final String STT = "/audio/stt"; + //创建视频生成任务 + private final String CREATE_VIDEO = "/video/generations"; + + public HutoolServiceImpl(final AIConfig config) { + //初始化hutool客户端 + super(config); + } + + @Override + public String chat(final List messages) { + String paramJson = buildChatRequestBody(messages); + Response response = sendPost(CHAT_ENDPOINT, paramJson); + return response.bodyStr(); + } + + @Override + public void chat(List messages,Consumer callback) { + Map paramMap = buildChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "hutool-chat-sse").start(); + } + + @Override + public String chatVision(String prompt, final List images, String detail) { + String paramJson = buildChatVisionRequestBody(prompt, images, detail); + Response response = sendPost(CHAT_ENDPOINT, paramJson); + return response.bodyStr(); + } + + @Override + public void chatVision(String prompt, List images, String detail, Consumer callback) { + Map paramMap = buildChatVisionStreamRequestBody(prompt, images, detail); + System.out.println(JSONUtil.toJsonStr(paramMap)); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "hutool-chatVision-sse").start(); + } + + @Override + public String tokenizeText(String text) { + String paramJson = buildTokenizeRequestBody(text); + Response response = sendPost(TOKENIZE_TEXT, paramJson); + return response.bodyStr(); + } + + @Override + public String imagesGenerations(String prompt) { + String paramJson = buildImagesGenerationsRequestBody(prompt); + Response response = sendPost(IMAGES_GENERATIONS, paramJson); + return response.bodyStr(); + } + + + @Override + public String embeddingVision(String text, String image) { + String paramJson = buildEmbeddingVisionRequestBody(text, image); + Response response = sendPost(EMBEDDING_VISION, paramJson); + return response.bodyStr(); + } + + @Override + public InputStream tts(String input, final HutoolCommon.HutoolSpeech voice) { + try { + String paramJson = buildTTSRequestBody(input, voice.getVoice()); + Response response = sendPost(TTS, paramJson); + + // 检查响应内容类型 + String contentType = response.header("Content-Type"); + if (contentType != null && contentType.startsWith("application/json")) { + // 如果是JSON响应,说明有错误 + String errorBody = response.bodyStr(); + throw new AIException("TTS请求失败: " + errorBody); + } + // 默认返回音频流 + return response.bodyStream(); + } catch (Exception e) { + throw new AIException("TTS处理失败: " + e.getMessage(), e); + } + } + + @Override + public String stt(final File file) { + final Map paramMap = buildSTTRequestBody(file); + Response response = sendFormData(STT, paramMap); + return response.bodyStr(); + } + + + @Override + public String videoTasks(String text, String image, final List videoParams) { + String paramJson = buildGenerationsTasksRequestBody(text, image, videoParams); + Response response = sendPost(CREATE_VIDEO, paramJson); + return response.bodyStr(); + } + + @Override + public String getVideoTasksInfo(String taskId) { + Response response = sendGet(CREATE_VIDEO + "/" + taskId); + return response.bodyStr(); + } + + + // 构建chat请求体 + private String buildChatRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatStreamRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + //构建chatVision请求体 + private String buildChatVisionRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return JSONUtil.toJsonStr(paramMap); + } + + private Map buildChatVisionStreamRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return paramMap; + } + + + //构建分词请求体 + private String buildTokenizeRequestBody(String text) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("text", text); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + //构建文生图请求体 + private String buildImagesGenerationsRequestBody(String prompt) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("prompt", prompt); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + //构建图文向量化请求体 + private String buildEmbeddingVisionRequestBody(String text, String image) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + + final List input = new ArrayList<>(); + //添加文本参数 + if (!StrUtil.isBlank(text)) { + final Map textMap = new HashMap<>(); + textMap.put("type", "text"); + textMap.put("text", text); + input.add(textMap); + } + //添加图片参数 + if (!StrUtil.isBlank(image)) { + final Map imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + final Map urlMap = new HashMap<>(); + urlMap.put("url", image); + imgUrlMap.put("image_url", urlMap); + input.add(imgUrlMap); + } + + paramMap.put("input", input); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + System.out.println(JSONUtil.toJsonStr(paramMap)); + return JSONUtil.toJsonStr(paramMap); + } + + + //构建TTS请求体 + private String buildTTSRequestBody(String input, String voice) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("input", input); + paramMap.put("voice", voice); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return JSONUtil.toJsonStr(paramMap); + } + + //构建STT请求体 + private Map buildSTTRequestBody(final File file) { + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + paramMap.put("file", file); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + + //构建创建视频任务请求体 + private String buildGenerationsTasksRequestBody(String text, String image, final List videoParams) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("model", config.getModel()); + + final List content = new ArrayList<>(); + //添加文本参数 + final Map textMap = new HashMap<>(); + if (!StrUtil.isBlank(text)) { + textMap.put("type", "text"); + textMap.put("text", text); + content.add(textMap); + } + //添加图片参数 + if (!StrUtil.isBlank(image)) { + final Map imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + final Map urlMap = new HashMap<>(); + urlMap.put("url", image); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + //添加视频参数 + if (videoParams != null && !videoParams.isEmpty()) { + //如果有文本参数就加在后面 + if (textMap != null && !textMap.isEmpty()) { + int textIndex = content.indexOf(textMap); + StringBuilder textBuilder = new StringBuilder(text); + for (HutoolCommon.HutoolVideo videoParam : videoParams) { + textBuilder.append(" ").append(videoParam.getType()).append(" ").append(videoParam.getValue()); + } + textMap.put("type", "text"); + textMap.put("text", textBuilder.toString()); + + if (textIndex != -1) { + content.set(textIndex, textMap); + } else { + content.add(textMap); + } + } else { + //如果没有文本参数就重新增加 + StringBuilder textBuilder = new StringBuilder(); + for (HutoolCommon.HutoolVideo videoParam : videoParams) { + textBuilder.append(videoParam.getType()).append(videoParam.getValue()).append(" "); + } + textMap.put("type", "text"); + textMap.put("text", textBuilder.toString()); + content.add(textMap); + } + } + + paramMap.put("content", content); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + System.out.println(JSONUtil.toJsonStr(paramMap)); + return JSONUtil.toJsonStr(paramMap); + } + +} diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/package-info.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/package-info.java new file mode 100644 index 000000000..dc3cd9275 --- /dev/null +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/hutool/package-info.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * 对hutool的封装实现 + * + * @author elichow + * @since 6.0.0 + */ + +package org.dromara.hutool.ai.model.hutool; diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/openai/OpenaiService.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/openai/OpenaiService.java index 1c8dbddd2..b1e05bf65 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/openai/OpenaiService.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/openai/OpenaiService.java @@ -21,7 +21,9 @@ import org.dromara.hutool.ai.core.Message; import java.io.File; import java.io.InputStream; +import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; /** * openai支持的扩展接口 @@ -42,6 +44,17 @@ public interface OpenaiService extends AIService { */ String chatVision(String prompt, final List images, String detail); + /** + * 图像理解-SSE流式输出 + * + * @param prompt 题词 + * @param images 图片列表/或者图片Base64编码图片列表(URI形式) + * @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + void chatVision(String prompt, final List images, String detail,final Consumer callback); + /** * 图像理解:模型会依据传入的图片信息以及问题,给出回复。 * @@ -54,6 +67,18 @@ public interface OpenaiService extends AIService { return chatVision(prompt, images, OpenaiCommon.OpenaiVision.AUTO.getDetail()); } + /** + * 图像理解-SSE流式输出 + * + * @param prompt 题词 + * @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式) + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + default void chatVision(String prompt, final List images, final Consumer callback){ + chatVision(prompt, images, OpenaiCommon.OpenaiVision.AUTO.getDetail(), callback); + } + /** * 文生图 请设置config中model为支持图片功能的模型 DALL·E系列 * @@ -166,7 +191,28 @@ public interface OpenaiService extends AIService { * @return AI回答 * @since 6.0.0 */ - String chatReasoning(String prompt, String reasoningEffort); + default String chatReasoning(String prompt, String reasoningEffort){ + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + return chatReasoning(messages, reasoningEffort); + } + + /** + * 推理chat-SSE流式输出 + * 支持o3-mini和o1 + * + * @param prompt 对话题词 + * @param reasoningEffort 推理程度 + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + default void chatReasoning(String prompt, String reasoningEffort, final Consumer callback){ + final List messages = new ArrayList<>(); + messages.add(new Message("system", "You are a helpful assistant")); + messages.add(new Message("user", prompt)); + chatReasoning(messages, reasoningEffort, callback); + } /** * 推理chat @@ -180,6 +226,18 @@ public interface OpenaiService extends AIService { return chatReasoning(prompt, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort()); } + /** + * 推理chat-SSE流式输出 + * 支持o3-mini和o1 + * + * @param prompt 对话题词 + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + default void chatReasoning(String prompt, final Consumer callback) { + chatReasoning(prompt, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort(), callback); + } + /** * 推理chat * 支持o3-mini和o1 @@ -191,6 +249,17 @@ public interface OpenaiService extends AIService { */ String chatReasoning(final List messages, String reasoningEffort); + /** + * 推理chat-SSE流式输出 + * 支持o3-mini和o1 + * + * @param messages 消息列表 + * @param reasoningEffort 推理程度 + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + void chatReasoning(final List messages, String reasoningEffort, final Consumer callback); + /** * 推理chat * 支持o3-mini和o1 @@ -203,4 +272,16 @@ public interface OpenaiService extends AIService { return chatReasoning(messages, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort()); } + /** + * 推理chat-SSE流式输出 + * 支持o3-mini和o1 + * + * @param messages 消息列表 + * @param callback 流式数据回调函数 + * @since 6.0.0 + */ + default void chatReasoning(final List messages, final Consumer callback) { + chatReasoning(messages, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort(), callback); + } + } diff --git a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/openai/OpenaiServiceImpl.java b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/openai/OpenaiServiceImpl.java index e337a0017..06403a848 100644 --- a/hutool-ai/src/main/java/org/dromara/hutool/ai/model/openai/OpenaiServiceImpl.java +++ b/hutool-ai/src/main/java/org/dromara/hutool/ai/model/openai/OpenaiServiceImpl.java @@ -20,6 +20,7 @@ import org.dromara.hutool.ai.core.AIConfig; import org.dromara.hutool.ai.core.BaseAIService; import org.dromara.hutool.ai.core.Message; import org.dromara.hutool.core.text.StrUtil; +import org.dromara.hutool.core.thread.ThreadUtil; import org.dromara.hutool.http.client.Response; import org.dromara.hutool.json.JSONUtil; @@ -29,6 +30,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; /** * openai服务,AI具体功能的实现 @@ -60,15 +62,6 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService { super(config); } - @Override - public String chat(String prompt) { - // 定义消息结构 - final List messages = new ArrayList<>(); - messages.add(new Message("system", "You are a helpful assistant")); - messages.add(new Message("user", prompt)); - return chat(messages); - } - @Override public String chat(final List messages) { String paramJson = buildChatRequestBody(messages); @@ -76,6 +69,12 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService { return response.bodyStr(); } + @Override + public void chat(List messages, Consumer callback) { + Map paramMap = buildChatStreamRequestBody(messages); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "openai-chat-sse").start(); + } + @Override public String chatVision(String prompt, final List images, String detail) { String paramJson = buildChatVisionRequestBody(prompt, images, detail); @@ -83,6 +82,12 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService { return response.bodyStr(); } + @Override + public void chatVision(String prompt, List images, String detail, Consumer callback) { + Map paramMap = buildChatVisionStreamRequestBody(prompt, images, detail); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "openai-chatVision-sse").start(); + } + @Override public String imagesGenerations(String prompt) { String paramJson = buildImagesGenerationsRequestBody(prompt); @@ -132,15 +137,6 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService { return response.bodyStr(); } - @Override - public String chatReasoning(String prompt, String reasoningEffort) { - // 定义消息结构 - final List messages = new ArrayList<>(); - messages.add(new Message("system", "You are a helpful assistant")); - messages.add(new Message("user", prompt)); - return chat(messages); - } - @Override public String chatReasoning(final List messages, String reasoningEffort) { String paramJson = buildChatReasoningRequestBody(messages, reasoningEffort); @@ -148,6 +144,12 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService { return response.bodyStr(); } + @Override + public void chatReasoning(List messages, String reasoningEffort, Consumer callback) { + Map paramMap = buildChatReasoningStreamRequestBody(messages, reasoningEffort); + ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "openai-chatReasoning-sse").start(); + } + // 构建chat请求体 private String buildChatRequestBody(final List messages) { //使用JSON工具 @@ -160,6 +162,18 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService { return JSONUtil.toJsonStr(paramMap); } + private Map buildChatStreamRequestBody(final List messages) { + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + //构建chatVision请求体 private String buildChatVisionRequestBody(String prompt, final List images, String detail) { // 定义消息结构 @@ -191,6 +205,37 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService { return JSONUtil.toJsonStr(paramMap); } + private Map buildChatVisionStreamRequestBody(String prompt, final List images, String detail) { + // 定义消息结构 + final List messages = new ArrayList<>(); + final List content = new ArrayList<>(); + + final Map contentMap = new HashMap<>(); + contentMap.put("type", "text"); + contentMap.put("text", prompt); + content.add(contentMap); + for (String img : images) { + HashMap imgUrlMap = new HashMap<>(); + imgUrlMap.put("type", "image_url"); + HashMap urlMap = new HashMap<>(); + urlMap.put("url", img); + urlMap.put("detail", detail); + imgUrlMap.put("image_url", urlMap); + content.add(imgUrlMap); + } + + messages.add(new Message("user", content)); + + //使用JSON工具 + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + return paramMap; + } + //构建文生图请求体 private String buildImagesGenerationsRequestBody(String prompt) { final Map paramMap = new HashMap<>(); @@ -305,4 +350,16 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService { return JSONUtil.toJsonStr(paramMap); } + private Map buildChatReasoningStreamRequestBody(final List messages, String reasoningEffort) { + final Map paramMap = new HashMap<>(); + paramMap.put("stream", true); + paramMap.put("model", config.getModel()); + paramMap.put("messages", messages); + paramMap.put("reasoning_effort", reasoningEffort); + //合并其他参数 + paramMap.putAll(config.getAdditionalConfigMap()); + + return paramMap; + } + } diff --git a/hutool-ai/src/main/resources/META-INF/services/org.dromara.hutool.ai.core.AIConfig b/hutool-ai/src/main/resources/META-INF/services/org.dromara.hutool.ai.core.AIConfig index 211e9ad56..89b240e6c 100644 --- a/hutool-ai/src/main/resources/META-INF/services/org.dromara.hutool.ai.core.AIConfig +++ b/hutool-ai/src/main/resources/META-INF/services/org.dromara.hutool.ai.core.AIConfig @@ -1,3 +1,4 @@ +org.dromara.hutool.ai.model.hutool.HutoolConfig org.dromara.hutool.ai.model.deepseek.DeepSeekConfig org.dromara.hutool.ai.model.openai.OpenaiConfig org.dromara.hutool.ai.model.doubao.DoubaoConfig diff --git a/hutool-ai/src/main/resources/META-INF/services/org.dromara.hutool.ai.core.AIServiceProvider b/hutool-ai/src/main/resources/META-INF/services/org.dromara.hutool.ai.core.AIServiceProvider index 97a1bb272..a2ae919e2 100644 --- a/hutool-ai/src/main/resources/META-INF/services/org.dromara.hutool.ai.core.AIServiceProvider +++ b/hutool-ai/src/main/resources/META-INF/services/org.dromara.hutool.ai.core.AIServiceProvider @@ -1,3 +1,4 @@ +org.dromara.hutool.ai.model.hutool.HutoolProvider org.dromara.hutool.ai.model.deepseek.DeepSeekProvider org.dromara.hutool.ai.model.openai.OpenaiProvider org.dromara.hutool.ai.model.doubao.DoubaoProvider diff --git a/hutool-ai/src/test/java/org/dromara/hutool/ai/AIUtilTest.java b/hutool-ai/src/test/java/org/dromara/hutool/ai/AIUtilTest.java index ab7d46456..4dbfa1fc9 100644 --- a/hutool-ai/src/test/java/org/dromara/hutool/ai/AIUtilTest.java +++ b/hutool-ai/src/test/java/org/dromara/hutool/ai/AIUtilTest.java @@ -22,6 +22,7 @@ import org.dromara.hutool.ai.core.Message; import org.dromara.hutool.ai.model.deepseek.DeepSeekService; import org.dromara.hutool.ai.model.doubao.DoubaoService; import org.dromara.hutool.ai.model.grok.GrokService; +import org.dromara.hutool.ai.model.hutool.HutoolService; import org.dromara.hutool.ai.model.openai.OpenaiService; import org.junit.jupiter.api.Test; @@ -46,6 +47,12 @@ class AIUtilTest { assertNotNull(aiService); } + @Test + void getHutoolService() { + final HutoolService hutoolService = AIUtil.getHutoolService(new AIConfigBuilder(ModelName.HUTOOL.getValue()).setApiKey(key).build()); + assertNotNull(hutoolService); + } + @Test void getDeepSeekService() { final DeepSeekService deepSeekService = AIUtil.getDeepSeekService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build()); diff --git a/hutool-ai/src/test/java/org/dromara/hutool/ai/model/deepseek/DeepSeekServiceTest.java b/hutool-ai/src/test/java/org/dromara/hutool/ai/model/deepseek/DeepSeekServiceTest.java index 47249046c..87258b6a4 100644 --- a/hutool-ai/src/test/java/org/dromara/hutool/ai/model/deepseek/DeepSeekServiceTest.java +++ b/hutool-ai/src/test/java/org/dromara/hutool/ai/model/deepseek/DeepSeekServiceTest.java @@ -20,11 +20,15 @@ import org.dromara.hutool.ai.AIServiceFactory; import org.dromara.hutool.ai.ModelName; import org.dromara.hutool.ai.core.AIConfigBuilder; import org.dromara.hutool.ai.core.Message; +import org.dromara.hutool.core.thread.ThreadUtil; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertNotNull; class DeepSeekServiceTest { @@ -35,7 +39,30 @@ class DeepSeekServiceTest { @Disabled void chat(){ final String chat = deepSeekService.chat("写一个疯狂星期四广告词"); - System.out.println(chat); + assertNotNull(chat); + } + + @Test + @Disabled + void chatStream() { + String prompt = "写一个疯狂星期四广告词"; + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + + deepSeekService.chat(prompt, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } } @Test @@ -45,27 +72,50 @@ class DeepSeekServiceTest { messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话")); messages.add(new Message("user","给我说一个笑话")); final String chat = deepSeekService.chat(messages); - System.out.println(chat); + assertNotNull(chat); } @Test @Disabled void beta() { final String beta = deepSeekService.beta("写一个疯狂星期四广告词"); - System.out.println(beta); + assertNotNull(beta); + } + + @Test + @Disabled + void betaStream() { + String beta = "写一个疯狂星期四广告词"; + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + + deepSeekService.beta(beta, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } } @Test @Disabled void models() { final String models = deepSeekService.models(); - System.out.println(models); + assertNotNull(models); } @Test @Disabled void balance() { final String balance = deepSeekService.balance(); - System.out.println(balance); + assertNotNull(balance); } } diff --git a/hutool-ai/src/test/java/org/dromara/hutool/ai/model/doubao/DoubaoServiceTest.java b/hutool-ai/src/test/java/org/dromara/hutool/ai/model/doubao/DoubaoServiceTest.java index f3147f2b4..04b0f7614 100644 --- a/hutool-ai/src/test/java/org/dromara/hutool/ai/model/doubao/DoubaoServiceTest.java +++ b/hutool-ai/src/test/java/org/dromara/hutool/ai/model/doubao/DoubaoServiceTest.java @@ -21,6 +21,7 @@ import org.dromara.hutool.ai.ModelName; import org.dromara.hutool.ai.Models; import org.dromara.hutool.ai.core.AIConfigBuilder; import org.dromara.hutool.ai.core.Message; +import org.dromara.hutool.core.thread.ThreadUtil; import org.dromara.hutool.swing.img.ImgUtil; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -29,6 +30,9 @@ import java.awt.*; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertNotNull; class DoubaoServiceTest { @@ -39,7 +43,30 @@ class DoubaoServiceTest { @Disabled void chat(){ final String chat = doubaoService.chat("写一个疯狂星期四广告词"); - System.out.println(chat); + assertNotNull(chat); + } + + @Test + @Disabled + void chatStream() { + String prompt = "写一个疯狂星期四广告词"; + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + + doubaoService.chat(prompt, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } } @Test @@ -49,7 +76,7 @@ class DoubaoServiceTest { messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话")); messages.add(new Message("user","给我说一个笑话")); final String chat = doubaoService.chat(messages); - System.out.println(chat); + assertNotNull(chat); } @Test @@ -59,7 +86,34 @@ class DoubaoServiceTest { .setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class); final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png"); final String chatVision = doubaoService.chatVision("图片上有些什么?", Arrays.asList(base64)); - System.out.println(chatVision); + assertNotNull(chatVision); + } + + @Test + @Disabled + void testChatVisionStream() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class); + + String prompt = "图片上有些什么?"; + List images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + doubaoService.chatVision(prompt,images, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } } @Test @@ -68,17 +122,17 @@ class DoubaoServiceTest { final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) .setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class); final String chatVision = doubaoService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"),DoubaoCommon.DoubaoVision.HIGH.getDetail()); - System.out.println(chatVision); + assertNotNull(chatVision); } @Test @Disabled void videoTasks() { final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) - .setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class); + .setApiKey(key).setModel(Models.Doubao.Doubao_Seedance_1_0_lite_i2v.getModel()).build(), DoubaoService.class); final String videoTasks = doubaoService.videoTasks("生成一段动画视频,主角是大耳朵图图,一个活泼可爱的小男孩。视频中图图在公园里玩耍," + "画面采用明亮温暖的卡通风格,色彩鲜艳,动作流畅。背景音乐轻快活泼,带有冒险感,音效包括鸟叫声、欢笑声和山洞回声。", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); - System.out.println(videoTasks);//cgt-20250306170051-6r9gk + assertNotNull(videoTasks);//cgt-20250306170051-6r9gk } @Test @@ -88,7 +142,7 @@ class DoubaoServiceTest { final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) .setApiKey(key).build(), DoubaoService.class); final String videoTasksInfo = doubaoService.getVideoTasksInfo("cgt-20250306170051-6r9gk"); - System.out.println(videoTasksInfo); + assertNotNull(videoTasksInfo); } @Test @@ -97,7 +151,7 @@ class DoubaoServiceTest { final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) .setApiKey(key).setModel(Models.Doubao.DOUBAO_EMBEDDING_TEXT_240715.getModel()).build(), DoubaoService.class); final String embeddingText = doubaoService.embeddingText(new String[]{"阿斯顿", "马丁"}); - System.out.println(embeddingText); + assertNotNull(embeddingText); } @Test @@ -106,7 +160,7 @@ class DoubaoServiceTest { final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) .setApiKey(key).setModel(Models.Doubao.DOUBAO_EMBEDDING_VISION.getModel()).build(), DoubaoService.class); final String embeddingVision = doubaoService.embeddingVision("天空好难", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); - System.out.println(embeddingVision); + assertNotNull(embeddingVision); } @Test @@ -118,14 +172,41 @@ class DoubaoServiceTest { messages.add(new Message("system","你是什么都可以")); messages.add(new Message("user","你想做些什么")); final String botsChat = doubaoService.botsChat(messages); - System.out.println(botsChat); + assertNotNull(botsChat); + } + + @Test + @Disabled + void botsChatStream() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel("your bots id").build(), DoubaoService.class); + final ArrayList messages = new ArrayList<>(); + messages.add(new Message("system","你是什么都可以")); + messages.add(new Message("user","你想做些什么")); + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + doubaoService.botsChat(messages, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } } @Test @Disabled void tokenization() { final String tokenization = doubaoService.tokenization(new String[]{"阿斯顿", "马丁"}); - System.out.println(tokenization); + assertNotNull(tokenization); } @Test @@ -134,7 +215,7 @@ class DoubaoServiceTest { final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) .setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class); final String batchChat = doubaoService.batchChat("写首歌词"); - System.out.println(batchChat); + assertNotNull(batchChat); } @Test @@ -146,7 +227,7 @@ class DoubaoServiceTest { messages.add(new Message("system","你是个抽象大师")); messages.add(new Message("user","写一个KFC的抽象广告")); final String batchChat = doubaoService.batchChat(messages); - System.out.println(batchChat); + assertNotNull(batchChat); } @Test @@ -157,7 +238,7 @@ class DoubaoServiceTest { final List messages = new ArrayList<>(); messages.add(new Message("system","你是个抽象大师,你真的很抽象")); final String context = doubaoService.createContext(messages);//ctx-20250307092153-cvslm - System.out.println(context); + assertNotNull(context); } @Test @@ -168,17 +249,16 @@ class DoubaoServiceTest { final List messages = new ArrayList<>(); messages.add(new Message("system","你是个抽象大师,你真的很抽象")); final String context = doubaoService.createContext(messages,DoubaoCommon.DoubaoContext.COMMON_PREFIX.getMode()); - System.out.println(context);//ctx-20250307092153-cvslm + assertNotNull(context); } @Test @Disabled void chatContext() { - //ctx-20250307092153-cvslm final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) .setApiKey(key).setModel("eyour Endpoint ID").build(), DoubaoService.class); - final String chatContext = doubaoService.chatContext("你是谁?", "ctx-20250307092153-cvslm"); - System.out.println(chatContext); + final String chatContext = doubaoService.chatContext("你是谁?", "your contextId"); + assertNotNull(chatContext); } @Test @@ -188,7 +268,43 @@ class DoubaoServiceTest { .setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class); final List messages = new ArrayList<>(); messages.add(new Message("user","你怎么看待意大利面拌水泥?")); - final String chatContext = doubaoService.chatContext(messages, "ctx-20250307092153-cvslm"); - System.out.println(chatContext); + final String chatContext = doubaoService.chatContext(messages, "your contextId"); + assertNotNull(chatContext); + } + + @Test + @Disabled + void testChatContextStream() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class); + final List messages = new ArrayList<>(); + messages.add(new Message("user","你怎么看待意大利面拌水泥?")); + String contextId = "your contextId"; + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + doubaoService.chatContext(messages,contextId, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void imagesGenerations() { + final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue()) + .setApiKey(key).setModel(Models.Doubao.DOUBAO_SEEDREAM_3_0_T2I.getModel()).build(), DoubaoService.class); + final String imagesGenerations = doubaoService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。"); + assertNotNull(imagesGenerations); } } diff --git a/hutool-ai/src/test/java/org/dromara/hutool/ai/model/grok/GrokServiceTest.java b/hutool-ai/src/test/java/org/dromara/hutool/ai/model/grok/GrokServiceTest.java index 13584798b..d98d4e6ba 100644 --- a/hutool-ai/src/test/java/org/dromara/hutool/ai/model/grok/GrokServiceTest.java +++ b/hutool-ai/src/test/java/org/dromara/hutool/ai/model/grok/GrokServiceTest.java @@ -21,6 +21,7 @@ import org.dromara.hutool.ai.ModelName; import org.dromara.hutool.ai.Models; import org.dromara.hutool.ai.core.AIConfigBuilder; import org.dromara.hutool.ai.core.Message; +import org.dromara.hutool.core.thread.ThreadUtil; import org.dromara.hutool.swing.img.ImgUtil; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -29,6 +30,7 @@ import java.awt.*; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.jupiter.api.Assertions.*; @@ -42,7 +44,30 @@ class GrokServiceTest { @Disabled void chat(){ final String chat = grokService.chat("写一个疯狂星期四广告词"); - System.out.println(chat); + assertNotNull(chat); + } + + @Test + @Disabled + void chatStream() { + String prompt = "写一个疯狂星期四广告词"; + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + + grokService.chat(prompt, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } } @Test @@ -52,14 +77,37 @@ class GrokServiceTest { messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话")); messages.add(new Message("user","给我说一个笑话")); final String chat = grokService.chat(messages); - System.out.println(chat); + assertNotNull(chat); } @Test @Disabled void message() { final String message = grokService.message("给我一个KFC的广告词", 4096); - System.out.println(message); + assertNotNull(message); + } + + @Test + @Disabled + void messageStream() { + String prompt = "给我一个KFC的广告词"; + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + grokService.message(prompt, 4096, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } } @Test @@ -68,7 +116,32 @@ class GrokServiceTest { final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class); final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png"); final String chatVision = grokService.chatVision("图片上有些什么?", Arrays.asList(base64)); - System.out.println(chatVision); + assertNotNull(chatVision); + } + + @Test + @Disabled + void testChatVisionStream() { + final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class); + String prompt = "图片上有些什么?"; + List images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + grokService.chatVision(prompt,images, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } } @Test @@ -76,7 +149,7 @@ class GrokServiceTest { void testChatVision() { final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class); final String chatVision = grokService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544")); - System.out.println(chatVision); + assertNotNull(chatVision); } @Test @@ -120,4 +193,13 @@ class GrokServiceTest { final String deferred = grokService.deferredCompletion(key); assertNotNull(deferred); } + + @Test + @Disabled + void imagesGenerations() { + final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()) + .setApiKey(key).setModel(Models.Grok.GROK_2_IMAGE.getModel()).build(), GrokService.class); + final String imagesGenerations = grokService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。"); + assertNotNull(imagesGenerations); + } } diff --git a/hutool-ai/src/test/java/org/dromara/hutool/ai/model/hutool/HutoolServiceTest.java b/hutool-ai/src/test/java/org/dromara/hutool/ai/model/hutool/HutoolServiceTest.java new file mode 100644 index 000000000..5d9786da9 --- /dev/null +++ b/hutool-ai/src/test/java/org/dromara/hutool/ai/model/hutool/HutoolServiceTest.java @@ -0,0 +1,193 @@ +/* + * Copyright (c) 2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.dromara.hutool.ai.model.hutool; + +import org.dromara.hutool.ai.AIException; +import org.dromara.hutool.ai.AIServiceFactory; +import org.dromara.hutool.ai.ModelName; +import org.dromara.hutool.ai.core.AIConfigBuilder; +import org.dromara.hutool.ai.core.Message; +import org.dromara.hutool.core.io.file.FileUtil; +import org.dromara.hutool.core.thread.ThreadUtil; +import org.dromara.hutool.swing.img.ImgUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.awt.*; +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.*; + +class HutoolServiceTest { + + String key = "请前往Hutool-AI官网:https://ai.hutool.cn 获取"; + HutoolService hutoolService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.HUTOOL.getValue()).setApiKey(key).build(), HutoolService.class); + + + @Test + @Disabled + void chat(){ + final String chat = hutoolService.chat("写一个疯狂星期四广告词"); + assertNotNull(chat); + } + + @Test + @Disabled + void chatStream() { + String prompt = "写一个疯狂星期四广告词"; + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + + hutoolService.chat(prompt, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void testChat(){ + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话")); + messages.add(new Message("user","给我说一个笑话")); + final String chat = hutoolService.chat(messages); + assertNotNull(chat); + } + + + @Test + @Disabled + void chatVision() { + final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png"); + final String chatVision = hutoolService.chatVision("图片上有些什么?", Arrays.asList(base64)); + assertNotNull(chatVision); + } + + @Test + @Disabled + void testChatVisionStream() { + String prompt = "图片上有些什么?"; + List images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + hutoolService.chatVision(prompt,images, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } + } + + @Test + @Disabled + void testChatVision() { + final String chatVision = hutoolService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544")); + assertNotNull(chatVision); + } + + @Test + @Disabled + void tokenizeText() { + final String tokenizeText = hutoolService.tokenizeText(key); + assertNotNull(tokenizeText); + } + + @Test + @Disabled + void imagesGenerations() { + final String imagesGenerations = hutoolService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。"); + assertNotNull(imagesGenerations); + } + + @Test + @Disabled + void embeddingVision() { + final String embeddingVision = hutoolService.embeddingVision("天空好难", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + System.out.println(embeddingVision); + assertNotNull(embeddingVision); + } + + @Test + @Disabled + void textToSpeech() { + try { + // 测试正常音频流返回 + final InputStream inputStream = hutoolService.tts("万里山河一夜白,\n" + + "千峰尽染玉龙哀。\n" + + "长风卷起琼花碎,\n" + + "直上九霄揽月来。", HutoolCommon.HutoolSpeech.NOVA); + assertNotNull(inputStream); + + // 保存音频文件 + final String filePath = "your filePath"; + FileUtil.writeFromStream(inputStream, new File(filePath)); + + } catch (Exception e) { + throw new AIException("TTS测试失败: " + e.getMessage()); + } + + } + + @Test + @Disabled + void speechToText() { + final File file = FileUtil.file("your filePath"); + final String speechToText = hutoolService.stt(file); + assertNotNull(speechToText); + } + + @Test + @Disabled + void videoTasks() { + final String videoTasks = hutoolService.videoTasks("生成一段动画视频,主角是大耳朵图图,一个活泼可爱的小男孩。视频中图图在公园里玩耍," + + "画面采用明亮温暖的卡通风格,色彩鲜艳,动作流畅。背景音乐轻快活泼,带有冒险感,音效包括鸟叫声、欢笑声和山洞回声。", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); + assertNotNull(videoTasks);//cgt-20250529154621-d7dq9 + } + + @Test + @Disabled + void getVideoTasksInfo() { + final String videoTasksInfo = hutoolService.getVideoTasksInfo("cgt-20250529154621-d7dq9"); + System.out.println(videoTasksInfo); + assertNotNull(videoTasksInfo); + } + +} diff --git a/hutool-ai/src/test/java/org/dromara/hutool/ai/model/openai/OpenaiServiceTest.java b/hutool-ai/src/test/java/org/dromara/hutool/ai/model/openai/OpenaiServiceTest.java index 55a88885d..e302e17d2 100644 --- a/hutool-ai/src/test/java/org/dromara/hutool/ai/model/openai/OpenaiServiceTest.java +++ b/hutool-ai/src/test/java/org/dromara/hutool/ai/model/openai/OpenaiServiceTest.java @@ -22,6 +22,7 @@ import org.dromara.hutool.ai.Models; import org.dromara.hutool.ai.core.AIConfigBuilder; import org.dromara.hutool.ai.core.Message; import org.dromara.hutool.core.io.file.FileUtil; +import org.dromara.hutool.core.thread.ThreadUtil; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -35,6 +36,9 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.assertNotNull; class OpenaiServiceTest { @@ -46,7 +50,30 @@ class OpenaiServiceTest { @Disabled void chat(){ final String chat = openaiService.chat("写一个疯狂星期四广告词"); - System.out.println(chat); + assertNotNull(chat); + } + + @Test + @Disabled + void chatStream() { + String prompt = "写一个疯狂星期四广告词"; + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + + openaiService.chat(prompt, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } } @Test @@ -56,7 +83,33 @@ class OpenaiServiceTest { messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话")); messages.add(new Message("user","给我说一个笑话")); final String chat = openaiService.chat(messages); - System.out.println(chat); + assertNotNull(chat); + } + + @Test + @Disabled + void testChatVisionStream() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class); + String prompt = "图片上有些什么?"; + List images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\",\"https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800"); + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + openaiService.chatVision(prompt,images, data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } } @Test @@ -65,7 +118,7 @@ class OpenaiServiceTest { final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) .setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class); final String chatVision = openaiService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544","https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800")); - System.out.println(chatVision); + assertNotNull(chatVision); } @Test @@ -74,8 +127,7 @@ class OpenaiServiceTest { final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) .setApiKey(key).setModel(Models.Openai.DALL_E_3.getModel()).build(), OpenaiService.class); final String imagesGenerations = openaiService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。"); - System.out.println(imagesGenerations); - //https://oaidalleapiprodscus.blob.core.windows.net/private/org-l99H6T0zCZejctB2TqdYrXFB/user-LilDVU1V8cUxJYwVAGRkUwYd/img-yA9kNatHnBiUHU5lZGim1hP2.png?st=2025-03-07T01%3A04%3A18Z&se=2025-03-07T03%3A04%3A18Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=d505667d-d6c1-4a0a-bac7-5c84a87759f8&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2025-03-06T15%3A04%3A42Z&ske=2025-03-07T15%3A04%3A42Z&sks=b&skv=2024-08-04&sig=rjcRzC5U7Y3pEDZ4ME0CiviAPdIpoGO2rRTXw3m8rHw%3D + assertNotNull(imagesGenerations); } @Test @@ -85,7 +137,7 @@ class OpenaiServiceTest { .setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class); final File file = FileUtil.file("your imgUrl"); final String imagesEdits = openaiService.imagesEdits("茂密的森林中,有一只九色鹿若隐若现",file); - System.out.println(imagesEdits); + assertNotNull(imagesEdits); } @Test @@ -95,7 +147,7 @@ class OpenaiServiceTest { .setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class); final File file = FileUtil.file("your imgUrl"); final String imagesVariations = openaiService.imagesVariations(file); - System.out.println(imagesVariations); + assertNotNull(imagesVariations); } @Test @@ -130,7 +182,7 @@ class OpenaiServiceTest { .setApiKey(key).setModel(Models.Openai.WHISPER_1.getModel()).build(), OpenaiService.class); final File file = FileUtil.file("your filePath"); final String speechToText = openaiService.speechToText(file); - System.out.println(speechToText); + assertNotNull(speechToText); } @Test @@ -139,7 +191,7 @@ class OpenaiServiceTest { final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) .setApiKey(key).setModel(Models.Openai.TEXT_EMBEDDING_3_SMALL.getModel()).build(), OpenaiService.class); final String embeddingText = openaiService.embeddingText("萬里山河一夜白,千峰盡染玉龍哀,長風捲起瓊花碎,直上九霄闌月來"); - System.out.println(embeddingText); + assertNotNull(embeddingText); } @Test @@ -148,7 +200,7 @@ class OpenaiServiceTest { final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) .setApiKey(key).setModel(Models.Openai.OMNI_MODERATION_LATEST.getModel()).build(), OpenaiService.class); final String moderations = openaiService.moderations("你要杀人", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"); - System.out.println(moderations); + assertNotNull(moderations); } @Test @@ -160,6 +212,33 @@ class OpenaiServiceTest { messages.add(new Message("system","你是现代抽象家")); messages.add(new Message("user","给我一个KFC疯狂星期四的文案")); final String chatReasoning = openaiService.chatReasoning(messages, OpenaiCommon.OpenaiReasoning.HIGH.getEffort()); - System.out.println(chatReasoning); + assertNotNull(chatReasoning); + } + + @Test + @Disabled + void chatReasoningStream() { + final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue()) + .setApiKey(key).setModel(Models.Openai.O3_MINI.getModel()).build(), OpenaiService.class); + final List messages = new ArrayList<>(); + messages.add(new Message("system","你是现代抽象家")); + messages.add(new Message("user","给我一个KFC疯狂星期四的文案")); + + // 使用AtomicBoolean作为结束标志 + AtomicBoolean isDone = new AtomicBoolean(false); + openaiService.chatReasoning(messages,OpenaiCommon.OpenaiReasoning.HIGH.getEffort(), data -> { + assertNotNull(data); + if (data.contains("[DONE]")) { + // 设置结束标志 + isDone.set(true); + } else if (data.contains("\"error\"")) { + isDone.set(true); + } + + }); + // 轮询检查结束标志 + while (!isDone.get()) { + ThreadUtil.sleep(100); + } } }