mirror of
https://gitee.com/dromara/liteFlow.git
synced 2026-06-13 11:14:38 +08:00
Feat: 意图识别节点实现,添加 chat 测试与 classify 测试
This commit is contained in:
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
* 判断是否支持指定的注解类型
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.yomahub.liteflow.test.ai.core.proxy.cmp;
|
||||
package com.yomahub.liteflow.test.ai.core.chat.cmp;
|
||||
|
||||
/**
|
||||
* 测试结构化输出使用
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
@@ -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>
|
||||
@@ -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:
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user