百度文心一言 java 支持流式输出,Springboot+ sse的demo

news2024/11/16 5:27:22

参考:GitHub - mmciel/wenxin-api-java: 百度文心一言Java库,支持问答和对话,支持流式输出和同步输出。提供SpringBoot调用样例。提供拓展能力。

1、依赖

<dependency>
<groupId>com.baidu.aip</groupId>
<artifactId>java-sdk</artifactId>
<version>4.16.18</version>
</dependency>

2、配置apikey和secretkey

3、主要使用的接口

4、返回的json格式 

3、WenxinEventSourceListener  事件监听器

和其他的接口不一样 需要 CompletionsResponse.data  封装下 ,不然前端页面需要兼容非json的格式

@Slf4j
public class WenxinEventSourceListener extends EventSourceListener {

    private long tokens;

    private SseEmitter sseEmitter;

    public WenxinEventSourceListener(SseEmitter sseEmitter) {
        this.sseEmitter = sseEmitter;
    }

    @Override
    public void onOpen(EventSource eventSource, Response response) {
        log.info("建立sse连接...");
    }

    @SneakyThrows
    @Override
    @JsonIgnoreProperties(ignoreUnknown = true)
    public void onEvent(EventSource eventSource, String id, String type, String data) {
        ChatResponse bean = JSONUtil.parseObj(data).toBean(ChatResponse.class);
        log.info("返回数据:{}", data);
        if (bean.getIs_end()) {
            log.info("返回数据结束了");
            sseEmitter.send(SseEmitter.event()
                    .id("[TOKENS]")
                    .data("<br/><br/>tokens:" + tokens())
                    .reconnectTime(3000));
            sseEmitter.send(SseEmitter.event()
                    .id("[DONE]")
                    .data("[DONE]")
                    .reconnectTime(3000));
            // 传输完成后自动关闭sse
            sseEmitter.complete();
            return;
        }
        log.info("OpenAI返回数据:{}", data);
        tokens += 1;
        if (data.equals("[DONE]")) {
            log.info("OpenAI返回数据结束了");
            sseEmitter.send(SseEmitter.event()
                    .id("[TOKENS]")
                    .data("<br/><br/>tokens:" + tokens())
                    .reconnectTime(3000));
            sseEmitter.send(SseEmitter.event()
                    .id("[DONE]")
                    .data("[DONE]")
                    .reconnectTime(3000));
            // 传输完成后自动关闭sse
            sseEmitter.complete();
            return;
        }

        CompletionsResponse completionResponse = new CompletionsResponse();
        CompletionsResponse.Data dataResult = new CompletionsResponse.Data();
        dataResult.setText(bean.getResult());

        completionResponse.setData(dataResult);
        try {
            sseEmitter.send(SseEmitter.event()
                    .id(bean.getId())
                    .data(completionResponse.getData())
                    .reconnectTime(3000));
        } catch (Exception e) {
            log.error("sse信息推送失败!");
            eventSource.cancel();
            e.printStackTrace();
        }
    }

    @Override
    public void onClosed(EventSource eventSource) {
        log.info("关闭sse连接...");
    }

    @SneakyThrows
    @Override
    public void onFailure(EventSource eventSource, Throwable t, Response response) {
        if(Objects.isNull(response)){
            log.error("sse连接异常:{}", t);
            eventSource.cancel();
            return;
        }
        ResponseBody body = response.body();
        if (Objects.nonNull(body)) {
            // 错误处理 {"error_code":110,"error_msg":"Access token invalid or no longer valid"},异常:{}
            log.error("sse连接异常data:{},异常:{}", body.string(), t);
        } else {
            log.error("sse连接异常data:{},异常:{}", response, t);
        }
        eventSource.cancel();
    }

    /**
     * tokens
     * @return
     */
    public long tokens() {
        return tokens;
    }
}

4、WenXinClient  流式主要看下 streamChat 方式,之前从千帆上找到流式例子 返回type是json的,所以之前自己手写的demo总报异常。

 public void streamChat(ChatBody chatBody, EventSourceListener eventSourceListener, ModelE modelE) {
        if (Objects.isNull(eventSourceListener)) {
            throw new WenXinException("参数异常:EventSourceListener不能为空");
        }
        chatBody.setStream(true);
        try {
            EventSource.Factory factory = EventSources.createFactory(this.okHttpClient);
            Request request = new Request.Builder().url(assembleUrl(modelE))
                    .post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()),
                            new ObjectMapper().writeValueAsString(chatBody))).build();
            factory.newEventSource(request, eventSourceListener);
        } catch (Exception e) {
            log.error("请求参数解析异常:", e);
            e.printStackTrace();
        }
    }

