From 5929ef821a3e8fb1ff33670431d5cd69557b8eaf Mon Sep 17 00:00:00 2001 From: Artur-Fedjukevits <87441995+Artur-Fedjukevits@users.noreply.github.com> Date: Sat, 9 Nov 2024 23:08:42 +0100 Subject: [PATCH] [ChatGPT] Enhance binding (#17320) Signed-off-by: Artur-Fedjukevits --- bundles/org.openhab.binding.chatgpt/README.md | 71 ++- .../internal/ChatGPTChannelConfiguration.java | 8 +- .../internal/ChatGPTConfiguration.java | 8 + .../chatgpt/internal/ChatGPTHandler.java | 153 +++++-- .../chatgpt/internal/dto/ChatFunction.java | 78 ++++ .../internal/dto/ChatFunctionCall.java | 43 ++ .../chatgpt/internal/dto/ChatMessage.java | 104 +++++ .../chatgpt/internal/dto/ChatRequestBody.java | 101 +++++ .../chatgpt/internal/dto/ChatResponse.java | 101 ++++- .../chatgpt/internal/dto/ChatToolCalls.java | 50 ++ .../chatgpt/internal/dto/ChatTools.java | 44 ++ .../chatgpt/internal/dto/Parameters.java | 75 +++ .../chatgpt/internal/dto/ToolChoice.java | 32 ++ .../internal/dto/functions/ItemsControl.java | 52 +++ .../internal/hli/ChatGPTHLIConstants.java | 24 + .../internal/hli/ChatGPTHLIService.java | 427 ++++++++++++++++++ .../resources/OH-INF/thing/thing-types.xml | 99 +++- .../src/main/resources/json/tools.json | 27 ++ 18 files changed, 1406 insertions(+), 91 deletions(-) create mode 100644 bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatFunction.java create mode 100644 bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatFunctionCall.java create mode 100644 bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatMessage.java create mode 100644 bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatRequestBody.java create mode 100644 bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatToolCalls.java create mode 100644 bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatTools.java create mode 100644 bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/Parameters.java create mode 100644 bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ToolChoice.java create mode 100644 bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/functions/ItemsControl.java create mode 100644 bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/hli/ChatGPTHLIConstants.java create mode 100644 bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/hli/ChatGPTHLIService.java create mode 100644 bundles/org.openhab.binding.chatgpt/src/main/resources/json/tools.json diff --git a/bundles/org.openhab.binding.chatgpt/README.md b/bundles/org.openhab.binding.chatgpt/README.md index d7a5a6bcc1a..451b4c286ea 100644 --- a/bundles/org.openhab.binding.chatgpt/README.md +++ b/bundles/org.openhab.binding.chatgpt/README.md @@ -1,9 +1,16 @@ # ChatGPT Binding -The openHAB ChatGPT Binding allows openHAB to communicate with the ChatGPT language model provided by OpenAI. +The openHAB ChatGPT Binding allows openHAB to communicate with the ChatGPT language model provided by OpenAI and manage openHAB system via [Function calling](https://platform.openai.com/docs/guides/function-calling). -ChatGPT is a powerful natural language processing (NLP) tool that can be used to understand and respond to a wide range of text-based commands and questions. -With this binding, you can use ChatGPT to formulate proper sentences for any kind of information that you would like to output. +ChatGPT is a powerful natural language processing (NLP) tool that can be used to understand and respond to a wide range of text-based commands and questions. +With this binding, users can: + +- Control openHAB Devices: Manage lights, climate systems, media players, and more with natural language commands. +- Multi-language Support: Issue commands in almost any language, enhancing accessibility. +- Engage in Conversations: Have casual conversations, ask questions, and receive informative responses. +- Extended Capabilities: Utilize all other functionalities of ChatGPT, from composing creative content to answering complex questions. + +This integration significantly enhances user experience, providing seamless control over smart home environments and access to the full range of ChatGPT’s capabilities. ## Supported Things @@ -11,14 +18,22 @@ The binding supports a single thing type `account`, which corresponds to the Ope ## Thing Configuration -The `account` thing requires a single configuration parameter, which is the API key that allows accessing the account. +The `account` thing requires the API key that allows accessing the account. API keys can be created and managed under . -| Name | Type | Description | Default | Required | Advanced | -|-----------------|---------|-----------------------------------------------------------|--------------------------------------------|----------|----------| -| apiKey | text | The API key to be used for the requests | N/A | yes | no | -| apiUrl | text | The server API where to reach the AI service | https://api.openai.com/v1/chat/completions | no | yes | -| modelUrl | text | The model url where to retrieve the available models from | https://api.openai.com/v1/models | no | yes | +| Name | Type | Description | Default | Required | Advanced | +|------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------|----------|----------| +| apiKey | text | The API key to be used for the requests | N/A | yes | no | +| temperature | decimal | A value between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. | 0.5 | no | no | +| topP | decimal | A value between 0 and 1. An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both. | 1.0 | no | yes | +| apiUrl | text | The server API where to reach the AI service | https://api.openai.com/v1/chat/completions | no | yes | +| modelUrl | text | The model url where to retrieve the available models from | https://api.openai.com/v1/models | no | yes | +| model | text | The model to be used for the HLI service | gpt-4o-mini | no | yes | +| systemMessage | text | Here you need to describe your openHAB system that will help AI control your smart home. | N/A | if HLI | yes | +| maxTokens | decimal | The maximum number of tokens to generate in the completion. | 500 | no | yes | +| keepContext | decimal | How long should the HLI service retain context between requests (in minutes) | 2 | no | yes | +| contextThreshold | decimal | Limit total tokens included in context. | 10000 | no | yes | +| useSemanticModel | boolean | Use the semantic model to determine the location of an item. | true | no | yes | The advanced parameters `apiUrl` and `modelUrl` can be used, if any other ChatGPT-compatible service is used, e.g. a local installation of [LocalAI](https://github.com/go-skynet/LocalAI). @@ -33,32 +48,41 @@ It is possible to extend the thing with further channels of type `chat`, so that Each channel of type `chat` takes the following configuration parameters: -| Name | Type | Description | Default | Required | Advanced | -|-----------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|----------| -| model | text | The model to be used for the responses. | gpt-3.5-turbo | no | no | -| temperature | decimal | A value between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. | 0.5 | no | no | -| systemMessage | text | The system message helps set the behavior of the assistant. | N/A | no | no | -| maxTokens | decimal | The maximum number of tokens to generate in the completion. | 500 | no | yes | +| Name | Type | Description | Default | Required | Advanced | +|---------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------|----------| +| model | text | The model to be used for the responses. | gpt-4o | yes | no | +| systemMessage | text | The system message helps set the behavior of the assistant. | N/A | yes | no | +| temperature | decimal | A value between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. | 0.5 | no | yes | +| topP | decimal | A value between 0 and 1. An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both. | 1.0 | no | yes | +| maxTokens | decimal | The maximum number of tokens to generate in the completion. | 1000 | no | yes | + + +## Items Configuration + +Items to be used by the HLI service must be tagged with the [ "ChatGPT" ] tag. +If no semantic model is set up, you can set the parameter `useSemanticModel` to false. +In this case, the item names must follow the naming convention '_***', for example "Kitchen_Light". The label of the items are expected to briefly describe the item in more detail. ## Full Example ### Thing Configuration ```java -Thing chatgpt:account:1 [apiKey=""] { +Thing chatgpt:account:1 [ + apiKey="", + ] { Channels: Type chat : chat "Weather Advice" [ - model="gpt-3.5-turbo", + model="gpt-4o-mini", temperature="1.5", systemMessage="Answer briefly, in 2-3 sentences max. Behave like Eddie Murphy and give an advice for the day based on the following weather data:" ] Type chat : morningMessage "Morning Message" [ - model="gpt-3.5-turbo", + model="gpt-4o-mini", temperature="0.5", systemMessage="You are Marvin, a very depressed robot. You wish a good morning and tell the current time." - ] + ] } - ``` ### Item Configuration @@ -69,8 +93,14 @@ String Morning_Message { channel="chatgpt:account:1:morningMessage" } Number Temperature_Forecast_Low Number Temperature_Forecast_High +Dimmer Kitchen_Dimmer "Kitchen main light" [ "ChatGPT" ] ``` +### UI Configuration of the HLI Service + +To enable the HLI service, go to Settings -> Voice and choose "ChatGPT Human Language Interpreter". +A text-to-speech service must be configured. + ### Example Rules ```java @@ -106,3 +136,4 @@ and ``` The state updates can be used for a text-to-speech output and they will give your announcements at home a personal touch. + diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTChannelConfiguration.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTChannelConfiguration.java index 118ad7d9378..03db855d45f 100644 --- a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTChannelConfiguration.java +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTChannelConfiguration.java @@ -22,11 +22,13 @@ import org.eclipse.jdt.annotation.NonNullByDefault; @NonNullByDefault public class ChatGPTChannelConfiguration { - public String model = "gpt-3.5-turbo"; + public String model = "gpt-4o-mini"; - public float temperature = 0.5f; + public Double temperature = 0.5; + + public Double topP = 1.0; public String systemMessage = ""; - int maxTokens = 500; + public int maxTokens = 500; } diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTConfiguration.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTConfiguration.java index 90bfa1345e4..fd8a06b7e23 100644 --- a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTConfiguration.java +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTConfiguration.java @@ -25,4 +25,12 @@ public class ChatGPTConfiguration { public String apiKey = ""; public String apiUrl = "https://api.openai.com/v1/chat/completions"; public String modelUrl = "https://api.openai.com/v1/models"; + public boolean useSemanticModel = true; + public String model = "gpt-4o-mini"; + public Double temperature = 1.0; + public Integer maxTokens = 1000; + public Double topP = 1.0; + public String systemMessage = ""; + public Integer keepContext = 2; + public Integer contextThreshold = 10000; } diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTHandler.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTHandler.java index 19a5775ea42..595dddd7df1 100644 --- a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTHandler.java +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/ChatGPTHandler.java @@ -12,8 +12,6 @@ */ package org.openhab.binding.chatgpt.internal; -import static org.openhab.binding.chatgpt.internal.ChatGPTBindingConstants.*; - import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -29,7 +27,10 @@ import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.util.StringContentProvider; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; +import org.openhab.binding.chatgpt.internal.dto.ChatMessage; +import org.openhab.binding.chatgpt.internal.dto.ChatRequestBody; import org.openhab.binding.chatgpt.internal.dto.ChatResponse; +import org.openhab.binding.chatgpt.internal.hli.ChatGPTHLIService; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.Channel; @@ -40,20 +41,20 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.types.Command; -import org.openhab.core.types.RefreshType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.Gson; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; /** * The {@link ChatGPTHandler} is responsible for handling commands, which are * sent to one of the channels. * * @author Kai Kreuzer - Initial contribution + * @author Artur Fedjukevits - Replaced gson with jackson */ @NonNullByDefault public class ChatGPTHandler extends BaseThingHandler { @@ -62,14 +63,11 @@ public class ChatGPTHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(ChatGPTHandler.class); private HttpClient httpClient; - private Gson gson = new Gson(); - + private @Nullable ChatGPTConfiguration config; private String apiKey = ""; private String apiUrl = ""; private String modelUrl = ""; - private String lastPrompt = ""; - private List models = List.of(); public ChatGPTHandler(Thing thing, HttpClientFactory httpClientFactory) { @@ -79,55 +77,101 @@ public class ChatGPTHandler extends BaseThingHandler { @Override public void handleCommand(ChannelUID channelUID, Command command) { - if (command instanceof RefreshType && !"".equals(lastPrompt)) { - String response = sendPrompt(channelUID, lastPrompt); - processChatResponse(channelUID, response); - } if (command instanceof StringType stringCommand) { lastPrompt = stringCommand.toFullString(); - String response = sendPrompt(channelUID, lastPrompt); - processChatResponse(channelUID, response); + + String queryJson = prepareRequestBody(channelUID); + + if (queryJson != null) { + String response = sendPrompt(queryJson); + processChatResponse(channelUID, response); + } } } private void processChatResponse(ChannelUID channelUID, @Nullable String response) { if (response != null) { - ChatResponse chatResponse = gson.fromJson(response, ChatResponse.class); + + ObjectMapper objectMapper = new ObjectMapper(); + ChatResponse chatResponse; + try { + chatResponse = objectMapper.readValue(response, ChatResponse.class); + } catch (JsonProcessingException e) { + logger.error("Failed to parse ChatGPT response: {}", e.getMessage(), e); + return; + } + if (chatResponse != null) { - String msg = chatResponse.getChoices().get(0).getMessage().getContent(); - updateState(channelUID, new StringType(msg)); + + String finishReason = chatResponse.getChoices().get(0).getFinishReason(); + + if ("length".equals(finishReason)) { + logger.warn("Token length exceeded. Increase maximum token limit to avoid the issue."); + return; + } + + @Nullable + ChatMessage chatResponseMessage = chatResponse.getChoices().get(0).getChatMessage(); + + if (chatResponseMessage == null) { + logger.error("ChatGPT response does not contain a message."); + return; + } + + @Nullable + String msg = chatResponseMessage.getContent(); + if (msg != null) { + updateState(channelUID, new StringType(msg)); + } + } else { logger.warn("Didn't receive any response from ChatGPT - this is unexpected."); } } } - private @Nullable String sendPrompt(ChannelUID channelUID, String prompt) { + private @Nullable String prepareRequestBody(ChannelUID channelUID) { Channel channel = getThing().getChannel(channelUID); if (channel == null) { logger.error("Channel with UID '{}' cannot be found on Thing '{}'.", channelUID, getThing().getUID()); return null; } + ChatGPTChannelConfiguration channelConfig = channel.getConfiguration().as(ChatGPTChannelConfiguration.class); - JsonObject root = new JsonObject(); - root.addProperty("temperature", channelConfig.temperature); - root.addProperty("model", channelConfig.model); - root.addProperty("max_tokens", channelConfig.maxTokens); + List messages = new ArrayList<>(); - JsonObject systemMessage = new JsonObject(); - systemMessage.addProperty("role", "system"); - systemMessage.addProperty("content", channelConfig.systemMessage); - JsonObject userMessage = new JsonObject(); - userMessage.addProperty("role", "user"); - userMessage.addProperty("content", prompt); - JsonArray messages = new JsonArray(2); + ChatMessage systemMessage = new ChatMessage(); + systemMessage.setRole(ChatMessage.Role.SYSTEM.value()); + systemMessage.setContent(channelConfig.systemMessage); messages.add(systemMessage); - messages.add(userMessage); - root.add("messages", messages); - String queryJson = gson.toJson(root); + ChatMessage userMessage = new ChatMessage(); + userMessage.setRole(ChatMessage.Role.USER.value()); + userMessage.setContent(lastPrompt); + messages.add(userMessage); + + ChatRequestBody chatRequestBody = new ChatRequestBody(); + + chatRequestBody.setModel(channelConfig.model); + chatRequestBody.setTemperature(channelConfig.temperature); + chatRequestBody.setMaxTokens(channelConfig.maxTokens); + chatRequestBody.setTopP(channelConfig.topP); + chatRequestBody.setMessages(messages); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(Include.NON_NULL); + + try { + return objectMapper.writeValueAsString(chatRequestBody); + } catch (JsonProcessingException e) { + logger.error("Failed to serialize ChatGPT request: {}", e.getMessage(), e); + return null; + } + } + + public @Nullable String sendPrompt(String queryJson) { Request request = httpClient.newRequest(apiUrl).method(HttpMethod.POST) .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).header("Content-Type", "application/json") .header("Authorization", "Bearer " + apiKey).content(new StringContentProvider(queryJson)); @@ -150,9 +194,13 @@ public class ChatGPTHandler extends BaseThingHandler { } } + public @Nullable ChatGPTConfiguration getConfigAs() { + return this.config; + } + @Override public void initialize() { - ChatGPTConfiguration config = getConfigAs(ChatGPTConfiguration.class); + this.config = getConfigAs(ChatGPTConfiguration.class); String apiKey = config.apiKey; @@ -175,19 +223,28 @@ public class ChatGPTHandler extends BaseThingHandler { ContentResponse response = request.send(); if (response.getStatus() == 200) { updateStatus(ThingStatus.ONLINE); - JsonObject jsonObject = gson.fromJson(response.getContentAsString(), JsonObject.class); - if (jsonObject != null) { - JsonArray data = jsonObject.getAsJsonArray("data"); + ObjectMapper objectMapper = new ObjectMapper(); + try { + JsonNode models = objectMapper.readTree(response.getContentAsString()); + JsonNode data = models.get("data"); - List modelIds = new ArrayList<>(); - for (JsonElement element : data) { - JsonObject model = element.getAsJsonObject(); - String id = model.get("id").getAsString(); - modelIds.add(id); + if (data != null) { + logger.debug("Models: {}", data.toString()); + List modelList = new ArrayList<>(); + data.forEach(model -> { + JsonNode id = model.get("id"); + if (id != null) { + modelList.add(id.asText()); + } + }); + + this.models = List.copyOf(modelList); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "@text/offline.communication-error"); } - this.models = List.copyOf(modelIds); - } else { - logger.warn("Did not receive a valid JSON response from the models endpoint."); + } catch (JsonProcessingException e) { + logger.warn("Failed to parse models: {}", e.getMessage(), e); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, @@ -205,6 +262,6 @@ public class ChatGPTHandler extends BaseThingHandler { @Override public Collection> getServices() { - return List.of(ChatGPTModelOptionProvider.class); + return List.of(ChatGPTModelOptionProvider.class, ChatGPTHLIService.class); } } diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatFunction.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatFunction.java new file mode 100644 index 00000000000..b6f585fff4d --- /dev/null +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatFunction.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.chatgpt.internal.dto; + +import java.util.function.Function; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ChatFunction { + + private String name; + private String description; + private Parameters parameters; + + @JsonIgnore + private Function executor; + + @JsonIgnore + private Class parametersClass; + + public ChatFunction() { + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public Parameters getParameters() { + return parameters; + } + + public Function getExecutor() { + return executor; + } + + public Class getParametersClass() { + return parametersClass; + } + + public void setName(String name) { + this.name = name; + } + + public void setDescription(String description) { + this.description = description; + } + + public void setExecutor(Function executor) { + this.executor = executor; + } + + public void setParameters(Parameters requestClass) { + this.parameters = requestClass; + } + + public void setParametersClass(Class parametersClass) { + this.parametersClass = parametersClass; + } +} diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatFunctionCall.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatFunctionCall.java new file mode 100644 index 00000000000..d5c7fbd1ef2 --- /dev/null +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatFunctionCall.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.chatgpt.internal.dto; + +import org.eclipse.jdt.annotation.Nullable; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ChatFunctionCall { + + private @Nullable String name; + private @Nullable String arguments; + + public String getName() { + return name; + } + + public String getArguments() { + return arguments; + } + + public void setName(String name) { + this.name = name; + } + + public void setArguments(String arguments) { + this.arguments = arguments; + } +} diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatMessage.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatMessage.java new file mode 100644 index 00000000000..ac539654197 --- /dev/null +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatMessage.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.chatgpt.internal.dto; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ChatMessage { + + public enum Role { + USER("user"), + ASSISTANT("assistant"), + SYSTEM("system"), + TOOL("tool"); + + private final String value; + + Role(String value) { + this.value = value; + } + + public String value() { + return value; + } + } + + private String role; + private String content; + + @JsonProperty("tool_call_id") + private String toolCallId; + + private String name; + + @JsonProperty("function_call") + ChatFunctionCall functionCall; + + @JsonProperty("tool_calls") + List toolCalls; + + public String getRole() { + return role; + } + + public String getContent() { + return content; + } + + public ChatFunctionCall getFunctionCall() { + return functionCall; + } + + public List getToolCalls() { + return toolCalls; + } + + public String getToolCallId() { + return toolCallId; + } + + public String getName() { + return name; + } + + public void setRole(String role) { + this.role = role; + } + + public void setContent(String content) { + this.content = content; + } + + public void setFunctionCall(ChatFunctionCall functionCall) { + this.functionCall = functionCall; + } + + public void setToolCalls(List toolCalls) { + this.toolCalls = toolCalls; + } + + public void setToolCallId(String toolCallId) { + this.toolCallId = toolCallId; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatRequestBody.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatRequestBody.java new file mode 100644 index 00000000000..d20819b9244 --- /dev/null +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatRequestBody.java @@ -0,0 +1,101 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.chatgpt.internal.dto; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ChatRequestBody { + + private String model; + private List messages; + private Double temperature; + @JsonProperty("top_p") + private Double topP; + @JsonProperty("max_tokens") + private Integer maxTokens; + private String user; + private List tools; + @JsonProperty("tool_choice") + private String toolChoice; + + public String getModel() { + return model; + } + + public List getMessages() { + return messages; + } + + public Double getTemperature() { + return temperature; + } + + public Double getTopP() { + return topP; + } + + public Integer getMaxTokens() { + return maxTokens; + } + + public String getUser() { + return user; + } + + public List getTools() { + return tools; + } + + public String getToolChoice() { + return toolChoice; + } + + public void setModel(String model) { + this.model = model; + } + + public void setMessages(List messages) { + this.messages = messages; + } + + public void setTemperature(Double temperature) { + this.temperature = temperature; + } + + public void setTopP(Double topP) { + this.topP = topP; + } + + public void setMaxTokens(Integer maxTokens) { + this.maxTokens = maxTokens; + } + + public void setUser(String user) { + this.user = user; + } + + public void setTools(List tools) { + this.tools = tools; + } + + public void setToolChoice(String toolChoice) { + this.toolChoice = toolChoice; + } +} diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatResponse.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatResponse.java index 59213732a86..a99735ec4cf 100644 --- a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatResponse.java +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatResponse.java @@ -14,14 +14,17 @@ package org.openhab.binding.chatgpt.internal.dto; import java.util.List; -import com.google.gson.annotations.SerializedName; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; /** * This is a dto used for parsing the JSON response from ChatGPT. * * @author Kai Kreuzer - Initial contribution + * @author Artur Fedjukevits - Added fields and edited the class * */ +@JsonIgnoreProperties(ignoreUnknown = true) public class ChatResponse { private List choices; @@ -29,6 +32,7 @@ public class ChatResponse { private String object; private int created; private String model; + private Usage usage; public List getChoices() { return choices; @@ -50,15 +54,46 @@ public class ChatResponse { return model; } - public static class Choice { - private Message message; + public Usage getUsage() { + return usage; + } - @SerializedName("finish_reason") + public void setChoices(List choices) { + this.choices = choices; + } + + public void setId(String id) { + this.id = id; + } + + public void setObject(String object) { + this.object = object; + } + + public void setCreated(int created) { + this.created = created; + } + + public void setModel(String model) { + this.model = model; + } + + public void setUsage(Usage usage) { + this.usage = usage; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Choice { + + @JsonProperty("message") + private ChatMessage chatMessage; + + @JsonProperty("finish_reason") private String finishReason; private int index; - public Message getMessage() { - return message; + public ChatMessage getChatMessage() { + return chatMessage; } public String getFinishReason() { @@ -68,18 +103,54 @@ public class ChatResponse { public int getIndex() { return index; } - } - public static class Message { - private String role; - private String content; - - public String getRole() { - return role; + public void setChatMessage(ChatMessage chatMessage) { + this.chatMessage = chatMessage; } - public String getContent() { - return content; + public void setFinishReason(String finishReason) { + this.finishReason = finishReason; + } + + public void setIndex(int index) { + this.index = index; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Usage { + + @JsonProperty("prompt_tokens") + private int promptTokens; + + @JsonProperty("completion_tokens") + private int completionTokens; + + @JsonProperty("total_tokens") + private int totalTokens; + + public int getPromptTokens() { + return promptTokens; + } + + public int getCompletionTokens() { + return completionTokens; + } + + public int getTotalTokens() { + return totalTokens; + } + + public void setPromptTokens(int promptTokens) { + this.promptTokens = promptTokens; + } + + public void setCompletionTokens(int completionTokens) { + this.completionTokens = completionTokens; + } + + public void setTotalTokens(int totalTokens) { + this.totalTokens = totalTokens; } } } diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatToolCalls.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatToolCalls.java new file mode 100644 index 00000000000..32ac2c652de --- /dev/null +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatToolCalls.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.chatgpt.internal.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ChatToolCalls { + + String id; + ChatFunctionCall function; + String type; + + public String getId() { + return id; + } + + public ChatFunctionCall getFunction() { + return function; + } + + public String getType() { + return type; + } + + public void setId(String id) { + this.id = id; + } + + public void setFunction(ChatFunctionCall function) { + this.function = function; + } + + public void setType(String type) { + this.type = type; + } +} diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatTools.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatTools.java new file mode 100644 index 00000000000..694c6d7d00b --- /dev/null +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ChatTools.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.chatgpt.internal.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ChatTools { + + private String type; + private ChatFunction function; + + public ChatTools() { + } + + public String getType() { + return type; + } + + public ChatFunction getFunction() { + return function; + } + + public void setType(String type) { + this.type = type; + } + + public void setFunction(ChatFunction function) { + this.function = function; + } +} diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/Parameters.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/Parameters.java new file mode 100644 index 00000000000..80c8cc9c774 --- /dev/null +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/Parameters.java @@ -0,0 +1,75 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.chatgpt.internal.dto; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Parameters { + + private String type; + private Map properties; + private List required; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Map getProperties() { + return properties; + } + + public void setProperties(Map properties) { + this.properties = properties; + } + + public List getRequired() { + return required; + } + + public void setRequired(List required) { + this.required = required; + } + + public static class Property { + + private String type; + private String description; + + public String getType() { + return type; + } + + public String getDescription() { + return description; + } + + public void setType(String type) { + this.type = type; + } + + public void setDescription(String description) { + this.description = description; + } + } +} diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ToolChoice.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ToolChoice.java new file mode 100644 index 00000000000..c7081e5ee88 --- /dev/null +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/ToolChoice.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.chatgpt.internal.dto; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +public enum ToolChoice { + + NONE("none"), + AUTO("auto"); + + private final String value; + + ToolChoice(String value) { + this.value = value; + } + + public String value() { + return value; + } +} diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/functions/ItemsControl.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/functions/ItemsControl.java new file mode 100644 index 00000000000..568c69a23da --- /dev/null +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/dto/functions/ItemsControl.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.chatgpt.internal.dto.functions; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +public class ItemsControl { + + @JsonProperty("name") + private String name; + @JsonProperty("type") + private String type; + @JsonProperty("state") + private String state; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getState() { + return state; + } + + public void setState(String state) { + this.state = state; + } +} diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/hli/ChatGPTHLIConstants.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/hli/ChatGPTHLIConstants.java new file mode 100644 index 00000000000..8dbf3bd4116 --- /dev/null +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/hli/ChatGPTHLIConstants.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.chatgpt.internal.hli; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Artur Fedjukevits - Initial contribution + */ +@NonNullByDefault +public class ChatGPTHLIConstants { + + public static final String SERVICE_ID = "chatgpthli"; +} diff --git a/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/hli/ChatGPTHLIService.java b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/hli/ChatGPTHLIService.java new file mode 100644 index 00000000000..8f4d03d3e2a --- /dev/null +++ b/bundles/org.openhab.binding.chatgpt/src/main/java/org/openhab/binding/chatgpt/internal/hli/ChatGPTHLIService.java @@ -0,0 +1,427 @@ +/** + * Copyright (c) 2010-2024 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.chatgpt.internal.hli; + +import static org.openhab.binding.chatgpt.internal.hli.ChatGPTHLIConstants.SERVICE_ID; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.chatgpt.internal.ChatGPTConfiguration; +import org.openhab.binding.chatgpt.internal.ChatGPTHandler; +import org.openhab.binding.chatgpt.internal.dto.ChatFunction; +import org.openhab.binding.chatgpt.internal.dto.ChatFunctionCall; +import org.openhab.binding.chatgpt.internal.dto.ChatMessage; +import org.openhab.binding.chatgpt.internal.dto.ChatRequestBody; +import org.openhab.binding.chatgpt.internal.dto.ChatResponse; +import org.openhab.binding.chatgpt.internal.dto.ChatToolCalls; +import org.openhab.binding.chatgpt.internal.dto.ChatTools; +import org.openhab.binding.chatgpt.internal.dto.ToolChoice; +import org.openhab.binding.chatgpt.internal.dto.functions.ItemsControl; +import org.openhab.core.events.EventPublisher; +import org.openhab.core.items.Item; +import org.openhab.core.items.ItemNotFoundException; +import org.openhab.core.items.ItemRegistry; +import org.openhab.core.items.events.ItemEventFactory; +import org.openhab.core.library.items.RollershutterItem; +import org.openhab.core.library.items.SwitchItem; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.UpDownType; +import org.openhab.core.model.script.actions.Semantics; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.CommandDescription; +import org.openhab.core.types.CommandOption; +import org.openhab.core.types.TypeParser; +import org.openhab.core.voice.text.HumanLanguageInterpreter; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * The {@link ChatGPTHLIService} is responsible for handling the human language interpretation using ChatGPT. + * + * @author Artur Fedjukevits - Initial contribution + */ +@Component(service = { ChatGPTHLIService.class, HumanLanguageInterpreter.class }) +@NonNullByDefault +public class ChatGPTHLIService implements ThingHandlerService, HumanLanguageInterpreter { + + private @Nullable ThingHandler thingHandler; + private List messages = new ArrayList<>(); + + private LocalTime lastMessageTime = LocalTime.now(); + private List tools = new ArrayList<>(); + private final Logger logger = LoggerFactory.getLogger(ChatGPTHLIService.class); + private final Map functions = new HashMap<>(); + private @Nullable ItemRegistry itemRegistry; + private @Nullable EventPublisher eventPublisher; + private @Nullable ChatGPTConfiguration config; + + @Activate + public ChatGPTHLIService(@Reference ItemRegistry itemRegistry, @Reference EventPublisher eventPublisher) { + this.itemRegistry = itemRegistry; + this.eventPublisher = eventPublisher; + + try (InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("/json/tools.json"); + InputStreamReader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) { + + ObjectMapper mapper = new ObjectMapper(); + JsonNode node = mapper.readTree(reader); + + try { + this.tools = Arrays.asList(mapper.treeToValue(node, ChatTools[].class)); + } catch (JsonProcessingException e) { + logger.debug("Error processing tools.json", e); + } + } catch (IOException e) { + logger.error("Error reading tools.json", e); + } + + for (ChatTools tool : tools) { + logger.debug("Loaded tool: {}", tool.getFunction().getName()); + } + + functions.clear(); + functions.putAll(tools.stream().collect(HashMap::new, (map, tool) -> { + ChatFunction function = tool.getFunction(); + String functionName = function.getName(); + + map.put(functionName, function); + }, HashMap::putAll)); + + ChatFunction itemControlFunction = functions.get("items_control"); + if (itemControlFunction != null) { + itemControlFunction.setParametersClass(ItemsControl.class); + + itemControlFunction.setExecutor(p -> { + ItemsControl parameters = (ItemsControl) p; + return sendCommand(parameters.getName(), parameters.getState()); + }); + } + logger.debug("ChatGPTHLIService activated"); + } + + @Override + public String getId() { + return SERVICE_ID; + } + + @Override + public Set getSupportedGrammarFormats() { + return Set.of(); + } + + @Override + public String interpret(Locale locale, String text) { + String requestBody = prepareRequestBody(text); + if (requestBody == null) { + return "Failed to prepare request body"; + } + + if (thingHandler instanceof ChatGPTHandler chatGPTHandler) { + String response = chatGPTHandler.sendPrompt(requestBody); + return processChatResponse(response); + } + return "Failed to interpret text"; + } + + @Override + public Set getSupportedLocales() { + return Set.of(); + } + + @Override + public String getLabel(@Nullable Locale locale) { + return "ChatGPT Human Language Interpreter"; + } + + @Override + @Nullable + public String getGrammar(Locale locale, String format) { + return "null"; + } + + @Override + public void setThingHandler(ThingHandler handler) { + this.thingHandler = handler; + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return thingHandler; + } + + @Override + public void activate() { + } + + private String processChatResponse(@Nullable String response) { + if (response == null || response.isEmpty()) { + return ""; + } + + logger.trace("Received response: {}", response); + + ObjectMapper objectMapper = new ObjectMapper(); + ChatResponse chatResponse; + try { + chatResponse = objectMapper.readValue(response, ChatResponse.class); + } catch (JsonProcessingException e) { + logger.debug("Failed to parse ChatGPT response: {}", e.getMessage(), e); + return ""; + } + + if (chatResponse == null) { + logger.warn("Didn't receive any response from ChatGPT - this is unexpected."); + return ""; + } + + this.lastMessageTime = LocalTime.now(); + + if (chatResponse.getUsage().getTotalTokens() > this.config.contextThreshold) { + + Integer lastUserMessageIndex = null; + for (int i = messages.size() - 1; i >= 0; i--) { + if (messages.get(i).getRole().equals(ChatMessage.Role.USER.value())) { + lastUserMessageIndex = i; + break; + } + } + + if (lastUserMessageIndex != null) { + messages.subList(1, lastUserMessageIndex).clear(); + messages.set(0, generateSystemMessage()); + } + } + + String finishReason = chatResponse.getChoices().get(0).getFinishReason(); + + if ("length".equals(finishReason)) { + logger.warn("Token length exceeded. Increase the maximum token limit to avoid the issue."); + return ""; + } + + @Nullable + ChatMessage chatResponseMessage = chatResponse.getChoices().get(0).getChatMessage(); + + if (chatResponseMessage == null) { + logger.debug("ChatGPT response does not contain a message."); + return ""; + } + + this.messages.add(chatResponseMessage); + + if ("tool_calls".equals(finishReason)) { + executeToolCalls(chatResponseMessage.getToolCalls()); + return ""; + } else { + return (chatResponseMessage.getContent() == null) ? "" : chatResponseMessage.getContent(); + } + } + + private void executeToolCalls(@Nullable List toolCalls) { + toolCalls.forEach(tool -> { + if (tool.getType().equals("function")) { + ChatFunctionCall functionCall = tool.getFunction(); + if (functionCall != null) { + String functionName = functionCall.getName(); + ChatFunction function = functions.get(functionName); + if (function != null) { + ObjectMapper objectMapper = new ObjectMapper(); + String arguments = functionCall.getArguments(); + Object argumentsObject; + + logger.debug("Function '{}' with arguments: {}", functionName, arguments); + JsonNode argumentsNode; + try { + argumentsNode = objectMapper.readTree(arguments); + Class parametersClass = function.getParametersClass(); + argumentsObject = objectMapper.treeToValue(argumentsNode, parametersClass); + } catch (JsonProcessingException e) { + logger.debug("Failed to parse arguments: {}", e.getMessage(), e); + return; + } + Object result = function.getExecutor().apply(argumentsObject); + String resultString = String.valueOf(result); + ChatMessage message = new ChatMessage(); + message.setRole(ChatMessage.Role.TOOL.value()); + message.setName(functionName); + message.setToolCallId(tool.getId()); + message.setContent(resultString); + messages.add(message); + } else { + logger.debug("Function '{}' not found", functionName); + } + } + } + }); + } + + private @Nullable String prepareRequestBody(String message) { + if (this.config == null) { + if (thingHandler instanceof ChatGPTHandler chatGPTHandler) { + this.config = chatGPTHandler.getConfigAs(); + } + } + + if (this.config == null) { + logger.debug("Could not get configuration"); + return null; + } + + LocalTime currentTime = LocalTime.now(); + if (currentTime.isAfter(this.lastMessageTime.plusMinutes(config.keepContext))) { + this.messages.clear(); + } + if (this.messages.isEmpty()) { + ChatMessage systemMessage = generateSystemMessage(); + this.messages.add(systemMessage); + } + + this.lastMessageTime = currentTime; + ChatMessage userMessage = new ChatMessage(); + userMessage.setRole(ChatMessage.Role.USER.value()); + userMessage.setContent(message); + this.messages.add(userMessage); + ChatRequestBody chatRequestBody = new ChatRequestBody(); + + if (this.config.model == null || this.config.model.isEmpty()) { + logger.debug("Model is not set"); + return null; + } + + chatRequestBody.setModel(this.config.model); + chatRequestBody.setTemperature(this.config.temperature); + chatRequestBody.setMaxTokens(this.config.maxTokens); + chatRequestBody.setTopP(this.config.topP); + chatRequestBody.setToolChoice(ToolChoice.AUTO.value()); + chatRequestBody.setTools(this.tools); + chatRequestBody.setMessages(this.messages); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(Include.NON_NULL); + + try { + return objectMapper.writeValueAsString(chatRequestBody); + } catch (JsonProcessingException e) { + logger.debug("Failed to serialize ChatGPT request: {}", e.getMessage(), e); + return null; + } + } + + private ChatMessage generateSystemMessage() { + ChatMessage systemMessage = new ChatMessage(); + systemMessage.setRole(ChatMessage.Role.SYSTEM.value()); + StringBuilder content = new StringBuilder(); + content.append(this.config.systemMessage); + + Collection openaiItems = itemRegistry.getItemsByTag("ChatGPT"); + + if (!openaiItems.isEmpty()) { + openaiItems.forEach(item -> { + String location = ""; + String itemType = item.getType(); + CommandDescription description = item.getCommandDescription(); + List options = new ArrayList<>(); + + if (description != null) { + options = description.getCommandOptions(); + } + + content.append("name: \"").append(item.getName()).append("\", type: \"").append(itemType); + + if (config.useSemanticModel) { + Item locationItem = Semantics.getLocation(item); + if (locationItem != null) { + location = locationItem.getName(); + } + } else { + String[] nameParts = item.getName().split("_"); + location = nameParts[0]; + } + + if (!location.isEmpty()) { + content.append("\", location: \"").append(location); + } + + if (!options.isEmpty()) { + content.append("\", accepted commands: \""); + options.forEach(option -> { + content.append(option.getCommand()).append(", "); + }); + content.delete(content.length() - 2, content.length()); + content.append("\"").append(System.lineSeparator()); + } + + content.append("\", description: \"").append(item.getLabel()).append("\", state: \"") + .append(item.getState().toString()).append("\"").append(System.lineSeparator()); + }); + } + + systemMessage.setContent(content.toString()); + return systemMessage; + } + + public String sendCommand(String itemName, String commandString) { + try { + Item item = itemRegistry.getItem(itemName); + Command command = null; + if ("toggle".equalsIgnoreCase(commandString) + && (item instanceof SwitchItem || item instanceof RollershutterItem)) { + if (OnOffType.ON.equals(item.getStateAs(OnOffType.class))) { + command = OnOffType.OFF; + } else if (OnOffType.OFF.equals(item.getStateAs(OnOffType.class))) { + command = OnOffType.ON; + } else if (UpDownType.UP.equals(item.getStateAs(UpDownType.class))) { + command = UpDownType.DOWN; + } else if (UpDownType.DOWN.equals(item.getStateAs(UpDownType.class))) { + command = UpDownType.UP; + } + } else { + command = TypeParser.parseCommand(item.getAcceptedCommandTypes(), commandString); + } + if (command != null) { + logger.debug("Received command '{}' for item '{}'", commandString, itemName); + eventPublisher.post(ItemEventFactory.createCommandEvent(itemName, command)); + return "Done"; + } else { + return "Invalid command"; + } + } catch (ItemNotFoundException e) { + logger.warn("Received command '{}' for a non-existent item '{}'", commandString, itemName); + return "Item not found"; + } + } +} diff --git a/bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/thing/thing-types.xml index f7aedc57626..5b89027c57a 100644 --- a/bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.chatgpt/src/main/resources/OH-INF/thing/thing-types.xml @@ -14,12 +14,20 @@ - + + + Authentication for connecting to OpenAI API. + + + + Configure HLI service. + + password API key to access the account - + The server API where to reach the AI service. https://api.openai.com/v1/chat/completions @@ -29,7 +37,7 @@ false - + The model url where to retrieve the available models from. https://api.openai.com/v1/models @@ -39,6 +47,79 @@ false + + + ID of the model to use. + gpt-4o-mini + true + + + + What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more + random, while lower values like 0.2 will make it more focused and deterministic. + 0.5 + true + + + + + An alternative to sampling with temperature, called nucleus sampling, where the model considers the + results of the + tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability + mass are considered. + We generally recommend altering this or temperature but not both. + + 1 + true + + + + Override the default system message of the assistant. + You are the manager of the openHAB smart home. You know how to manage devices in a smart home or provide + their current status. You can also answer a question not related to devices in the house. Or, for example, you can + compose a story upon request. + I will provide information about the smart home; if necessary, you can perform the + function; if there is not enough + information to perform it, then clarify briefly, without listing all the available + devices and parameters for the + function. If the question is not related to devices in a smart home, then answer the + question briefly, + maximum 3 sentences in everyday language. + + The name, current status and location of devices is + displayed in 'Available devices'. + Use the items_control function only for the requested action, not for providing + current states. + + Available devices: + + true + + + + The maximum number of tokens that can be generated in the chat completion. + 1000 + true + + + + How long to store the context in minutes. + 2 + true + + + + Limit total tokens included in context. + 10000 + true + + + + Use a semantic model to determine the location of an item. Otherwise, item names must begin with a + "location_" e.g. "Kitchen_Light" + true + true + @@ -52,7 +133,7 @@ The model to be used for the responses false - gpt-3.5-turbo + gpt-4o-mini @@ -67,7 +148,15 @@ The maximum number of tokens to generate in the completion. - 500 + 1000 + true + + + + An alternative to sampling with temperature, called nucleus sampling, where the model considers the + results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability + mass are considered. We generally recommend altering this or temperature but not both. + 1 true diff --git a/bundles/org.openhab.binding.chatgpt/src/main/resources/json/tools.json b/bundles/org.openhab.binding.chatgpt/src/main/resources/json/tools.json new file mode 100644 index 00000000000..ff9cd5daffc --- /dev/null +++ b/bundles/org.openhab.binding.chatgpt/src/main/resources/json/tools.json @@ -0,0 +1,27 @@ +[ + { + "type": "function", + "function": { + "name": "items_control", + "description": "Use this function to control item in openHAB", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Item name in openHAB" + }, + "type": { + "type": "string", + "description": "Item type" + }, + "state": { + "type": "string", + "description": "New state for item" + } + }, + "required": ["type", "name", "state"] + } + } + } +] \ No newline at end of file