Feat: 意图识别节点实现,添加 chat 测试与 classify 测试

This commit is contained in:
LuanY77
2025-08-25 19:08:05 +08:00
parent b4d8697dbc
commit 6f0b861b13
32 changed files with 619 additions and 176 deletions

View File

@@ -1,7 +1,11 @@
package com.yomahub.liteflow.ai.annotation;
import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatOptions;
import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatRequest;
import com.yomahub.liteflow.ai.context.ChatContext;
import com.yomahub.liteflow.ai.engine.tool.registry.DelegatingToolRegistry;
import com.yomahub.liteflow.ai.engine.tool.registry.ScanningToolRegistry;
import com.yomahub.liteflow.ai.engine.tool.registry.StaticToolRegistry;
import com.yomahub.liteflow.ai.engine.tool.registry.ToolRegistry;
import com.yomahub.liteflow.ai.tool.SpringBeanToolRegistry;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -19,41 +23,6 @@ import java.lang.annotation.Target;
@Target(ElementType.TYPE)
public @interface AIClassify {
/**
* 从上下文中获取的请求参数的上下文路径表达式。
* <p>
* 如果你不希望在注解中进行静态的模型配置,或者你希望使用特定厂商实现的 ChatRequest(其中可能存在一些独有的参数)
* 请使用这个属性,并在上下文中提供对应的 {@link ChatRequest} 值。
* 如果不提供,框架将统一使用 {@link ChatRequest} 发起请求。
* <p>
* {@link ChatRequest} 可以是对应提供商的具体实现类。
* <p>
* 如果你使用了这个属性可以不进行 {@link AIChat#systemPrompt()} 和 {@link AIChat#userPrompt()} 和 {@link AIChat#streaming()} 的配置,
* 因为这些配置会从 {@link ChatRequest} 中获取。如果进行了配置,优先使用 {@link ChatRequest} 中的配置。
* <p>
* 以及,你会发现 {@link ChatRequest#getOptions()} 的 {@link ChatOptions} 和 {@link AIComponent} 中的配置有重合。
* 同样的,即使你进行了 {@link AIComponent} 的配置,会优先使用 {@link ChatRequest} 的配置。
* <p>
* <b>请注意:如果相关的配置为空或默认值,则会使用 {@link AIComponent} 和 {@link AIChat} 中的配置!!!</b>
* <p>
* 该方法用于从一个必须提供 {@code get} 方法的上下文中检索数据。
* <p>
* 表达式支持以下两种形式:
* <ul>
* <li>
* <b>直接属性检索:</b><br>
* 例如,从上下文中获取 OpenAI 的 ChatRequest他的名字为 {@code openAIChatRequest}
* 表达式应为:{@code "openAIChatRequest"}。
* </li>
* <li>
* <b>嵌套属性检索 (例如 Map):</b><br>
* 例如,从上下文的一个名为 {@code requestMap} 的 Map 对象中,获取键为 {@code openAI} 的 ChatRequest 对象,
* 表达式应为:{@code "requestMap.openAI"}。
* </li>
* </ul>
*/
String getChatRequest() default "";
/**
* 系统提示词
*/
@@ -73,4 +42,22 @@ public @interface AIClassify {
* 是否多标签分类,默认 false
*/
boolean multiLabel() default false;
/**
* 需要启用的工具名列表(默认全部启用)
* <p>
* 工具注册于 {@link ChatContext#getToolRegistry()},请于上下文中传入
* <p>
* 如果上下文中没有传入工具注册器,则会使用注册为 Spring Bean 的 {@link ToolRegistry}
* ,框架默认实现了 {@link SpringBeanToolRegistry}。
* 可以将 Tool 注册为 Bean 实现自动发现与注册。
* <p>
*
* @see ToolRegistry
* @see StaticToolRegistry
* @see ScanningToolRegistry
* @see DelegatingToolRegistry
* @see SpringBeanToolRegistry
*/
String[] toolNames() default {};
}

View File

@@ -1,9 +1,10 @@
package com.yomahub.liteflow.ai.domain.dto;
import java.util.ArrayList;
import java.util.List;
/**
* TODO
* AIClassify注解解析后配置
*
* @author 苍镜月
* @since TODO
@@ -15,6 +16,8 @@ public class ParsedClassifyAnnotationConfig extends ParsedAnnotationConfig {
private boolean multiLabel;
private List<String> toolNames = new ArrayList<>();
public List<String> getCategories() {
return categories;
}
@@ -23,6 +26,10 @@ public class ParsedClassifyAnnotationConfig extends ParsedAnnotationConfig {
return multiLabel;
}
public List<String> getToolNames() {
return toolNames;
}
public void setCategories(List<String> categories) {
this.categories = categories;
}
@@ -30,4 +37,8 @@ public class ParsedClassifyAnnotationConfig extends ParsedAnnotationConfig {
public void setMultiLabel(boolean multiLabel) {
this.multiLabel = multiLabel;
}
public void setToolNames(List<String> toolNames) {
this.toolNames = toolNames;
}
}

View File

@@ -1,12 +1,31 @@
package com.yomahub.liteflow.ai.parse.assemble;
import com.yomahub.liteflow.ai.context.ChatContext;
import com.yomahub.liteflow.ai.context.StreamHandler;
import com.yomahub.liteflow.ai.domain.dto.ModelConfigAggregator;
import com.yomahub.liteflow.ai.domain.dto.ParsedClassifyAnnotationConfig;
import com.yomahub.liteflow.ai.engine.interact.transport.TransportType;
import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatOptions;
import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatRequest;
import com.yomahub.liteflow.ai.engine.model.chat.message.Message;
import com.yomahub.liteflow.ai.engine.model.chat.message.SystemMessage;
import com.yomahub.liteflow.ai.engine.model.chat.message.UserMessage;
import com.yomahub.liteflow.ai.engine.model.output.ResponseType;
import com.yomahub.liteflow.ai.engine.model.output.structure.TypeReference;
import com.yomahub.liteflow.ai.engine.tool.registry.StaticToolRegistry;
import com.yomahub.liteflow.ai.engine.tool.registry.ToolRegistry;
import com.yomahub.liteflow.ai.model.ModelFactory;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.yomahub.liteflow.ai.util.SetUtil.setIfPresent;
/**
* TODO
* ChatRequest 组装器(意图识别)
*
* @author 苍镜月
* @since TODO
@@ -14,12 +33,113 @@ import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatRequest;
public class ClassifyRequestAssembler extends AbstractRequestAssembler<ParsedClassifyAnnotationConfig> {
@SuppressWarnings("rawtypes")
@Override
protected ChatRequest doAssemble(ParsedClassifyAnnotationConfig annotationConfig, ModelConfigAggregator config, ChatContext context) {
// TODO
ChatRequest.Builder<?> builder = ModelFactory.getChatRequestBuilder(config.getProvider());
// 1. 连接 StreamHandler 回调
StreamHandler streamHandler = context.getStreamHandler();
if (Objects.nonNull(streamHandler)) {
LOG.info("Connecting StreamHandler to ChatRequest");
builder.onStart(streamHandler::onStart)
.onClose(streamHandler::onClose)
.onError(streamHandler::onError)
.onText(streamHandler::onText)
.onThinking(streamHandler::onThinking)
.onToolsCalling(streamHandler::onToolsCalling)
.onUsage(streamHandler::onUsage)
.onGrounding(streamHandler::onGrounding)
.onCompletion(streamHandler::onCompletion)
.onFinal(streamHandler::onFinal);
}
// 2. ChatOptions
ChatOptions.Builder<?> optionsBuilder = ChatOptions.builder();
setIfPresent(optionsBuilder::temperature, config.getTemperature());
setIfPresent(optionsBuilder::topP, config.getTopP());
setIfPresent(optionsBuilder::topK, config.getTopK());
setIfPresent(optionsBuilder::maxTokens, config.getMaxTokens());
setIfPresent(optionsBuilder::seed, config.getSeed());
setIfPresent(optionsBuilder::enableThinking, config.getEnableThinking().toBool());
builder.options(optionsBuilder.build());
// 3. Message
List<Message> messages = new ArrayList<>();
// 意图分类的系统消息
messages.add(buildClassifyMessage(annotationConfig));
setIfPresent(t -> messages.add(new SystemMessage(t)), annotationConfig.getSystemPrompt());
setIfPresent(t -> messages.add(new UserMessage(t)), annotationConfig.getUserPrompt());
builder.messages(messages);
// 4. streaming 相关参数
// 定死使用阻塞式传输
builder.streaming(false);
builder.transportType(TransportType.HTTP);
return null;
// 4. 结构化输出
// 如果开启了多意图分类,那么返回值就是 List<String>,否则就是单一的 String
if (annotationConfig.isMultiLabel()) {
builder.targetType(new TypeReference<List<String>>() {
});
builder.responseType(ResponseType.JSON);
builder.strict(true);
} else {
builder.targetType(new TypeReference<String>() {
});
builder.responseType(ResponseType.TEXT);
builder.strict(false);
}
// 5. 工具调用
ToolRegistry toolRegistry = context.getToolRegistry();
if (Objects.nonNull(toolRegistry)) {
StaticToolRegistry staticToolRegistry = new StaticToolRegistry();
HashSet<String> targetToolNames = new HashSet<>(annotationConfig.getToolNames());
toolRegistry.getAllTools()
.stream()
.filter(tool -> targetToolNames.isEmpty() || targetToolNames.contains(tool.getName()))
.forEach(staticToolRegistry::register);
builder.toolRegistry(staticToolRegistry);
}
return builder.build();
}
/**
* 构建意图分类的系统消息
*
* @param annotationConfig 注解配置
* @return 系统消息
*/
private Message buildClassifyMessage(ParsedClassifyAnnotationConfig annotationConfig) {
StringBuilder sb = new StringBuilder();
sb.append("You are an expert intent classifier.\n");
sb.append("Your task is to analyze the user's query and classify it based on the predefined categories.\n\n");
sb.append("Available categories are:\n");
String categoriesString = annotationConfig.getCategories().stream()
.map(c -> String.format("- %s", c))
.collect(Collectors.joining("\n"));
sb.append(categoriesString);
sb.append("\n\n");
sb.append("Follow these rules strictly:\n");
if (annotationConfig.isMultiLabel()) {
sb.append("1. You may select one or more categories that are relevant to the user's query.\n");
sb.append("2. Your response MUST be a valid JSON array of strings, containing only the names of the selected categories.\n");
sb.append("3. For example: [\"category1\", \"category2\"]\n");
} else {
sb.append("1. You must select only ONE category that best matches the user's query.\n");
sb.append("2. Your response MUST be only the name of that single category.\n");
sb.append("3. For example: category1\n");
}
sb.append("4. Do NOT provide any explanations, introductions, or any text other than the category name(s) in the specified format.");
return new SystemMessage(sb.toString());
}
}

