diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 641e97c..8b6e513 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -1,15 +1,15 @@ - + - + CyclomaticComplexMethod:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = CURRENT_XML) - CyclomaticComplexMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> + CyclomaticComplexMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> LongMethod:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = CURRENT_XML) - LongMethod:Mobibot.kt$Mobibot.Companion$@JvmStatic @Throws(Exception::class) fun main(args: Array<String>) - LongMethod:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List<Message> - LongMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> + LongMethod:Mobibot.kt$Mobibot.Companion$@JvmStatic @Throws(Exception::class) fun main(args: Array<String>) + LongMethod:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List<Message> + LongMethod:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> 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: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 ) MagicNumber:ChatGpt.kt$ChatGpt$400 MagicNumber:ChatGpt.kt$ChatGpt.Companion$200 MagicNumber:ChatGpt.kt$ChatGpt.Companion$429 @@ -20,6 +20,7 @@ MagicNumber:CurrencyConverter.kt$CurrencyConverter.Companion$4 MagicNumber:Cycle.kt$Cycle$10 MagicNumber:Cycle.kt$Cycle$1000L + MagicNumber:Gemini.kt$Gemini$400 MagicNumber:Ignore.kt$Ignore$8 MagicNumber:Info.kt$Info.Companion$30 MagicNumber:Info.kt$Info.Companion$365 @@ -52,22 +53,22 @@ NestedBlockDepth:Comment.kt$Comment$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic @Throws(ModuleException::class) fun loadSymbols(apiKey: String?) NestedBlockDepth:CurrencyConverter.kt$CurrencyConverter.Companion$@JvmStatic fun convertCurrency(apiKey: String?, query: String): Message - NestedBlockDepth:EntryLink.kt$EntryLink$private fun setTags(tags: List<String?>) + NestedBlockDepth:EntryLink.kt$EntryLink$private fun setTags(tags: List<String?>) NestedBlockDepth:FeedsManager.kt$FeedsManager.Companion$@JvmStatic @Throws(IOException::class, FeedException::class) fun loadFeed(entries: Entries, currentFile: String = CURRENT_XML): String NestedBlockDepth:FeedsManager.kt$FeedsManager.Companion$@JvmStatic fun saveFeed(entries: Entries, currentFile: String = CURRENT_XML) NestedBlockDepth:GoogleSearch.kt$GoogleSearch$override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) - NestedBlockDepth:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List<Message> + NestedBlockDepth:GoogleSearch.kt$GoogleSearch.Companion$@JvmStatic @Throws(ModuleException::class) fun searchGoogle( query: String, apiKey: String?, cseKey: String?, quotaUser: String = ReleaseInfo.PROJECT ): List<Message> NestedBlockDepth:LinksManager.kt$LinksManager$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) NestedBlockDepth:Lookup.kt$Lookup$override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) NestedBlockDepth:Mastodon.kt$Mastodon.Companion$@JvmStatic @Throws(ModuleException::class) fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String NestedBlockDepth:Posting.kt$Posting$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) NestedBlockDepth:Seen.kt$Seen$override fun commandResponse(channel: String, args: String, event: GenericMessageEvent) - NestedBlockDepth:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List<Message> + NestedBlockDepth:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List<Message> NestedBlockDepth:Tell.kt$Tell$fun send(event: GenericUserEvent) NestedBlockDepth:Utils.kt$Utils$@JvmStatic fun loadSerialData(file: String, default: Any, logger: Logger, description: String): Any NestedBlockDepth:Utils.kt$Utils$@JvmStatic fun saveSerialData(file: String, data: Any, logger: Logger, description: String) NestedBlockDepth:Weather2.kt$Weather2$override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) - NestedBlockDepth:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> + NestedBlockDepth:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> ReturnCount:Addons.kt$Addons$fun exec(channel: String, cmd: String, args: String, event: GenericMessageEvent): Boolean ReturnCount:Addons.kt$Addons$fun help(channel: String, topic: String, event: GenericMessageEvent): Boolean ReturnCount:ExceptionSanitizer.kt$ExceptionSanitizer$fun ModuleException.sanitize(vararg sanitize: String): ModuleException @@ -76,13 +77,14 @@ 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?, maxTokens: Int): 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: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:Mastodon.kt$Mastodon.Companion$@JvmStatic @Throws(ModuleException::class) fun toot(apiKey: String?, instance: String?, handle: String?, message: String, isDm: Boolean): String - ThrowsCount:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List<Message> + ThrowsCount:StockQuote.kt$StockQuote.Companion$@JvmStatic @Throws(ModuleException::class) fun getQuote(symbol: String, apiKey: String?): List<Message> ThrowsCount:StockQuote.kt$StockQuote.Companion$@Throws(ModuleException::class) private fun getJsonResponse(response: String, debugMessage: String): JSONObject - ThrowsCount:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> + ThrowsCount:Weather2.kt$Weather2.Companion$@JvmStatic @Throws(ModuleException::class) fun getWeather(query: String, apiKey: String?): List<Message> ThrowsCount:WolframAlpha.kt$WolframAlpha.Companion$@JvmStatic @Throws(ModuleException::class) fun queryWolfram(query: String, units: String = IMPERIAL, appId: String?): String + TooGenericExceptionCaught:Gemini.kt$Gemini.Companion$e: Exception TooGenericExceptionCaught:StockQuote.kt$StockQuote.Companion$e: NullPointerException TooGenericExceptionCaught:Weather2.kt$Weather2.Companion$e: NullPointerException TooManyFunctions:EntryLink.kt$EntryLink : Serializable @@ -94,6 +96,7 @@ WildcardImport:FeedMgrTest.kt$import assertk.assertions.* WildcardImport:FeedReaderTest.kt$import assertk.assertions.* WildcardImport:FeedsManager.kt$import com.rometools.rome.feed.synd.* + WildcardImport:GeminiTest.kt$import assertk.assertions.* WildcardImport:GoogleSearchTest.kt$import assertk.assertions.* WildcardImport:JokeTest.kt$import assertk.assertions.* WildcardImport:Mobibot.kt$import java.io.* diff --git a/deploy.sh b/deploy.sh index 58dd32d..d1c1f23 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,8 +1,8 @@ #!/bin/bash -./bld jar deploy -[ $? -eq 0 ] && sftp nix3.thauvin.us < jars = new ArrayList<>(); + runtimeClasspathJars().forEach(f -> jars.add("./lib/" + f.getName())); compileClasspathJars().forEach(f -> jars.add("./lib/" + f.getName())); jarOperation() .manifestAttribute(Attributes.Name.MAIN_CLASS, mainClass()) @@ -149,6 +151,9 @@ public class MobibotBuild extends Project { for (var jar : compileClasspathJars()) { FileUtils.copy(jar, new File(lib, jar.getName())); } + for (var jar : runtimeClasspathJars()) { + FileUtils.copy(jar, new File(lib, jar.getName())); + } FileUtils.copy(new File(buildDistDirectory(), jarFileName()), new File(deploy, "mobibot.jar")); } diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt index ac4d6af..6b9c21a 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/Mobibot.kt @@ -400,6 +400,7 @@ class Mobibot(nickname: String, val channel: String, logsDirPath: String, p: Pro addons.add(CryptoPrices()) addons.add(CurrencyConverter()) addons.add(Dice()) + addons.add(Gemini()) addons.add(GoogleSearch()) addons.add(Info(tell, seen)) addons.add(Joke()) diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt b/src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt index 7a14ddd..f75b334 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/ReleaseInfo.kt @@ -14,12 +14,12 @@ import java.time.ZoneId */ object ReleaseInfo { const val PROJECT = "mobibot" - const val VERSION = "0.8.0-rc+20231125224843" + const val VERSION = "0.8.0-rc+20231217213124" @JvmField @Suppress("MagicNumber") val BUILD_DATE: LocalDateTime = LocalDateTime.ofInstant( - Instant.ofEpochMilli(1700981323173L), ZoneId.systemDefault() + Instant.ofEpochMilli(1702877484912L), ZoneId.systemDefault() ) const val WEBSITE = "https://www.mobitopia.org/mobibot/" diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt index 802bbde..4594da3 100644 --- a/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt +++ b/src/main/kotlin/net/thauvin/erik/mobibot/commands/Versions.kt @@ -42,7 +42,7 @@ class Versions : AbstractCommand() { private val allVersions = listOf( "Version: ${ReleaseInfo.VERSION} (${ReleaseInfo.BUILD_DATE.toIsoLocalDate()})", "${System.getProperty("os.name")} ${System.getProperty("os.version")} (${System.getProperty("os.arch")})" + - ", JVM ${System.getProperty("java.runtime.version")}", + ", JVM ${System.getProperty("java.version")}", "Kotlin ${KotlinVersion.CURRENT}, PircBotX ${PircBotX.VERSION}" ) override val name = "versions" diff --git a/src/main/kotlin/net/thauvin/erik/mobibot/modules/Gemini.kt b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Gemini.kt new file mode 100644 index 0000000..2f7c842 --- /dev/null +++ b/src/main/kotlin/net/thauvin/erik/mobibot/modules/Gemini.kt @@ -0,0 +1,116 @@ +package net.thauvin.erik.mobibot.modules + +import com.google.auth.Credentials +import com.google.cloud.vertexai.VertexAI +import com.google.cloud.vertexai.api.GenerateContentResponse +import com.google.cloud.vertexai.api.GenerationConfig +import com.google.cloud.vertexai.generativeai.preview.ChatSession +import com.google.cloud.vertexai.generativeai.preview.GenerativeModel +import com.google.cloud.vertexai.generativeai.preview.ResponseHandler +import net.thauvin.erik.mobibot.Utils +import net.thauvin.erik.mobibot.Utils.sendMessage +import okio.IOException +import org.apache.commons.text.WordUtils +import org.pircbotx.hooks.types.GenericMessageEvent +import org.slf4j.Logger +import org.slf4j.LoggerFactory + + +class Gemini : AbstractModule() { + private val logger: Logger = LoggerFactory.getLogger(Gemini::class.java) + + override val name = GEMINI_NAME + + override fun commandResponse(channel: String, cmd: String, args: String, event: GenericMessageEvent) { + if (args.isNotBlank()) { + try { + val answer = chat( + args.trim(), + properties[PROJECT_ID_PROP], + properties[LOCATION_PROPR], + properties.getOrDefault(MAX_TOKENS_PROP, "1024").toInt() + ) + if (!answer.isNullOrEmpty()) { + event.sendMessage(answer) + } else { + event.respond("$name is stumped.") + } + } catch (e: ModuleException) { + if (logger.isWarnEnabled) logger.warn(e.debugMessage, e) + e.message?.let { + event.respond(it) + } + } + } else { + helpResponse(event) + } + } + + companion object { + /** + * The service name. + */ + const val GEMINI_NAME = "Gemini" + + /** + * The Google cloud project ID. + */ + const val PROJECT_ID_PROP = "gemini-project-id" + + /** + * The Vertex AI location. + */ + const val LOCATION_PROPR = "gemini-location" + + /** + * The max tokens property. + */ + const val MAX_TOKENS_PROP = "gemini-max-tokens" + + // ChatGPT command + private const val GEMINI_CMD = "gemini" + + @JvmStatic + @Throws(ModuleException::class) + fun chat( + query: String, + projectId: String?, + location: String?, + maxToken: Int + ): String? { + if (!projectId.isNullOrEmpty() && !location.isNullOrEmpty()) { + try { + VertexAI(projectId, location).use { vertexAI -> + val generationConfig = GenerationConfig.newBuilder().setMaxOutputTokens(maxToken).build() + val model = GenerativeModel("gemini-pro-vision", generationConfig, vertexAI) + val session = ChatSession(model) + val response = session.sendMessage(query) + + return ResponseHandler.getText(response); + } + } catch (e: Exception) { + throw ModuleException( + "$GEMINI_CMD($query): IO", + "An IO error has occurred while conversing with ${GEMINI_NAME}.", + e + ) + } + } else { + throw ModuleException("${GEMINI_CMD}($query)", "No ${GEMINI_NAME} Project ID or Location specified.") + } + } + } + + init { + commands.add(GEMINI_CMD) + with(help) { + add("To get answers from $name:") + add(Utils.helpFormat("%c ${GEMINI_CMD} ")) + add("For example:") + add(Utils.helpFormat("%c ${GEMINI_CMD} explain quantum computing in simple terms")) + add(Utils.helpFormat("%c ${GEMINI_CMD} how do I make an HTTP request in Javascript?")) + } + initProperties(PROJECT_ID_PROP, LOCATION_PROPR, MAX_TOKENS_PROP) + } + +} diff --git a/src/test/kotlin/net/thauvin/erik/mobibot/modules/GeminiTest.kt b/src/test/kotlin/net/thauvin/erik/mobibot/modules/GeminiTest.kt new file mode 100644 index 0000000..28845a9 --- /dev/null +++ b/src/test/kotlin/net/thauvin/erik/mobibot/modules/GeminiTest.kt @@ -0,0 +1,66 @@ +/* + * ChatGptTest.kt + * + * Copyright 2004-2023 Erik C. Thauvin (erik@thauvin.net) + * + * 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.assertFailure +import assertk.assertThat +import assertk.assertions.* +import net.thauvin.erik.mobibot.DisableOnCi +import net.thauvin.erik.mobibot.LocalProperties +import kotlin.test.Test + +class GeminiTest : LocalProperties() { + @Test + fun testApiKey() { + assertFailure { Gemini.chat("1 gallon to liter", "", "", 1024) } + .isInstanceOf(ModuleException::class.java) + .hasNoCause() + } + + @Test + @DisableOnCi + fun chatPrompt() { + val projectId = getProperty(Gemini.PROJECT_ID_PROP) + val location = getProperty(Gemini.LOCATION_PROPR) + val maxTokens = getProperty(Gemini.MAX_TOKENS_PROP).toInt() + + assertThat( + Gemini.chat("how do I make an HTTP request in Javascript?", projectId, location, maxTokens) + ).isNotNull().contains("XMLHttpRequest") + + assertThat( + Gemini.chat("how do I encode a URL in java?", projectId, location, 60) + ).isNotNull().contains("URLEncoder") + + assertFailure { Gemini.chat("1 liter to gallon", projectId, "blah", 40) } + .isInstanceOf(ModuleException::class.java) + } +} diff --git a/website/index.html b/website/index.html index 1ffaad2..fffe650 100644 --- a/website/index.html +++ b/website/index.html @@ -38,6 +38,7 @@
  • Apache Commons Net
  • CryptoPrice
  • exp4j
  • +
  • Google Vertex AI
  • JokeAPI
  • jsoup
  • kotlinx-cli
  • @@ -80,9 +81,10 @@
  • Performing Google searches
    mobibot: google mobitopia on irc
  • -
  • Getting answers from Wolfram Alpha and ChatGPT +
  • Getting answers from Wolfram Alpha, ChatGPT and Google Gemini
    mobibot: wolfram days until christmas
    mobibot: chatgpt explain quantum computing in simple terms
    +
    mobibot: gemini what are all the colors in a rainbow?
  • Displaying weather information
    mobibot: weather san francisco