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