]> git.basschouten.com Git - openhab-addons.git/blob
e39b1bc11908538e4aedfaa80e0b0619fc46fe18
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.chatgpt.internal;
14
15 import static org.openhab.binding.chatgpt.internal.ChatGPTBindingConstants.*;
16
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;
22
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;
45
46 import com.google.gson.Gson;
47 import com.google.gson.JsonArray;
48 import com.google.gson.JsonElement;
49 import com.google.gson.JsonObject;
50
51 /**
52  * The {@link ChatGPTHandler} is responsible for handling commands, which are
53  * sent to one of the channels.
54  *
55  * @author Kai Kreuzer - Initial contribution
56  */
57 @NonNullByDefault
58 public class ChatGPTHandler extends BaseThingHandler {
59
60     private final Logger logger = LoggerFactory.getLogger(ChatGPTHandler.class);
61
62     private HttpClient httpClient;
63     private Gson gson = new Gson();
64
65     private String apiKey = "";
66     private String lastPrompt = "";
67
68     private List<String> models = List.of();
69
70     public ChatGPTHandler(Thing thing, HttpClientFactory httpClientFactory) {
71         super(thing);
72         this.httpClient = httpClientFactory.getCommonHttpClient();
73     }
74
75     @Override
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);
80         }
81
82         if (command instanceof StringType stringCommand) {
83             lastPrompt = stringCommand.toFullString();
84             String response = sendPrompt(channelUID, lastPrompt);
85             processChatResponse(channelUID, response);
86         }
87     }
88
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));
95             } else {
96                 logger.warn("Didn't receive any response from ChatGPT - this is unexpected.");
97             }
98         }
99     }
100
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());
105             return null;
106         }
107         ChatGPTChannelConfiguration channelConfig = channel.getConfiguration().as(ChatGPTChannelConfiguration.class);
108
109         JsonObject root = new JsonObject();
110         root.addProperty("temperature", channelConfig.temperature);
111         root.addProperty("model", channelConfig.model);
112         root.addProperty("max_tokens", channelConfig.maxTokens);
113
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);
124
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);
130         try {
131             ContentResponse response = request.send();
132             updateStatus(ThingStatus.ONLINE);
133             if (response.getStatus() == HttpStatus.OK_200) {
134                 return response.getContentAsString();
135             } else {
136                 logger.error("ChatGPT request resulted in HTTP {} with message: {}", response.getStatus(),
137                         response.getReason());
138                 return null;
139             }
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);
144             return null;
145         }
146     }
147
148     @Override
149     public void initialize() {
150         ChatGPTConfiguration config = getConfigAs(ChatGPTConfiguration.class);
151
152         String apiKey = config.apiKey;
153
154         if (apiKey.isBlank()) {
155             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
156                     "@text/offline.configuration-error");
157             return;
158         }
159
160         this.apiKey = apiKey;
161         updateStatus(ThingStatus.UNKNOWN);
162
163         scheduler.execute(() -> {
164             try {
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");
173
174                         List<String> modelIds = new ArrayList<>();
175                         for (JsonElement element : data) {
176                             JsonObject model = element.getAsJsonObject();
177                             String id = model.get("id").getAsString();
178                             modelIds.add(id);
179                         }
180                         this.models = List.copyOf(modelIds);
181                     } else {
182                         logger.warn("Did not receive a valid JSON response from the models endpoint.");
183                     }
184                 } else {
185                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
186                             "@text/offline.communication-error");
187                 }
188             } catch (InterruptedException | ExecutionException | TimeoutException e) {
189                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
190             }
191         });
192     }
193
194     List<String> getModels() {
195         return models;
196     }
197
198     @Override
199     public Collection<Class<? extends ThingHandlerService>> getServices() {
200         return List.of(ChatGPTModelOptionProvider.class);
201     }
202 }