2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.chatgpt.internal;
15 import static org.openhab.binding.chatgpt.internal.ChatGPTBindingConstants.*;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.List;
20 import java.util.concurrent.ExecutionException;
21 import java.util.concurrent.TimeoutException;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.eclipse.jetty.client.HttpClient;
26 import org.eclipse.jetty.client.api.ContentResponse;
27 import org.eclipse.jetty.client.api.Request;
28 import org.eclipse.jetty.client.util.StringContentProvider;
29 import org.eclipse.jetty.http.HttpMethod;
30 import org.eclipse.jetty.http.HttpStatus;
31 import org.openhab.binding.chatgpt.internal.dto.ChatResponse;
32 import org.openhab.core.io.net.http.HttpClientFactory;
33 import org.openhab.core.library.types.StringType;
34 import org.openhab.core.thing.Channel;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.binding.BaseThingHandler;
40 import org.openhab.core.thing.binding.ThingHandlerService;
41 import org.openhab.core.types.Command;
42 import org.openhab.core.types.RefreshType;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
46 import com.google.gson.Gson;
47 import com.google.gson.JsonArray;
48 import com.google.gson.JsonElement;
49 import com.google.gson.JsonObject;
52 * The {@link ChatGPTHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author Kai Kreuzer - Initial contribution
58 public class ChatGPTHandler extends BaseThingHandler {
60 private final Logger logger = LoggerFactory.getLogger(ChatGPTHandler.class);
62 private HttpClient httpClient;
63 private Gson gson = new Gson();
65 private String apiKey = "";
66 private String lastPrompt = "";
68 private List<String> models = List.of();
70 public ChatGPTHandler(Thing thing, HttpClientFactory httpClientFactory) {
72 this.httpClient = httpClientFactory.getCommonHttpClient();
76 public void handleCommand(ChannelUID channelUID, Command command) {
77 if (command instanceof RefreshType && !"".equals(lastPrompt)) {
78 String response = sendPrompt(channelUID, lastPrompt);
79 processChatResponse(channelUID, response);
82 if (command instanceof StringType stringCommand) {
83 lastPrompt = stringCommand.toFullString();
84 String response = sendPrompt(channelUID, lastPrompt);
85 processChatResponse(channelUID, response);
89 private void processChatResponse(ChannelUID channelUID, @Nullable String response) {
90 if (response != null) {
91 ChatResponse chatResponse = gson.fromJson(response, ChatResponse.class);
92 if (chatResponse != null) {
93 String msg = chatResponse.getChoices().get(0).getMessage().getContent();
94 updateState(channelUID, new StringType(msg));
96 logger.warn("Didn't receive any response from ChatGPT - this is unexpected.");
101 private @Nullable String sendPrompt(ChannelUID channelUID, String prompt) {
102 Channel channel = getThing().getChannel(channelUID);
103 if (channel == null) {
104 logger.error("Channel with UID '{}' cannot be found on Thing '{}'.", channelUID, getThing().getUID());
107 ChatGPTChannelConfiguration channelConfig = channel.getConfiguration().as(ChatGPTChannelConfiguration.class);
109 JsonObject root = new JsonObject();
110 root.addProperty("temperature", channelConfig.temperature);
111 root.addProperty("model", channelConfig.model);
112 root.addProperty("max_tokens", channelConfig.maxTokens);
114 JsonObject systemMessage = new JsonObject();
115 systemMessage.addProperty("role", "system");
116 systemMessage.addProperty("content", channelConfig.systemMessage);
117 JsonObject userMessage = new JsonObject();
118 userMessage.addProperty("role", "user");
119 userMessage.addProperty("content", prompt);
120 JsonArray messages = new JsonArray(2);
121 messages.add(systemMessage);
122 messages.add(userMessage);
123 root.add("messages", messages);
125 String queryJson = gson.toJson(root);
126 Request request = httpClient.newRequest(OPENAI_API_URL).method(HttpMethod.POST)
127 .header("Content-Type", "application/json").header("Authorization", "Bearer " + apiKey)
128 .content(new StringContentProvider(queryJson));
129 logger.trace("Query '{}'", queryJson);
131 ContentResponse response = request.send();
132 updateStatus(ThingStatus.ONLINE);
133 if (response.getStatus() == HttpStatus.OK_200) {
134 return response.getContentAsString();
136 logger.error("ChatGPT request resulted in HTTP {} with message: {}", response.getStatus(),
137 response.getReason());
140 } catch (InterruptedException | TimeoutException | ExecutionException e) {
141 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
142 "Could not connect to OpenAI API: " + e.getMessage());
143 logger.debug("Request to OpenAI failed: {}", e.getMessage(), e);
149 public void initialize() {
150 ChatGPTConfiguration config = getConfigAs(ChatGPTConfiguration.class);
152 String apiKey = config.apiKey;
154 if (apiKey.isBlank()) {
155 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
156 "@text/offline.configuration-error");
160 this.apiKey = apiKey;
161 updateStatus(ThingStatus.UNKNOWN);
163 scheduler.execute(() -> {
165 Request request = httpClient.newRequest(OPENAI_MODELS_URL).method(HttpMethod.GET)
166 .header("Authorization", "Bearer " + apiKey);
167 ContentResponse response = request.send();
168 if (response.getStatus() == 200) {
169 updateStatus(ThingStatus.ONLINE);
170 JsonObject jsonObject = gson.fromJson(response.getContentAsString(), JsonObject.class);
171 if (jsonObject != null) {
172 JsonArray data = jsonObject.getAsJsonArray("data");
174 List<String> modelIds = new ArrayList<>();
175 for (JsonElement element : data) {
176 JsonObject model = element.getAsJsonObject();
177 String id = model.get("id").getAsString();
180 this.models = List.copyOf(modelIds);
182 logger.warn("Did not receive a valid JSON response from the models endpoint.");
185 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
186 "@text/offline.communication-error");
188 } catch (InterruptedException | ExecutionException | TimeoutException e) {
189 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
194 List<String> getModels() {
199 public Collection<Class<? extends ThingHandlerService>> getServices() {
200 return List.of(ChatGPTModelOptionProvider.class);