迁移文心一言

This commit is contained in:
cherishsince 2024-03-12 21:44:07 +08:00
parent f6ea1bda76
commit 1828d1953a
8 changed files with 608 additions and 0 deletions

View File

@ -0,0 +1,97 @@
package cn.iocoder.yudao.framework.ai.chatyiyan;
import cn.iocoder.yudao.framework.ai.chatyiyan.api.YiYanAuthRes;
import cn.iocoder.yudao.framework.ai.chatyiyan.api.YiYanChatCompletion;
import cn.iocoder.yudao.framework.ai.chatyiyan.api.YiYanChatCompletionRequest;
import cn.iocoder.yudao.framework.ai.chatyiyan.exception.YiYanApiException;
import lombok.Data;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 文心一言
* <p>
* author: fansili
* time: 2024/3/8 21:47
*/
@Data
public class YiYanApi {
private static final String DEFAULT_BASE_URL = "https://aip.baidubce.com";
private static final String AUTH_2_TOKEN_URI = "/oauth/2.0/token";
public static final String DEFAULT_CHAT_MODEL = "ERNIE 4.0";
// 获取access_token流程 https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Ilkkrb0i5
private String appKey;
private String secretKey;
private String token;
// token刷新时间()
private int refreshTokenSecondTime;
// 发送请求 webClient
private final WebClient webClient;
// 使用的模型
private YiYanChatModel useChatModel;
public YiYanApi(String appKey, String secretKey, YiYanChatModel useChatModel, int refreshTokenSecondTime) {
this.appKey = appKey;
this.secretKey = secretKey;
this.useChatModel = useChatModel;
this.refreshTokenSecondTime = refreshTokenSecondTime;
this.webClient = WebClient.builder()
.baseUrl(DEFAULT_BASE_URL)
.build();
token = getToken();
}
private String getToken() {
// 文档地址: https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Ilkkrb0i5
ResponseEntity<YiYanAuthRes> response = this.webClient.post()
.uri(uriBuilder -> uriBuilder.path(AUTH_2_TOKEN_URI)
.queryParam("grant_type", "client_credentials")
.queryParam("client_id", appKey)
.queryParam("client_secret", secretKey)
.build()
)
.retrieve()
.toEntity(YiYanAuthRes.class)
.block();
// 检查请求状态
if (HttpStatusCode.valueOf(200) != response.getStatusCode()) {
throw new YiYanApiException("一言认证失败! apihttps://aip.baidubce.com/oauth/2.0/token 请检查 client_id、client_secret 是否正确!");
}
YiYanAuthRes body = response.getBody();
return body.getAccess_token();
}
public ResponseEntity<YiYanChatCompletion> chatCompletionEntity(YiYanChatCompletionRequest request) {
// TODO: 2024/3/10 小范 这里错误信息返回的结构不一样
// {"error_code":17,"error_msg":"Open api daily request limit reached"}
return this.webClient.post()
.uri(uriBuilder
-> uriBuilder.path(useChatModel.getUri())
.queryParam("access_token", token)
.build())
.body(Mono.just(request), YiYanChatCompletionRequest.class)
.retrieve()
.toEntity(YiYanChatCompletion.class)
.block();
}
public Flux<YiYanChatCompletion> chatCompletionStream(YiYanChatCompletionRequest request) {
return this.webClient.post()
.uri(uriBuilder
-> uriBuilder.path(useChatModel.getUri())
.queryParam("access_token", token)
.build())
.body(Mono.just(request), YiYanChatCompletionRequest.class)
.retrieve()
.bodyToFlux(YiYanChatCompletion.class);
}
}

View File

