Feat: 打通 core 与 engine 模块沟通桥梁。大幅重构注解解析模块代码。补充注解注释信息,完善注解参数配置。

This commit is contained in:
LuanY77
2025-08-06 17:23:44 +08:00
parent 648e906678
commit 4186fec236
32 changed files with 915 additions and 457 deletions

View File

@@ -1,5 +1,6 @@
package com.yomahub.liteflow.ai.annotation;
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;
@@ -22,8 +23,9 @@ public @interface AIChat {
/**
* 从上下文中获取的请求参数的上下文路径表达式。
* <p>
* 如果你不希望在注解中进行静态的模型配置,请使用这个属性
* 并在上下文中提供对应的 {@link ChatRequest} 值
* 如果你不希望在注解中进行静态的模型配置,或者你希望使用特定厂商实现的 ChatRequest(其中可能存在一些独有的参数)
* 请使用这个属性,并在上下文中提供对应的 {@link ChatRequest} 值
* 如果不提供,框架将统一使用 {@link ChatRequest} 发起请求。
* <p>
* {@link ChatRequest} 可以是对应提供商的具体实现类。
* <p>
@@ -33,9 +35,10 @@ public @interface AIChat {
* 以及,你会发现 {@link ChatRequest#getOptions()} 的 {@link ChatOptions} 和 {@link AIComponent} 中的配置有重合。
* 同样的,即使你进行了 {@link AIComponent} 的配置,会优先使用 {@link ChatRequest} 的配置。
* <p>
* 但是如果相关的配置为空或默认值,则会使用 {@link AIComponent} 和 {@link AIChat} 中的配置
* <b>请注意:如果相关的配置为空或默认值,则会使用 {@link AIComponent} 和 {@link AIChat} 中的配置!!!</b>
* <p>
* 该方法用于从一个必须提供 {@code get} 方法的上下文中检索数据。
* <p>
* 用于从一个必须提供 {@code get} 方法的上下文中检索数据。
* 表达式支持以下两种形式:
* <ul>
* <li>
@@ -63,8 +66,15 @@ public @interface AIChat {
String userPrompt() default "";
/**
* 是否开启 stream默认 false
* 是否开启 stream默认 true
*/
boolean streaming() default false;
boolean streaming() default true;
/**
* 传输类型,默认 SSE
* <p>
* 请在此查看传输类型 -> {@link TransportType}
*/
TransportType transportType() default TransportType.SSE;
}

View File

@@ -1,5 +1,8 @@
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 java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -16,6 +19,41 @@ 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 "";
/**
* 系统提示词
*/

View File

@@ -7,6 +7,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.Duration;
/**
* AI 组件注解,提供配置和标识功能
@@ -134,12 +135,90 @@ public @interface AIComponent {
* 是否并行 ToolCall
*/
TriState parallelToolCalls() default TriState.UNSET;
/**
* 是否自动进行 ToolCall
*/
TriState autoToolCallEnabled() default TriState.UNSET;
// --- 网络和日志参数 ---
/**
* 超时时间
* <p>连接超时时间</p>
*
* <p>该值最终会被解析为一个 {@link Duration} 对象。为了提供灵活性,
* 支持以下两种字符串格式:</p>
*
* <ol>
* <li><b>标准 ISO-8601 格式</b>:
* 这是由 {@link Duration#parse(CharSequence)} 支持的标准格式。
* <p><b>示例:</b></p>
* <ul>
* <li>{@code "PT30S"} 代表 30 秒。</li>
* <li>{@code "PT10M"} 代表 10 分钟。</li>
* <li>{@code "PT2H"} 代表 2 小时。</li>
* <li>{@code "P1D"} 代表 1 天。</li>
* </ul>
* </li>
* <li><b>自定义简化格式</b>:
* 为了方便配置,也支持由数字和单位后缀组成的简化格式 (单位不区分大小写)。
* <p><b>支持的单位:</b></p>
* <ul>
* <li>{@code s} - 秒</li>
* <li>{@code m} - 分钟</li>
* <li>{@code h} - 小时</li>
* </ul>
* <p><b>示例:</b></p>
* <ul>
* <li>{@code "60s"} 代表 60 秒。</li>
* <li>{@code "5m"} 代表 5 分钟。</li>
* </ul>
* </li>
* </ol>
*
* <p><b>默认值行为:</b><br>
* 如果该值保持默认的空字符串 ({@code ""}),处理该注解的系统将会应用一个预设的、
* 全局的默认超时时间(60s)。</p>
* @see Duration
*/
String timeout() default "";
String connectTimeout() default "";
/**
* <p>读取超时时间</p>
*
* <p>该值最终会被解析为一个 {@link Duration} 对象。为了提供灵活性,
* 支持以下两种字符串格式:</p>
*
* <ol>
* <li><b>标准 ISO-8601 格式</b>:
* 这是由 {@link Duration#parse(CharSequence)} 支持的标准格式。
* <p><b>示例:</b></p>
* <ul>
* <li>{@code "PT30S"} 代表 30 秒。</li>
* <li>{@code "PT10M"} 代表 10 分钟。</li>
* <li>{@code "PT2H"} 代表 2 小时。</li>
* <li>{@code "P1D"} 代表 1 天。</li>
* </ul>
* </li>
* <li><b>自定义简化格式</b>:
* 为了方便配置,也支持由数字和单位后缀组成的简化格式 (单位不区分大小写)。
* <p><b>支持的单位:</b></p>
* <ul>
* <li>{@code s} - 秒</li>
* <li>{@code m} - 分钟</li>
* <li>{@code h} - 小时</li>
* </ul>
* <p><b>示例:</b></p>
* <ul>
* <li>{@code "60s"} 代表 60 秒。</li>
* <li>{@code "5m"} 代表 5 分钟。</li>
* </ul>
* </li>
* </ol>
*
* <p><b>默认值行为:</b><br>
* 如果该值保持默认的空字符串 ({@code ""}),处理该注解的系统将会应用一个预设的、
* 全局的默认超时时间(60s)。</p>
* @see Duration
*/
String readTimeout() default "";
/**
* 最大重试次数
*/
@@ -158,5 +237,9 @@ public @interface AIComponent {
* 自定义请求头
*/
KeyValue[] customHeaders() default {};
/**
* 是否开启思考模式
*/
TriState enableThinking() default TriState.UNSET;
}

View File

