From c2e11743c68fb1a312af784c9145de950565db61 Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Sun, 4 Dec 2022 16:14:03 -0800 Subject: [PATCH] Implemented ChatGPT module --- .idea/kotlinc.xml | 2 +- build.gradle | 10 +- config/detekt/baseline.xml | 3 + config/pmd.xml | 1 - gradle/wrapper/gradle-wrapper.properties | 2 +- properties/mobibot.properties | 5 + .../net/thauvin/erik/mobibot/Mobibot.kt | 2 + .../thauvin/erik/mobibot/modules/ChatGpt.kt | 153 ++++++++++++++++++ .../thauvin/erik/mobibot/FeedReaderTest.kt | 6 +- .../erik/mobibot/modules/ChatGptTest.kt | 58 +++++++ version.properties | 6 +- 11 files changed, 234 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt.kt create mode 100644 src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGptTest.kt diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 0e65cea..4251b72 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/build.gradle b/build.gradle index 2c288cf..e3d2267 100644 --- a/build.gradle +++ b/build.gradle @@ -5,8 +5,8 @@ plugins { id 'io.gitlab.arturbosch.detekt' version '1.22.0' id 'java' id 'net.thauvin.erik.gradle.semver' version '1.0.4' - id 'org.jetbrains.kotlin.jvm' version '1.7.21' - id 'org.jetbrains.kotlin.kapt' version '1.7.21' + id 'org.jetbrains.kotlin.jvm' version '1.7.22' + id 'org.jetbrains.kotlin.kapt' version '1.7.22' id 'org.jetbrains.kotlinx.kover' version '0.6.1' id 'org.sonarqube' version '3.5.0.2730' id 'pmd' @@ -30,7 +30,7 @@ mainClassName = packageName + '.Mobibot' ext.versions = [ log4j: '2.19.0', - pmd : '6.51.0', + pmd : '6.52.0', ] repositories { @@ -52,7 +52,7 @@ dependencies { implementation 'org.apache.commons:commons-lang3:3.12.0' implementation 'org.apache.commons:commons-text:1.10.0' implementation 'commons-codec:commons-codec:1.15' - implementation 'commons-net:commons-net:3.8.0' + implementation 'commons-net:commons-net:3.9.0' // Google implementation 'com.google.code.gson:gson:2.10' @@ -65,7 +65,7 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-cli:0.3.5' // Logging - implementation 'org.slf4j:slf4j-api:2.0.4' + implementation 'org.slf4j:slf4j-api:2.0.5' implementation "org.apache.logging.log4j:log4j-api:$versions.log4j" implementation "org.apache.logging.log4j:log4j-core:$versions.log4j" implementation "org.apache.logging.log4j:log4j-slf4j2-impl:$versions.log4j" diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 49d6c04..370754e 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -11,6 +11,7 @@ LongParameterList:Comment.kt$Comment$( channel: String, cmd: String, entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent ) LongParameterList:EntryLink.kt$EntryLink$( // Link's comments val comments: MutableList<EntryComment> = mutableListOf(), // Tags/categories val tags: MutableList<SyndCategory> = mutableListOf(), // Channel var channel: String, // Creation date var date: Date = Calendar.getInstance().time, // Link's URL var link: String, // Author's login var login: String = "", // Author's nickname var nick: String, // Link's title var title: String ) LongParameterList:Twitter.kt$Twitter.Companion$( consumerKey: String?, consumerSecret: String?, token: String?, tokenSecret: String?, handle: String?, message: String, isDm: Boolean ) + MagicNumber:ChatGpt.kt$ChatGpt.Companion$200 MagicNumber:Comment.kt$Comment$3 MagicNumber:CryptoPrices.kt$CryptoPrices$10 MagicNumber:CurrencyConverter.kt$CurrencyConverter$11 @@ -46,6 +47,7 @@ MaxLineLength:TwitterOAuth.kt$TwitterOAuth$* NestedBlockDepth:Addons.kt$Addons$fun add(command: AbstractCommand) NestedBlockDepth:Addons.kt$Addons$fun add(module: AbstractModule) + NestedBlockDepth:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?): String NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic fun convertCurrency(query: String): Message NestedBlockDepth:EntryLink.kt$EntryLink$private fun setTags(tags: List<String?>) @@ -72,6 +74,7 @@ SwallowedException:GoogleSearchTest.kt$GoogleSearchTest$e: ModuleException SwallowedException:StockQuoteTest.kt$StockQuoteTest$e: ModuleException SwallowedException:WolframAlphaTest.kt$WolframAlphaTest$e: ModuleException + ThrowsCount:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?): String ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List<Message> ThrowsCount:Joke.kt$Joke.Companion$@JvmStatic @Throws(ModuleException::class) fun randomJoke(): List<Message> ThrowsCount:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List<Message> diff --git a/config/pmd.xml b/config/pmd.xml index 890a490..e23c3fa 100644 --- a/config/pmd.xml +++ b/config/pmd.xml @@ -72,7 +72,6 @@ - diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e9ae037..f398c33 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-rc-3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/properties/mobibot.properties b/properties/mobibot.properties index facff63..981fced 100644 --- a/properties/mobibot.properties +++ b/properties/mobibot.properties @@ -69,3 +69,8 @@ tell-max-size=50 # #wolfram-appid= #wolfram-units=imperial + +# +# ChatGPT/OpenAI API key from: https://beta.openai.com/account/api-keys +# +#chatgpt-api-key= diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt index 6dbe080..94c7a8c 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt @@ -68,6 +68,7 @@ import net.thauvin.erik.mobibot.commands.links.View import net.thauvin.erik.mobibot.commands.seen.Seen import net.thauvin.erik.mobibot.commands.tell.Tell import net.thauvin.erik.mobibot.modules.Calc +import net.thauvin.erik.mobibot.modules.ChatGpt import net.thauvin.erik.mobibot.modules.CryptoPrices import net.thauvin.erik.mobibot.modules.CurrencyConverter import net.thauvin.erik.mobibot.modules.Dice @@ -432,6 +433,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro // Load the modules addons.add(Calc()) + addons.add(ChatGpt()) addons.add(CryptoPrices()) addons.add(CurrencyConverter()) addons.add(Dice()) diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt.kt new file mode 100644 index 0000000..8593cb6 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/ChatGpt.kt @@ -0,0 +1,153 @@ +/* + * ChatGpt.kt + * + * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package net.thauvin.erik.mobibot.modules + +import net.thauvin.erik.mobibot.Utils +import net.thauvin.erik.mobibot.Utils.sendMessage +import org.json.JSONException +import org.json.JSONObject +import org.json.JSONWriter +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.IOException +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse + +class ChatGpt : ThreadedModule() { + private val logger: Logger = LoggerFactory.getLogger(ChatGpt::class.java) + + override val name = "ChatGPT" + + override fun run(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + if (args.isNotBlank()) { + try { + event.sendMessage( + chat( + args.trim(), + properties[CHATGPT_API_KEY] + ) + ) + } catch (e: ModuleException) { + if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) + e.message?.let { + event.respond(it) + } + } + } else { + helpResponse(event) + } + } + + companion object { + /** + * The ChatGPT API Key property. + */ + const val CHATGPT_API_KEY = "chatgpt-api-key" + + + // ChatGPT command + private const val CHATGPT_CMD = "chatgpt" + + // ChatGPT API URL + private const val API_URL = "https://api.openai.com/v1/completions" + + @JvmStatic + @Throws(ModuleException::class) + fun chat(query: String, apiKey: String?): String { + if (!apiKey.isNullOrEmpty()) { + val prompt = JSONWriter.valueToString("Q:$query\nA:") + val request = HttpRequest.newBuilder() + .uri(URI.create(API_URL)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer $apiKey") + .POST( + HttpRequest.BodyPublishers.ofString( + """{ + "model": "text-davinci-003", + "prompt": $prompt, + "temperature": 0, + "max_tokens": 100, + "top_p": 1, + "frequency_penalty": 0, + "presence_penalty": 0, + "stop": ["\n"] + }""".trimIndent() + ) + ) + .build() + try { + val response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString()) + if (response.statusCode() == 200) { + try { + val jsonResponse = JSONObject(response.body()) + println(response.body()); + var choices = jsonResponse.getJSONArray("choices") + return choices.getJSONObject(0).getString("text").trim() + } catch (e: JSONException) { + throw ModuleException( + "chatgpt($query): JSON", + "A JSON error has occurred while conversing with ChatGPT.", + e + ) + } + } else { + throw IOException("Status Code: " + response.statusCode()) + } + } catch (e: IOException) { + throw ModuleException( + "chatgpt($query): IO", + "An IO error has occurred while conversing with GhatGPT.", + e + ) + } + } else { + throw ModuleException("chatgpt($query)", "No ChatGPT API key specified.") + } + } + } + + init { + commands.add(CHATGPT_CMD) + with(help) { + add("To get answers from ChatGPT:") + add(Utils.helpFormat("%c $CHATGPT_CMD ")) + add("For example:") + add(Utils.helpFormat("%c $CHATGPT_CMD explain quantum computing in simple terms")) + add(Utils.helpFormat("%c $CHATGPT_CMD how do I make an HTTP request in Javascript?")) + } + initProperties(CHATGPT_API_KEY) + } +} diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt index 4e75ef0..2fa58b6 100644 --- a/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt +++ b/src/test/kotlin/net/thauvin/erik/mobibot/FeedReaderTest.kt @@ -60,12 +60,12 @@ class FeedReaderTest { index(1).prop(Message::msg).contains("erik.thauvin.net") } - messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=0") + messages = readFeed("https://www.mobitopia.org/mobibot/logs/2021-10-27.xml") assertThat(messages, "messages").index(0).prop(Message::msg).contains("nothing") - messages = readFeed("https://lorem-rss.herokuapp.com/feed?length=42", 42) + messages = readFeed("https://www.mobitopia.org/mobibot/logs/2005-10-11.xml", 42) assertThat(messages, "messages").size().isEqualTo(84) - assertThat(messages.last(), "messages.last").prop(Message::msg).contains("http://example.com/test/") + assertThat(messages.last(), "messages.last").prop(Message::msg).contains("techdigest.tv") assertThat { readFeed("blah") }.isFailure().isInstanceOf(MalformedURLException::class.java) diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGptTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGptTest.kt new file mode 100644 index 0000000..f230831 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/ChatGptTest.kt @@ -0,0 +1,58 @@ +/* + * ChatGptTest.kt + * + * Copyright (c) 2004-2022, Erik C. Thauvin (erik@thauvin.net) + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * Neither the name of this project nor the names of its contributors may be + * used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package net.thauvin.erik.mobibot.modules + +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.hasNoCause +import assertk.assertions.isFailure +import assertk.assertions.isInstanceOf +import net.thauvin.erik.mobibot.LocalProperties +import org.testng.annotations.Test + +class ChatGptTest : LocalProperties() { + @Test(groups = ["modules"]) + fun testApiKey() { + assertThat { ChatGpt.chat("1 gallon to liter", "") } + .isFailure() + .isInstanceOf(ModuleException::class.java) + .hasNoCause() + } + + @Test(groups = ["modules"]) + fun testChat() { + val apiKey = getProperty(ChatGpt.CHATGPT_API_KEY) + assertThat( + ChatGpt.chat("how do I make an HTTP request in Javascript?", apiKey) + ).contains("XMLHttpRequest") + } +} diff --git a/version.properties b/version.properties index c9d4380..ce4a4d7 100644 --- a/version.properties +++ b/version.properties @@ -1,9 +1,9 @@ #Generated by the Semver Plugin for Gradle -#Mon Nov 21 10:09:13 PST 2022 -version.buildmeta=764 +#Sun Dec 04 16:10:05 PST 2022 +version.buildmeta=798 version.major=0 version.minor=8 version.patch=0 version.prerelease=rc version.project=mobibot -version.semver=0.8.0-rc+764 +version.semver=0.8.0-rc+798