View File

@@ -59,19 +59,18 @@ public class ContextAccessor {
if (StrUtil.isBlank(expression) || Objects.isNull(value)) return;
// 检查结果是否为 Response 类型
if (!(value instanceof Response)) {
throw new LiteFlowAIException("AI node output value must be of type Response.");
}
// 如果 request 是 ChatRequest 且 value 是 ChatResponse则尝试进行结构化转换
if (context.getModelRequest() instanceof ChatRequest && value instanceof ChatResponse) {
ChatRequest chatRequest = context.getModelRequest().toChatRequest();
if (ResponseType.JSON.equals(chatRequest.getResponseType())) {
value = ((ChatResponse) value).as(chatRequest.getOutputParser());
if (value instanceof Response) {
// 如果 request 是 ChatRequest 且 value 是 ChatResponse则尝试进行结构化转换
if (context.getModelRequest() instanceof ChatRequest && value instanceof ChatResponse) {
ChatRequest chatRequest = context.getModelRequest().toChatRequest();
if (ResponseType.JSON.equals(chatRequest.getResponseType())) {
value = ((ChatResponse) value).as(chatRequest.getOutputParser());
} else {
value = ((ChatResponse) value).getContent();
}
} else {
value = ((ChatResponse) value).getContent();
value = ((Response<?>) value).getContent();
}
} else {
value = ((Response<?>) value).getContent();
}
NodeComponent nodeComponent = context.getNodeComponent();

View File

@@ -129,7 +129,7 @@ public abstract class AbstractAIComponentHandler<T extends Annotation> {
.subclass(nodeComponentClass)
.name(generateProxyClassName(wrapBean))
.implement(wrapBean.getInterfaceClass())
.method(getInterceptMethodName())
.method(getInterceptMethodName(wrapBean))
.intercept(InvocationHandlerAdapter.of(getInvocationHandler(wrapBean)))
.make()
.load(this.getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
@@ -174,7 +174,7 @@ public abstract class AbstractAIComponentHandler<T extends Annotation> {
*
* @return 拦截方法名称
*/
protected abstract ElementMatcher<? super MethodDescription> getInterceptMethodName();
protected abstract ElementMatcher<? super MethodDescription> getInterceptMethodName(AIProxyWrapBean<T> wrapBean);
/**
* 判断是否支持指定的注解类型

View File

@@ -45,7 +45,7 @@ public class ChatComponentHandler extends AbstractAIComponentHandler<AIChat> {
}
@Override
protected ElementMatcher<? super MethodDescription> getInterceptMethodName() {
protected ElementMatcher<? super MethodDescription> getInterceptMethodName(AIProxyWrapBean<AIChat> wrapBean) {
return ElementMatchers.named(INTERCEPT_METHOD_NAME);
}
}

View File

@@ -21,7 +21,8 @@ import java.lang.reflect.InvocationHandler;
*/
public class ClassifyComponentHandler extends AbstractAIComponentHandler<AIClassify> {
private static final String INTERCEPT_METHOD_NAME = "processSwitch";
private static final String INTERCEPT_SWITCH_METHOD_NAME = "processSwitch";
private static final String INTERCEPT_MULTI_SWITCH_METHOD_NAME = "processMultiSwitch";
@Override
public AITypeEnum getAIType() {
@@ -45,7 +46,11 @@ public class ClassifyComponentHandler extends AbstractAIComponentHandler<AIClass
}
@Override
protected ElementMatcher<? super MethodDescription> getInterceptMethodName() {
return ElementMatchers.named(INTERCEPT_METHOD_NAME);
protected ElementMatcher<? super MethodDescription> getInterceptMethodName(AIProxyWrapBean<AIClassify> wrapBean) {
if (wrapBean.getAnnotation().multiLabel()) {
return ElementMatchers.named(INTERCEPT_MULTI_SWITCH_METHOD_NAME);
} else {
return ElementMatchers.named(INTERCEPT_SWITCH_METHOD_NAME);
}
}
}

View File

@@ -2,6 +2,7 @@ package com.yomahub.liteflow.ai.proxy.invocation;
import com.yomahub.liteflow.ai.domain.dto.ParsedClassifyAnnotationConfig;
import com.yomahub.liteflow.ai.engine.model.chat.ChatModel;
import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatResponse;
import com.yomahub.liteflow.ai.exception.LiteFlowAIException;
import com.yomahub.liteflow.ai.model.ModelFactory;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
@@ -30,18 +31,18 @@ public class ClassifyAIInvocationHandler extends AbstractAIInvocationHandler<Cla
if (SetUtil.isNotPresent(annotationConfig.getCategories())) {
throw new LiteFlowAIException("Categories cannot be empty for classification");
}
// 校验多标签分类
if (!annotationConfig.isMultiLabel() && annotationConfig.getCategories().size() > 1) {
throw new LiteFlowAIException("Multi-label classification is not allowed when multiLabel is false");
}
if (annotationConfig.isMultiLabel() && annotationConfig.getCategories().size() < 2) {
throw new LiteFlowAIException("At least two categories are required for multi-label classification");
}
}
@Override
protected Object doExecuteAIProcess(ProcessorContext<?> processorContext, Object[] args) {
ChatModel chatModel = ModelFactory.getChatModel(wrapBean);
return chatModel.chat(processorContext.getModelRequest().toChatRequest());
ChatResponse response = chatModel.chat(processorContext.getModelRequest().toChatRequest());
ParsedClassifyAnnotationConfig annotationConfig = (ParsedClassifyAnnotationConfig) processorContext.getParsedAnnotationConfig();
// 如果是多标签则返回结构化转换的list, 单标签返回 String
if (annotationConfig.isMultiLabel()) {
return response.as(processorContext.getModelRequest().toChatRequest().getOutputParser());
} else {
return response.getContent().getContent();
}
}
}

View File

@@ -1,4 +1,4 @@
package com.yomahub.liteflow.test.ai.core.proxy;
package com.yomahub.liteflow.test.ai.core.chat;
import com.yomahub.liteflow.ai.context.ChatContext;
import com.yomahub.liteflow.ai.context.StreamHandler;
@@ -21,24 +21,17 @@ import javax.annotation.Resource;
* @since TODO
*/
@TestPropertySource(properties = {"spring.config.location=classpath:core/proxy/application.yaml"})
@TestPropertySource(properties = {"spring.config.location=classpath:core/chat/application.yaml"})
@SpringBootTest(classes = {ChatTest.class, SpringUtil.class})
@EnableAutoConfiguration
@ComponentScan({"com.yomahub.liteflow.test.ai.core.proxy.cmp"})
@ComponentScan({"com.yomahub.liteflow.test.ai.core.chat.cmp"})
public class ChatTest {
@Resource
private FlowExecutor flowExecutor;
@Test
public void testBlockingChat() {
LiteflowResponse liteflowResponse = flowExecutor.execute2Resp("chain1", null, ChatContext.class);
Assertions.assertTrue(liteflowResponse.isSuccess());
}
@Test
public void testStreaming() {
StreamHandler streamHandler = StreamHandler.builder()
private StreamHandler getStreamHandler() {
return StreamHandler.builder()
.onStart(context -> System.out.println("chat start"))
.onClose(context -> System.out.println("chat close"))
.onError((context, t) -> {
@@ -61,10 +54,45 @@ public class ChatTest {
return response;
})
.build();
}
ChatContext chatContext = new ChatContext(streamHandler);
@Test
public void testDashScopeChat() {
LiteflowResponse liteflowResponse = flowExecutor.execute2Resp("chain1", null, ChatContext.class);
Assertions.assertTrue(liteflowResponse.isSuccess());
}
@Test
public void testDashScopeStream() {
ChatContext chatContext = new ChatContext(getStreamHandler());
LiteflowResponse liteflowResponse = flowExecutor.execute2Resp("chain2", null, chatContext);
Assertions.assertTrue(liteflowResponse.isSuccess());
}
@Test
public void testOllamaChat() {
LiteflowResponse liteflowResponse = flowExecutor.execute2Resp("chain3", null, ChatContext.class);
Assertions.assertTrue(liteflowResponse.isSuccess());
}
@Test
public void testOllamaStream() {
ChatContext chatContext = new ChatContext(getStreamHandler());
LiteflowResponse liteflowResponse = flowExecutor.execute2Resp("chain4", null, chatContext);
Assertions.assertTrue(liteflowResponse.isSuccess());
}
@Test
public void testOpenAIChat() {
LiteflowResponse liteflowResponse = flowExecutor.execute2Resp("chain5", null, ChatContext.class);
Assertions.assertTrue(liteflowResponse.isSuccess());
}
@Test
public void testOpenAIStream() {
ChatContext chatContext = new ChatContext(getStreamHandler());
LiteflowResponse liteflowResponse = flowExecutor.execute2Resp("chain6", null, chatContext);
Assertions.assertTrue(liteflowResponse.isSuccess());
}
}

View File

@@ -1,4 +1,4 @@
package com.yomahub.liteflow.test.ai.core.proxy.cmp;
package com.yomahub.liteflow.test.ai.core.chat.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.springframework.stereotype.Component;

View File

@@ -1,4 +1,4 @@
package com.yomahub.liteflow.test.ai.core.proxy.cmp;
package com.yomahub.liteflow.test.ai.core.chat.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.springframework.stereotype.Component;

View File

@@ -1,4 +1,4 @@
package com.yomahub.liteflow.test.ai.core.proxy.cmp;
package com.yomahub.liteflow.test.ai.core.chat.cmp;
/**
* 测试结构化输出使用

View File

@@ -1,4 +1,4 @@
package com.yomahub.liteflow.test.ai.core.proxy.cmp;
package com.yomahub.liteflow.test.ai.core.chat.cmp.dashscope;
import com.yomahub.liteflow.ai.annotation.*;
import com.yomahub.liteflow.ai.engine.interact.transport.TransportType;
@@ -13,20 +13,17 @@ import com.yomahub.liteflow.ai.util.TriState;
*/
@AIComponent(
nodeId = "aiBlockingChatCmpId",
nodeName = "aiBlockingChatCmpName",
// provider = "ollama",
// apiUrl = "http://localhost:11434",
// model = "qwen3:32b",
nodeId = "DashScopeChat",
nodeName = "DashScopeChat",
provider = "dashscope",
apiUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1",
model = "deepseek-r1",
model = "qwen-flash",
enableThinking = TriState.FALSE,
readTimeout = "10m",
connectTimeout = "10m"
)
@AIChat(
systemPrompt = "classpath:core/proxy/system_prompt.txt",
systemPrompt = "classpath:core/chat/system_prompt.txt",
userPrompt = "{{question}}",
streaming = false,
transportType = TransportType.HTTP
@@ -38,10 +35,10 @@ import com.yomahub.liteflow.ai.util.TriState;
)
@AIOutput(
responseType = ResponseType.JSON,
typeName = "com.yomahub.liteflow.test.ai.core.proxy.cmp.Output",
typeName = "com.yomahub.liteflow.test.ai.core.chat.cmp.Output",
methodExpress = "setData",
useKeyIndex = true,
key = "result"
)
public interface AIBlockingChatCmp {
public interface DashScopeChatCmp {
}

View File

@@ -1,4 +1,4 @@
package com.yomahub.liteflow.test.ai.core.proxy.cmp;
package com.yomahub.liteflow.test.ai.core.chat.cmp.dashscope;
import com.yomahub.liteflow.ai.annotation.*;
import com.yomahub.liteflow.ai.engine.interact.transport.TransportType;
@@ -13,13 +13,8 @@ import com.yomahub.liteflow.ai.util.TriState;
*/
@AIComponent(
nodeId = "aiStreamingChatCmpId",
nodeName = "aiStreamingChatCmpName",
// provider = "ollama",
// apiUrl = "http://localhost:11434",
// model = "qwen3:32b",
// provider = "openai",
// apiUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1",
nodeId = "DashScopeStream",
nodeName = "DashScopeStream",
provider = "dashscope",
apiUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1",
model = "deepseek-r1",
@@ -28,7 +23,7 @@ import com.yomahub.liteflow.ai.util.TriState;
connectTimeout = "10m"
)
@AIChat(
systemPrompt = "classpath:core/proxy/system_prompt.txt",
systemPrompt = "classpath:core/chat/system_prompt.txt",
userPrompt = "{{question}}",
streaming = true,
transportType = TransportType.SSE
@@ -40,10 +35,10 @@ import com.yomahub.liteflow.ai.util.TriState;
)
@AIOutput(
responseType = ResponseType.TEXT,
typeName = "com.yomahub.liteflow.test.ai.core.proxy.cmp.Output",
typeName = "com.yomahub.liteflow.test.ai.core.chat.cmp.Output",
methodExpress = "setData",
useKeyIndex = true,
key = "result"
)
public interface AIStreamingChatCmp {
public interface DashScopeStreamCmp {
}

View File

@@ -0,0 +1,44 @@
package com.yomahub.liteflow.test.ai.core.chat.cmp.ollama;
import com.yomahub.liteflow.ai.annotation.*;
import com.yomahub.liteflow.ai.engine.interact.transport.TransportType;
import com.yomahub.liteflow.ai.engine.model.output.ResponseType;
import com.yomahub.liteflow.ai.util.TriState;
/**
* TODO
*
* @author 苍镜月
* @since TODO
*/
@AIComponent(
nodeId = "OllamaChat",
nodeName = "OllamaChat",
provider = "ollama",
apiUrl = "http://localhost:11434",
model = "qwen3:32b",
enableThinking = TriState.FALSE,
readTimeout = "10m",
connectTimeout = "10m"
)
@AIChat(
systemPrompt = "classpath:core/chat/system_prompt.txt",
userPrompt = "{{question}}",
streaming = false,
transportType = TransportType.HTTP
)
@AIInput(
mapping = {
@InputField(name = "question", expression = "test", defaultValue = "What is LiteFlow?"),
}
)
@AIOutput(
responseType = ResponseType.JSON,
typeName = "com.yomahub.liteflow.test.ai.core.chat.cmp.Output",
methodExpress = "setData",
useKeyIndex = true,
key = "result"
)
public interface OllamaChatCmp {
}

View File

@@ -0,0 +1,44 @@
package com.yomahub.liteflow.test.ai.core.chat.cmp.ollama;
import com.yomahub.liteflow.ai.annotation.*;
import com.yomahub.liteflow.ai.engine.interact.transport.TransportType;
import com.yomahub.liteflow.ai.engine.model.output.ResponseType;
import com.yomahub.liteflow.ai.util.TriState;
/**
* TODO
*
* @author 苍镜月
* @since TODO
*/
@AIComponent(
nodeId = "OllamaStream",
nodeName = "OllamaStream",
provider = "ollama",
apiUrl = "http://localhost:11434",
model = "qwen3:32b",
enableThinking = TriState.TRUE,
readTimeout = "10m",
connectTimeout = "10m"
)
@AIChat(
systemPrompt = "classpath:core/chat/system_prompt.txt",
userPrompt = "{{question}}",
streaming = true,
transportType = TransportType.DnJson
)
@AIInput(
mapping = {
@InputField(name = "question", expression = "test", defaultValue = "简短讲解什么是 LiteFlow"),
}
)
@AIOutput(
responseType = ResponseType.TEXT,
typeName = "com.yomahub.liteflow.test.ai.core.chat.cmp.Output",
methodExpress = "setData",
useKeyIndex = true,
key = "result"
)
public interface OllamaStreamCmp {
}

View File

@@ -0,0 +1,44 @@
package com.yomahub.liteflow.test.ai.core.chat.cmp.openai;
import com.yomahub.liteflow.ai.annotation.*;
import com.yomahub.liteflow.ai.engine.interact.transport.TransportType;
import com.yomahub.liteflow.ai.engine.model.output.ResponseType;
import com.yomahub.liteflow.ai.util.TriState;
/**
* TODO
*
* @author 苍镜月
* @since TODO
*/
@AIComponent(
nodeId = "OpenAIChat",
nodeName = "OpenAIChat",
provider = "openai",
apiUrl = "https://ark.cn-beijing.volces.com/api/v3",
model = "doubao-seed-1-6-250615",
enableThinking = TriState.FALSE,
readTimeout = "10m",
connectTimeout = "10m"
)
@AIChat(
systemPrompt = "classpath:core/chat/system_prompt.txt",
userPrompt = "{{question}}",
streaming = false,
transportType = TransportType.HTTP
)
@AIInput(
mapping = {
@InputField(name = "question", expression = "test", defaultValue = "What is LiteFlow?"),
}
)
@AIOutput(
responseType = ResponseType.JSON,
typeName = "com.yomahub.liteflow.test.ai.core.chat.cmp.Output",
methodExpress = "setData",
useKeyIndex = true,
key = "result"
)
public interface OpenAIChatCmp {
}

View File

@@ -0,0 +1,44 @@
package com.yomahub.liteflow.test.ai.core.chat.cmp.openai;
import com.yomahub.liteflow.ai.annotation.*;
import com.yomahub.liteflow.ai.engine.interact.transport.TransportType;
import com.yomahub.liteflow.ai.engine.model.output.ResponseType;
import com.yomahub.liteflow.ai.util.TriState;
/**
* TODO
*
* @author 苍镜月
* @since TODO
*/
@AIComponent(
nodeId = "OpenAIStream",
nodeName = "OpenAIStream",
provider = "openai",
apiUrl = "https://ark.cn-beijing.volces.com/api/v3",
model = "doubao-seed-1-6-250615",
enableThinking = TriState.TRUE,
readTimeout = "10m",
connectTimeout = "10m"
)
@AIChat(
systemPrompt = "classpath:core/chat/system_prompt.txt",
userPrompt = "{{question}}",
streaming = true,
transportType = TransportType.SSE
)
@AIInput(
mapping = {
@InputField(name = "question", expression = "test", defaultValue = "简短讲解什么是 LiteFlow"),
}
)
@AIOutput(
responseType = ResponseType.TEXT,
typeName = "com.yomahub.liteflow.test.ai.core.chat.cmp.Output",
methodExpress = "setData",
useKeyIndex = true,
key = "result"
)
public interface OpenAIStreamCmp {
}

View File

@@ -0,0 +1,37 @@
package com.yomahub.liteflow.test.ai.core.classify;
import com.yomahub.liteflow.ai.context.ChatContext;
import com.yomahub.liteflow.ai.util.SpringUtil;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.LiteflowResponse;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.TestPropertySource;
import javax.annotation.Resource;
/**
* TODO
*
* @author 苍镜月
* @since TODO
*/
@TestPropertySource(properties = {"spring.config.location=classpath:core/classify/application.yaml"})
@SpringBootTest(classes = {ClassifyTest.class, SpringUtil.class})
@EnableAutoConfiguration
@ComponentScan({"com.yomahub.liteflow.test.ai.core.classify.cmp"})
public class ClassifyTest {
@Resource
private FlowExecutor flowExecutor;
@Test
public void testClassify() {
LiteflowResponse response = flowExecutor.execute2Resp("chain1", null, ChatContext.class);
Assertions.assertTrue(response.isSuccess());
}
}

View File

@@ -0,0 +1,20 @@
package com.yomahub.liteflow.test.ai.core.classify.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.springframework.stereotype.Component;
/**
* TODO
*
* @author 苍镜月
* @since TODO
*/
@Component("a")
public class ACmp extends NodeComponent {
@Override
public void process() throws Exception {
System.out.println("ACmp executed!");
}
}

View File

@@ -0,0 +1,41 @@
package com.yomahub.liteflow.test.ai.core.classify.cmp;
import com.yomahub.liteflow.ai.annotation.*;
import com.yomahub.liteflow.ai.util.TriState;
/**
* TODO
*
* @author 苍镜月
* @since TODO
*/
@AIComponent(
nodeId = "ai",
nodeName = "ai",
// provider = "ollama",
// apiUrl = "http://localhost:11434",
// model = "qwen3:32b",
provider = "openai",
apiUrl = "https://ark.cn-beijing.volces.com/api/v3",
model = "doubao-seed-1-6-250615",
enableThinking = TriState.FALSE,
readTimeout = "10m",
connectTimeout = "10m"
)
@AIClassify(
systemPrompt = "你是一个意图识别高手,你需要识别用户的意图",
userPrompt = "{{question}}",
categories = {"java", "python"})
@AIInput(
mapping = {
@InputField(name = "question", expression = "test", defaultValue = "请帮我写一段Java代码"),
}
)
@AIOutput(
methodExpress = "setData",
useKeyIndex = true,
key = "result"
)
public interface AIClassifyCmp {
}

View File

@@ -0,0 +1,20 @@
package com.yomahub.liteflow.test.ai.core.classify.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.springframework.stereotype.Component;
/**
* TODO
*
* @author 苍镜月
* @since TODO
*/
@Component("java")
public class JavaCmp extends NodeComponent {
@Override
public void process() throws Exception {
System.out.println("Java component executed.");
}
}

View File

@@ -0,0 +1,20 @@
package com.yomahub.liteflow.test.ai.core.classify.cmp;
import com.yomahub.liteflow.core.NodeComponent;
import org.springframework.stereotype.Component;
/**
* TODO
*
* @author 苍镜月
* @since TODO
*/
@Component("python")
public class PythonCmp extends NodeComponent {
@Override
public void process() throws Exception {
System.out.println("Python component executed.");
}
}

View File

@@ -1,58 +0,0 @@
package com.yomahub.liteflow.test.ai.core.proxy;
/**
* TODO
*
* @author 苍镜月
* @since TODO
*/
public class TmpTest {
//
// @Test
// public void test1() {
// Provider provider = new Provider() {
// @Override
// public Optional<String> getString() {
// return Optional.empty();
// }
//
// @Override
// public Optional<Double> getDouble() {
// return Optional.empty();
// }
// };
//
// System.out.println(provider.getDouble().orElseThrow(() -> new RuntimeException("No value present")));
//
// }
//
//
// static interface Provider {
// Optional<String> getString();
//
// Optional<Double> getDouble();
// }
//
//
//
// @Test
// public void testProxyFactoryBean() throws Exception {
//// AIComponentProxyFactoryBean<cmp> factoryBean = new AIComponentProxyFactoryBean<>(cmp.class);
////
//// System.out.println(cmp.class.getAnnotations());
//// Class<cmp> interfaceClass = factoryBean.getInterfaceClass();
//// Class<?> objectType = factoryBean.getObjectType();
////
//// System.out.println(interfaceClass);
//// System.out.println(Arrays.toString(interfaceClass.getAnnotations()));
////
//// System.out.println(objectType);
//// System.out.println(Arrays.toString(objectType.getAnnotations()));
// }
//
// @AIComponent
// @AIChat
// @AIInput
// public interface cmp {}
}

View File

@@ -20,9 +20,12 @@ import com.yomahub.liteflow.ai.util.TriState;
// provider = "ollama",
// apiUrl = "http://localhost:11434",
// model = "qwen3:32b",
provider = "dashscope",
apiUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1",
model = "deepseek-r1",
// provider = "dashscope",
// apiUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1",
// model = "deepseek-r1",
provider = "openai",
apiUrl = "https://ark.cn-beijing.volces.com/api/v3",
model = "doubao-seed-1-6-250615",
enableThinking = TriState.FALSE,
readTimeout = "10m",
connectTimeout = "10m"

View File

@@ -0,0 +1,10 @@
liteflow:
rule-source: core/chat/flow.el.xml
ai:
base-packages:
- com.yomahub.liteflow.test.ai.core.chat.cmp
enable: true
openai:
api-key:
dashscope:
api-key:

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE flow PUBLIC "liteflow" "liteflow.dtd">
<flow>
<chain name="chain1">
THEN(a, DashScopeChat, b);
</chain>
<chain name="chain2">
THEN(a, DashScopeStream);
</chain>
<chain name="chain3">
THEN(a, OllamaChat, b);
</chain>
<chain name="chain4">
THEN(a, OllamaStream);
</chain>
<chain name="chain5">
THEN(a, OpenAIChat, b);
</chain>
<chain name="chain6">
THEN(a, OpenAIStream);
</chain>
</flow>

View File

@@ -0,0 +1,10 @@
liteflow:
rule-source: core/classify/flow.el.xml
ai:
base-packages:
- com.yomahub.liteflow.test.ai.core.classify.cmp
enable: true
openai:
api-key:
dashscope:
api-key:

View File

@@ -2,10 +2,6 @@
<!DOCTYPE flow PUBLIC "liteflow" "liteflow.dtd">
<flow>
<chain name="chain1">
THEN(a, aiBlockingChatCmpId, b);
</chain>
<chain name="chain2">
THEN(a, aiStreamingChatCmpId);
THEN(a, SWITCH(ai).TO(java, python));
</chain>
</flow>

View File

@@ -1,6 +0,0 @@
liteflow:
rule-source: core/proxy/flow.el.xml
ai:
base-packages:
- com.yomahub.liteflow.test.ai.core.proxy.cmp
enable: true

View File

@@ -3,4 +3,8 @@ liteflow:
ai:
base-packages:
- com.yomahub.liteflow.test.ai.core.tool.cmp
enable: true
enable: true
openai:
api-key:
dashscope:
api-key: