|
| 1 | +# tio-boot handler see openai chatgpt |
| 2 | + |
| 3 | +## Introudction |
| 4 | + |
| 5 | +使用 tio-boot 框架的 handler 组件从 openai chatgpt 获取流程响应并以流式的方法返回给客户端 |
| 6 | +使用了`tio-boot`框架,集成了与 OpenAI ChatGPT 进行交互的功能,并以服务器发送事件(Server-Sent Events, SSE)方式流式返回数据给客户端。 |
| 7 | + |
| 8 | +## 概念扩展 |
| 9 | + |
| 10 | +- **Server-Sent Events (SSE)**:一种允许服务器向浏览器客户端发送更新的技术。适用于需要实时数据更新的场景,如股票行情、新闻直播等。 |
| 11 | +- **OpenAI API**:这是利用人工智能模型生成文本的 API,可用于聊天机器人、内容生成等多种应用。 |
| 12 | +- **JFinal Aop**:轻量级 AOP(面向切面编程)框架,支持依赖注入和面向切面编程,使 Java 开发更简洁。 |
| 13 | + |
| 14 | +## 代码讲解 |
| 15 | + |
| 16 | +### Maven 配置 (`pom.xml`) |
| 17 | + |
| 18 | +这个 `pom.xml` 文件定义了项目的构建配置和依赖管理。主要部分包括: |
| 19 | + |
| 20 | +- **项目信息**:定义了项目的基本元数据,如 `groupId`, `artifactId`, 和 `version`。 |
| 21 | +- **属性**:定义了常用的属性,如 Java 版本和项目依赖版本。这样可以在整个项目中重复使用这些属性,便于维护和更新。 |
| 22 | +- **依赖**:列出了项目所需的库,例如日志框架 `logback`, JSON 处理库 `fastjson2`, HTTP 客户端库 `okhttp3`。 |
| 23 | +- **构建配置**:分为开发和生产两种配置,使用了 Spring Boot 的 Maven 插件来简化打包和运行过程。 |
| 24 | + |
| 25 | +### 主类 (`HelloApp`) |
| 26 | + |
| 27 | +该类是程序的入口点: |
| 28 | + |
| 29 | +- 使用 `@AComponentScan` 注解来自动扫描和注册 JFinal AOP 容器中的组件。 |
| 30 | +- 在 `main` 方法中,记录程序启动和结束时间,使用 `TioApplication.run()` 方法启动应用。 |
| 31 | + |
| 32 | +### HTTP 请求处理配置 (`HttpServerRequestHandlerConfig`) |
| 33 | + |
| 34 | +该类配置了 HTTP 请求的处理逻辑: |
| 35 | + |
| 36 | +- `@BeforeStartConfiguration` 表明这个配置是在服务器启动之前进行的。 |
| 37 | +- 使用 JFinal AOP 的 `Aop.get` 方法从 AOP 容器获取实例,便于依赖注入和管理。 |
| 38 | +- 创建 `SimpleHttpRoutes` 对象并添加路由,将特定的 HTTP 请求映射到对应的处理器。 |
| 39 | + |
| 40 | +### 消息处理器 (`OpenaiV1ChatHandler`) |
| 41 | + |
| 42 | +该类负责处理来自客户端的 HTTP 请求,并与 OpenAI ChatGPT API 交互: |
| 43 | + |
| 44 | +- **SSE Header**:设置 HTTP 响应头为 SSE 格式,允许服务器推送实时数据到客户端。 |
| 45 | +- **HTTP 请求处理**:接收 HTTP 请求,解析并发送请求到 OpenAI 服务器,然后将响应以 SSE 形式发送给客户端。 |
| 46 | +- **API 请求构建**:构建发送到 OpenAI 的 HTTP 请求,包括设置请求头和请求体。 |
| 47 | +- **流处理**:读取 OpenAI 响应,并将每一行数据以 SSE 格式实时发送给客户端。 |
| 48 | + |
| 49 | +### 数据模型 (`ChatgptMessage` 和 `CompletionsModel`) |
| 50 | + |
| 51 | +- `ChatgptMessage` 定义了与 OpenAI 交互的基本消息结构,包括角色和内容。 |
| 52 | +- `CompletionsModel` 封装了发送到 OpenAI 的请求数据,包括模型选择、消息列表和是否启用流式传输。 |
| 53 | + |
| 54 | +## 代码内容 |
| 55 | + |
| 56 | +### pom.xml |
| 57 | + |
| 58 | +```xml |
| 59 | +pom.xml |
| 60 | +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> |
| 61 | + <modelVersion>4.0.0</modelVersion> |
| 62 | + <groupId>com.litongjava</groupId> |
| 63 | + <artifactId>tio-boot-sse-chatgpt-demo</artifactId> |
| 64 | + <version>1.0.0</version> |
| 65 | + <properties> |
| 66 | + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
| 67 | + <java.version>1.8</java.version> |
| 68 | + <maven.compiler.source>${java.version}</maven.compiler.source> |
| 69 | + <maven.compiler.target>${java.version}</maven.compiler.target> |
| 70 | + <graalvm.version>23.1.1</graalvm.version> |
| 71 | + <tio-boot.version>1.4.2</tio-boot.version> |
| 72 | + <lombok-version>1.18.30</lombok-version> |
| 73 | + <hotswap-classloader.version>1.2.2</hotswap-classloader.version> |
| 74 | + <final.name>web-hello</final.name> |
| 75 | + <main.class>com.litongjava.tio.web.hello.HelloApp</main.class> |
| 76 | + </properties> |
| 77 | + <dependencies> |
| 78 | + <dependency> |
| 79 | + <groupId>ch.qos.logback</groupId> |
| 80 | + <artifactId>logback-classic</artifactId> |
| 81 | + <version>1.2.3</version> |
| 82 | + |
| 83 | + </dependency> |
| 84 | + <dependency> |
| 85 | + <groupId>com.litongjava</groupId> |
| 86 | + <artifactId>tio-boot</artifactId> |
| 87 | + <version>${tio-boot.version}</version> |
| 88 | + </dependency> |
| 89 | + |
| 90 | + <dependency> |
| 91 | + <groupId>org.projectlombok</groupId> |
| 92 | + <artifactId>lombok</artifactId> |
| 93 | + <version>${lombok-version}</version> |
| 94 | + <optional>true</optional> |
| 95 | + <scope>provided</scope> |
| 96 | + </dependency> |
| 97 | + |
| 98 | + <dependency> |
| 99 | + <groupId>com.litongjava</groupId> |
| 100 | + <artifactId>hotswap-classloader</artifactId> |
| 101 | + <version>${hotswap-classloader.version}</version> |
| 102 | + </dependency> |
| 103 | + |
| 104 | + <dependency> |
| 105 | + <groupId>com.squareup.okhttp3</groupId> |
| 106 | + <artifactId>okhttp</artifactId> |
| 107 | + <version>3.11.0</version> |
| 108 | + </dependency> |
| 109 | + |
| 110 | + <dependency> |
| 111 | + <groupId>com.alibaba.fastjson2</groupId> |
| 112 | + <artifactId>fastjson2</artifactId> |
| 113 | + <version>2.0.12</version> |
| 114 | + </dependency> |
| 115 | + |
| 116 | + <dependency> |
| 117 | + <groupId>junit</groupId> |
| 118 | + <artifactId>junit</artifactId> |
| 119 | + <version>4.12</version> |
| 120 | + <scope>test</scope> |
| 121 | + </dependency> |
| 122 | + |
| 123 | + |
| 124 | + </dependencies> |
| 125 | + <profiles> |
| 126 | + <!-- development --> |
| 127 | + <profile> |
| 128 | + <id>development</id> |
| 129 | + <activation> |
| 130 | + <activeByDefault>true</activeByDefault> |
| 131 | + </activation> |
| 132 | + <build> |
| 133 | + <plugins> |
| 134 | + <plugin> |
| 135 | + <groupId>org.springframework.boot</groupId> |
| 136 | + <artifactId>spring-boot-maven-plugin</artifactId> |
| 137 | + <version>2.7.4</version> |
| 138 | + <configuration> |
| 139 | + <fork>true</fork> |
| 140 | + <mainClass>${main.class}</mainClass> |
| 141 | + <excludeGroupIds>org.projectlombok</excludeGroupIds> |
| 142 | + <arguments> |
| 143 | + <argument>--mode=dev</argument> |
| 144 | + </arguments> |
| 145 | + </configuration> |
| 146 | + </plugin> |
| 147 | + </plugins> |
| 148 | + </build> |
| 149 | + </profile> |
| 150 | + |
| 151 | + <!-- production --> |
| 152 | + <profile> |
| 153 | + <id>production</id> |
| 154 | + <build> |
| 155 | + <plugins> |
| 156 | + <plugin> |
| 157 | + <groupId>org.springframework.boot</groupId> |
| 158 | + <artifactId>spring-boot-maven-plugin</artifactId> |
| 159 | + <version>2.7.4</version> |
| 160 | + <configuration> |
| 161 | + <mainClass>${main.class}</mainClass> |
| 162 | + <excludeGroupIds>org.projectlombok</excludeGroupIds> |
| 163 | + </configuration> |
| 164 | + <executions> |
| 165 | + <execution> |
| 166 | + <goals> |
| 167 | + <goal>repackage</goal> |
| 168 | + </goals> |
| 169 | + </execution> |
| 170 | + </executions> |
| 171 | + </plugin> |
| 172 | + </plugins> |
| 173 | + </build> |
| 174 | + </profile> |
| 175 | + </profiles> |
| 176 | +</project> |
| 177 | +``` |
| 178 | + |
| 179 | +### HelloApp |
| 180 | + |
| 181 | +```java |
| 182 | +package com.litongjava.tio.chatgpt; |
| 183 | + |
| 184 | +import com.litongjava.jfinal.aop.annotation.AComponentScan; |
| 185 | +import com.litongjava.tio.boot.TioApplication; |
| 186 | + |
| 187 | +@AComponentScan |
| 188 | +public class HelloApp { |
| 189 | + public static void main(String[] args) { |
| 190 | + long start = System.currentTimeMillis(); |
| 191 | + TioApplication.run(HelloApp.class, args); |
| 192 | + long end = System.currentTimeMillis(); |
| 193 | + System.out.println((end - start) + "ms"); |
| 194 | + } |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +### HttpServerRequestHanlderConfig |
| 199 | + |
| 200 | +```java |
| 201 | +package com.litongjava.tio.chatgpt.config; |
| 202 | + |
| 203 | +import com.litongjava.jfinal.aop.Aop; |
| 204 | +import com.litongjava.jfinal.aop.annotation.AInitialization; |
| 205 | +import com.litongjava.jfinal.aop.annotation.BeforeStartConfiguration; |
| 206 | +import com.litongjava.tio.boot.server.TioBootServer; |
| 207 | +import com.litongjava.tio.chatgpt.handler.OpenaiV1ChatHandler; |
| 208 | +import com.litongjava.tio.http.server.router.SimpleHttpRoutes; |
| 209 | + |
| 210 | +@BeforeStartConfiguration |
| 211 | +public class HttpServerRequestHanlderConfig { |
| 212 | + |
| 213 | + @AInitialization |
| 214 | + public void httpRoutes() { |
| 215 | + |
| 216 | + // 创建simpleHttpRoutes |
| 217 | + SimpleHttpRoutes simpleHttpRoutes = new SimpleHttpRoutes(); |
| 218 | + // 创建controller |
| 219 | + OpenaiV1ChatHandler openaiV1ChatCompletionsHandler = Aop.get(OpenaiV1ChatHandler.class); |
| 220 | + |
| 221 | + // 添加action |
| 222 | + simpleHttpRoutes.add("/openai/v1/chat/completions", openaiV1ChatCompletionsHandler::completions); |
| 223 | + |
| 224 | + // 将simpleHttpRoutes添加到TioBootServer |
| 225 | + TioBootServer.me().setHttpRoutes(simpleHttpRoutes); |
| 226 | + } |
| 227 | + |
| 228 | +} |
| 229 | +``` |
| 230 | + |
| 231 | +### ChatgptMessage |
| 232 | + |
| 233 | +```java |
| 234 | +package com.litongjava.tio.chatgpt.handler; |
| 235 | + |
| 236 | +import lombok.AllArgsConstructor; |
| 237 | +import lombok.Data; |
| 238 | +import lombok.NoArgsConstructor; |
| 239 | + |
| 240 | +@Data |
| 241 | +@NoArgsConstructor |
| 242 | +@AllArgsConstructor |
| 243 | +public class ChatgptMessage { |
| 244 | + // system,assistant,user |
| 245 | + private String role, content; |
| 246 | +} |
| 247 | +``` |
| 248 | + |
| 249 | +### CompletionsModel |
| 250 | + |
| 251 | +```java |
| 252 | +package com.litongjava.tio.chatgpt.handler; |
| 253 | + |
| 254 | +import java.util.List; |
| 255 | + |
| 256 | +import lombok.AllArgsConstructor; |
| 257 | +import lombok.Data; |
| 258 | +import lombok.NoArgsConstructor; |
| 259 | + |
| 260 | +@NoArgsConstructor |
| 261 | +@AllArgsConstructor |
| 262 | +@Data |
| 263 | +public class CompletionsModel { |
| 264 | + private String model; |
| 265 | + private List<ChatgptMessage> messages; |
| 266 | + private boolean stream; |
| 267 | +} |
| 268 | +``` |
| 269 | + |
| 270 | +### OpenaiV1ChatHandler |
| 271 | + |
| 272 | +```java |
| 273 | +package com.litongjava.tio.chatgpt.handler; |
| 274 | + |
| 275 | +import java.io.BufferedReader; |
| 276 | +import java.io.IOException; |
| 277 | +import java.io.InputStreamReader; |
| 278 | +import java.util.ArrayList; |
| 279 | +import java.util.List; |
| 280 | + |
| 281 | +import com.litongjava.jfinal.aop.Aop; |
| 282 | +import com.litongjava.tio.core.ChannelContext; |
| 283 | +import com.litongjava.tio.core.Tio; |
| 284 | +import com.litongjava.tio.http.common.HttpRequest; |
| 285 | +import com.litongjava.tio.http.common.HttpResponse; |
| 286 | +import com.litongjava.tio.http.server.sse.SseBytesPacket; |
| 287 | +import com.litongjava.tio.utils.environment.EnvironmentUtils; |
| 288 | +import com.litongjava.tio.utils.json.Json; |
| 289 | + |
| 290 | +import okhttp3.MediaType; |
| 291 | +import okhttp3.OkHttpClient; |
| 292 | +import okhttp3.Request; |
| 293 | +import okhttp3.RequestBody; |
| 294 | +import okhttp3.Response; |
| 295 | +import okhttp3.ResponseBody; |
| 296 | + |
| 297 | +public class OpenaiV1ChatHandler { |
| 298 | + |
| 299 | + public HttpResponse completions(HttpRequest httpRequest) throws IOException { |
| 300 | + ChannelContext channelContext = httpRequest.getChannelContext(); |
| 301 | + |
| 302 | + // 设置sse请求头 |
| 303 | + HttpResponse httpResponse = new HttpResponse(httpRequest).setServerSentEventsHeader(); |
| 304 | + |
| 305 | + // 发送http响应包,告诉客户端保持连接 |
| 306 | + Tio.send(channelContext, httpResponse); |
| 307 | + |
| 308 | + // 处理数据 |
| 309 | + processData(channelContext); |
| 310 | + |
| 311 | + // 告诉处理器不要将消息发送给客户端 |
| 312 | + return new HttpResponse().setSend(false); |
| 313 | + } |
| 314 | + |
| 315 | + private void processData(ChannelContext channelContext) throws IOException { |
| 316 | + |
| 317 | + String apiKey = EnvironmentUtils.get("OPENAI_API_KEY"); |
| 318 | + |
| 319 | + OkHttpClient client = Aop.get(OkHttpClient.class); |
| 320 | + |
| 321 | + List<ChatgptMessage> messages = new ArrayList<>(); |
| 322 | + messages.add(new ChatgptMessage("system", "你是一名经验丰富的软件开发工程师")); |
| 323 | + messages.add(new ChatgptMessage("user", "简述浏览器发送请求到收到响应的流程")); |
| 324 | + |
| 325 | + CompletionsModel completionsModel = new CompletionsModel(); |
| 326 | +// completionsModel.setModel("gpt-3.5-turbo"); |
| 327 | + completionsModel.setModel("gpt-4-turbo"); |
| 328 | + completionsModel.setMessages(messages); |
| 329 | + completionsModel.setStream(true); |
| 330 | + |
| 331 | + String url = "https://api.openai.com/v1/chat/completions"; |
| 332 | + MediaType mediaType = MediaType.parse("application/json"); |
| 333 | + String content = Json.getJson().toJson(completionsModel); |
| 334 | + |
| 335 | + RequestBody body = RequestBody.create(mediaType, content); |
| 336 | + |
| 337 | + Request request = new Request.Builder() // |
| 338 | + .url(url) // |
| 339 | + .method("POST", body) // |
| 340 | + .addHeader("Content-Type", "application/json") // |
| 341 | + .addHeader("Authorization", "Bearer " + apiKey) // |
| 342 | + .build(); |
| 343 | + Response response = client.newCall(request).execute(); |
| 344 | + |
| 345 | + ResponseBody responseBody = response.body(); |
| 346 | + if (responseBody != null) { |
| 347 | + // 转成BufferedReader是将字节转为字符 |
| 348 | + try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody.byteStream()))) { |
| 349 | + String line; |
| 350 | + while ((line = reader.readLine()) != null) { |
| 351 | + System.out.println(line); |
| 352 | + // byte[] bytes = (line + "\n\n").getBytes(); |
| 353 | + // 必须添加一个回车符号 |
| 354 | + byte[] bytes = (line + "\n").getBytes(); |
| 355 | + SseBytesPacket ssePacket = new SseBytesPacket(bytes); |
| 356 | + // 再次向客户端发送消息 |
| 357 | + Tio.send(channelContext, ssePacket); |
| 358 | + } |
| 359 | + } |
| 360 | + // |
| 361 | + } |
| 362 | + Tio.remove(channelContext, "remove sse"); |
| 363 | + } |
| 364 | +} |
| 365 | +``` |
0 commit comments