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 {
}