@ -0,0 +1,137 @@
package cn.iocoder.yudao.framework.ai.chatyiyan;
import cn.iocoder.yudao.framework.ai.chat.ChatClient;
import cn.iocoder.yudao.framework.ai.chat.ChatResponse;
import cn.iocoder.yudao.framework.ai.chat.Generation;
import cn.iocoder.yudao.framework.ai.chat.StreamingChatClient;
import cn.iocoder.yudao.framework.ai.chat.messages.Message;
import cn.iocoder.yudao.framework.ai.chat.prompt.Prompt;
import cn.iocoder.yudao.framework.ai.chatyiyan.api.YiYanChatCompletion;
import cn.iocoder.yudao.framework.ai.chatyiyan.api.YiYanChatCompletionMessage;
import cn.iocoder.yudao.framework.ai.chatyiyan.api.YiYanChatCompletionRequest;
import cn.iocoder.yudao.framework.ai.chatyiyan.exception.YiYanApiException;
import cn.iocoder.yudao.framework.ai.model.function.AbstractFunctionCallSupport;
import cn.iocoder.yudao.framework.ai.model.function.FunctionCallbackContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.RetryListener;
import org.springframework.retry.support.RetryTemplate;
import reactor.core.publisher.Flux;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
/**
* 文心一言
*
* author: fansili
* time: 2024/3/8 19:11
*/
@Slf4j
public class YiYanChatClient
extends AbstractFunctionCallSupport<YiYanChatCompletionMessage, YiYanChatCompletionRequest, ResponseEntity<YiYanChatCompletion>>
implements ChatClient, StreamingChatClient {
private YiYanApi yiYanApi;
public YiYanChatClient(YiYanApi yiYanApi) {
super(new FunctionCallbackContext());
this.yiYanApi = yiYanApi;
}
public final RetryTemplate retryTemplate = RetryTemplate.builder()
// 最大重试次数 10
.maxAttempts(10)
.retryOn(YiYanApiException.class)
// 最大重试5次第一次间隔3000ms第二次3000ms * 2第三次3000ms * 3以此类推最大间隔3 * 60000ms
.exponentialBackoff(Duration.ofMillis(3000), 2, Duration.ofMillis(3 * 60000))
.withListener(new RetryListener() {
@Override
public <T extends Object, E extends Throwable> void onError(RetryContext context,
RetryCallback<T, E> callback, Throwable throwable) {
log.warn("重试异常:" + context.getRetryCount(), throwable);
};
})
.build();
@Override
public String call(String message) {
return ChatClient.super.call(message);
}
@Override
public ChatResponse call(Prompt prompt) {
return this.retryTemplate.execute(ctx -> {
// ctx 会有重试的信息
// 创建 request 请求stream模式需要供应商支持
YiYanChatCompletionRequest request = this.createRequest(prompt, false);
// 调用 callWithFunctionSupport 发送请求
ResponseEntity<YiYanChatCompletion> response = this.callWithFunctionSupport(request);
// 获取结果封装 ChatResponse
YiYanChatCompletion chatCompletion = response.getBody();
return new ChatResponse(List.of(new Generation(chatCompletion.getResult())));
});
}
private YiYanChatCompletionRequest createRequest(Prompt prompt, boolean stream) {
List<YiYanChatCompletionRequest.Message> messages = new ArrayList<>();
List<Message> instructions = prompt.getInstructions();
for (Message instruction : instructions) {
YiYanChatCompletionRequest.Message message = new YiYanChatCompletionRequest.Message();
message.setContent(instruction.getContent());
message.setRole(instruction.getMessageType().getValue());
messages.add(message);
}
YiYanChatCompletionRequest request = new YiYanChatCompletionRequest(messages);
request.setStream(stream);
return request;
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
// ctx 会有重试的信息
// 创建 request 请求stream模式需要供应商支持
YiYanChatCompletionRequest request = this.createRequest(prompt, true);
// 调用 callWithFunctionSupport 发送请求
Flux<YiYanChatCompletion> response = this.yiYanApi.chatCompletionStream(request);
// response.subscribe(new Consumer<YiYanChatCompletion>() {
// @Override
// public void accept(YiYanChatCompletion chatCompletion) {
// // {"id":"as-p0nfjuuasg","object":"chat.completion","created":1710033402,"sentence_id":0,"is_end":false,"is_truncated":false,"result":"编程语","need_clear_history":false,"finish_reason":"normal","usage":{"prompt_tokens":5,"completion_tokens":0,"total_tokens":5}}
// System.err.println(chatCompletion);
// }
// });
return response.map(res -> {
return new ChatResponse(List.of(new Generation(res.getResult())));
});
}
@Override
protected YiYanChatCompletionRequest doCreateToolResponseRequest(YiYanChatCompletionRequest previousRequest, YiYanChatCompletionMessage responseMessage, List<YiYanChatCompletionMessage> conversationHistory) {
return null;
}
@Override
protected List<YiYanChatCompletionMessage> doGetUserMessages(YiYanChatCompletionRequest request) {
return null;
}
@Override
protected YiYanChatCompletionMessage doGetToolResponseMessage(ResponseEntity<YiYanChatCompletion> response) {
return null;
}
@Override
protected ResponseEntity<YiYanChatCompletion> doChatCompletion(YiYanChatCompletionRequest request) {
return yiYanApi.chatCompletionEntity(request);
}
@Override
protected boolean isToolFunctionCall(ResponseEntity<YiYanChatCompletion> response) {
return false;
}
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.framework.ai.chatyiyan;
import lombok.Getter;
/**
* 一言模型
*
* 可参考百度文档https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t
*
* author: fansili
* time: 2024/3/9 12:01
*/
@Getter
public enum YiYanChatModel {
ERNIE4_0("ERNIE 4.0", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions_pro"),
ERNIE4_3_5_8K("ERNIE-3.5-8K", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions"),
ERNIE4_3_5_8K_0205("ERNIE-3.5-8K-0205", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-3.5-8k-0205"),
ERNIE4_3_5_8K_1222("ERNIE-3.5-8K-1222", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-3.5-8k-1222"),
ERNIE4_BOT_8K("ERNIE-Bot-8K", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie_bot_8k"),
ERNIE4_3_5_4K_0205("ERNIE-3.5-4K-0205", "/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/ernie-3.5-4k-0205"),
;
YiYanChatModel(String value, String uri) {
this.value = value;
this.uri = uri;
}
private String value;
private String uri;
}

View File

@ -0,0 +1,48 @@
package cn.iocoder.yudao.framework.ai.chatyiyan.api;
import lombok.Data;
/**
* 一言 获取access_token
*
* author: fansili
* time: 2024/3/10 08:51
*/
@Data
public class YiYanAuthRes {
/**
* 访问凭证
*/
private String access_token;
/**
* 有效期Access Token的有效期
* 说明单位是秒有效期30天
*/
private int expires_in;
/**
* 错误码说明响应失败时返回该字段成功时不返回
*/
private String error;
/**
* 错误描述信息帮助理解和解决发生的错误
* 说明响应失败时返回该字段成功时不返回
*/
private String error_description;
/**
* 暂时未使用可忽略
*/
private String session_key;
/**
* 暂时未使用可忽略
*/
private String refresh_token;
/**
* 暂时未使用可忽略
*/
private String scope;
/**
* 暂时未使用可忽略
*/
private String session_secret;
}

View File

@ -0,0 +1,91 @@
package cn.iocoder.yudao.framework.ai.chatyiyan.api;
import lombok.Data;
/**
* 聊天返回
* 百度链接: https://cloud.baidu.com/doc/WENXINWORKSHOP/s/clntwmv7t
*
* author: fansili
* time: 2024/3/9 10:34
*/
@Data
public class YiYanChatCompletion {
/**
* 本轮对话的id
*/
private String id;
/**
* 回包类型chat.completion多轮对话返回
*/
private String object;
/**
* 时间戳
*/
private int created;
/**
* 表示当前子句的序号只有在流式接口模式下会返回该字段
*/
private int sentence_id;
/**
* 表示当前子句是否是最后一句只有在流式接口模式下会返回该字段
*/
private boolean is_end;
/**
* 当前生成的结果是否被截断
*/
private boolean is_truncated;
/**
* 输出内容标识说明
* · normal输出内容完全由大模型生成未触发截断替换
* · stop输出结果命中入参stop中指定的字段后被截断
* · length达到了最大的token数根据EB返回结果is_truncated来截断
* · content_filter输出内容被截断兜底替换为**
*/
private String finish_reason;
/**
* 搜索数据当请求参数enable_citation为true并且触发搜索时会返回该字段
*/
private String search_info;
/**
* 对话返回结果
*/
private String result;
/**
* 表示用户输入是否存在安全是否关闭当前会话清理历史会话信息
* true表示用户输入存在安全风险建议关闭当前会话清理历史会话信息
* false表示用户输入无安全风险
*/
private boolean need_clear_history;
/**
* 说明
* · 0正常返回
* · 其他非正常
*/
private int flag;
/**
* 当need_clear_history为true时此字段会告知第几轮对话有敏感信息如果是当前问题ban_round=-1
*/
private int ban_round;
/**
* token统计信息
*/
private Usage usage;
@Data
public static class Usage {
/**
* 问题tokens数
*/
private int prompt_tokens;
/**
* 回答tokens数
*/
private int completion_tokens;
/**
* tokens总数
*/
private int total_tokens;
}
}

View File

@ -0,0 +1,8 @@
package cn.iocoder.yudao.framework.ai.chatyiyan.api;
/**
* author: fansili
* time: 2024/3/9 10:37
*/
public class YiYanChatCompletionMessage {
}

View File

@ -0,0 +1,176 @@
package cn.iocoder.yudao.framework.ai.chatyiyan.api;
import lombok.Data;
import java.util.List;
/**
* 一言 Completion req
*
* 百度千帆文档https://cloud.baidu.com/doc/WENXINWORKSHOP/s/jlil56u11
*
* author: fansili
* time: 2024/3/9 10:34
*/
@Data
public class YiYanChatCompletionRequest {
public YiYanChatCompletionRequest(List<Message> messages) {
this.messages = messages;
}
/**
* 聊天上下文信息
* 必填
*/
private List<Message> messages;
/**
* 一个可触发函数的描述列表说明
* 1支持的function数量无限制
* 2长度限制最后一个message的content长度即此轮对话的问题functions和system字段总内容不能超过20480 个字符且不能超过5120 tokens
* 必填
*/
private List<Function> functions;
/**
* 说明
* 1较高的数值会使输出更加随机而较低的数值会使其更加集中和确定
* 2默认0.8范围 (0, 1.0]不能为0
* 必填
*/
private String temperature;
/**
* 说明
* 1影响输出文本的多样性取值越大生成文本的多样性越强
* 2默认0.8取值范围 [0, 1.0]
* 必填
*/
private String top_p;
/**
* 通过对已生成的token增加惩罚减少重复生成的现象说明
* 1值越大表示惩罚越大
* 2默认1.0取值范围[1.0, 2.0]
*
* 必填
*/
private String penalty_score;
/**
* 是否以流式接口的形式返回数据默认false
* 必填
*/
private Boolean stream;
/**
* 模型人设主要用于人设设定例如你是xxx公司制作的AI助手说明
* 1长度限制最后一个message的content长度即此轮对话的问题functions和system字段总内容不能超过20480 个字符且不能超过5120 tokens
* 2如果同时使用system和functions可能暂无法保证使用效果持续进行优化
* 必填
*/
private String system;
/**
* 生成停止标识当模型生成结果以stop中某个元素结尾时停止文本生成说明
* 1每个元素长度不超过20字符
* 2最多4个元素
* 必填
*/
private String stop;
/**
* 是否强制关闭实时搜索功能默认false表示不关闭
* 必填
*/
private Boolean disable_search;
/**
* 是否开启上角标返回说明
* 1开启后有概率触发搜索溯源信息search_infosearch_info内容见响应参数介绍
* 2默认false不开启
* 必填
*/
private Boolean enable_citation;
/**
* 指定模型最大输出token数范围[2, 2048]
* 必填
*/
private Integer max_output_tokens;
/**
* 指定响应内容的格式说明
* 1可选值
* · json_object以json格式返回可能出现不满足效果情况
* · text以文本格式返回
* 2如果不填写参数response_format值默认为text
* 必填
*/
private String response_format;
/**
* 表示最终用户的唯一标识符
* 必填
*/
private String user_id;
/**
* 在函数调用场景下提示大模型选择指定的函数非强制说明指定的函数名必须在functions中存在
* 必填
*/
private String tool_choice;
@Data
public static class Message {
private String role;
private String content;
}
@Data
public static class ToolChoice {
/**
* 指定工具类型function
* 必填:
*/
private String type;
/**
* 指定要使用的函数
* 必填:
*/
private Function function;
/**
* 指定要使用的函数名
* 必填:
*/
private String name;
}
@Data
public static class Function {
/**
* 函数名
* 必填:
*/
private String name;
/**
* 函数描述
* 必填:
*/
private String description;
/**
* 函数请求参数说明
* 1JSON Schema 格式参考JSON Schema描述
* 2如果函数没有请求参数parameters值格式如下
* {"type": "object","properties": {}}
* 必填:
*/
private String parameters;
/**
* 函数响应参数JSON Schema 格式参考JSON Schema描述
* 必填:
*/
private String responses;
/**
* function调用的一些历史示例说明
* 1可以提供正例正常触发和反例无需触发的example
* ·正例从历史请求数据中获取
* ·反例
* 当role = user不会触发请求的query
* 当role = assistant有固定的格式function_call的name为空arguments是空对象:"{}"thought可以填固定的:"我不需要调用任何工具"
* 2兼容之前的 List(example) 格式
*/
private String examples;
}
}

View File

@ -0,0 +1,16 @@
package cn.iocoder.yudao.framework.ai.chatyiyan.exception;
/**
* 一言 api 调用异常
*/
public class YiYanApiException extends RuntimeException {
public YiYanApiException(String message) {
super(message);
}
public YiYanApiException(String message, Throwable cause) {
super(message, cause);
}
}