@@ -1,6 +1,6 @@
package com.yomahub.liteflow.ai.annotation;
import com.yomahub.liteflow.ai.domain.enums.ResponseType;
import com.yomahub.liteflow.ai.engine.model.output.ResponseType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -95,7 +95,7 @@ public @interface AIOutput {
/**
* 如需启用,请设置 {@link AIOutput#responseType()} 为 {@link ResponseType#JSON}
* <p>
* 表示输出的 JSON Schema 定义。如果需要添加描述信息,请使用 LangChain4j 的相关注解
* 表示输出的 JSON Schema 定义。如果需要添加描述信息,请使用{@link com.yomahub.liteflow.ai.engine.model.output.structure.Description}
*/
Class<?> entityClass() default String.class;

View File

@@ -2,6 +2,7 @@ package com.yomahub.liteflow.ai.context;
import com.yomahub.liteflow.ai.engine.interact.pipeline.InteractContext;
import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatResponse;
import com.yomahub.liteflow.ai.engine.model.output.TokenUsage;
/**
* 流式输出处理器
@@ -11,6 +12,29 @@ import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatResponse;
*/
public interface StreamHandler {
/**
* 请求开始时的回调方法
*
* @param context 聊天上下文,包含处理过程中的状态和信息
*/
void onStart(InteractContext context);
/**
* 请求关闭时的回调方法
*
* @param context 聊天上下文,包含处理过程中的状态和信息
*/
void onClose(InteractContext context);
/**
* 处理过程中发生错误的回调方法。
*
* @param context 聊天上下文,包含处理过程中的状态和信息
* @param t
*/
void onError(InteractContext context, Throwable t);
/**
* 处理文本消息的回调方法。需要启用流式调用
*
@@ -42,8 +66,7 @@ public interface StreamHandler {
* @param content Token 统计信息内容
* @param context 聊天上下文,包含处理过程中的状态和信息
*/
// TODO args
Object onUsage(Object content, InteractContext context);
TokenUsage onUsage(Object content, InteractContext context);
/**
* 处理基础信息/搜索结果的回调方法。需要启用流式调用
@@ -61,20 +84,8 @@ public interface StreamHandler {
* @param context 聊天上下文,包含处理过程中的状态和信息
* @return 处理后的结果
*/
// TODO args
ChatResponse onCompletion(ChatResponse response, InteractContext context);
/**
* 处理过程中发生错误的回调方法。
*
* @param response 处理后的聊天响应结果,可能包含错误信息
* @param context 聊天上下文,包含处理过程中的状态和信息
* @param e
* @return 处理后的结果
*/
// TODO args
ChatResponse onError(ChatResponse response, InteractContext context, Exception e);
/**
* 最终结果处理的回调方法。无论是否发生错误均会调用此方法。
*
@@ -82,6 +93,5 @@ public interface StreamHandler {
* @param context 聊天上下文,包含处理过程中的状态和信息
* @return 处理后的结果
*/
// TODO args
ChatResponse onFinal(ChatResponse response, InteractContext context);
}

View File

@@ -1,224 +0,0 @@
package com.yomahub.liteflow.ai.domain;
import com.yomahub.liteflow.ai.annotation.AIComponent;
import com.yomahub.liteflow.ai.util.KeyValue;
import com.yomahub.liteflow.ai.util.TriState;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* 模型配置
*
* @author 苍镜月
* @since TODO
*/
public final class ModelConfig {
private final String provider;
private final String apiUrl;
private final String endPoint;
private final String model;
private final String apiKey;
private final String version;
private final String timeout;
private final int maxRetries;
private final double temperature;
private final double topP;
private final int topK;
private final int maxTokens;
private final List<String> stop;
private final int seed;
private final double repeatPenalty;
private final double presencePenalty;
private final double frequencyPenalty;
private final TriState parallelToolCalls;
private final TriState logRequests;
private final TriState logResponses;
private final List<KeyValue> customHeaders;
public ModelConfig(String provider, String apiUrl, String endPoint, String model, String apiKey,
String version, String timeout, int maxRetries, double temperature, double topP,
int topK, int maxTokens, List<String> stop, int seed, double repeatPenalty,
double presencePenalty, double frequencyPenalty,
TriState parallelToolCalls, TriState logRequests,
TriState logResponses, List<KeyValue> customHeaders) {
this.provider = provider;
this.apiUrl = apiUrl;
this.endPoint = endPoint;
this.model = model;
this.apiKey = apiKey;
this.version = version;
this.timeout = timeout;
this.maxRetries = maxRetries;
this.temperature = temperature;
this.topP = topP;
this.topK = topK;
this.maxTokens = maxTokens;
this.stop = stop;
this.seed = seed;
this.repeatPenalty = repeatPenalty;
this.presencePenalty = presencePenalty;
this.frequencyPenalty = frequencyPenalty;
this.parallelToolCalls = parallelToolCalls;
this.logRequests = logRequests;
this.logResponses = logResponses;
this.customHeaders = customHeaders;
}
public static ModelConfig fromAnnotation(AIComponent anno) {
return new ModelConfig(
anno.provider(), anno.apiUrl(), anno.endPoint(), anno.model(), anno.apiKey(), anno.version(),
anno.timeout(), anno.maxRetries(), anno.temperature(), anno.topP(), anno.topK(),
anno.maxTokens(), Arrays.asList(anno.stop()), anno.seed(), anno.repeatPenalty(),
anno.presencePenalty(), anno.frequencyPenalty(), anno.parallelToolCalls(), anno.logRequests(),
anno.logResponses(), Arrays.asList(anno.customHeaders())
);
}
public String getProvider() {
return provider;
}
public String getApiUrl() {
return apiUrl;
}
public String getEndPoint() {
return endPoint;
}
public String getModel() {
return model;
}
public String getApiKey() {
return apiKey;
}
public String getVersion() {
return version;
}
public String getTimeout() {
return timeout;
}
public int getMaxRetries() {
return maxRetries;
}
public double getTemperature() {
return temperature;
}
public double getTopP() {
return topP;
}
public int getTopK() {
return topK;
}
public int getMaxTokens() {
return maxTokens;
}
public List<String> getStop() {
return stop;
}
public int getSeed() {
return seed;
}
public double getRepeatPenalty() {
return repeatPenalty;
}
public double getPresencePenalty() {
return presencePenalty;
}
public double getFrequencyPenalty() {
return frequencyPenalty;
}
public TriState getParallelToolCalls() {
return parallelToolCalls;
}
public TriState getLogRequests() {
return logRequests;
}
public TriState getLogResponses() {
return logResponses;
}
public List<KeyValue> getCustomHeaders() {
return customHeaders;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ModelConfig that = (ModelConfig) o;
return maxRetries == that.maxRetries &&
Double.compare(that.temperature, temperature) == 0 &&
Double.compare(that.topP, topP) == 0 &&
topK == that.topK &&
maxTokens == that.maxTokens &&
seed == that.seed &&
Double.compare(that.repeatPenalty, repeatPenalty) == 0 &&
Double.compare(that.presencePenalty, presencePenalty) == 0 &&
Double.compare(that.frequencyPenalty, frequencyPenalty) == 0 &&
Objects.equals(provider, that.provider) &&
Objects.equals(apiUrl, that.apiUrl) &&
Objects.equals(endPoint, that.endPoint) &&
Objects.equals(model, that.model) &&
Objects.equals(apiKey, that.apiKey) &&
Objects.equals(version, that.version) &&
Objects.equals(timeout, that.timeout) &&
Objects.equals(stop, that.stop) &&
parallelToolCalls == that.parallelToolCalls &&
logRequests == that.logRequests &&
logResponses == that.logResponses &&
Objects.equals(customHeaders, that.customHeaders);
}
@Override
public int hashCode() {
return Objects.hash(provider, apiUrl, endPoint, model, apiKey, version, timeout, maxRetries, temperature, topP, topK, maxTokens, stop, seed, repeatPenalty, presencePenalty, frequencyPenalty, parallelToolCalls, logRequests, logResponses, customHeaders);
}
@Override
public String toString() {
return "ModelConfig{" +
"provider='" + provider + '\'' +
", apiUrl='" + apiUrl + '\'' +
", endPoint='" + endPoint + '\'' +
", model='" + model + '\'' +
", apiKey='" + apiKey + '\'' +
", version='" + version + '\'' +
", timeout='" + timeout + '\'' +
", maxRetries=" + maxRetries +
", temperature=" + temperature +
", topP=" + topP +
", topK=" + topK +
", maxTokens=" + maxTokens +
", stop=" + stop +
", seed=" + seed +
", repeatPenalty=" + repeatPenalty +
", presencePenalty=" + presencePenalty +
", frequencyPenalty=" + frequencyPenalty +
", parallelToolCalls=" + parallelToolCalls +
", logRequests=" + logRequests +
", logResponses=" + logResponses +
", customHeaders=" + customHeaders +
'}';
}
}

View File

@@ -0,0 +1,251 @@
package com.yomahub.liteflow.ai.domain.dto;
import com.yomahub.liteflow.ai.annotation.AIComponent;
import com.yomahub.liteflow.ai.util.DurationUtil;
import com.yomahub.liteflow.ai.util.KeyValue;
import com.yomahub.liteflow.ai.util.TriState;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* 模型配置聚合(与 {@link AIComponent} 相互映射)
*
* @author 苍镜月
* @since TODO
*/
public final class ModelConfigAggregator {
private final String provider;
// --- 连接、鉴权参数 ---
private final String apiUrl;
private final String endPoint;
private final String model;
private final String apiKey;
private final String version;
// --- 核心参数 ---
private final double temperature;
private final double topP;
private final int topK;
private final int maxTokens;
private final List<String> stop;
private final int seed;
// --- 惩罚参数 ---
private final double repeatPenalty;
private final double presencePenalty;
private final double frequencyPenalty;
// --- Tool Calling ----
private final TriState parallelToolCalls;
private final TriState autoToolCallEnabled;
// --- 网络和日志参数 ---
private final Duration connectTimeout;
private final Duration readTimeout;
private final int maxRetries;
private final TriState logRequests;
private final TriState logResponses;
// --- 其他参数 ---
private final List<KeyValue> customHeaders;
private final TriState enableThinking;
/**
* 从 {@link AIComponent} 注解中解析模型配置
*
* @param aiComponent AIComponent 注解实例
* @return ModelConfigAggregator 实例
*/
public static ModelConfigAggregator parseFromAnnotation(AIComponent aiComponent) {
return new ModelConfigAggregator(
aiComponent.provider(),
aiComponent.apiUrl(),
aiComponent.endPoint(),
aiComponent.model(),
aiComponent.apiKey(),
aiComponent.version(),
aiComponent.temperature(),
aiComponent.topP(),
aiComponent.topK(),
aiComponent.maxTokens(),
Objects.nonNull(aiComponent.stop()) ? Arrays.asList(aiComponent.stop()) : Collections.emptyList(),
aiComponent.seed(),
aiComponent.repeatPenalty(),
aiComponent.presencePenalty(),
aiComponent.frequencyPenalty(),
aiComponent.parallelToolCalls(),
aiComponent.autoToolCallEnabled(),
DurationUtil.toDuration(aiComponent.connectTimeout(), Duration.ofSeconds(60)),
DurationUtil.toDuration(aiComponent.readTimeout(), Duration.ofSeconds(60)),
aiComponent.maxRetries() <= 0 ? 3 : aiComponent.maxRetries(),
aiComponent.logRequests(),
aiComponent.logResponses(),
Objects.nonNull(aiComponent.customHeaders()) ? Arrays.asList(aiComponent.customHeaders()) : Collections.emptyList(),
aiComponent.enableThinking()
);
}
private ModelConfigAggregator(String provider, String apiUrl, String endPoint,
String model, String apiKey, String version,
double temperature, double topP, int topK,
int maxTokens, List<String> stop, int seed,
double repeatPenalty, double presencePenalty,
double frequencyPenalty, TriState parallelToolCalls,
TriState autoToolCallEnabled, Duration connectTimeout,
Duration readTimeout, int maxRetries, TriState logRequests,
TriState logResponses, List<KeyValue> customHeaders, TriState enableThinking) {
this.provider = provider;
this.apiUrl = apiUrl;
this.endPoint = endPoint;
this.model = model;
this.apiKey = apiKey;
this.version = version;
this.temperature = temperature;
this.topP = topP;
this.topK = topK;
this.maxTokens = maxTokens;
this.stop = stop;
this.seed = seed;
this.repeatPenalty = repeatPenalty;
this.presencePenalty = presencePenalty;
this.frequencyPenalty = frequencyPenalty;
this.parallelToolCalls = parallelToolCalls;
this.autoToolCallEnabled = autoToolCallEnabled;
this.connectTimeout = connectTimeout;
this.readTimeout = readTimeout;
this.maxRetries = maxRetries;
this.logRequests = logRequests;
this.logResponses = logResponses;
this.customHeaders = customHeaders;
this.enableThinking = enableThinking;
}
/**
* 获取默认的模型配置聚合实例
*/
public static ModelConfigAggregator getDefault() {
return new ModelConfigAggregator(
"",
"",
"",
"",
"",
"",
-1.0,
-1.0,
-1,
-1,
Collections.emptyList(),
-1,
-1.0,
-1.0,
-1.0,
TriState.UNSET,
TriState.UNSET,
Duration.ofSeconds(60),
Duration.ofSeconds(60),
-1,
TriState.UNSET,
TriState.UNSET,
Collections.emptyList(),
TriState.UNSET
);
}
public String getProvider() {
return provider;
}
public String getApiUrl() {
return apiUrl;
}
public String getEndPoint() {
return endPoint;
}
public String getModel() {
return model;
}
public String getApiKey() {
return apiKey;
}
public String getVersion() {
return version;
}
public double getTemperature() {
return temperature;
}
public double getTopP() {
return topP;
}
public int getTopK() {
return topK;
}
public int getMaxTokens() {
return maxTokens;
}
public List<String> getStop() {
return stop;
}
public int getSeed() {
return seed;
}
public double getRepeatPenalty() {
return repeatPenalty;
}
public double getPresencePenalty() {
return presencePenalty;
}
public double getFrequencyPenalty() {
return frequencyPenalty;
}
public TriState getParallelToolCalls() {
return parallelToolCalls;
}
public TriState getAutoToolCallEnabled() {
return autoToolCallEnabled;
}
public Duration getConnectTimeout() {
return connectTimeout;
}
public Duration getReadTimeout() {
return readTimeout;
}
public int getMaxRetries() {
return maxRetries;
}
public TriState getLogRequests() {
return logRequests;
}
public TriState getLogResponses() {
return logResponses;
}
public List<KeyValue> getCustomHeaders() {
return customHeaders;
}
public TriState getEnableThinking() {
return enableThinking;
}
}

View File

@@ -0,0 +1,50 @@
package com.yomahub.liteflow.ai.domain.dto;
import com.yomahub.liteflow.ai.engine.model.output.ResponseType;
/**
* 解析后的注解配置类
*
* @author 苍镜月
* @since TODO
*/
public class ParsedAnnotationConfig {
private String systemPrompt;
private String userPrompt;
private ResponseType responseType = ResponseType.TEXT;
private Class<?> entityClass = String.class;
public String getSystemPrompt() {
return systemPrompt;
}
public String getUserPrompt() {
return userPrompt;
}
public ResponseType getResponseType() {
return responseType;
}
public Class<?> getEntityClass() {
return entityClass;
}
public void setSystemPrompt(String systemPrompt) {
this.systemPrompt = systemPrompt;
}
public void setUserPrompt(String userPrompt) {
this.userPrompt = userPrompt;
}
public void setResponseType(ResponseType responseType) {
this.responseType = responseType;
}
public void setEntityClass(Class<?> entityClass) {
this.entityClass = entityClass;
}
}

View File

@@ -1,10 +1,16 @@
package com.yomahub.liteflow.ai.parse;
import cn.hutool.core.util.StrUtil;
import com.yomahub.liteflow.ai.annotation.AIComponent;
import com.yomahub.liteflow.ai.annotation.AIInput;
import com.yomahub.liteflow.ai.annotation.AIOutput;
import com.yomahub.liteflow.ai.domain.dto.ModelConfigAggregator;
import com.yomahub.liteflow.ai.domain.dto.ParsedAnnotationConfig;
import com.yomahub.liteflow.ai.domain.enums.AITypeEnum;
import com.yomahub.liteflow.ai.domain.enums.ResponseType;
import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatRequest;
import com.yomahub.liteflow.ai.engine.model.output.ResponseType;
import com.yomahub.liteflow.ai.parse.assemble.ChatRequestAssembler;
import com.yomahub.liteflow.ai.parse.assemble.RequestAssembler;
import com.yomahub.liteflow.ai.parse.context.ContextAccessor;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
import com.yomahub.liteflow.ai.parse.prompt.PromptTemplateParser;
@@ -31,6 +37,8 @@ public abstract class AbstractAnnotationProcessor<A extends Annotation, T extend
protected final LFLog LOG = LFLoggerManager.getLogger(this.getClass());
protected static final RequestAssembler<ChatRequest> CHAT_REQUEST_ASSEMBLER = new ChatRequestAssembler();
@Override
public void afterPropertiesSet() throws Exception {
AnnotationParser.register(getAIType().getCode(), this);
@@ -43,6 +51,23 @@ public abstract class AbstractAnnotationProcessor<A extends Annotation, T extend
*/
protected abstract AITypeEnum getAIType();
/**
* 解析 {@link AIComponent} 模型配置
*
* @param context 处理器上下文
*/
protected void parseModelConfig(ProcessorContext<T> context) {
T wrapBean = context.getWrapBean();
// 获取模型配置聚合器
AIComponent aiComponent = wrapBean.getAiComponent();
if (Objects.isNull(aiComponent)) {
LOG.warn("AIComponent annotation is null, using default model configuration.");
wrapBean.setConfig(ModelConfigAggregator.getDefault());
return;
}
wrapBean.setConfig(ModelConfigAggregator.parseFromAnnotation(aiComponent));
}
/**
* 解析提示词
*
@@ -85,15 +110,16 @@ public abstract class AbstractAnnotationProcessor<A extends Annotation, T extend
if (Objects.isNull(outputAnno)) return;
// 这里仅处理结构化输出相关的参数,其他参数交给 after 逻辑进行处理
T wrapBean = context.getWrapBean();
ParsedAnnotationConfig annotationConfig = context.getParsedAnnotationConfig();
// 是否需要结构化输出
if (Objects.equals(ResponseType.JSON, outputAnno.responseType())) {
wrapBean.setResponseType(ResponseType.JSON);
annotationConfig.setResponseType(ResponseType.JSON);
// 设置输出实体类
wrapBean.setEntityClass(outputAnno.entityClass());
annotationConfig.setEntityClass(outputAnno.entityClass());
} else {
annotationConfig.setResponseType(ResponseType.TEXT);
// 文本输出,设置 entityClass 为 String
wrapBean.setEntityClass(String.class);
annotationConfig.setEntityClass(String.class);
}
}

View File

@@ -1,8 +1,11 @@
package com.yomahub.liteflow.ai.parse.anno;
import com.yomahub.liteflow.ai.annotation.AIChat;
import com.yomahub.liteflow.ai.domain.dto.ParsedAnnotationConfig;
import com.yomahub.liteflow.ai.domain.enums.AITypeEnum;
import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatRequest;
import com.yomahub.liteflow.ai.parse.AbstractAnnotationProcessor;
import com.yomahub.liteflow.ai.parse.context.ContextAccessor;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
import com.yomahub.liteflow.ai.proxy.wrap.ChatProxyWrapBean;
import com.yomahub.liteflow.ai.util.SetUtil;
@@ -17,21 +20,29 @@ public class ChatAnnotationProcessor extends AbstractAnnotationProcessor<AIChat,
@Override
public void postProcessBeforeTrigger(AIChat annotation, ProcessorContext<ChatProxyWrapBean> context) {
// 解析模型配置
parseModelConfig(context);
ChatProxyWrapBean wrapBean = context.getWrapBean();
ParsedAnnotationConfig annotationConfig = context.getParsedAnnotationConfig();
// 设置基本属性
SetUtil.setIfPresent(wrapBean::setStreaming, annotation.streaming());
// 处理系统提示词
parsePrompt(annotation.systemPrompt(), context, wrapBean::setSystemPrompt);
parsePrompt(annotation.systemPrompt(), context, annotationConfig::setSystemPrompt);
// 处理用户提示词
parsePrompt(annotation.userPrompt(), context, wrapBean::setUserPrompt);
parsePrompt(annotation.userPrompt(), context, annotationConfig::setUserPrompt);
// 处理结构化输出参数绑定
parseOutput(context);
// TODO 组装 Request
// 从上下文获取动态 ChatRequest
ChatRequest contextChatRequest = ContextAccessor.searchContextByExpression(annotation.getChatRequest(), context);
// 组装 ChatRequest
CHAT_REQUEST_ASSEMBLER.assemble(contextChatRequest, context);
}
@Override

View File

@@ -1,8 +1,11 @@
package com.yomahub.liteflow.ai.parse.anno;
import com.yomahub.liteflow.ai.annotation.AIClassify;
import com.yomahub.liteflow.ai.domain.dto.ParsedAnnotationConfig;
import com.yomahub.liteflow.ai.domain.enums.AITypeEnum;
import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatRequest;
import com.yomahub.liteflow.ai.parse.AbstractAnnotationProcessor;
import com.yomahub.liteflow.ai.parse.context.ContextAccessor;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
import com.yomahub.liteflow.ai.proxy.wrap.ClassifyProxyWrapBean;
import com.yomahub.liteflow.ai.util.SetUtil;
@@ -21,20 +24,30 @@ public class ClassifyAnnotationProcessor extends AbstractAnnotationProcessor<AIC
@Override
public void postProcessBeforeTrigger(AIClassify annotation, ProcessorContext<ClassifyProxyWrapBean> context) {
// 解析模型配置
parseModelConfig(context);
ClassifyProxyWrapBean wrapBean = context.getWrapBean();
ParsedAnnotationConfig annotationConfig = context.getParsedAnnotationConfig();
SetUtil.setIfPresent(wrapBean::setCategories, Arrays.stream(annotation.categories()).collect(Collectors.toList()));
SetUtil.setIfPresent(wrapBean::setMultiLabel, annotation.multiLabel());
// 处理系统提示词
parsePrompt(annotation.systemPrompt(), context, wrapBean::setSystemPrompt);
parsePrompt(annotation.systemPrompt(), context, annotationConfig::setSystemPrompt);
// 处理用户提示词
parsePrompt(annotation.userPrompt(), context, wrapBean::setUserPrompt);
parsePrompt(annotation.userPrompt(), context, annotationConfig::setUserPrompt);
// 处理结构化输出参数绑定
parseOutput(context);
// 从上下文获取动态 ChatRequest
ChatRequest contextChatRequest = ContextAccessor.searchContextByExpression(annotation.getChatRequest(), context);
// 组装 ChatRequest
CHAT_REQUEST_ASSEMBLER.assemble(contextChatRequest, context);
}
@Override

View File

@@ -0,0 +1,74 @@
package com.yomahub.liteflow.ai.parse.assemble;
import com.yomahub.liteflow.ai.context.ChatContext;
import com.yomahub.liteflow.ai.domain.dto.ModelConfigAggregator;
import com.yomahub.liteflow.ai.domain.dto.ParsedAnnotationConfig;
import com.yomahub.liteflow.ai.engine.model.ModelRequest;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
import com.yomahub.liteflow.ai.util.SetUtil;
import com.yomahub.liteflow.log.LFLog;
import com.yomahub.liteflow.log.LFLoggerManager;
import java.util.function.Supplier;
/**
* 抽象请求组装器
*
* @author 苍镜月
* @since TODO
*/
public abstract class AbstractRequestAssembler<R extends ModelRequest> implements RequestAssembler<R> {
protected final LFLog LOG = LFLoggerManager.getLogger(this.getClass());
/**
* 组装请求对象(最终请求对象存储于 {@link ProcessorContext} 中的 {@link ModelRequest} 属性中)
*
* @param contextRequest 上下文中的请求示例(可以为 null)
* @param context 处理器上下文
*/
public final void assemble(R contextRequest, ProcessorContext<?> context) {
ParsedAnnotationConfig annotationConfig = context.getParsedAnnotationConfig();
ModelConfigAggregator config = context.getWrapBean().getConfig();
// 组装请求
R req = doAssemble(contextRequest, annotationConfig, config, context.getChatContext());
// 设置到处理器上下文
context.setModelRequest(req);
}
/**
* 实际的请求组装逻辑
*
* @param contextRequest 上下文中的请求示例(可以为 null)
* @param annotationConfig 注解解析配置
* @param config 模型配置聚合
* @param context chat上下文
* @return 最终请求对象
*/
protected abstract R doAssemble(
R contextRequest,
ParsedAnnotationConfig annotationConfig,
ModelConfigAggregator config,
ChatContext context
);
/**
* 合并多个 Supplier 的值,返回第一个非空的值
*
* @param suppliers 多个 Supplier
* @param <T> 值的类型
* @return 第一个非空的值,如果都为空则返回 null
*/
@SafeVarargs
protected static <T> T merge(Supplier<T>... suppliers) {
for (Supplier<T> supplier : suppliers) {
T value = supplier.get();
if (SetUtil.isPresent(value)) {
return value;
}
}
return null;
}
}

View File

@@ -0,0 +1,73 @@
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.ParsedAnnotationConfig;
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.util.SetUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
/**
* ChatRequest 组装器
*
* @author 苍镜月
* @since TODO
*/
public class ChatRequestAssembler extends AbstractRequestAssembler<ChatRequest> {
@Override
protected ChatRequest doAssemble(ChatRequest contextRequest, ParsedAnnotationConfig annotationConfig, ModelConfigAggregator config, ChatContext context) {
ChatOptions contextOptions = Optional.ofNullable(contextRequest)
.map(ChatRequest::getOptions)
.orElse(ChatOptions.builder().build());
ChatRequest.Builder<?> builder = ChatRequest.builder();
// 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();
optionsBuilder.temperature(merge(contextOptions::getTemperature, config::getTemperature));
optionsBuilder.topP(merge(contextOptions::getTopP, config::getTopP));
optionsBuilder.topK(merge(contextOptions::getTopK, config::getTopK));
optionsBuilder.maxTokens(merge(contextOptions::getMaxTokens, config::getMaxTokens));
optionsBuilder.seed(merge(contextOptions::getSeed, config::getSeed));
optionsBuilder.enableThinking(merge(contextOptions::getEnableThinking, () -> config.getEnableThinking().toBool()));
builder.options(optionsBuilder.build());
// 3. 合并 Message
List<Message> messages = new ArrayList<>();
SetUtil.setIfPresent(t -> messages.add(new SystemMessage(t)), annotationConfig.getSystemPrompt());
SetUtil.setIfPresent(t -> messages.add(new UserMessage(t)), annotationConfig.getUserPrompt());
builder.messages(
merge(() -> Objects.nonNull(contextRequest) ? contextRequest.getMessages() : null, () -> messages)
);
return builder.build();
}
}

View File

@@ -0,0 +1,22 @@
package com.yomahub.liteflow.ai.parse.assemble;
import com.yomahub.liteflow.ai.engine.model.ModelRequest;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
/**
* 请求组装器接口
*
* @author 苍镜月
* @since TODO
*/
public interface RequestAssembler<R extends ModelRequest> {
/**
* 组装请求对象(最终请求对象存储于 {@link ProcessorContext} 中的 {@link ModelRequest} 属性中)
*
* @param contextRequest 上下文中的请求示例(可以为 null)
* @param context 处理器上下文
*/
void assemble(R contextRequest, ProcessorContext<?> context);
}

View File

@@ -30,7 +30,7 @@ public class ContextAccessor {
* @param context 处理器上下文
* @return 查找到的值
*/
public static String searchContextByExpression(String expression, ProcessorContext<?> context) {
public static <T> T searchContextByExpression(String expression, ProcessorContext<?> context) {
if (StrUtil.isBlank(expression)) {
return null;
}

View File

@@ -3,6 +3,8 @@ package com.yomahub.liteflow.ai.parse.context;
import com.yomahub.liteflow.ai.annotation.AIInput;
import com.yomahub.liteflow.ai.annotation.AIOutput;
import com.yomahub.liteflow.ai.context.ChatContext;
import com.yomahub.liteflow.ai.domain.dto.ParsedAnnotationConfig;
import com.yomahub.liteflow.ai.engine.model.ModelRequest;
import com.yomahub.liteflow.ai.parse.prompt.loader.DefaultPromptResourceLoader;
import com.yomahub.liteflow.ai.parse.prompt.loader.PromptResourceLoader;
import com.yomahub.liteflow.ai.proxy.wrap.AIProxyWrapBean;
@@ -18,14 +20,18 @@ import com.yomahub.liteflow.core.NodeComponent;
public class ProcessorContext<T extends AIProxyWrapBean<?>> {
private final T wrapBean;
private final ParsedAnnotationConfig parsedAnnotationConfig;
private final ChatContext chatContext;
private final NodeComponent nodeComponent;
private final AIInput aiInputAnno;
private final AIOutput aiOutputAnno;
private final PromptResourceLoader resourceLoader;
private ModelRequest modelRequest;
public ProcessorContext(T wrapBean, ChatContext chatContext, NodeComponent nodeComponent) {
this.wrapBean = wrapBean;
parsedAnnotationConfig = new ParsedAnnotationConfig();
this.chatContext = chatContext;
this.nodeComponent = nodeComponent;
this.aiInputAnno = wrapBean.getInterfaceClass().getAnnotation(AIInput.class);
@@ -56,4 +62,16 @@ public class ProcessorContext<T extends AIProxyWrapBean<?>> {
public PromptResourceLoader getResourceLoader() {
return resourceLoader;
}
public ParsedAnnotationConfig getParsedAnnotationConfig() {
return parsedAnnotationConfig;
}
public ModelRequest getModelRequest() {
return modelRequest;
}
public void setModelRequest(ModelRequest modelRequest) {
this.modelRequest = modelRequest;
}
}

View File

@@ -1,7 +1,7 @@
package com.yomahub.liteflow.ai.proxy.invocation;
import com.yomahub.liteflow.ai.context.ChatContext;
import com.yomahub.liteflow.ai.domain.ModelConfig;
import com.yomahub.liteflow.ai.domain.dto.ModelConfigAggregator;
import com.yomahub.liteflow.ai.exception.LiteFlowAIException;
import com.yomahub.liteflow.ai.parse.AnnotationParser;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
@@ -77,19 +77,15 @@ public abstract class AbstractAIInvocationHandler<T extends AIProxyWrapBean<?>>
* @param processorContext 处理器上下文
*/
protected void checkValidation(ProcessorContext<T> processorContext) {
// 校验Prompt
if (SetUtil.isNotPresent(wrapBean.getUserPrompt()) && SetUtil.isNotPresent(wrapBean.getSystemPrompt())) {
throw new LiteFlowAIException("User prompt and system prompt cannot both be empty");
}
// 校验必需参数
ModelConfig modelConfig = wrapBean.getConfig();
if (SetUtil.isNotPresent(modelConfig.getProvider())) {
ModelConfigAggregator modelConfigAggregator = wrapBean.getConfig();
if (SetUtil.isNotPresent(modelConfigAggregator.getProvider())) {
throw new LiteFlowAIException("Provider cannot be empty for AI node: " + wrapBean.getNodeId());
}
if (SetUtil.isNotPresent(modelConfig.getApiUrl())) {
if (SetUtil.isNotPresent(modelConfigAggregator.getApiUrl())) {
throw new LiteFlowAIException("API URL cannot be empty for AI node: " + wrapBean.getNodeId());
}
if (SetUtil.isNotPresent(modelConfig.getModel())) {
if (SetUtil.isNotPresent(modelConfigAggregator.getModel())) {
throw new LiteFlowAIException("Model cannot be empty for AI node: " + wrapBean.getNodeId());
}
}

View File

@@ -5,7 +5,6 @@ import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatRequest;
import com.yomahub.liteflow.ai.model.ModelFactory;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
import com.yomahub.liteflow.ai.proxy.wrap.ChatProxyWrapBean;
import com.yomahub.liteflow.core.NodeComponent;
/**
* 聊天组件的调用处理器
@@ -28,50 +27,14 @@ public class ChatAIInvocationHandler extends AbstractAIInvocationHandler<ChatPro
@Override
protected Object doExecuteAIProcess(ProcessorContext<ChatProxyWrapBean> processorContext, Object[] args) {
NodeComponent nodeComponent = processorContext.getNodeComponent();
ChatModel chatModel = ModelFactory.getChatModel(wrapBean);
ChatRequest chatRequest = processorContext.getModelRequest().toChatRequest();
if (wrapBean.isStreaming()) {
return processStreaming(nodeComponent);
chatModel.stream(chatRequest);
return null;
} else {
return processBlocking(nodeComponent);
return chatModel.chat(chatRequest);
}
}
private Void processStreaming(NodeComponent nodeComponent) {
ChatModel chatModel = ModelFactory.getChatModel(wrapBean);
// StreamingChatModel streamingChatModel = ModelFactory.getStreamingChatModel(wrapBean);
// // 创建AI服务实例
// Object aiService = AiServiceFactory.createAiService(wrapBean.getEntityClass(), streamingChatModel);
//
// try {
// TokenStream tokenStream = AiServiceFactory.chatStream(aiService, wrapBean.getUserPrompt(), wrapBean.getSystemPrompt());
// // 处理流式响应
// ChatContext chatContext = nodeComponent.getContextBean(ChatContext.class);
// // 如果存在流处理器则将TokenStream传递给它
// if (Objects.nonNull(chatContext)) {
// Optional.of(chatContext.getStreamHandler())
// .ifPresent(streamHandler -> streamHandler.acceptTokenStream(tokenStream));
// }
// } catch (Throwable e) {
// throw new LiteFlowAIException("Error during streaming chat processing", e);
// }
return null;
}
private Object processBlocking(NodeComponent nodeComponent) {
ChatModel chatModel = ModelFactory.getChatModel(wrapBean);
chatModel.chat(ChatRequest.builder().build());
// ChatModel chatModel = ModelFactory.getChatModel(wrapBean);
//
// // 创建AI服务实例
// Object aiService = AiServiceFactory.createAiService(wrapBean.getEntityClass(), chatModel);
//
// try {
// Object result = AiServiceFactory.chat(aiService, wrapBean.getUserPrompt(), wrapBean.getSystemPrompt());
// LOG.info("Chat response: {}", result);
// return result;
// } catch (Throwable e) {
// throw new LiteFlowAIException("Error during blocking chat processing", e);
// }
return null;
}
}

View File

@@ -1,6 +1,8 @@
package com.yomahub.liteflow.ai.proxy.invocation;
import com.yomahub.liteflow.ai.engine.model.chat.ChatModel;
import com.yomahub.liteflow.ai.exception.LiteFlowAIException;
import com.yomahub.liteflow.ai.model.ModelFactory;
import com.yomahub.liteflow.ai.parse.context.ProcessorContext;
import com.yomahub.liteflow.ai.proxy.wrap.ClassifyProxyWrapBean;
import com.yomahub.liteflow.ai.util.SetUtil;
@@ -37,9 +39,7 @@ public class ClassifyAIInvocationHandler extends AbstractAIInvocationHandler<Cla
@Override
protected Object doExecuteAIProcess(ProcessorContext<ClassifyProxyWrapBean> processorContext, Object[] args) {
// ChatModel chatModel = ModelFactory.getChatModel(wrapBean);
return null;
ChatModel chatModel = ModelFactory.getChatModel(processorContext.getWrapBean());
return chatModel.chat(processorContext.getModelRequest().toChatRequest());
}
}

View File

@@ -1,8 +1,7 @@
package com.yomahub.liteflow.ai.proxy.wrap;
import com.yomahub.liteflow.ai.annotation.AIComponent;
import com.yomahub.liteflow.ai.domain.ModelConfig;
import com.yomahub.liteflow.ai.domain.enums.ResponseType;
import com.yomahub.liteflow.ai.domain.dto.ModelConfigAggregator;
import java.lang.annotation.Annotation;
@@ -15,31 +14,24 @@ import java.lang.annotation.Annotation;
public abstract class AIProxyWrapBean<T extends Annotation> {
protected T annotation;
// AI组件注解
protected final T annotation;
// AI 组件配置注解
protected final AIComponent aiComponent;
// LiteFlow Node ID
protected final String nodeId;
// LiteFlow Node 名称
protected final String nodeName;
// 代理接口名称
protected final Class<?> interfaceClass;
// Bean 名称
protected final String beanName;
// 模型配置聚合
protected ModelConfigAggregator config;
protected ModelConfig config;
protected String nodeId;
protected String nodeName;
protected Class<?> interfaceClass;
protected String beanName;
protected String systemPrompt;
protected String userPrompt;
protected ResponseType responseType = ResponseType.TEXT;
protected Class<?> entityClass = String.class;
public AIProxyWrapBean() {
}
public AIProxyWrapBean(AIComponent aiComponent, Class<?> interfaceClass, String beanName) {
this.config = ModelConfig.fromAnnotation(aiComponent);
public AIProxyWrapBean(AIComponent aiComponent, T annotation, Class<?> interfaceClass, String beanName) {
this.aiComponent = aiComponent;
this.annotation = annotation;
this.nodeId = aiComponent.nodeId();
this.nodeName = aiComponent.nodeName();
this.interfaceClass = interfaceClass;
@@ -58,7 +50,11 @@ public abstract class AIProxyWrapBean<T extends Annotation> {
return nodeName;
}
public ModelConfig getConfig() {
public AIComponent getAiComponent() {
return aiComponent;
}
public ModelConfigAggregator getConfig() {
return config;
}
@@ -70,55 +66,7 @@ public abstract class AIProxyWrapBean<T extends Annotation> {
return beanName;
}
public String getSystemPrompt() {
return systemPrompt;
}
public String getUserPrompt() {
return userPrompt;
}
public ResponseType getResponseType() {
return responseType;
}
public Class<?> getEntityClass() {
return entityClass;
}
public void setNodeId(String nodeId) {
this.nodeId = nodeId;
}
public void setNodeName(String nodeName) {
this.nodeName = nodeName;
}
public void setConfig(ModelConfig config) {
public void setConfig(ModelConfigAggregator config) {
this.config = config;
}
public void setInterfaceClass(Class<?> interfaceClass) {
this.interfaceClass = interfaceClass;
}
public void setBeanName(String beanName) {
this.beanName = beanName;
}
public void setSystemPrompt(String systemPrompt) {
this.systemPrompt = systemPrompt;
}
public void setUserPrompt(String userPrompt) {
this.userPrompt = userPrompt;
}
public void setResponseType(ResponseType responseType) {
this.responseType = responseType;
}
public void setEntityClass(Class<?> entityClass) {
this.entityClass = entityClass;
}
}

View File

@@ -14,14 +14,8 @@ public class ChatProxyWrapBean extends AIProxyWrapBean<AIChat> {
private boolean streaming;
public ChatProxyWrapBean() {
super();
}
public ChatProxyWrapBean(AIComponent aiComponent, AIChat annotation,
Class<?> interfaceClass, String beanName) {
super(aiComponent, interfaceClass, beanName);
this.annotation = annotation;
public ChatProxyWrapBean(AIComponent aiComponent, AIChat annotation, Class<?> interfaceClass, String beanName) {
super(aiComponent, annotation, interfaceClass, beanName);
}
public boolean isStreaming() {

View File

@@ -18,14 +18,8 @@ public class ClassifyProxyWrapBean extends AIProxyWrapBean<AIClassify> {
private boolean multiLabel;
public ClassifyProxyWrapBean() {
super();
}
public ClassifyProxyWrapBean(AIComponent aiComponent, AIClassify annotation,
Class<?> interfaceClass, String beanName) {
super(aiComponent, interfaceClass, beanName);
this.annotation = annotation;
public ClassifyProxyWrapBean(AIComponent aiComponent, AIClassify annotation, Class<?> interfaceClass, String beanName) {
super(aiComponent, annotation, interfaceClass, beanName);
}
public List<String> getCategories() {

View File

@@ -12,13 +12,7 @@ import com.yomahub.liteflow.ai.annotation.AIRetrieval;
public class RetrievalProxyWrapBean extends AIProxyWrapBean<AIRetrieval> {
public RetrievalProxyWrapBean() {
super();
}
public RetrievalProxyWrapBean(AIComponent aiComponent, AIRetrieval annotation,
Class<?> interfaceClass, String beanName) {
super(aiComponent, interfaceClass, beanName);
this.annotation = annotation;
public RetrievalProxyWrapBean(AIComponent aiComponent, AIRetrieval annotation, Class<?> interfaceClass, String beanName) {
super(aiComponent, annotation, interfaceClass, beanName);
}
}

View File

@@ -0,0 +1,108 @@
package com.yomahub.liteflow.ai.util;
import cn.hutool.core.util.StrUtil;
import java.time.Duration;
import java.time.format.DateTimeParseException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 将时间字符串转换为 {@link java.time.Duration} 工具类
*
* @author 苍镜月
* @since TODO
*/
public class DurationUtil {
/**
* 用于解析简化时间格式 (e.g., "10s", "5m", "1h") 的正则表达式.
* 它捕获一个或多个数字作为第一组,以及单位 's', 'm', 或 'h' 作为第二组.
*/
private static final Pattern SIMPLE_FORMAT_PATTERN = Pattern.compile("(\\d+)([smh])", Pattern.CASE_INSENSITIVE);
/**
* 私有构造函数
*/
private DurationUtil() {
}
/**
* <p>将一个字符串解析为 {@link Duration} 对象。</p>
*
* <p>该方法提供了两种格式的解析支持:</p>
* <ol>
* <li><b>标准 ISO 8601 持续时间格式:</b> 这是 Java 内置支持的标准格式,以 {@code "PT"} 开头。
* 这是首选的解析方式。</li>
* <li><b>自定义简化格式:</b> 为了方便用户配置,支持一种更简洁的格式,由数字和单位后缀组成。
* 支持的单位包括:
* <ul>
* <li>{@code s} - 秒</li>
* <li>{@code m} - 分钟</li>
* <li>{@code h} - 小时</li>
* </ul>
* 单位不区分大小写 (例如, {@code "10S"} 和 {@code "10s"} 效果相同)。
* </li>
* </ol>
*
* <p><b>解析逻辑:</b></p>
* <p>方法会首先尝试使用标准的 ISO 8601 格式进行解析。如果解析失败,它会回退 (fallback) 到自定义的
* 简化格式进行尝试。如果两种格式都无法解析,或者输入字符串为 {@code null} 或空,
* 将返回指定的默认 {@code Duration} 对象。</p>
*
* @param durationStr 要解析的持续时间字符串。
* <p><b>ISO 8601 格式示例:</b></p>
* <ul>
* <li>{@code "PT20S"} - 20 秒</li>
* <li>{@code "PT15M"} - 15 分钟</li>
* <li>{@code "PT10H"} - 10 小时</li>
* <li>{@code "P2D"} - 2 天</li>
* </ul>
* <p><b>自定义简化格式示例:</b></p>
* <ul>
* <li>{@code "30s"} - 30 秒</li>
* <li>{@code "5m"} - 5 分钟</li>
* <li>{@code "1h"} - 1 小时</li>
* </ul>
* @param defaultDuration 当输入字符串无法被解析、为 {@code null} 或为空时,返回的默认 {@code Duration} 对象。
* 此参数不能为 {@code null}。
* @return 解析后的 {@link Duration} 对象,或在无法解析时返回 {@code defaultDuration}。
* @throws NullPointerException 如果 {@code defaultDuration} 为 {@code null}。
* @see java.time.Duration#parse(CharSequence)
*/
public static Duration toDuration(String durationStr, Duration defaultDuration) {
if (StrUtil.isBlank(durationStr)) {
return defaultDuration;
}
// 1. 尝试使用标准的 ISO 8601 格式进行解析
try {
return Duration.parse(durationStr);
} catch (DateTimeParseException ignore) {
}
// 2. 尝试使用自定义的简化格式进行解析
Matcher matcher = SIMPLE_FORMAT_PATTERN.matcher(durationStr.trim());
if (matcher.matches()) {
try {
long value = Long.parseLong(matcher.group(1));
String unit = matcher.group(2).toLowerCase();
switch (unit) {
case "s":
return Duration.ofSeconds(value);
case "m":
return Duration.ofMinutes(value);
case "h":
return Duration.ofHours(value);
default:
break;
}
} catch (Exception ignore) {
}
}
// 3. 如果两种格式都无法解析,返回默认的 Duration
return defaultDuration;
}
}

View File

@@ -11,4 +11,17 @@ public enum TriState {
TRUE,
FALSE,
UNSET,
;
public Boolean toBool() {
switch (this) {
case TRUE:
return true;
case FALSE:
return false;
case UNSET:
default:
return null;
}
}
}

View File

@@ -32,7 +32,6 @@ public class HttpTransport implements Transport {
.builder()
.connectTimeout(config.getTimeout())
.readTimeout(config.getTimeout())
.writeTimeout(config.getTimeout())
.build()) {
// 构建请求体
String requestBody = buildRequestBody(config, request);

View File

@@ -1,5 +1,6 @@
package com.yomahub.liteflow.ai.engine.model;
import com.yomahub.liteflow.ai.engine.model.chat.entity.ChatRequest;
import com.yomahub.liteflow.ai.engine.util.request.RequestBodyConvertible;
/**
@@ -11,4 +12,7 @@ import com.yomahub.liteflow.ai.engine.util.request.RequestBodyConvertible;
public interface ModelRequest extends RequestBodyConvertible {
default ChatRequest toChatRequest() {
return (ChatRequest) this;
}
}

View File

@@ -13,23 +13,23 @@ import com.yomahub.liteflow.ai.engine.util.request.RequestBody;
public class ChatOptions implements ModelOptions {
public static final ChatOptions DEFAULT = ChatOptions.builder()
.temperature(0.8f)
.topP(0.9f)
.topK(50f)
.temperature(0.8)
.topP(0.9)
.topK(50)
.maxTokens(512)
.seed(null)
.enableThinking(false)
.build();
protected Float temperature;
protected Double temperature;
protected Float topP;
protected Double topP;
protected Float topK;
protected Integer topK;
protected Integer maxTokens;
protected String seed;
protected Integer seed;
protected Boolean enableThinking;
@@ -49,11 +49,11 @@ public class ChatOptions implements ModelOptions {
}
public ChatOptions(
Float temperature,
Float topP,
Float topK,
Double temperature,
Double topP,
Integer topK,
Integer maxTokens,
String seed,
Integer seed,
Boolean enableThinking
) {
this.temperature = temperature;
@@ -83,15 +83,15 @@ public class ChatOptions implements ModelOptions {
.putIfNotEmpty(THINK_KEY, enableThinking);
}
public Float getTemperature() {
public Double getTemperature() {
return temperature;
}
public Float getTopP() {
public Double getTopP() {
return topP;
}
public Float getTopK() {
public Integer getTopK() {
return topK;
}
@@ -99,7 +99,7 @@ public class ChatOptions implements ModelOptions {
return maxTokens;
}
public String getSeed() {
public Integer getSeed() {
return seed;
}
@@ -107,15 +107,15 @@ public class ChatOptions implements ModelOptions {
return enableThinking;
}
public void setTemperature(Float temperature) {
public void setTemperature(Double temperature) {
this.temperature = temperature;
}
public void setTopP(Float topP) {
public void setTopP(Double topP) {
this.topP = topP;
}
public void setTopK(Float topK) {
public void setTopK(Integer topK) {
this.topK = topK;
}
@@ -123,7 +123,7 @@ public class ChatOptions implements ModelOptions {
this.maxTokens = maxTokens;
}
public void setSeed(String seed) {
public void setSeed(Integer seed) {
this.seed = seed;
}
@@ -136,28 +136,28 @@ public class ChatOptions implements ModelOptions {
}
public static abstract class Builder<B extends Builder<B>> {
protected Float temperature;
protected Float topP;
protected Float topK;
protected Double temperature;
protected Double topP;
protected Integer topK;
protected Integer maxTokens;
protected String seed;
protected Integer seed;
protected Boolean enableThinking;
protected abstract B self();
public abstract ChatOptions build();
public B temperature(Float temperature) {
public B temperature(Double temperature) {
this.temperature = temperature;
return self();
}
public B topP(Float topP) {
public B topP(Double topP) {
this.topP = topP;
return self();
}
public B topK(Float topK) {
public B topK(Integer topK) {
this.topK = topK;
return self();
}
@@ -167,7 +167,7 @@ public class ChatOptions implements ModelOptions {
return self();
}
public B seed(String seed) {
public B seed(Integer seed) {
this.seed = seed;
return self();
}

View File

@@ -19,7 +19,4 @@ public class UserMessage extends AbstractMessage {
public UserMessage(String textContent) {
this(textContent, new HashMap<>());
}
// TODO 添加 Resource 支持
}

View File

@@ -1,4 +1,4 @@
package com.yomahub.liteflow.ai.domain.enums;
package com.yomahub.liteflow.ai.engine.model.output;
/**
* AI响应类型枚举

View File

@@ -37,7 +37,6 @@ public final class HttpUtil implements AutoCloseable {
this.syncClient = new OkHttpClient.Builder()
.connectTimeout(builder.connectTimeout)
.readTimeout(builder.readTimeout)
.writeTimeout(builder.writeTimeout)
.build();
}
@@ -305,9 +304,8 @@ public final class HttpUtil implements AutoCloseable {
* 用于创建 {@link HttpUtil} 的构建器。
*/
public static class Builder {
private Duration connectTimeout = Duration.ofSeconds(30);
private Duration connectTimeout = Duration.ofSeconds(60);
private Duration readTimeout = Duration.ofSeconds(60);
private Duration writeTimeout = Duration.ofSeconds(30);
public Builder connectTimeout(Duration duration) {
this.connectTimeout = Objects.requireNonNull(duration, "connectTimeout 不能为空");
@@ -319,11 +317,6 @@ public final class HttpUtil implements AutoCloseable {
return this;
}
public Builder writeTimeout(Duration duration) {
this.writeTimeout = Objects.requireNonNull(duration, "writeTimeout 不能为空");
return this;
}
public HttpUtil build() {
return new HttpUtil(this);
}

View File

@@ -1,7 +1,7 @@
package com.yomahub.liteflow.test.ai.core.proxy.cmp;
import com.yomahub.liteflow.ai.annotation.*;
import com.yomahub.liteflow.ai.domain.enums.ResponseType;
import com.yomahub.liteflow.ai.engine.model.output.ResponseType;
/**
* TODO