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

参考: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>

最后的呈现效果如下:

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

7天精通Web APIs——正则阶段案例(理论+实战)(第六天)

正则表达式的定义和使用 定义&#xff1a;是一种匹配模式&#xff0c;用于匹配字符串中字符组合 作用&#xff1a;表单验证&#xff08;匹配&#xff09;、过滤敏感词&#xff08;替换&#xff09;、字符串中提取我们想要的部分&#xff08;提取&#xff09; 使用分为两步&…

基于C#开发web网页管理系统模板流程-登录界面

前言&#xff0c;首先介绍一下本项目将要实现的功能 &#xff08;一&#xff09;登录界面 实现一个不算特别美观的登录窗口&#xff0c;当然这一步跟开发者本身的设计美学相关&#xff0c;像蒟蒻博主就没啥艺术细胞&#xff0c;勉强能用能看就行…… &#xff08;二&#xff09…

STK12 RPO模块学习(3)

一、Maintain NMC RPO Sequence Maintain Natural Motion Circumnavigation RPO序列在目标星和追踪星经历不同的力的情况下保持NMC。通常这种差异是由于阻力和太阳光压造成的。这些是主要不同力当执行接近任务的时候&#xff0c;因为重力和相对三体摄动力非常小当相对距离在10…

思源笔记如何结合群晖WebDav实现云同步数据

文章目录 1. 开启群晖WebDav 服务2. 本地局域网IP同步测试3. 群晖安装Cpolar4. 配置远程同步地址5. 笔记远程同步测试6. 固定公网地址7. 配置固定远程同步地址 在数字化时代&#xff0c;信息的同步与共享变得尤为重要。无论是个人用户还是企业团队&#xff0c;都渴望能够实现跨…

架构的设计

文章目录 架构设计2024心得优秀博客mall微服务项目架构mall单体项目架构 架构设计2024 心得 优秀博客 mall优秀开源仓库地址Spring Cloud各种组件的教程 mall微服务项目架构 图片和文档引用地址 https://gitee.com/macrozheng/springcloud-learning 架构设计 前端通过ngin…

计算机网络5——运输层4TCP拥塞控制

文章目录 一、拥塞控制的一般原理二、举例三、理解四、TCP 的拥塞控制方法1、慢开始和拥塞避免 五、主动队列管理AOM1、背景2、介绍3、实现 一、拥塞控制的一般原理 在计算机网络中的链路容量(即带宽)、交换节点中的缓存和处理机等都是网络的资源。在某段时间&#xff0c;若对…

一道dp错题

dis(a,b)就是两点之间的距离公式 那么这道题该怎么解呢,.先看数据范围x,y<1e4,so,18个点两点之间距离最大18*1e4*sqrt(2)<2^18,所以如果跳过的点大于18个点,那么显然一个区间内最多不会跳跃超过17个点 现在我们想知道前i个点跳跃几次在哪跳跃能够达到最小花费,不妨设跳…

Vue的学习 —— <vue响应式基础>

目录 前言 正文 单文件组件 什么是单文件组件 单文件组件使用方法 数据绑定 什么是数据绑定 数据绑定的使用方法 响应式数据绑定 响应式数据绑定的使用方法 ref() 函数 reactive()函数 toRef()函数 toRefs()函数 案例练习 前言 Vue.js 以其高效的数据绑定和视图…

2024统计建模中国新质生产力统计测度与时空演变及其驱动因素研究

高质量成品论文46页word版本1.5w字书写完整数据集1000行py代码一等奖论文&#xff01;这里仅展示部分内容&#xff0c;完整版在下面的链接。 【1.5w字全网最佳】2024统计建模大赛高质量成品论文39页配套完整代码运行全套数据集https://www.jdmm.cc/file/2710661/ 中国新质生产…

【码农日常】将mp4转换为逐帧图片

