From 5cb64025bde6142661fceaef10a2f4b025eddf42 Mon Sep 17 00:00:00 2001 From: LuanY77 <2307984361@qq.com> Date: Thu, 31 Jul 2025 11:55:47 +0800 Subject: [PATCH] =?UTF-8?q?Feat:=20=E6=B7=BB=E5=8A=A0=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8C=96=E8=BE=93=E5=87=BA=E5=8F=82=E6=95=B0=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=EF=BC=8C=E5=BC=95=E5=85=A5=20AiServiceFactory=20=E5=B8=AE?= =?UTF-8?q?=E5=8A=A9=E5=88=9B=E5=BB=BA=20AiService?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../liteflow/ai/annotation/AIOutput.java | 2 +- .../liteflow/ai/context/StreamHandler.java | 57 ++++- .../liteflow/ai/model/ModelFactory.java | 45 ++-- .../liteflow/ai/model/ModelProvider.java | 14 +- .../ai/parse/AbstractAnnotationProcessor.java | 25 +++ .../parse/anno/ChatAnnotationProcessor.java | 3 + .../anno/ClassifyAnnotationProcessor.java | 4 +- .../invocation/ChatAIInvocationHandler.java | 37 +++- .../ClassifyAIInvocationHandler.java | 2 +- .../invocation/service/AiServiceFactory.java | 202 ++++++++++++++++++ .../ai/proxy/wrap/AIProxyWrapBean.java | 74 +++++++ .../ai/proxy/wrap/ChatProxyWrapBean.java | 20 -- .../ai/proxy/wrap/ClassifyProxyWrapBean.java | 21 -- .../ai/ollama/model/OllamaModelProvider.java | 29 +-- .../com/yomahub/liteflow/test/cmp/AICmp.java | 13 +- 15 files changed, 453 insertions(+), 95 deletions(-) create mode 100644 liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/invocation/service/AiServiceFactory.java diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/annotation/AIOutput.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/annotation/AIOutput.java index a8ba9b0b4..8b1cbe8cb 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/annotation/AIOutput.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/annotation/AIOutput.java @@ -97,7 +97,7 @@ public @interface AIOutput { *

* 表示输出的 JSON Schema 定义。如果需要添加描述信息,请使用 LangChain4j 的相关注解 */ - Class entityClass() default Object.class; + Class entityClass() default String.class; /** * 如需启用,请设置 {@link AIOutput#responseType()} 为 {@link ResponseType#JSON} diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/context/StreamHandler.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/context/StreamHandler.java index f273eaf57..44ee82904 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/context/StreamHandler.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/context/StreamHandler.java @@ -1,6 +1,12 @@ package com.yomahub.liteflow.ai.context; import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.rag.RetrievalAugmentor; +import dev.langchain4j.rag.content.Content; +import dev.langchain4j.service.TokenStream; +import dev.langchain4j.service.tool.ToolExecution; + +import java.util.List; /** * 流式输出处理器 @@ -11,9 +17,58 @@ import dev.langchain4j.model.chat.response.ChatResponse; public interface StreamHandler { + /** + * 当语言模型生成新的部分响应(通常是单个 token)时,将调用此方法。 + * + * @param partialResponse 新生成的部分响应文本。 + */ void onPartialResponse(String partialResponse); - void onCompleteResponse(ChatResponse completeResponse); + /** + * 当使用 {@link RetrievalAugmentor} 检索到任何 {@link Content} 时,将调用此方法。 + *

+ * 此调用发生在与语言模型进行任何交互之前。 + * + * @param retrievedContents 所有被检索到的内容列表。 + */ + void onRetrieved(List retrievedContents); + /** + * 当任何工具被执行后,将调用此方法。 + *

+ * 此调用发生在工具方法执行完成之后,下一个工具执行之前。 + * + * @param toolExecution 包含已执行工具的名称、参数和结果的对象。 + */ + void onToolExecuted(ToolExecution toolExecution); + + /** + * 当语言模型完成流式响应时,将调用此方法。 + * + * @param chatResponse 完整的聊天响应结果。 + */ + void onCompleteResponse(ChatResponse chatResponse); + + /** + * 当流式处理过程中发生错误时,将调用此方法。 + * + * @param error 捕获到的异常或错误。 + */ void onError(Throwable error); + + /** + * 接受一个 {@link TokenStream} 对象,并注册流式处理的回调。 + *

+ * 自动开启 TokenStream 的处理流程,并在流式响应的各个阶段调用相应的方法。 + * + * @param tokenStream 要处理的 {@link TokenStream} 对象。 + */ + default void acceptTokenStream(TokenStream tokenStream) { + tokenStream.onPartialResponse(this::onPartialResponse) + .onRetrieved(this::onRetrieved) + .onToolExecuted(this::onToolExecuted) + .onCompleteResponse(this::onCompleteResponse) + .onError(this::onError) + .start(); + } } diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/model/ModelFactory.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/model/ModelFactory.java index b2bb9f8b6..69773f1b0 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/model/ModelFactory.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/model/ModelFactory.java @@ -1,7 +1,7 @@ package com.yomahub.liteflow.ai.model; -import com.yomahub.liteflow.ai.domain.ModelConfig; import com.yomahub.liteflow.ai.exception.LiteFlowAIException; +import com.yomahub.liteflow.ai.proxy.wrap.AIProxyWrapBean; import com.yomahub.liteflow.log.LFLog; import com.yomahub.liteflow.log.LFLoggerManager; import dev.langchain4j.model.chat.ChatModel; @@ -27,9 +27,9 @@ public class ModelFactory { private static final Map MODEL_PROVIDER_MAP = new ConcurrentHashMap<>(); // 模型类型缓存 - private static final Map CHAT_MODEL_CACHE = new ConcurrentHashMap<>(); - private static final Map STREAMING_CHAT_MODEL_CACHE = new ConcurrentHashMap<>(); - private static final Map EMBEDDING_MODEL_CACHE = new ConcurrentHashMap<>(); + private static final Map, ChatModel> CHAT_MODEL_CACHE = new ConcurrentHashMap<>(); + private static final Map, StreamingChatModel> STREAMING_CHAT_MODEL_CACHE = new ConcurrentHashMap<>(); + private static final Map, EmbeddingModel> EMBEDDING_MODEL_CACHE = new ConcurrentHashMap<>(); // 私有化构造函数 private ModelFactory() { @@ -50,45 +50,48 @@ public class ModelFactory { /** * 获取指定提供者名称的ChatModel实例 * - * @param config 模型配置 + * @param wrapBean AI 节点包装 Bean,从中获取模型配置信息 * @return ChatModel实例 */ - public static ChatModel getChatModel(ModelConfig config) { - return CHAT_MODEL_CACHE.computeIfAbsent(config, key -> { - ModelProvider provider = getProvider(key.getProvider()); + public static ChatModel getChatModel(AIProxyWrapBean wrapBean) { + return CHAT_MODEL_CACHE.computeIfAbsent(wrapBean, key -> { + String providerName = key.getConfig().getProvider(); + ModelProvider provider = getProvider(providerName); // 创建 ChatModel 实例 return provider.createChatModel(key) - .orElseThrow(() -> new LiteFlowAIException("ChatModel is not supported for provider: " + key.getProvider())); + .orElseThrow(() -> new LiteFlowAIException("ChatModel is not supported for provider: " + providerName)); }); } /** * 获取指定提供者名称的StreamingChatModel实例 * - * @param config 模型配置 + * @param wrapBean AI 节点包装 Bean,从中获取模型配置信息 * @return StreamingChatModel实例 */ - public static StreamingChatModel getStreamingChatModel(ModelConfig config) { - return STREAMING_CHAT_MODEL_CACHE.computeIfAbsent(config, key -> { - ModelProvider provider = getProvider(config.getProvider()); + public static StreamingChatModel getStreamingChatModel(AIProxyWrapBean wrapBean) { + return STREAMING_CHAT_MODEL_CACHE.computeIfAbsent(wrapBean, key -> { + String providerName = key.getConfig().getProvider(); + ModelProvider provider = getProvider(providerName); // 创建 StreamingChatModel 实例 - return provider.createStreamingChatModel(config) - .orElseThrow(() -> new LiteFlowAIException("StreamingChatModel is not supported for provider: " + key.getProvider())); + return provider.createStreamingChatModel(wrapBean) + .orElseThrow(() -> new LiteFlowAIException("StreamingChatModel is not supported for provider: " + providerName)); }); } /** * 获取指定提供者名称的EmbeddingModel实例 * - * @param config 模型配置 + * @param wrapBean AI 节点包装 Bean,从中获取模型配置信息 * @return EmbeddingModel实例 */ - public static EmbeddingModel getEmbeddingModel(ModelConfig config) { - return EMBEDDING_MODEL_CACHE.computeIfAbsent(config, key -> { - ModelProvider provider = getProvider(key.getProvider()); + public static EmbeddingModel getEmbeddingModel(AIProxyWrapBean wrapBean) { + return EMBEDDING_MODEL_CACHE.computeIfAbsent(wrapBean, key -> { + String providerName = key.getConfig().getProvider(); + ModelProvider provider = getProvider(providerName); // 创建 EmbeddingModel 实例 - return provider.createEmbeddingModel(config) - .orElseThrow(() -> new LiteFlowAIException("EmbeddingModel is not supported for provider: " + key.getProvider())); + return provider.createEmbeddingModel(wrapBean) + .orElseThrow(() -> new LiteFlowAIException("EmbeddingModel is not supported for provider: " + providerName)); }); } diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/model/ModelProvider.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/model/ModelProvider.java index 237ee4858..0a2c8a7e9 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/model/ModelProvider.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/model/ModelProvider.java @@ -1,6 +1,6 @@ package com.yomahub.liteflow.ai.model; -import com.yomahub.liteflow.ai.domain.ModelConfig; +import com.yomahub.liteflow.ai.proxy.wrap.AIProxyWrapBean; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.embedding.EmbeddingModel; @@ -26,30 +26,30 @@ public interface ModelProvider { /** * 创建ChatModel实例 * - * @param config 模型配置 + * @param wrapBean AI 节点包装 Bean,从中获取模型配置信息 * @return ChatModel实例 */ - default Optional createChatModel(ModelConfig config) { + default Optional createChatModel(AIProxyWrapBean wrapBean) { return Optional.empty(); } /** * 创建StreamingChatModel实例 * - * @param config 模型配置 + * @param wrapBean AI 节点包装 Bean,从中获取模型配置信息 * @return StreamingChatModel实例 */ - default Optional createStreamingChatModel(ModelConfig config) { + default Optional createStreamingChatModel(AIProxyWrapBean wrapBean) { return Optional.empty(); } /** * 创建EmbeddingModel实例 * - * @param config 模型配置 + * @param wrapBean AI 节点包装 Bean,从中获取模型配置信息 * @return EmbeddingModel实例 */ - default Optional createEmbeddingModel(ModelConfig config) { + default Optional createEmbeddingModel(AIProxyWrapBean wrapBean) { return Optional.empty(); } } diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/parse/AbstractAnnotationProcessor.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/parse/AbstractAnnotationProcessor.java index 12829fb9f..a8d4af324 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/parse/AbstractAnnotationProcessor.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/parse/AbstractAnnotationProcessor.java @@ -2,7 +2,9 @@ package com.yomahub.liteflow.ai.parse; import cn.hutool.core.util.StrUtil; import com.yomahub.liteflow.ai.annotation.AIInput; +import com.yomahub.liteflow.ai.annotation.AIOutput; import com.yomahub.liteflow.ai.domain.enums.AITypeEnum; +import com.yomahub.liteflow.ai.domain.enums.ResponseType; import com.yomahub.liteflow.ai.parse.prompt.PromptTemplateParser; import com.yomahub.liteflow.ai.parse.prompt.resource.PromptResource; import com.yomahub.liteflow.ai.proxy.wrap.AIProxyWrapBean; @@ -11,6 +13,7 @@ import com.yomahub.liteflow.log.LFLoggerManager; import org.springframework.beans.factory.InitializingBean; import java.lang.annotation.Annotation; +import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; @@ -69,4 +72,26 @@ public abstract class AbstractAnnotationProcessor context) { + AIOutput outputAnno = context.getAiOutputAnno(); + if (Objects.isNull(outputAnno)) return; + + // 这里仅处理结构化输出相关的参数,其他参数交给 after 逻辑进行处理 + T wrapBean = context.getWrapBean(); + // 是否需要结构化输出 + if (Objects.equals(ResponseType.JSON, outputAnno.responseType())) { + wrapBean.setResponseType(ResponseType.JSON); + // 设置输出实体类 + wrapBean.setEntityClass(outputAnno.entityClass()); + } else { + // 文本输出,设置 entityClass 为 String + wrapBean.setEntityClass(String.class); + } + } } diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/parse/anno/ChatAnnotationProcessor.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/parse/anno/ChatAnnotationProcessor.java index 1ec944cb9..2a7bf647a 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/parse/anno/ChatAnnotationProcessor.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/parse/anno/ChatAnnotationProcessor.java @@ -27,6 +27,9 @@ public class ChatAnnotationProcessor extends AbstractAnnotationProcessor context, Object result) { - } @Override diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/invocation/ChatAIInvocationHandler.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/invocation/ChatAIInvocationHandler.java index 62c2ccf84..a0791a76d 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/invocation/ChatAIInvocationHandler.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/invocation/ChatAIInvocationHandler.java @@ -1,11 +1,18 @@ package com.yomahub.liteflow.ai.proxy.invocation; +import com.yomahub.liteflow.ai.context.ChatContext; +import com.yomahub.liteflow.ai.exception.LiteFlowAIException; import com.yomahub.liteflow.ai.model.ModelFactory; import com.yomahub.liteflow.ai.parse.ProcessorContext; +import com.yomahub.liteflow.ai.proxy.invocation.service.AiServiceFactory; import com.yomahub.liteflow.ai.proxy.wrap.ChatProxyWrapBean; import com.yomahub.liteflow.core.NodeComponent; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.service.TokenStream; + +import java.util.Objects; +import java.util.Optional; /** * 聊天组件的调用处理器 @@ -31,15 +38,35 @@ public class ChatAIInvocationHandler extends AbstractAIInvocationHandler streamHandler.acceptTokenStream(tokenStream)); + } + } catch (Throwable e) { + throw new LiteFlowAIException("Error during streaming chat processing", e); + } return null; } - private Void processBlocking(NodeComponent nodeComponent) { - ChatModel chatModel = ModelFactory.getChatModel(wrapBean.getConfig()); + private Object processBlocking(NodeComponent nodeComponent) { + ChatModel chatModel = ModelFactory.getChatModel(wrapBean); - LOG.info("Processing chat request with model: {}", chatModel.getClass().getSimpleName()); - return null; + // 创建AI服务实例 + Object aiService = AiServiceFactory.createAiService(wrapBean.getEntityClass(), chatModel); + + try { + return AiServiceFactory.chat(aiService, wrapBean.getUserPrompt(), wrapBean.getSystemPrompt()); + } catch (Throwable e) { + throw new LiteFlowAIException("Error during blocking chat processing", e); + } } } diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/invocation/ClassifyAIInvocationHandler.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/invocation/ClassifyAIInvocationHandler.java index 1fc6d6180..f71d516ea 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/invocation/ClassifyAIInvocationHandler.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/invocation/ClassifyAIInvocationHandler.java @@ -21,7 +21,7 @@ public class ClassifyAIInvocationHandler extends AbstractAIInvocationHandler processorContext, Object[] args) { - ChatModel chatModel = ModelFactory.getChatModel(wrapBean.getConfig()); + ChatModel chatModel = ModelFactory.getChatModel(wrapBean); return null; } diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/invocation/service/AiServiceFactory.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/invocation/service/AiServiceFactory.java new file mode 100644 index 000000000..fcda66e46 --- /dev/null +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/invocation/service/AiServiceFactory.java @@ -0,0 +1,202 @@ +package com.yomahub.liteflow.ai.proxy.invocation.service; + +import com.yomahub.liteflow.ai.util.SetUtil; +import com.yomahub.liteflow.log.LFLog; +import com.yomahub.liteflow.log.LFLoggerManager; +import com.yomahub.liteflow.util.SerialsUtil; +import dev.langchain4j.model.chat.ChatModel; +import dev.langchain4j.model.chat.StreamingChatModel; +import dev.langchain4j.service.*; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.description.modifier.Visibility; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; + +import java.lang.reflect.Method; + +/** + * LangChain4j AI 服务的动态工厂类。 + *

+ * 这个工厂类帮助创建一个动态生成的接口,目的是将静态的泛型类型动态注入到接口方法中,从而复用LangChain4j的结构化输出的能力 + *

+ * 目标接口结构示例: + *

+ * 该工厂旨在创建如下结构的 {@code LiteFlowAIAssistant} 接口的实例: + *

{@code
+ * public interface LiteFlowAIAssistant {
+ *
+ * // 以同步方式进行对话,并返回一个结构化的响应。
+ * // @param userMessage 用户的输入消息。
+ * // @param systemMessage 预设的系统级指令。
+ * // @param  响应结果的泛型类型。
+ * // @return 包含模型响应结果的 Result 对象。
+ * @SystemMessage("{{systemMessage}}")
+ * @UserMessage("{{userMessage}}")
+ *  Result chat(@V("userMessage") String userMessage, @V("systemMessage") String systemMessage);
+ *
+ * // 以流式方式进行对话,逐步返回模型的响应。
+ * // @param userMessage 用户的输入消息。
+ * // @param systemMessage 预设的系统级指令。
+ * // @return 一个 TokenStream 对象,用于处理流式响应。
+ * @SystemMessage("{{systemMessage}}")
+ * @UserMessage("{{userMessage}}")
+ * TokenStream chatStream(@V("userMessage") String userMessage, @V("systemMessage") String systemMessage);
+ *
+ * }
+ * }
+ * + * @author 苍镜月 + * @see dev.langchain4j.service.AiServices + * @see dev.langchain4j.service.SystemMessage + * @see dev.langchain4j.service.UserMessage + * @since TODO + */ + +public class AiServiceFactory { + + private static final LFLog LOG = LFLoggerManager.getLogger(AiServiceFactory.class); + private static final String DYNAMIC_INTERFACE_PREFIX = "com.yomahub.liteflow.ai.proxy.invocation.service.DynamicLiteFlowAIAssistant_"; + + /** + * 使用 {@link AiServices} 创建一个 AI 服务实例。 + * + * @param dynamicResultType 动态指定的返回类型 + * @param chatModel chat 模型 + * @return 动态生成的 AI 服务实例 + */ + public static Object createAiService(Class dynamicResultType, ChatModel chatModel) { + return doCreateAiService(dynamicResultType, chatModel, null); + } + + /** + * 使用 {@link AiServices} 创建一个 AI 服务实例。 + * + * @param dynamicResultType 动态指定的返回类型 + * @param streamingChatModel 流式 chat 模型 + * @return 动态生成的 AI 服务实例 + */ + public static Object createAiService(Class dynamicResultType, StreamingChatModel streamingChatModel) { + return doCreateAiService(dynamicResultType, null, streamingChatModel); + } + + /** + * 使用 {@link AiServices} 创建一个 AI 服务实例。 + * + * @param dynamicResultType 动态指定的返回类型 + * @param chatModel chat 模型 + * @param streamingChatModel 流式 chat 模型 + * @return 动态生成的 AI 服务实例 + */ + private static Object doCreateAiService(Class dynamicResultType, ChatModel chatModel, StreamingChatModel streamingChatModel) { + // 动态创建一个接口 + Class dynamicInterface = createLiteFlowAIAssistant(dynamicResultType); + LOG.info("successfully created dynamic interface for AiServices: {}", dynamicInterface.getName()); + + // 创建 AiService 实例 + AiServices builder = AiServices.builder(dynamicInterface); + SetUtil.setIfPresent(builder::chatModel, chatModel); + SetUtil.setIfPresent(builder::streamingChatModel, streamingChatModel); + return builder.build(); + } + + /** + * 动态创建一个接口,用于 LangChain4j 的 AI 服务 + * 使用动态代理是因为需要将静态的泛型类型动态注入 + * + * @param dynamicResultType 动态指定的 chat 方法返回结果类型,例如 String.class + * @return 动态生成的接口 Class 对象 + */ + private static Class createLiteFlowAIAssistant(Class dynamicResultType) { + // 获取动态指定的泛型类型 + TypeDescription.Generic genericResultType = TypeDescription.Generic.Builder + .parameterizedType(Result.class, dynamicResultType) + .build(); + + DynamicType.Builder builder = new ByteBuddy() + .makeInterface() + .name(getDynamicInterfaceName(dynamicResultType)); + + // 定义 chat 方法 + builder = defineChatMethod(builder, genericResultType); + // 定义 chatStream 方法 + builder = defineChatStreamMethod(builder); + + DynamicType.Unloaded dynamicType = builder.make(); + + return dynamicType + .load(AiServiceFactory.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) + .getLoaded(); + } + + /** + * 获取动态生成的接口名称 + * + * @param dynamicResultType 动态指定的返回类型 + * @return 动态生成的接口名称 + */ + private static String getDynamicInterfaceName(Class dynamicResultType) { + return DYNAMIC_INTERFACE_PREFIX + + dynamicResultType.getSimpleName() + + "_" + SerialsUtil.generateShortUUID(); + } + + /** + * 定义 chat 方法 + */ + private static DynamicType.Builder defineChatMethod(DynamicType.Builder builder, TypeDescription.Generic genericResultType) { + return builder.defineMethod("chat", genericResultType, Visibility.PUBLIC) + .withParameter(String.class, "userMessage") + .annotateParameter(AnnotationDescription.Builder.ofType(V.class).define("value", "userMessage").build()) + .withParameter(String.class, "systemMessage") + .annotateParameter(AnnotationDescription.Builder.ofType(V.class).define("value", "systemMessage").build()) + .withoutCode() + .annotateMethod( + AnnotationDescription.Builder.ofType(SystemMessage.class).defineArray("value", "{{systemMessage}}").build(), + AnnotationDescription.Builder.ofType(UserMessage.class).defineArray("value", "{{userMessage}}").build() + ); + } + + /** + * 定义 chatStream 方法 + */ + private static DynamicType.Builder defineChatStreamMethod(DynamicType.Builder builder) { + return builder.defineMethod("chatStream", TokenStream.class, Visibility.PUBLIC) + .withParameter(String.class, "userMessage") + .annotateParameter(AnnotationDescription.Builder.ofType(V.class).define("value", "userMessage").build()) + .withParameter(String.class, "systemMessage") + .annotateParameter(AnnotationDescription.Builder.ofType(V.class).define("value", "systemMessage").build()) + .withoutCode() + .annotateMethod( + AnnotationDescription.Builder.ofType(SystemMessage.class).defineArray("value", "{{systemMessage}}").build(), + AnnotationDescription.Builder.ofType(UserMessage.class).defineArray("value", "{{userMessage}}").build() + ); + } + + /** + * 调用 chat 方法 + * + * @param aiService AI 服务实例 + * @param userMessage 用户消息 + * @param systemMessage 系统消息 + * @return 调用结果 + */ + public static Object chat(Object aiService, String userMessage, String systemMessage) throws Throwable { + Method chatMethod = aiService.getClass().getMethod("chat", String.class, String.class); + return chatMethod.invoke(aiService, userMessage, systemMessage); + } + + /** + * 调用 chatStream 方法 + * + * @param aiService AI 服务实例 + * @param userMessage 用户消息 + * @param systemMessage 系统消息 + * @return TokenStream 实例 + */ + public static TokenStream chatStream(Object aiService, String userMessage, String systemMessage) throws Throwable { + Method chatStreamMethod = aiService.getClass().getMethod("chatStream", String.class, String.class); + return (TokenStream) chatStreamMethod.invoke(aiService, userMessage, systemMessage); + } +} \ No newline at end of file diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/wrap/AIProxyWrapBean.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/wrap/AIProxyWrapBean.java index f2f01aa25..8e228512c 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/wrap/AIProxyWrapBean.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/wrap/AIProxyWrapBean.java @@ -2,8 +2,10 @@ package com.yomahub.liteflow.ai.proxy.wrap; import com.yomahub.liteflow.ai.annotation.AIComponent; import com.yomahub.liteflow.ai.domain.ModelConfig; +import com.yomahub.liteflow.ai.domain.enums.ResponseType; import java.lang.annotation.Annotation; +import java.util.Objects; /** * AI节点包装 Bean @@ -26,6 +28,14 @@ public abstract class AIProxyWrapBean { protected String beanName; + protected String systemPrompt; + + protected String userPrompt; + + protected ResponseType responseType = ResponseType.TEXT; + + protected Class entityClass = String.class; + public AIProxyWrapBean() { } @@ -61,6 +71,22 @@ public abstract class AIProxyWrapBean { return beanName; } + public String getSystemPrompt() { + return systemPrompt; + } + + public String getUserPrompt() { + return userPrompt; + } + + public ResponseType getResponseType() { + return responseType; + } + + public Class getEntityClass() { + return entityClass; + } + public void setNodeId(String nodeId) { this.nodeId = nodeId; } @@ -80,4 +106,52 @@ public abstract class AIProxyWrapBean { public void setBeanName(String beanName) { this.beanName = beanName; } + + public void setSystemPrompt(String systemPrompt) { + this.systemPrompt = systemPrompt; + } + + public void setUserPrompt(String userPrompt) { + this.userPrompt = userPrompt; + } + + public void setResponseType(ResponseType responseType) { + this.responseType = responseType; + } + + public void setEntityClass(Class entityClass) { + this.entityClass = entityClass; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + AIProxyWrapBean that = (AIProxyWrapBean) o; + return Objects.equals(getAnnotation(), that.getAnnotation()) && + Objects.equals(getConfig(), that.getConfig()) && + Objects.equals(getNodeId(), that.getNodeId()) && + Objects.equals(getNodeName(), that.getNodeName()) && + Objects.equals(getInterfaceClass(), that.getInterfaceClass()) && + Objects.equals(getBeanName(), that.getBeanName()) && + Objects.equals(getSystemPrompt(), that.getSystemPrompt()) && + Objects.equals(getUserPrompt(), that.getUserPrompt()) && + getResponseType() == that.getResponseType() && + Objects.equals(getEntityClass(), that.getEntityClass()); + } + + @Override + public int hashCode() { + return Objects.hash( + getAnnotation(), + getConfig(), + getNodeId(), + getNodeName(), + getInterfaceClass(), + getBeanName(), + getSystemPrompt(), + getUserPrompt(), + getResponseType(), + getEntityClass() + ); + } } diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/wrap/ChatProxyWrapBean.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/wrap/ChatProxyWrapBean.java index aa66b5dc8..2356b6f23 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/wrap/ChatProxyWrapBean.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/wrap/ChatProxyWrapBean.java @@ -12,10 +12,6 @@ import com.yomahub.liteflow.ai.annotation.AIComponent; public class ChatProxyWrapBean extends AIProxyWrapBean { - private String systemPrompt; - - private String userPrompt; - private boolean streaming; public ChatProxyWrapBean() { @@ -28,26 +24,10 @@ public class ChatProxyWrapBean extends AIProxyWrapBean { this.annotation = annotation; } - public String getSystemPrompt() { - return systemPrompt; - } - - public String getUserPrompt() { - return userPrompt; - } - public boolean isStreaming() { return streaming; } - public void setSystemPrompt(String systemPrompt) { - this.systemPrompt = systemPrompt; - } - - public void setUserPrompt(String userPrompt) { - this.userPrompt = userPrompt; - } - public void setStreaming(boolean streaming) { this.streaming = streaming; } diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/wrap/ClassifyProxyWrapBean.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/wrap/ClassifyProxyWrapBean.java index 760956f55..0188b0383 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/wrap/ClassifyProxyWrapBean.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/proxy/wrap/ClassifyProxyWrapBean.java @@ -14,11 +14,6 @@ import java.util.List; public class ClassifyProxyWrapBean extends AIProxyWrapBean { - - private String systemPrompt; - - private String userPrompt; - private List categories; private boolean multiLabel; @@ -33,14 +28,6 @@ public class ClassifyProxyWrapBean extends AIProxyWrapBean { this.annotation = annotation; } - public String getSystemPrompt() { - return systemPrompt; - } - - public String getUserPrompt() { - return userPrompt; - } - public List getCategories() { return categories; } @@ -49,14 +36,6 @@ public class ClassifyProxyWrapBean extends AIProxyWrapBean { return multiLabel; } - public void setSystemPrompt(String systemPrompt) { - this.systemPrompt = systemPrompt; - } - - public void setUserPrompt(String userPrompt) { - this.userPrompt = userPrompt; - } - public void setCategories(List categories) { this.categories = categories; } diff --git a/liteflow-ai/liteflow-ai-ollama/src/main/java/com/yomahub/liteflow/ai/ollama/model/OllamaModelProvider.java b/liteflow-ai/liteflow-ai-ollama/src/main/java/com/yomahub/liteflow/ai/ollama/model/OllamaModelProvider.java index 2332535f4..75ffed6ee 100644 --- a/liteflow-ai/liteflow-ai-ollama/src/main/java/com/yomahub/liteflow/ai/ollama/model/OllamaModelProvider.java +++ b/liteflow-ai/liteflow-ai-ollama/src/main/java/com/yomahub/liteflow/ai/ollama/model/OllamaModelProvider.java @@ -3,6 +3,7 @@ package com.yomahub.liteflow.ai.ollama.model; import com.yomahub.liteflow.ai.domain.ModelConfig; import com.yomahub.liteflow.ai.domain.constant.ProviderName; import com.yomahub.liteflow.ai.model.ModelProviderRegistrar; +import com.yomahub.liteflow.ai.proxy.wrap.AIProxyWrapBean; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.chat.StreamingChatModel; import dev.langchain4j.model.ollama.OllamaChatModel; @@ -25,24 +26,26 @@ public class OllamaModelProvider extends ModelProviderRegistrar { } @Override - public Optional createChatModel(ModelConfig config) { - // TODO - return Optional.of( - OllamaChatModel.builder() - .baseUrl(config.getBaseUrl()) - .modelName(config.getModel()) + public Optional createChatModel(AIProxyWrapBean wrapBean) { + // 设置模型配置和结构化输出 + ModelConfig modelConfig = wrapBean.getConfig(); + return Optional.of(OllamaChatModel + .builder() + .baseUrl(modelConfig.getBaseUrl()) + .modelName(modelConfig.getModel()) .build() ); } @Override - public Optional createStreamingChatModel(ModelConfig config) { - // TODO - return Optional.of( - OllamaStreamingChatModel.builder() - .baseUrl(config.getBaseUrl()) - .modelName(config.getModel()) - .build() + public Optional createStreamingChatModel(AIProxyWrapBean wrapBean) { + // 设置模型配置和结构化输出 + ModelConfig modelConfig = wrapBean.getConfig(); + return Optional.of(OllamaStreamingChatModel + .builder() + .baseUrl(modelConfig.getBaseUrl()) + .modelName(modelConfig.getModel()) + .build() ); } } diff --git a/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/cmp/AICmp.java b/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/cmp/AICmp.java index 396074ad3..73527f792 100644 --- a/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/cmp/AICmp.java +++ b/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/cmp/AICmp.java @@ -1,9 +1,7 @@ package com.yomahub.liteflow.test.cmp; -import com.yomahub.liteflow.ai.annotation.AIChat; -import com.yomahub.liteflow.ai.annotation.AIComponent; -import com.yomahub.liteflow.ai.annotation.AIInput; -import com.yomahub.liteflow.ai.annotation.InputField; +import com.yomahub.liteflow.ai.annotation.*; +import com.yomahub.liteflow.ai.domain.enums.ResponseType; /** * TODO @@ -29,5 +27,12 @@ import com.yomahub.liteflow.ai.annotation.InputField; @InputField(name = "answer", expression = "test", defaultValue = "The sky appears blue due to the scattering of sunlight by the atmosphere.") } ) +@AIOutput( + responseType = ResponseType.JSON, + entityClass = Integer.class, + methodExpress = "setData", + useKeyIndex = true, + key = "result" +) public interface AICmp { }