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

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