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

image-20230627195547417

使用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了

image-20230627200620750

实现每个用户独占一个对话框

在实现对话之前,需要了解一种技术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,向前端发送数据

image-20230701163145645

image-20230701163024619

从前端页面现实的数据与后端日志记录可以看到已经成功完成了,由服务端向客户端推送消息的功能。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!

 目录

Copyright © 2020 my blog
载入天数... 载入时分秒...