Bladeren bron

迁移文心一言

cherishsince 1 jaar geleden
bovenliggende
commit
1828d1953a

+ 97 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatyiyan/YiYanApi.java

@@ -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("一言认证失败! api:https://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);
+    }
+}

+ 137 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatyiyan/YiYanChatClient.java

@@ -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;
+    }
+}

+ 35 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatyiyan/YiYanChatModel.java

@@ -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;
+
+}

+ 48 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatyiyan/api/YiYanAuthRes.java

@@ -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;
+}

+ 91 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatyiyan/api/YiYanChatCompletion.java

@@ -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;
+    }
+}

+ 8 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatyiyan/api/YiYanChatCompletionMessage.java

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

+ 176 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatyiyan/api/YiYanChatCompletionRequest.java

@@ -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_info,search_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;
+        /**
+         * 函数请求参数,说明:
+         * (1)JSON 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;
+    }
+
+}

+ 16 - 0
yudao-module-ai/yudao-spring-boot-starter-ai/src/main/java/cn/iocoder/yudao/framework/ai/chatyiyan/exception/YiYanApiException.java

@@ -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);
+	}
+
+}