java-openai
本文最后更新于:1 年前
chatbox
在做一个项目,在springboot中集成openai的聊天机器人,需要通过openai-key调用。
OPENAI开源openai-java项目地址:https://github.com/TheoKanning/openai-java
注意:需要翻墙使用
使用openai-java
导入依赖
在springboot项目中引入相关依赖
<dependency>
<groupId>com.theokanning.openai-gpt3-java</groupId>
<artifactId>client</artifactId>
<version>0.12.0</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>adapter-rxjava2</artifactId>
<version>2.9.0</version>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson</artifactId>
<version>2.9.0</version>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
</exclusions>
</dependency>
准备好openai-key,官网https://platform.openai.com/account/api-keys
使用openai-gpt3-java
代码如下
public GlobalResult chat(@RequestBody JSONObject jsonObject) {
String message = jsonObject.getString("message");
if (StringUtils.isBlank(message)) {
throw new IllegalArgumentException("参数异常!");
}
ChatMessage chatMessage = new ChatMessage("user", message);
List<ChatMessage> list = new ArrayList<>(4);
list.add(chatMessage);
OpenAiService service = new OpenAiService(token, Duration.ofSeconds(180));
ChatCompletionRequest completionRequest = ChatCompletionRequest.builder()
.model("gpt-3.5-turbo")
.stream(true)
.messages(list)
.build();
service.streamChatCompletion(completionRequest).doOnError(Throwable::printStackTrace)
.blockingForEach(chunk -> {
String text = chunk.getChoices().get(0).getMessage().getContent();
if (text == null) {
return;
}
System.out.print(text);
});
service.shutdownExecutor();
return GlobalResultGenerator.genSuccessResult();
}
但是上述代码出现错误,原因是连接超时,应该就是代理出现了问题,虽然我开启了全局系统代理也没法解决。
解决方法如下,添加请求代理的配置
@Test
public void testOpenAi() {
ObjectMapper mapper = defaultObjectMapper();
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 7890));
OkHttpClient client = defaultClient(token,Duration.ofSeconds(10000))
.newBuilder()
.proxy(proxy)
.build();
Retrofit retrofit = defaultRetrofit(client, mapper);
OpenAiApi api = retrofit.create(OpenAiApi.class);
String message = "hello!";
if (StringUtils.isBlank(message)) {
throw new IllegalArgumentException("参数异常!");
}
ChatMessage chatMessage = new ChatMessage("user", message);
List<ChatMessage> list = new ArrayList<>(4);
list.add(chatMessage);
OpenAiService service = new OpenAiService(api);
// CompletionRequest completionRequest = CompletionRequest.builder()
// .model("gpt-3.5-turbo")
// .prompt("你知道java中的泛型是什么吗")
// .temperature(0.5)
// .maxTokens(2048)
// .topP(1D)
// .build();
ChatCompletionRequest completionRequest = ChatCompletionRequest.builder()
.model("gpt-3.5-turbo")
.stream(true)
.messages(list)
.build();
// service.createChatCompletion(completionRequest).getChoices().forEach(System.out::println);
service.streamChatCompletion(completionRequest).doOnError(Throwable::printStackTrace)
.blockingForEach(chunk -> {
String text = chunk.getChoices().get(0).getMessage().getContent();
if (text == null) {
return;
}
System.out.print(text);
// sseService.send(user.getIdUser(), text);
});
}
添加代理后,就能正常使用openai的api了
实现每个用户独占一个对话框
在实现对话之前,需要了解一种技术SSE(Server-Sent Events),是一种基于 HTTP 的服务器推送技术,它可以让服务端向客户端实时发送数据,而不需要客户端不断地发起请求。
在 Spring Boot 中,可以使用 SseEmitter 类来实现 SSE。SseEmitter 类是 Spring 框架提供的用于将服务器端数据异步地发送到客户端的类。
实现sse功能的代码
后端代码
服务层
@Slf4j
@Service
public class SseServiceImpl implements SseService {
private static final Map<Long, SseEmitter> sessionMap = new ConcurrentHashMap<>();
/**
* 该方法用于建立SSE连接
* @param idUser
* @return
*/
@Override
public SseEmitter connect(Long idUser) {
if (existsUser(idUser)) {
removeUser(idUser);
}
// 0L表示永不超时
SseEmitter sseEmitter = new SseEmitter(0L);
sseEmitter.onError((err) -> {
log.error("type: SseSession Error, msg: {} session Id : {}", err.getMessage(), idUser);
onError(idUser, err);
});
sseEmitter.onTimeout(() -> {
log.info("type: SseSession Timeout, session Id : {}", idUser);
removeUser(idUser);
});
sseEmitter.onCompletion(() -> {
log.info("type: SseSession Completion, session Id : {}", idUser);
removeUser(idUser);
});
addUser(idUser, sseEmitter);
log.info("type: SseSession Connect, session Id : {}", idUser);
return sseEmitter;
}
/**
* 用于向指定用户发送消息
* 首先,该方法会检查用户是否存在于sessionMap中。
* 如果存在,则调用相应用户的SseEmitter对象的send方法发送消息内容;
* 如果不存在,则抛出IllegalArgumentException异常。
* @param idUser
* @param content
* @return
*/
@Override
public boolean send(Long idUser, String content) {
if (existsUser(idUser)) {
try {
sendMessage(idUser, content);
return true;
} catch (IOException exception) {
log.error("type: SseSession send Error:IOException, msg: {} session Id : {}", exception.getMessage(), idUser);
}
} else {
throw new IllegalArgumentException("User Id " + idUser + " not Found");
}
return false;
}
/**
* 该方法用于关闭指定用户的SSE连接
*
* @param idUser
*/
@Override
public void close(Long idUser) {
log.info("type: SseSession Close, session Id : {}", idUser);
removeUser(idUser);
}
private void addUser(Long idUser, SseEmitter sseEmitter) {
sessionMap.put(idUser, sseEmitter);
}
/**
* 该方法用于处理SSE连接出现错误的情况
* @param sessionKey
* @param throwable
*/
private void onError(Long sessionKey, Throwable throwable) {
SseEmitter sseEmitter = sessionMap.get(sessionKey);
if (sseEmitter != null) {
sseEmitter.completeWithError(throwable);
}
}
/**
* 移除会话中的用户
* @param idUser
*/
private void removeUser(Long idUser) {
sessionMap.remove(idUser);
}
private boolean existsUser(Long idUser) {
return sessionMap.containsKey(idUser);
}
/**
* 向用户发送消息
* @param idUser
* @param content
* @throws IOException
*/
private void sendMessage(Long idUser, String content) throws IOException {
sessionMap.get(idUser).send(content);
log.info("send to {} : {}", idUser, content);
}
}
web层
比较简单,首先要建立连接,把订阅放到容器中,服务端才能知道向哪里发送消息
需要注意的是,使用SseEmitter我们不再需要在@GetMapping上写produces = “text/event-stream; charset=utf-8”
@RestController
@RequestMapping("/api/v1/sse")
public class SeeController {
@Resource
private SseService sseService;
@GetMapping(value = "/subscribe/{idUser}", produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
public SseEmitter subscribe(@PathVariable Long idUser) {
return sseService.connect(idUser);
}
@GetMapping(value = "/close/{idUser}")
public void close(@PathVariable Long idUser) {
sseService.close(idUser);
}
@GetMapping("/send")
public String sendMessage() {
sseService.send(9L, "hello world!");
return "success";
}
}
前端代码
SSE的基本特性:
- HTML5中的协议,是基于纯文本的简单协议;
- 在游览器端可供JavaScript使用的EventSource对象
EventSource提供了三个标准事件,同时默认支持断线重连
事件 | 描述 |
---|---|
onopen | 当成功与服务器建立连接时产生 |
onmessage | 当收到服务器发来的消息时发生 |
onerror | 当出现错误时发生 |
传输的数据有格式上的要求,必须为 [data:…\n…\n]或者是[retry:10\n], 但是使用SseEmitter时不用使用该格式
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SseEmitter</title>
</head>
<body>
<div>sse测试</div>
<div id="result"></div>
</body>
</html>
<script>
var source = new EventSource('http://localhost:8099/api/v1/sse/subscribe/9');
source.onmessage = function (event) {
text = document.getElementById('result').innerText;
text += '\n' + event.data;
document.getElementById('result').innerText = text;
};
// <!-- 添加一个开启回调 -->
source.onopen = function (event) {
text = document.getElementById('result').innerText;
text += '\n 开启: ';
console.log(event);
document.getElementById('result').innerText = text;
};
</script>
测试
首先访问html页面,访问后建立连接,之后访问'http://localhost:8099/api/v1/sse/send
,向前端发送数据
从前端页面现实的数据与后端日志记录可以看到已经成功完成了,由服务端向客户端推送消息的功能。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!