Implemented ChatGPT module

This commit is contained in:
Erik C. Thauvin 2022-12-04 16:14:03 -08:00
parent b6e1888e70
commit c2e11743c6
11 changed files with 234 additions and 14 deletions

2
.idea/kotlinc.xml generated
View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.7.21" />
<option name="version" value="1.7.22" />
</component>
</project>

View file

@ -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"

View file

@ -11,6 +11,7 @@
<ID>LongParameterList:Comment.kt$Comment$( channel: String, cmd: String, entry: EntryLink, entryIndex: Int, commentIndex: Int, event: GenericMessageEvent )</ID>
<ID>LongParameterList:EntryLink.kt$EntryLink$( // Link's comments val comments: MutableList&lt;EntryComment&gt; = mutableListOf(), // Tags/categories val tags: MutableList&lt;SyndCategory&gt; = 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 )</ID>
<ID>LongParameterList:Twitter.kt$Twitter.Companion$( consumerKey: String?, consumerSecret: String?, token: String?, tokenSecret: String?, handle: String?, message: String, isDm: Boolean )</ID>
<ID>MagicNumber:ChatGpt.kt$ChatGpt.Companion$200</ID>
<ID>MagicNumber:Comment.kt$Comment$3</ID>
<ID>MagicNumber:CryptoPrices.kt$CryptoPrices$10</ID>
<ID>MagicNumber:CurrencyConverter.kt$CurrencyConverter$11</ID>
@ -46,6 +47,7 @@
<ID>MaxLineLength:TwitterOAuth.kt$TwitterOAuth$*</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$fun add(command: AbstractCommand)</ID>
<ID>NestedBlockDepth:Addons.kt$Addons$fun add(module: AbstractModule)</ID>
<ID>NestedBlockDepth:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?): String</ID>
<ID>NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent)</ID>
<ID>NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic fun convertCurrency(query: String): Message</ID>
<ID>NestedBlockDepth:EntryLink.kt$EntryLink$private fun setTags(tags: List&lt;String?&gt;)</ID>
@ -72,6 +74,7 @@
<ID>SwallowedException:GoogleSearchTest.kt$GoogleSearchTest$e: ModuleException</ID>
<ID>SwallowedException:StockQuoteTest.kt$StockQuoteTest$e: ModuleException</ID>
<ID>SwallowedException:WolframAlphaTest.kt$WolframAlphaTest$e: ModuleException</ID>
<ID>ThrowsCount:ChatGpt.kt$ChatGpt.Companion$@JvmStatic @Throws(ModuleException::class) fun chat(query: String, apiKey: String?): String</ID>
<ID>ThrowsCount:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List&lt;Message&gt;</ID>
<ID>ThrowsCount:Joke.kt$Joke.Companion$@JvmStatic @Throws(ModuleException::class) fun randomJoke(): List&lt;Message&gt;</ID>
<ID>ThrowsCount:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List&lt;Message&gt;</ID>

View file

@ -72,7 +72,6 @@
<exclude name="AvoidFieldNameMatchingMethodName"/>
<exclude name="AvoidFieldNameMatchingTypeName"/>
<exclude name="AvoidLiteralsInIfCondition"/>
<exclude name="BeanMembersShouldSerialize"/>
<exclude name="EmptyCatchBlock"/>
<exclude name="NullAssignment"/>
</rule>

View file

@ -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

View file

@ -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=

View file

@ -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())

View file

@ -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 <query>"))
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)
}
}

View file

@ -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)

View file

@ -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")
}
}

View file

@ -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