private String assembleUrl(ModelE modelE) {
        accessToken = WenXinConfig.refreshAccessToken();
        return modelE.getApiHost() + "?access_token=" + accessToken;
    }

5、定义Sse的接口是实现方法

public interface SseService {
    /**
     * 创建SSE
     * @param uid
     * @return
     */
    SseEmitter createSse(String uid);

    /**
     * 关闭SSE
     * @param uid
     */
    void closeSse(String uid);

    /**
     * 客户端发送消息到服务端
     * @param uid
     * @param chatRequest
     */
    ChatResponse sseChat(String uid, ChatRequest chatRequest);
}
public class WenXinSseServiceImpl implements SseService {
    @Value("${chat.accessKeyId}")
    private String accessKeyId;
    @Value("${chat.accessKeySecret}")
    private String accessKeySecret;
    @Value("${chat.agentKey}")
    private String agentKey;
    @Value("${chat.appId}")
    private String appId;

    @Autowired
    WenXinClient wenXinClient;
    @Override
    public SseEmitter createSse(String uid) {
        //默认30秒超时,设置为0L则永不超时
        SseEmitter sseEmitter = new SseEmitter(0l);
        //完成后回调
        sseEmitter.onCompletion(() -> {
            log.info("[{}]结束连接...................", uid);
            LocalCache.CACHE.remove(uid);
        });
        //超时回调
        sseEmitter.onTimeout(() -> {
            log.info("[{}]连接超时...................", uid);
        });
        //异常回调
        sseEmitter.onError(
                throwable -> {
                    try {
                        log.info("[{}]连接异常,{}", uid, throwable.toString());
                        sseEmitter.send(SseEmitter.event()
                                .id(uid)
                                .name("发生异常!")
                                .data(Message.builder().content("发生异常请重试!").build())
                                .reconnectTime(3000));
                        LocalCache.CACHE.put(uid, sseEmitter);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
        );
        try {
            sseEmitter.send(SseEmitter.event().reconnectTime(5000));
        } catch (IOException e) {
            e.printStackTrace();
        }
        LocalCache.CACHE.put(uid, sseEmitter);
        log.info("[{}]创建sse连接成功!", uid);
        return sseEmitter;
    }

    @Override
    public void closeSse(String uid) {
        SseEmitter sse = (SseEmitter) LocalCache.CACHE.get(uid);
        if (sse != null) {
            sse.complete();
            //移除
            LocalCache.CACHE.remove(uid);
        }
    }

    @Override
    public ChatResponse sseChat(String uid, ChatRequest chatRequest) {

        if (StringUtils.isBlank(chatRequest.getMsg())) {
            log.error("参数异常,msg为null", uid);
            throw new BaseException("参数异常,msg不能为空~");
        }

        SseEmitter sseEmitter = (SseEmitter) LocalCache.CACHE.get(uid);

        if (sseEmitter == null) {
            log.info("聊天消息推送失败uid:[{}],没有创建连接,请重试。", uid);
            throw new BaseException("聊天消息推送失败uid:[{}],没有创建连接,请重试。~");
        }

        WenxinEventSourceListener openAIEventSourceListener = new WenxinEventSourceListener(sseEmitter);

        List<MessageItem> messages = new ArrayList<>();
        messages.add(MessageItem.builder().role(MessageItem.Role.USER).content(chatRequest.getMsg()).build());
        wenXinClient.streamChat(messages, openAIEventSourceListener, ModelE.ERNIE_Bot);


        LocalCache.CACHE.put("msg" + uid, JSONUtil.toJsonStr(messages), LocalCache.TIMEOUT);

        ChatResponse response = new ChatResponse();
        response.setQuestionTokens(1);

        return response;
    }
}

6、主要的controller接口

/**
     * 创建sse连接
     *
     * @param headers
     * @return
     */
    @CrossOrigin
    @GetMapping("/createSse")
    public SseEmitter createConnect(@RequestHeader Map<String, String> headers) {
        String uid = getUid(headers);
        return sseService.createSse(uid);
    }

    /**
     * 聊天接口
     *
     * @param chatRequest
     * @param headers
     */
    @CrossOrigin
    @PostMapping("/chat")
    @ResponseBody
    public ChatResponse sseChat(@RequestBody ChatRequest chatRequest, @RequestHeader Map<String, String> headers, HttpServletResponse response) {
        String uid = getUid(headers);
        return sseService.sseChat(uid, chatRequest);
    }

    /**
     * 关闭连接
     *
     * @param headers
     */
    @CrossOrigin
    @GetMapping("/closeSse")
    public void closeConnect(@RequestHeader Map<String, String> headers) {
        String uid = getUid(headers);
        sseService.closeSse(uid);
    }

7、主要的页面代码

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>智能问答</title>
  <link rel="stylesheet" href="styles.css"> <!-- 引入外部CSS -->
  <script src="HZRecorder.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
  <script src="js/markdown.min.js"></script>
  <script src="js/eventsource.min.js"></script>
  <script>

      function setText(text, uuid_str) {
        let content = document.getElementById(uuid_str);
        content.innerHTML = marked(text);
      }

      function uuid() {
        var s = [];
        var hexDigits = "0123456789abcdef";
        for (var i = 0; i < 36; i++) {
          s[i] = hexDigits.substr(Math.floor(Math.random() * 0x10), 1);
        }
        s[14] = "4"; // bits 12-15 of the time_hi_and_version field to 0010
        s[19] = hexDigits.substr((s[19] & 0x3) | 0x8, 1); // bits 6-7 of the clock_seq_hi_and_reserved to 01
        s[8] = s[13] = s[18] = s[23] = "-";

        var uuid = s.join("");
        console.log(uuid)
        return uuid;

      }



      window.onload = function () {
        /*let disconnectBtn = document.getElementById("disconnectSSE");*/
        let messageElement = document.getElementById("messageInput");
        let chat = document.getElementById("chat-messages");
        let sse;
        let uid = window.localStorage.getItem("uid");
        if (uid == null || uid == "" || uid == "null") {
          uid = uuid();
        }
        let text = "";
        let uuid_str;
        // 设置本地存储
        window.localStorage.setItem("uid", uid);

        // 发送消息按钮点击事件
        document.getElementById('sendTextButton').addEventListener('click', async function () {
          try {
            const userInput = document.getElementById('messageInput').value.trim();
            if (userInput) {
              await sseOneTurn(userInput)
              userInput.value = ''; // 清空输入框

            } else {
              alert('请输入文字消息!');
            }
          } catch (error) {
            alert('发送消息时发生错误: ' + error.message);
          }
        });

        // 回车事件
        messageElement.onkeydown = function () {
          if (window.event.keyCode === 13) {
            if (!messageElement.value) {
              return;
            }
            sseOneTurn(messageElement.value);
          }
        };

        function sseOneTurn(InputText) {
          uuid_str = uuid();
          //创建sse
          const eventSource = new EventSourcePolyfill("/createSse", {
            headers: {
              uid: uid,
            },
          });

          eventSource.onopen = (event) => {
            console.log("开始输出后端返回值");
            sse = event.target;
          };
          eventSource.onmessage = (event) => {
            debugger;
            if (event.lastEventId == "[TOKENS]") {
              text = text + event.data;
              setText(text, uuid_str);
              text = "";
              return;
            }
            if (event.data == "[DONE]") {
              text = "";
              if (sse) {
                sse.close();
              }
              return;
            }
            let json_data = JSON.parse(event.data);
            console.log(json_data);
            if (json_data.text == null || json_data.text == "null") {
              return;
            }
            text = text + json_data.text;
            setText(text, uuid_str);
          };
          eventSource.onerror = (event) => {
            console.log("onerror", event);
            alert("服务异常请重试并联系开发者!");
            if (event.readyState === EventSource.CLOSED) {
              console.log("connection is closed");
            } else {
              console.log("Error occured", event);
            }
            event.target.close();
          };
          eventSource.addEventListener("customEventName", (event) => {
            console.log("Message id is " + event.lastEventId);
          });
          eventSource.addEventListener("customEventName", (event) => {
            console.log("Message id is " + event.lastEventId);
          });
          $.ajax({
            type: "post",
            url: "/chat",
            data: JSON.stringify({
              msg: InputText,
            }),
            contentType: "application/json;charset=UTF-8",
            dataType: "json",
            headers: {
              uid: uid,
            },
            beforeSend: function (request) {},
            success: function (result) {
              //新增问题框
              debugger;
              chat.innerHTML +=
                      '<tr><td style="height: 30px;">' +
                      InputText +
                      "<br/><br/> tokens:" +
                      result.question_tokens +
                      "</td></tr>";
              InputText = null;
              //新增答案框
              chat.innerHTML +=
                      '<tr><td><article id="' +
                      uuid_str +
                      '" class="markdown-body"></article></td></tr>';
            },
            complete: function () {},
            error: function () {
              console.info("发送问题失败!");
            },
          });
        }

        /*disconnectBtn.onclick = function () {
          if (sse) {
            sse.close();
          }
        };*/
      };
    </script>
  </head>
<body>

<div class="chat-container">
  <div class="chat-header">
    <h1>智能问答</h1>
  </div>
  <div class="chat-messages" id="chat-messages">
    <!-- 聊天消息将会在这里显示 -->
  </div>
  <form class="message-form" onsubmit="return false;">
    <input type="text" id="messageInput" placeholder="输入消息..." autocomplete="off">
    <button type="button" id="sendTextButton">发送文字</button>
    <button type="button" id="recordAndUploadButton">按住录音</button>
    <progress id="uploadProgress" value="0" max="100" style="display:none;"></progress>
  </form>
</div>

</body>

</html>

最后的呈现效果如下:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1677450.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue3使用datav3报错的三个问题解决

我这里写的是按需引入 报错问题Cannot find module dataview/datav-vue3 修改datav源码中的package.json文件 修改为 "module": "./es/index.mjs", 然就就会遇见新的报错问题 报错问题TypeError: Cannot read properties of null (reading $el) 然后修改…

Day 46 139.单词拆分

单词拆分 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict&#xff0c;判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 说明&#xff1a; 拆分时可以重复使用字典中的单词。 你可以假设字典中没有重复的单词。 示例 1&#xff1a; 输入: s “leet…

智能监控与安全管理:安全帽检测算法的实践与应用

在工地、煤矿等高危工作环境中&#xff0c;安全帽的佩戴至关重要。安全帽能够有效防止因坠落物体或碰撞等引起的头部伤害&#xff0c;从而保护工作人员的生命安全。然而&#xff0c;传统的检查人员佩戴安全帽的方式主要依赖于现场监督和巡查&#xff0c;这种方法不仅耗费大量人…

PDF编辑阅读器PDF Expert for Mac v3.10.1中文激活版

PDF Expert for Mac是一款易于使用的 PDF 编辑器和注释器&#xff0c;专为 Mac 设备设计。它允许用户轻松查看、编辑、签名、注释和共享 PDF。该软件使用户能够向他们的 PDF 添加文本、图像、链接和形状&#xff0c;突出显示和标记文本&#xff0c;填写表格以及签署数字文档。它…

更高、更快、更强,受管文件传输应该注意的三个要素

受管文件在很多公司内部都存在这个分类&#xff0c;受管文件不是特定的某一文件&#xff0c;而是指的是在一个组织或企业内&#xff0c;被正式管理和控制的文件。这些文件通常包含重要的信息&#xff0c;如技术规格、标准操作程序&#xff08;SOPs&#xff09;、质量手册、设计…

SDN 实现 vxlan隧道

SDN vxlan隧道 官方介绍&#xff1a; VXLAN&#xff08;Virtual eXtensible Local Area Network&#xff0c;虚拟扩展局域网&#xff09;&#xff0c;是由IETF定义的NVO3&#xff08;Network Virtualization over Layer 3&#xff09;标准技术之一&#xff0c;是对传统VLAN协议…

社交媒体数据恢复:如流

如流&#xff0c;原名百度Hi&#xff0c;是百度公司开发的一款即时通讯软体。百度Hi具备文字消息、视讯、通话、文件传输等功能。 查找备份&#xff1a;如果您之前有备份如流中的数据&#xff0c;您可以尝试从备份中恢复。如流支持备份至云端&#xff0c;如百度网盘等。 联系客…

C++auto关键字、范围for循环

一、auto关键字 1.1auto简介 在早期C/C中auto的含义是&#xff1a;使用auto修饰的变量&#xff0c;是具有自动存储器的局部变量。 C11中&#xff0c;标准委员会赋予了auto全新的含义即&#xff1a;auto不再是一个存储类型指示符&#xff0c;而是作为一个新的类型指示符来指示编…

前端铺子后台管理系统:基于Vue3与Ant Design Vue的轻量级解决方案

一、引言 随着前端技术的飞速发展&#xff0c;构建高效、轻量且易于维护的后台管理系统成为了企业信息化建设的重要一环。前端铺子后台管理系统&#xff0c;作为一款基于Vue的前端框架&#xff0c;结合Ant Design Vue的UI组件库&#xff0c;为企业提供了一个高效、灵活的后台管…

视频推拉流/视频直播点播平台EasyDSS使用Mysql数据库接口报错502如何处理?

视频推拉流/视频直播点播EasyDSS互联网直播平台支持一站式的上传、转码、直播、回放、嵌入、分享功能&#xff0c;具有多屏播放、自由组合、接口丰富等特点。平台可以为用户提供专业、稳定的直播推流、转码、分发和播放服务&#xff0c;全面满足超低延迟、超高画质、超大并发访…

计算思维的理解

2006年&#xff0c;卡内基梅隆大学周以真教授首次系统性地定义了计算思维。这一年&#xff0c;她在美国计算机权威期刊《Communications of the ACM》上发表了题为《Computational Thinking》的论文&#xff0c;由此开启了计算思维大众化的全新历程。 周以真&#xff08;Jeanne…

指针(4)

目录 1. 字符指针变量 2.数组指针 2.1 数组指针和指针数组的区别 2.2访问数组指针 3. ⼆维数组传参的本质 4. 函数指针变量 4.1两段有趣的代码 4.2 typedef 关键字 5.函数指针数组 6.转移表 1. 字符指针变量 在指针的类型中我们知道有⼀种指针类型为字符指针 char* …

【devops】Linux 日常磁盘清理 ubuntu 清理大文件 docker 镜像清理

日常磁盘清理 1、查找大文件 find / -type f -size 1G2、清理docker无用镜像&#xff08;drone产生的残余镜像文件&#xff09; docker system prune -a一、清理服务器磁盘 1、查找大文件 在Ubuntu系统中&#xff0c;你可以使用find命令来查找大文件。find命令是一个强大的…

渣土车上路识别报警摄像机

随着城市建设的不断推进&#xff0c;渣土车在城市道路上的数量也逐渐增加。然而&#xff0c;一些不法渣土车司机往往会超载、超速行驶或者闯红灯&#xff0c;给道路交通安全和城市环境带来了一定的隐患。为了有效监管渣土车上路行驶的情况&#xff0c;渣土车上路识别报警摄像机…

企业微信hook接口协议,ipad协议http,获取二次验证二维码接口

获取二次验证二维码接口 参数名必选类型说明uuid是String每个实例的唯一标识&#xff0c;根据uuid操作具体企业微信 请求示例 {"uuid":"f5a22e9b-9664-4250-b40a-08741dba549c" } 返回示例 {"data": {"qrcode": "http://47.9…

Linux之内存管理-malloc \kmalloc\vmalloc\dma

1、malloc 函数 1.1分配内存小于128k,调用brk malloc是C库实现的函数&#xff0c;C库维护了一个缓存&#xff0c;当内存够用时&#xff0c;malloc直接从C库缓存分配&#xff0c;只有当C库缓存不够用&#xff1b; 当申请的内存小于128K时&#xff0c;通过系统调用brk&#xff…

C++的数据结构(八):线段树

线段树是一种高效的树形数据结构&#xff0c;用于处理区间查询和区间更新问题。它的基本思想是将一个大的区间分解为若干个小的、不相交的区间&#xff0c;每个小区间对应线段树中的一个节点。线段树的每个节点保存了该区间的信息&#xff08;如区间最大值、区间和等&#xff0…

Spring Boot实现多数据源快速入门

1.为什么需要多数据源&#xff1f; 多数据源既动态数据源&#xff0c;项目开发逐渐扩大&#xff0c;单个数据源、单一数据源已经无法满足需求项目的支撑需求。本文采用dynamic-datasource-spring-boot-starter实现多数据源&#xff0c; 主要特性 支持 数据源分组 &#xff0…

Ansible自动化运维中的User用户管理模块应用详解

作者主页&#xff1a;点击&#xff01; Ansible专栏&#xff1a;点击&#xff01; 创作时间&#xff1a;2024年5月14日14点12分 在Ansible中&#xff0c;user 模块主要用于管理系统用户账户。它可以创建、修改、删除用户&#xff0c;并管理用户的属性&#xff0c;比如密码、…

RK3576 Camera:资源介绍

RK3576是RK今年上市的中高端旗舰芯片&#xff0c;定位弱于RK3588。这篇文章主要分享一下RK3576这颗主控芯片的camera资源。 &#xff08;1&#xff09;RK3576 camera资源 ①RK3576 camera硬件框图 RK3576的camera硬件框图如图所示&#xff0c;拥有一路4lane的DCPHY&#xff…