]> git.basschouten.com Git - openhab-addons.git/blob
f5bae70ee806e778ede72e7c1f2378e30015ff08
[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.TimeUnit;
22 import java.util.concurrent.TimeoutException;
23
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.eclipse.jetty.client.api.ContentResponse;
28 import org.eclipse.jetty.client.api.Request;
29 import org.eclipse.jetty.client.util.StringContentProvider;
30 import org.eclipse.jetty.http.HttpMethod;
31 import org.eclipse.jetty.http.HttpStatus;
32 import org.openhab.binding.chatgpt.internal.dto.ChatResponse;
33 import org.openhab.core.io.net.http.HttpClientFactory;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.Channel;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.binding.BaseThingHandler;
41 import org.openhab.core.thing.binding.ThingHandlerService;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 import com.google.gson.Gson;
48 import com.google.gson.JsonArray;
49 import com.google.gson.JsonElement;
50 import com.google.gson.JsonObject;
51
52 /**
53  * The {@link ChatGPTHandler} is responsible for handling commands, which are
54  * sent to one of the channels.
55  *
56  * @author Kai Kreuzer - Initial contribution
57  */
58 @NonNullByDefault
59 public class ChatGPTHandler extends BaseThingHandler {
60
61     private static final int REQUEST_TIMEOUT_MS = 10_000;
62     private final Logger logger = LoggerFactory.getLogger(ChatGPTHandler.class);
63
64     private HttpClient httpClient;
65     private Gson gson = new Gson();
66
67     private String apiKey = "";
68     private String apiUrl = "";
69     private String modelUrl = "";
70
71     private String lastPrompt = "";
72
73     private List<String> models = List.of();
74
75     public ChatGPTHandler(Thing thing, HttpClientFactory httpClientFactory) {
76         super(thing);
77         this.httpClient = httpClientFactory.getCommonHttpClient();
78     }
79
80     @Override
81     public void handleCommand(ChannelUID channelUID, Command command) {
82         if (command instanceof RefreshType && !"".equals(lastPrompt)) {
83             String response = sendPrompt(channelUID, lastPrompt);
84             processChatResponse(channelUID, response);
85         }
86
87         if (command instanceof StringType stringCommand) {
88             lastPrompt = stringCommand.toFullString();
89             String response = sendPrompt(channelUID, lastPrompt);
90             processChatResponse(channelUID, response);
91         }
92     }
93
94     private void processChatResponse(ChannelUID channelUID, @Nullable String response) {
95         if (response != null) {
96             ChatResponse chatResponse = gson.fromJson(response, ChatResponse.class);
97             if (chatResponse != null) {
98                 String msg = chatResponse.getChoices().get(0).getMessage().getContent();
99                 updateState(channelUID, new StringType(msg));
100             } else {
101                 logger.warn("Didn't receive any response from ChatGPT - this is unexpected.");
102             }
103         }
104     }
105
106     private @Nullable String sendPrompt(ChannelUID channelUID, String prompt) {
107         Channel channel = getThing().getChannel(channelUID);
108         if (channel == null) {
109             logger.error("Channel with UID '{}' cannot be found on Thing '{}'.", channelUID, getThing().getUID());
110             return null;
111         }
112         ChatGPTChannelConfiguration channelConfig = channel.getConfiguration().as(ChatGPTChannelConfiguration.class);
113
114         JsonObject root = new JsonObject();
115         root.addProperty("temperature", channelConfig.temperature);
116         root.addProperty("model", channelConfig.model);
117         root.addProperty("max_tokens", channelConfig.maxTokens);
118
119         JsonObject systemMessage = new JsonObject();
120         systemMessage.addProperty("role", "system");
121         systemMessage.addProperty("content", channelConfig.systemMessage);
122         JsonObject userMessage = new JsonObject();
123         userMessage.addProperty("role", "user");
124         userMessage.addProperty("content", prompt);
125         JsonArray messages = new JsonArray(2);
126         messages.add(systemMessage);
127         messages.add(userMessage);
128         root.add("messages", messages);
129
130         String queryJson = gson.toJson(root);
131         Request request = httpClient.newRequest(apiUrl).method(HttpMethod.POST)
132                 .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).header("Content-Type", "application/json")
133                 .header("Authorization", "Bearer " + apiKey).content(new StringContentProvider(queryJson));
134         logger.trace("Query '{}'", queryJson);
135         try {
136             ContentResponse response = request.send();
137             updateStatus(ThingStatus.ONLINE);
138             if (response.getStatus() == HttpStatus.OK_200) {
139                 return response.getContentAsString();
140             } else {
141                 logger.error("ChatGPT request resulted in HTTP {} with message: {}", response.getStatus(),
142                         response.getReason());
143                 return null;
144             }
145         } catch (InterruptedException | TimeoutException | ExecutionException e) {
146             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
147                     "Could not connect to OpenAI API: " + e.getMessage());
148             logger.debug("Request to OpenAI failed: {}", e.getMessage(), e);
149             return null;
150         }
151     }
152
153     @Override
154     public void initialize() {
155         ChatGPTConfiguration config = getConfigAs(ChatGPTConfiguration.class);
156
157         String apiKey = config.apiKey;
158
159         if (apiKey.isBlank()) {
160             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
161                     "@text/offline.configuration-error");
162             return;
163         }
164
165         this.apiKey = apiKey;
166         this.apiUrl = config.apiUrl;
167         this.modelUrl = config.modelUrl;
168
169         updateStatus(ThingStatus.UNKNOWN);
170
171         scheduler.execute(() -> {
172             try {
173                 Request request = httpClient.newRequest(modelUrl).timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
174                         .method(HttpMethod.GET).header("Authorization", "Bearer " + apiKey);
175                 ContentResponse response = request.send();
176                 if (response.getStatus() == 200) {
177                     updateStatus(ThingStatus.ONLINE);
178                     JsonObject jsonObject = gson.fromJson(response.getContentAsString(), JsonObject.class);
179                     if (jsonObject != null) {
180                         JsonArray data = jsonObject.getAsJsonArray("data");
181
182                         List<String> modelIds = new ArrayList<>();
183                         for (JsonElement element : data) {
184                             JsonObject model = element.getAsJsonObject();
185                             String id = model.get("id").getAsString();
186                             modelIds.add(id);
187                         }
188                         this.models = List.copyOf(modelIds);
189                     } else {
190                         logger.warn("Did not receive a valid JSON response from the models endpoint.");
191                     }
192                 } else {
193                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
194                             "@text/offline.communication-error");
195                 }
196             } catch (InterruptedException | ExecutionException | TimeoutException e) {
197                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
198             }
199         });
200     }
201
202     List<String> getModels() {
203         return models;
204     }
205
206     @Override
207     public Collection<Class<? extends ThingHandlerService>> getServices() {
208         return List.of(ChatGPTModelOptionProvider.class);
209     }
210 }