Added Google Gemini module

This commit is contained in:
Erik C. Thauvin 2023-12-17 21:33:02 -08:00
parent 8412a8c26d
commit 4a4e702d9f
10 changed files with 230 additions and 26 deletions

View file

@ -50,7 +50,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.jar.Attributes;
import static rife.bld.dependencies.Repository.*;
import static rife.bld.dependencies.Repository.MAVEN_CENTRAL;
import static rife.bld.dependencies.Repository.MAVEN_LOCAL;
import static rife.bld.dependencies.Scope.compile;
import static rife.bld.dependencies.Scope.test;
@ -82,9 +83,9 @@ public class MobibotBuild extends Project {
// Google
.include(dependency("com.google.code.gson", "gson", "2.10.1"))
.include(dependency("com.google.guava", "guava", "32.1.3-jre"))
.include(dependency("com.google.cloud", "google-cloud-vertexai", version(0, 1, 0)))
// Kotlin
.include(dependency("org.jetbrains.kotlin", "kotlin-stdlib", kotlin))
.include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-common", kotlin))
.include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-jdk7", kotlin))
.include(dependency("org.jetbrains.kotlin", "kotlin-stdlib-jdk8", kotlin))
.include(dependency("org.jetbrains.kotlinx", "kotlinx-coroutines-core", "1.7.3"))
@ -100,19 +101,20 @@ public class MobibotBuild extends Project {
.include(dependency("net.aksingh", "owm-japis", "2.5.3.0"))
.include(dependency("net.objecthunter", "exp4j", "0.4.8"))
.include(dependency("org.json", "json", "20231013"))
.include(dependency("org.jsoup", "jsoup", "1.16.2"))
.include(dependency("org.jsoup", "jsoup", "1.17.1"))
// Thauvin
.include(dependency("net.thauvin.erik", "cryptoprice", "1.0.2"))
.include(dependency("net.thauvin.erik", "jokeapi", "0.9.1"))
.include(dependency("net.thauvin.erik", "pinboard-poster", "1.1.1"))
.include(dependency("net.thauvin.erik.urlencoder", "urlencoder-lib-jvm", "1.4.0"));
scope(test)
.include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 27, 0)))
.include(dependency("com.willowtreeapps.assertk", "assertk-jvm", version(0, 28, 0)))
.include(dependency("org.jetbrains.kotlin", "kotlin-test-junit5", version(1, 9, 21)))
.include(dependency("org.junit.jupiter", "junit-jupiter", version(5, 10, 1)))
.include(dependency("org.junit.platform", "junit-platform-console-standalone", version(1, 10, 1)));
List<String> 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"));
}

View file

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

View file

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

View file

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

View file

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

View file

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