Feat: 解析AIOutput,将大模型响应结果映射到上下文

This commit is contained in:
LuanY77
2025-07-31 16:33:21 +08:00
parent 58aa6fdab6
commit a23e6042b2
10 changed files with 173 additions and 37 deletions

View File

@@ -5,6 +5,8 @@ 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.context.ContextAccessor;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
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;
@@ -94,4 +96,32 @@ public abstract class AbstractAnnotationProcessor<A extends Annotation, T extend
wrapBean.setEntityClass(String.class);
}
}
/**
* 将处理结果映射到上下文中
*
* @param context 处理器上下文
* @param result 处理结果
*/
protected void mapOutput2Context(ProcessorContext<T> context, Object result) {
if (Objects.isNull(result)) {
LOG.warn("AI processing result is null, skipping context mapping.");
return;
}
AIOutput outputAnno = context.getAiOutputAnno();
if (Objects.isNull(outputAnno)) {
LOG.warn("No AIOutput annotation found, skipping context mapping.");
return;
}
// 将结果映射到上下文中
String expression = outputAnno.methodExpress();
if (StrUtil.isNotBlank(expression)) {
ContextAccessor.setContextValueByExpression(expression, context, result);
LOG.info("Mapped AI output to context with expression: {}", expression);
} else {
LOG.warn("AIOutput expression is blank, skipping context mapping.");
}
}
}

View File

