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"