项目场景&#xff1a; 拍摄了一段视频记录设备工作的状态和测量仪器的实时数据。由于测量仪器岁数比较大&#xff0c;不够智能&#xff0c;遂打算将视频转换为逐帧图片进行分析。 网上没找到现成工具&#xff0c;借鉴网上大神的操作方式打算用python写一个工具。 问题描述 用…

基于springboot实现政府管理系统项目【项目源码+论文说明】计算机毕业设计

基于springboot实现政府管理系统演示 摘要 信息数据从传统到当代&#xff0c;是一直在变革当中&#xff0c;突如其来的互联网让传统的信息管理看到了革命性的曙光&#xff0c;因为传统信息管理从时效性&#xff0c;还是安全性&#xff0c;还是可操作性等各个方面来讲&#xff…

华为设备display查看命令

display version //查看版本信息 display current-configuration //查看配置详情 display this //查看当前视图有效配置 display ip routing-table //查看路由表 display ip routing-table 192.168.3.1 //查看去往3.1的路由 display ip interface brief //查看接口下ip信息 dis…

PXIe规格i3/i5/i7单板计算机控制器

是专为PXIe混合测试系统设计的主控制器&#xff0c;3U 12HP PXIe规格。该产品采用Intel Core™i3/i5/i7 第四代高性能处理器&#xff0c;内存可支持高达16G DDR3L。该系统PXI Express的link配置为通用的4Port 4lane的模式&#xff0c;数据吞吐量高达8GB/S。 CX786x提供丰富灵活…

机器学习(2)

目录 2-1泛化能力 2-2过拟合和欠拟合 2-3三大问题 2-4评估方法 2-5调参和验证集 2-6性能度量 2-7比较检验 2-1泛化能力 如何进行模型评估与选择&#xff1f; 2-2过拟合和欠拟合 泛化误差&#xff1a;在“未来”样本上的误差 经验误差&#xff1a;在训练集上的误差&am…

什么叫拆分盘?什么是拆分盘!一篇文章带你了解!

随着互联网金融的快速发展&#xff0c;各种新型投资模式层出不穷&#xff0c;其中拆分盘作为一种只涨不跌的理财方式&#xff0c;吸引了众多投资者的目光。本文将结合一个简单的拆分盘示例&#xff0c;对拆分盘的投资逻辑进行解析&#xff0c;并探讨其潜在风险&#xff0c;以帮…

每日一题11:Pandas:数据重塑-透视

一、每日一题 解答&#xff1a; import pandas as pddef pivotTable(weather: pd.DataFrame) -> pd.DataFrame:df_pivot weather.pivot(indexmonth, columnscity, valuestemperature)return df_pivot 题源&#xff1a;力扣 二、总结 Pandas 是一个强大的 Python 数据分析…

怎么申请一年期免费的https证书

随着互联网的推广和普及&#xff0c;如今HTTPS证书的普及度还是比较高的了&#xff0c;大家对于https证书的需求度也在日益提升。针对于一些个人用户或是企业而言&#xff0c;实现网站的https访问已经成为了一种标配。从去年年底开始&#xff0c;各大SSL证书厂商陆续下架一年期…

FOTS:一种用于机器人操作技能Sim2Real学习的快速光学触觉仿真器

类 GelSight的视触觉传感器具有高分辨率和低制造成本的优势&#xff0c;但是在与现实中的物体进行频繁接触时易受磨损。而触觉仿真器可大幅降低硬件成本&#xff0c;同时为后续技能学习任务提供仿真训练环境。为此&#xff0c;来自东南大学自动化学院的钱堃副教授研究团队和伦敦…

LeetCode---循环队列

循环队列就是只有固定的内存&#xff0c;存数据&#xff0c;出数据&#xff0c;但是也和队列一样&#xff0c;先进先出。如下图所示&#xff0c;这是他的样子 在head出&#xff0c;tail进&#xff0c;但是这个如果用数组解决的话&#xff0c;就有问题&#xff0c;力扣给我们的接…