@@ -1,6 +1,7 @@
package com.yomahub.liteflow.ai.parse;
import com.yomahub.liteflow.ai.domain.enums.AITypeEnum;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
import com.yomahub.liteflow.log.LFLog;
import com.yomahub.liteflow.log.LFLoggerManager;
@@ -52,7 +53,7 @@ public class AnnotationParser {
/**
* 执行注解解析后处理
*
* @param annotation 待解析注解
* @param annotation 待解析注解(这里的注解参数是为了帮助找到对应的处理器)
* @param context 处理器上下文
* @param result 响应结果
*/
@@ -60,7 +61,7 @@ public class AnnotationParser {
public static void postProcessAfterTrigger(Annotation annotation, ProcessorContext<?> context, Object result) {
AITypeEnum type = AITypeEnum.fromAnnotationType(annotation);
AnnotationProcessor processor = getProcessor(type.getCode());
processor.postProcessAfterTrigger(annotation, context, result);
processor.postProcessAfterTrigger(context, result);
}
private static AnnotationProcessor<?, ?> getProcessor(Integer code) {

View File

@@ -1,5 +1,6 @@
package com.yomahub.liteflow.ai.parse;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
import com.yomahub.liteflow.ai.proxy.wrap.AIProxyWrapBean;
import java.lang.annotation.Annotation;
@@ -24,9 +25,8 @@ public interface AnnotationProcessor<A extends Annotation, T extends AIProxyWrap
/**
* 执行注解解析后处理
*
* @param annotation 待解析注解
* @param context 处理器上下文
* @param result 响应结果
*/
void postProcessAfterTrigger(A annotation, ProcessorContext<T> context, Object result);
void postProcessAfterTrigger(ProcessorContext<T> context, Object result);
}

View File

@@ -3,7 +3,7 @@ package com.yomahub.liteflow.ai.parse.anno;
import com.yomahub.liteflow.ai.annotation.AIChat;
import com.yomahub.liteflow.ai.domain.enums.AITypeEnum;
import com.yomahub.liteflow.ai.parse.AbstractAnnotationProcessor;
import com.yomahub.liteflow.ai.parse.ProcessorContext;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
import com.yomahub.liteflow.ai.proxy.wrap.ChatProxyWrapBean;
import com.yomahub.liteflow.ai.util.SetUtil;
@@ -33,8 +33,12 @@ public class ChatAnnotationProcessor extends AbstractAnnotationProcessor<AIChat,
}
@Override
public void postProcessAfterTrigger(AIChat annotation, ProcessorContext<ChatProxyWrapBean> context, Object result) {
// TODO: 处理输出参数绑定
public void postProcessAfterTrigger(ProcessorContext<ChatProxyWrapBean> context, Object result) {
ChatProxyWrapBean wrapBean = context.getWrapBean();
// 非流式输出,需要进行结构化处理
if (!wrapBean.isStreaming()) {
mapOutput2Context(context, result);
}
}
@Override

View File

@@ -3,7 +3,7 @@ package com.yomahub.liteflow.ai.parse.anno;
import com.yomahub.liteflow.ai.annotation.AIClassify;
import com.yomahub.liteflow.ai.domain.enums.AITypeEnum;
import com.yomahub.liteflow.ai.parse.AbstractAnnotationProcessor;
import com.yomahub.liteflow.ai.parse.ProcessorContext;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
import com.yomahub.liteflow.ai.proxy.wrap.ClassifyProxyWrapBean;
import com.yomahub.liteflow.ai.util.SetUtil;
@@ -38,7 +38,8 @@ public class ClassifyAnnotationProcessor extends AbstractAnnotationProcessor<AIC
}
@Override
public void postProcessAfterTrigger(AIClassify annotation, ProcessorContext<ClassifyProxyWrapBean> context, Object result) {
public void postProcessAfterTrigger(ProcessorContext<ClassifyProxyWrapBean> context, Object result) {
mapOutput2Context(context, result);
}
@Override

View File

@@ -3,7 +3,7 @@ package com.yomahub.liteflow.ai.parse.anno;
import com.yomahub.liteflow.ai.annotation.AIRetrieval;
import com.yomahub.liteflow.ai.domain.enums.AITypeEnum;
import com.yomahub.liteflow.ai.parse.AbstractAnnotationProcessor;
import com.yomahub.liteflow.ai.parse.ProcessorContext;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
import com.yomahub.liteflow.ai.proxy.wrap.RetrievalProxyWrapBean;
/**
@@ -21,7 +21,7 @@ public class RetrievalAnnotationProcessor extends AbstractAnnotationProcessor<AI
}
@Override
public void postProcessAfterTrigger(AIRetrieval annotation, ProcessorContext<RetrievalProxyWrapBean> context, Object result) {
public void postProcessAfterTrigger(ProcessorContext<RetrievalProxyWrapBean> context, Object result) {
}

View File

@@ -0,0 +1,119 @@
package com.yomahub.liteflow.ai.parse.context;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.yomahub.liteflow.ai.annotation.AIOutput;
import com.yomahub.liteflow.ai.annotation.OutputField;
import com.yomahub.liteflow.ai.exception.LiteFlowAIException;
import com.yomahub.liteflow.ai.util.SetUtil;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.log.LFLog;
import com.yomahub.liteflow.log.LFLoggerManager;
import dev.langchain4j.service.Result;
import java.util.Objects;
/**
* 上下文访问器
*
* @author 苍镜月
* @since TODO
*/
public class ContextAccessor {
private static final LFLog LOG = LFLoggerManager.getLogger(ContextAccessor.class);
/**
* 根据表达式在上下文中查找值(AIInput注解使用)
*
* @param expression 表达式
* @param context 处理器上下文
* @return 查找到的值
*/
public static String searchContextByExpression(String expression, ProcessorContext<?> context) {
if (StrUtil.isBlank(expression)) {
return null;
}
try {
NodeComponent nodeComponent = context.getNodeComponent();
return nodeComponent.getContextValue(expression);
} catch (Exception e) {
LOG.info("Failed to search context by expression: {}", expression);
return null;
}
}
/**
* 根据表达式在上下文中设置值(AIOutput注解使用)
*
* @param expression 表达式
* @param context 处理器上下文
* @param value 值
*/
public static void setContextValueByExpression(String expression, ProcessorContext<?> context, Object value) {
if (StrUtil.isBlank(expression) || Objects.isNull(value)) return;
// 检查结果是否为 Result 类型
if (!(value instanceof Result)) {
throw new LiteFlowAIException("AIOutput annotation value must be of type Result.");
}
// 获取 Result 的内容
value = ((Result<?>) value).content();
NodeComponent nodeComponent = context.getNodeComponent();
AIOutput outputAnno = context.getAiOutputAnno();
// 检查是否使用了嵌套索引
if (outputAnno.useKeyIndex()) {
// 如果同时使用了 key 和 index则抛出异常
if (SetUtil.isPresent(outputAnno.key()) && SetUtil.isPresent(outputAnno.index())) {
throw new LiteFlowAIException("AIOutput annotation cannot use both key and index together.");
}
// 如果 key 和 index 都未设置,则抛出异常
if (SetUtil.isNotPresent(outputAnno.key()) && SetUtil.isNotPresent(outputAnno.index())) {
throw new LiteFlowAIException("AIOutput annotation must specify either key or index when useKeyIndex is true.");
}
Object key = SetUtil.isPresent(outputAnno.key()) ? outputAnno.key() : outputAnno.index();
nodeComponent.setContextValue(expression, key, value);
} else {
// 如果未使用嵌套索引,则直接设置值
nodeComponent.setContextValue(expression, value);
}
// 处理输出字段映射
if (SetUtil.isPresent(outputAnno.mapping())) {
for (OutputField outputField : outputAnno.mapping()) {
// 获取源字段值
Object fieldValue = ReflectUtil.getFieldValue(value, outputField.sourceField());
if (Objects.isNull(fieldValue)) {
continue;
}
// 确定目标表达式,如果未指定,直接集成 AIOutput 的主表达式
String targetExpression = StrUtil.isNotBlank(outputField.methodExpress())
? outputField.methodExpress()
: outputAnno.methodExpress();
// 使用了嵌套索引
if (outputAnno.useKeyIndex()) {
// 如果同时使用了 key 和 index则抛出异常
if (SetUtil.isPresent(outputField.key()) && SetUtil.isPresent(outputField.index())) {
throw new LiteFlowAIException("OutputField annotation cannot use both key and index together.");
}
// 如果 key 和 index 都未设置,则抛出异常
if (SetUtil.isNotPresent(outputField.key()) && SetUtil.isNotPresent(outputField.index())) {
throw new LiteFlowAIException("OutputField annotation must specify either key or index when useKeyIndex is true.");
}
Object key = SetUtil.isPresent(outputField.key()) ? outputField.key() : outputField.index();
nodeComponent.setContextValue(targetExpression, key, fieldValue);
} else {
// 未使用嵌套索引,直接设置值
nodeComponent.setContextValue(targetExpression, fieldValue);
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
package com.yomahub.liteflow.ai.parse;
package com.yomahub.liteflow.ai.parse.context;
import com.yomahub.liteflow.ai.annotation.AIInput;
import com.yomahub.liteflow.ai.annotation.AIOutput;

View File

@@ -3,8 +3,8 @@ package com.yomahub.liteflow.ai.parse.prompt;
import cn.hutool.core.util.StrUtil;
import com.yomahub.liteflow.ai.annotation.InputField;
import com.yomahub.liteflow.ai.exception.LiteFlowAIException;
import com.yomahub.liteflow.ai.parse.ProcessorContext;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.ai.parse.context.ContextAccessor;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
import java.util.*;
import java.util.function.Function;
@@ -73,7 +73,7 @@ public class PromptTemplateParser {
// 第一优先级:使用 InputField 中的 expression 映射
if (Objects.nonNull(field)) {
// 使用表达式在上下文查找
String value = searchContextByExpression(field.expression(), context);
String value = ContextAccessor.searchContextByExpression(field.expression(), context);
if (StrUtil.isNotBlank(value)) {
return value;
}
@@ -88,7 +88,7 @@ public class PromptTemplateParser {
}
// 第二优先级,占位符作为表达式在上下文中查找
String value = searchContextByExpression(placeholder, context);
String value = ContextAccessor.searchContextByExpression(placeholder, context);
if (StrUtil.isNotBlank(value)) {
return value;
}
@@ -96,25 +96,4 @@ public class PromptTemplateParser {
// 如果都没有找到,返回原始占位符
return placeholder;
}
/**
* 根据表达式在上下文中查找值
* 这里可以根据实际需要实现更复杂的查找逻辑
*
* @param expression 表达式
* @param context 处理器上下文
* @return 查找到的值
*/
private static String searchContextByExpression(String expression, ProcessorContext<?> context) {
if (StrUtil.isBlank(expression)) {
return null;
}
try {
NodeComponent nodeComponent = context.getNodeComponent();
return nodeComponent.getContextValue(expression);
} catch (Exception e) {
return null;
}
}
}

View File

@@ -50,6 +50,7 @@ public class OllamaModelProvider extends ModelProviderRegistrar {
// TODO timeout 类型转换
// setIfPresent(ollamaChatModelBuilder::timeout, modelConfig.getTimeout());
setIfPresent(ollamaChatModelBuilder::maxRetries, modelConfig.getMaxRetries());
// TODO 自定义请求头
return ollamaChatModelBuilder;
})
// 结构化输出配置
@@ -82,6 +83,7 @@ public class OllamaModelProvider extends ModelProviderRegistrar {
setIfPresent(ollamaStreamingChatModelBuilder::stop, modelConfig.getStop());
// TODO timeout 类型转换
// setIfPresent(ollamaStreamingChatModelBuilder::timeout, modelConfig.getTimeout());
// TODO 自定义请求头
return ollamaStreamingChatModelBuilder;
})
// 结构化输出配置