From 3528d724b06dd5eb31fa167720e24fb9767daf65 Mon Sep 17 00:00:00 2001 From: LuanY77 <2307984361@qq.com> Date: Wed, 13 Aug 2025 00:37:10 +0800 Subject: [PATCH] =?UTF-8?q?Feat(core):=20=E7=BB=93=E6=9E=84=E5=8C=96?= =?UTF-8?q?=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../liteflow/ai/annotation/AIOutput.java | 8 ++++++-- .../liteflow/ai/annotation/OutputField.java | 2 +- .../liteflow/ai/context/ChatContext.java | 13 ++++++++++--- .../ai/domain/dto/ParsedAnnotationConfig.java | 19 ++++++++++++++----- .../liteflow/ai/model/ModelFactory.java | 14 ++++++++++++++ .../liteflow/ai/model/ModelProvider.java | 10 ++++++++++ .../ai/parse/AbstractAnnotationProcessor.java | 5 +++-- .../parse/assemble/ChatRequestAssembler.java | 17 +++++++++++++++-- .../engine/model/chat/entity/ChatRequest.java | 19 +++++++++++++++---- .../generator/JsonSchemaGenerator.java | 2 ++ .../ollama/model/OllamaModelProvider.java | 7 +++++++ .../liteflow/test/ai/core/proxy/ChatTest.java | 5 ++--- .../ai/core/proxy/cmp/AIBlockingChatCmp.java | 4 ++-- .../ai/core/proxy/cmp/AIStreamingChatCmp.java | 2 +- 14 files changed, 102 insertions(+), 25 deletions(-) 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 3ad5778f8..497cca314 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 @@ -99,8 +99,12 @@ public @interface AIOutput { *

* 表示输出的 JSON Schema 定义。如果需要添加描述信息,请使用{@link com.yomahub.liteflow.ai.engine.model.output.structure.Description} */ - Class entityClass() default String.class; -// String entityClass() default "java.lang.String"; + String typeName() default "java.lang.String"; + + /** + * 是否严格模式(默认为 true,输出模式为 JSON 时使用) + */ + boolean strict() default true; /** * 如需启用,请设置 {@link AIOutput#responseType()} 为 {@link ResponseType#JSON} diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/annotation/OutputField.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/annotation/OutputField.java index ad3aebbec..31865bd3f 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/annotation/OutputField.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/annotation/OutputField.java @@ -19,7 +19,7 @@ public @interface OutputField { /** * 源字段名称(必需)。 *

- * 指定要从结构化输出对象 ({@link AIOutput#entityClass()}) 中读取的字段名。 + * 指定要从结构化输出对象 ({@link AIOutput#typeName()}) 中读取的字段名。 */ String sourceField(); diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/context/ChatContext.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/context/ChatContext.java index b22f037fb..594470771 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/context/ChatContext.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/context/ChatContext.java @@ -1,21 +1,28 @@ package com.yomahub.liteflow.ai.context; +import com.yomahub.liteflow.slot.DefaultContext; + import java.util.UUID; /** - * chat 上下文 + * chat 上下文, 用于存储 StreamHandler + *

+ * 对于 StreamHandler 参数,可以通过流程参数传入,也可以通过 ChatContext 的构造函数传入 * * @author 苍镜月 * @since TODO */ -public class ChatContext { +public class ChatContext extends DefaultContext { private String chatId; private StreamHandler streamHandler; - public ChatContext() {} + public ChatContext() { + this.chatId = "chat_" + UUID.randomUUID(); + this.streamHandler = null; + } public ChatContext(StreamHandler streamHandler) { this.chatId = "chat_" + UUID.randomUUID(); diff --git a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/domain/dto/ParsedAnnotationConfig.java b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/domain/dto/ParsedAnnotationConfig.java index 8bbc511c4..55cbfc074 100644 --- a/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/domain/dto/ParsedAnnotationConfig.java +++ b/liteflow-ai/liteflow-ai-core/src/main/java/com/yomahub/liteflow/ai/domain/dto/ParsedAnnotationConfig.java @@ -14,7 +14,8 @@ public class ParsedAnnotationConfig { protected String systemPrompt; protected String userPrompt; protected ResponseType responseType = ResponseType.TEXT; - protected Class entityClass = String.class; + protected String typeName = "java.lang.String"; + protected boolean strict = true; public String getSystemPrompt() { return systemPrompt; @@ -28,8 +29,12 @@ public class ParsedAnnotationConfig { return responseType; } - public Class getEntityClass() { - return entityClass; + public String getTypeName() { + return typeName; + } + + public boolean isStrict() { + return strict; } public void setSystemPrompt(String systemPrompt) { @@ -44,7 +49,11 @@ public class ParsedAnnotationConfig { this.responseType = responseType; } - public void setEntityClass(Class entityClass) { - this.entityClass = entityClass; + public void setTypeName(String typeName) { + this.typeName = typeName; + } + + public void setStrict(boolean strict) { + this.strict = strict; } } 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 cb4b2cc0d..43c04d29d 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 @@ -2,6 +2,7 @@ package com.yomahub.liteflow.ai.model; import com.yomahub.liteflow.ai.domain.dto.ModelConfigAggregator; import com.yomahub.liteflow.ai.engine.model.chat.ChatModel; +import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatRequest; import com.yomahub.liteflow.ai.engine.model.embedding.EmbeddingModel; import com.yomahub.liteflow.ai.exception.LiteFlowAIException; import com.yomahub.liteflow.ai.parse.context.ProcessorContext; @@ -47,6 +48,19 @@ public class ModelFactory { LOG.info("Registered model provider: {}", provider.getProviderName()); } + /** + * 获取指定提供者名称的ChatRequest.Builder + * + * @param providerName 模型提供者名称 + * @return ChatRequest.Builder实例 + */ + public static ChatRequest.Builder getChatRequestBuilder(String providerName) { + return MODEL_PROVIDER_MAP + .get(providerName) + .createChatRequestBuilder() + .orElseThrow(() -> new LiteFlowAIException("ChatRequest.Builder is not supported for provider: " + providerName)); + } + /** * 获取指定提供者名称的ChatModel实例 * 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 e8f27ff3d..3ecc683b6 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 @@ -2,6 +2,7 @@ package com.yomahub.liteflow.ai.model; import com.yomahub.liteflow.ai.domain.dto.ModelConfigAggregator; import com.yomahub.liteflow.ai.engine.model.chat.ChatModel; +import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatRequest; import com.yomahub.liteflow.ai.engine.model.embedding.EmbeddingModel; import java.util.Optional; @@ -22,6 +23,15 @@ public interface ModelProvider { */ String getProviderName(); + /** + * 创建ChatRequest构建器 + * + * @return ChatRequest 的建造者 + */ + default Optional> createChatRequestBuilder() { + return Optional.of(ChatRequest.builder()); + } + /** * 创建ChatModel实例 * 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 5ec541209..1af0201e9 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 @@ -118,12 +118,13 @@ public abstract class AbstractAnnotationProcessor builder = ChatRequest.builder(); + ChatRequest.Builder builder = ModelFactory.getChatRequestBuilder(config.getProvider()); // 1. 连接 StreamHandler 回调 StreamHandler streamHandler = context.getStreamHandler(); @@ -76,7 +78,18 @@ public class ChatRequestAssembler extends AbstractRequestAssembler Objects.nonNull(contextRequest) ? contextRequest.getTransportType() : null, annotationConfig::getTransportType) ); - // TODO 4. 结构化输出 + // 4. 结构化输出 + builder.targetType( + merge(() -> Objects.nonNull(contextRequest) ? contextRequest.getTargetType() : null, + () -> new TypeReference(annotationConfig.getTypeName()) { + }.getType()) + ); + builder.responseType( + merge(() -> Objects.nonNull(contextRequest) ? contextRequest.getResponseType() : null, annotationConfig::getResponseType) + ); + builder.strict( + Boolean.TRUE.equals(merge(() -> Objects.nonNull(contextRequest) ? contextRequest.isStrict() : null, annotationConfig::isStrict)) + ); return builder.build(); } diff --git a/liteflow-ai/liteflow-ai-engine/src/main/java/com/yomahub/liteflow/ai/engine/model/chat/entity/ChatRequest.java b/liteflow-ai/liteflow-ai-engine/src/main/java/com/yomahub/liteflow/ai/engine/model/chat/entity/ChatRequest.java index 7d46198d7..3f8e50638 100644 --- a/liteflow-ai/liteflow-ai-engine/src/main/java/com/yomahub/liteflow/ai/engine/model/chat/entity/ChatRequest.java +++ b/liteflow-ai/liteflow-ai-engine/src/main/java/com/yomahub/liteflow/ai/engine/model/chat/entity/ChatRequest.java @@ -70,6 +70,11 @@ public class ChatRequest implements ModelRequest { */ protected final ResponseType responseType; + /** + * 是否为严格模式 + */ + protected final boolean strict; + /** * 输出解析器,用于解析模型的输出结果,由 targetType 生成 */ @@ -91,6 +96,7 @@ public class ChatRequest implements ModelRequest { this.responseType = ResponseType.TEXT; // 默认响应类型为文本 TypeReference targetType = new TypeReference() { }; // 默认目标类型为 String + this.strict = true; this.outputParser = OutputParser.fromTypeReference(targetType); } @@ -114,6 +120,7 @@ public class ChatRequest implements ModelRequest { this.resultHandler = resultHandler; this.chunkCallbackTransformer = chunkCallbackTransformer; this.responseType = responseType; + this.strict = strict; this.outputParser = OutputParser.fromTypeReference(targetType, strict); checkTransportConsistency(); checkResponseTypeConsistency(); @@ -133,7 +140,8 @@ public class ChatRequest implements ModelRequest { this.resultHandler = builder.resultHandler; this.chunkCallbackTransformer = builder.chunkCallbackTransformer; this.responseType = builder.responseType; - this.outputParser = OutputParser.fromTypeReference(builder.targetType, builder().strict); + this.strict = builder.strict; + this.outputParser = OutputParser.fromType(builder.targetType, builder.strict); checkTransportConsistency(); checkResponseTypeConsistency(); } @@ -220,6 +228,10 @@ public class ChatRequest implements ModelRequest { return outputParser; } + public boolean isStrict() { + return strict; + } + public void setResultHandler(ResultHandler resultHandler) { this.resultHandler = resultHandler; } @@ -238,8 +250,7 @@ public class ChatRequest implements ModelRequest { protected ResultHandler resultHandler; protected ChunkCallbackTransformer chunkCallbackTransformer; protected ResponseType responseType = ResponseType.TEXT; - protected TypeReference targetType = new TypeReference() { - }; + protected Type targetType = String.class; protected boolean strict = true; public abstract B self(); @@ -428,7 +439,7 @@ public class ChatRequest implements ModelRequest { * @param targetType 目标类型引用 * @see TypeReference */ - public B targetType(TypeReference targetType) { + public B targetType(Type targetType) { this.targetType = targetType; return self(); } diff --git a/liteflow-ai/liteflow-ai-engine/src/main/java/com/yomahub/liteflow/ai/engine/model/output/structure/generator/JsonSchemaGenerator.java b/liteflow-ai/liteflow-ai-engine/src/main/java/com/yomahub/liteflow/ai/engine/model/output/structure/generator/JsonSchemaGenerator.java index 431ed8b36..bd224dd2f 100644 --- a/liteflow-ai/liteflow-ai-engine/src/main/java/com/yomahub/liteflow/ai/engine/model/output/structure/generator/JsonSchemaGenerator.java +++ b/liteflow-ai/liteflow-ai-engine/src/main/java/com/yomahub/liteflow/ai/engine/model/output/structure/generator/JsonSchemaGenerator.java @@ -66,6 +66,8 @@ public class JsonSchemaGenerator { }); // 全部属性都应当为 required + // https://platform.openai.com/docs/guides/structured-outputs/supported-schemas?api-mode=chat#all-fields-must-be-required + // 详细可见 OpenAI 官方说明,翻译成人话就是,都得加上 Required,但是如果非 strict,那么 type 允许加个 null configBuilder.forFields().withRequiredCheck(field -> true); return configBuilder; diff --git a/liteflow-ai/liteflow-ai-ollama/src/main/java/com/yomahub/liteflow/ai/model/ollama/model/OllamaModelProvider.java b/liteflow-ai/liteflow-ai-ollama/src/main/java/com/yomahub/liteflow/ai/model/ollama/model/OllamaModelProvider.java index cc5f32941..d8b27fc3a 100644 --- a/liteflow-ai/liteflow-ai-ollama/src/main/java/com/yomahub/liteflow/ai/model/ollama/model/OllamaModelProvider.java +++ b/liteflow-ai/liteflow-ai-ollama/src/main/java/com/yomahub/liteflow/ai/model/ollama/model/OllamaModelProvider.java @@ -2,10 +2,12 @@ package com.yomahub.liteflow.ai.model.ollama.model; import com.yomahub.liteflow.ai.domain.dto.ModelConfigAggregator; import com.yomahub.liteflow.ai.engine.model.chat.ChatModel; +import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatRequest; import com.yomahub.liteflow.ai.engine.model.embedding.EmbeddingModel; import com.yomahub.liteflow.ai.model.ModelProviderRegistrar; import com.yomahub.liteflow.ai.model.ollama.constants.OllamaConstant; import com.yomahub.liteflow.ai.model.ollama.model.chat.OllamaChatModel; +import com.yomahub.liteflow.ai.model.ollama.model.chat.OllamaChatRequest; import java.util.Optional; @@ -20,6 +22,11 @@ import static com.yomahub.liteflow.ai.util.SetUtil.setIfPresent; public class OllamaModelProvider extends ModelProviderRegistrar { + @Override + public Optional> createChatRequestBuilder() { + return Optional.of(OllamaChatRequest.builder()); + } + @Override public Optional createChatModel(ModelConfigAggregator configAggregator) { return Optional.of(OllamaChatModel.builder()) diff --git a/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/ai/core/proxy/ChatTest.java b/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/ai/core/proxy/ChatTest.java index ce3cc9d0e..da66b3d75 100644 --- a/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/ai/core/proxy/ChatTest.java +++ b/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/ai/core/proxy/ChatTest.java @@ -4,7 +4,6 @@ import com.yomahub.liteflow.ai.context.ChatContext; import com.yomahub.liteflow.ai.context.StreamHandler; import com.yomahub.liteflow.core.FlowExecutor; import com.yomahub.liteflow.flow.LiteflowResponse; -import com.yomahub.liteflow.slot.DefaultContext; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -32,7 +31,7 @@ public class ChatTest { @Test public void testBlockingChat() { - LiteflowResponse liteflowResponse = flowExecutor.execute2Resp("chain1", null, ChatContext.class, DefaultContext.class); + LiteflowResponse liteflowResponse = flowExecutor.execute2Resp("chain1", null, ChatContext.class); Assertions.assertTrue(liteflowResponse.isSuccess()); } @@ -64,7 +63,7 @@ public class ChatTest { ChatContext chatContext = new ChatContext(streamHandler); - LiteflowResponse liteflowResponse = flowExecutor.execute2Resp("chain2", null, chatContext, DefaultContext.class); + LiteflowResponse liteflowResponse = flowExecutor.execute2Resp("chain2", null, chatContext); Assertions.assertTrue(liteflowResponse.isSuccess()); } } diff --git a/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/ai/core/proxy/cmp/AIBlockingChatCmp.java b/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/ai/core/proxy/cmp/AIBlockingChatCmp.java index 10e0ea464..4db8be20d 100644 --- a/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/ai/core/proxy/cmp/AIBlockingChatCmp.java +++ b/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/ai/core/proxy/cmp/AIBlockingChatCmp.java @@ -34,8 +34,8 @@ import com.yomahub.liteflow.ai.util.TriState; } ) @AIOutput( - responseType = ResponseType.TEXT, - entityClass = Output.class, + responseType = ResponseType.JSON, + typeName = "com.yomahub.liteflow.test.ai.core.proxy.cmp.Output", methodExpress = "setData", useKeyIndex = true, key = "result" diff --git a/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/ai/core/proxy/cmp/AIStreamingChatCmp.java b/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/ai/core/proxy/cmp/AIStreamingChatCmp.java index 4a269ca00..880ed3d06 100644 --- a/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/ai/core/proxy/cmp/AIStreamingChatCmp.java +++ b/liteflow-testcase-el/liteflow-testcase-el-ai/src/test/java/com/yomahub/liteflow/test/ai/core/proxy/cmp/AIStreamingChatCmp.java @@ -35,7 +35,7 @@ import com.yomahub.liteflow.ai.util.TriState; ) @AIOutput( responseType = ResponseType.TEXT, - entityClass = Output.class, + typeName = "com.yomahub.liteflow.test.ai.core.proxy.cmp.Output", methodExpress = "setData", useKeyIndex = true, key = "result"