diff --git a/build.gradle.kts b/build.gradle.kts index 6c4b063..36247df 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } group = "top.jie65535.mirai" -version = "1.2.1" +version = "1.3.0" repositories { mavenCentral() @@ -17,9 +17,12 @@ repositories { val openaiClientVersion = "3.8.2" val ktorVersion = "2.3.12" val jLatexMathVersion = "1.0.7" +val commonTextVersion = "1.13.0" dependencies { implementation("com.aallam.openai:openai-client:$openaiClientVersion") implementation("io.ktor:ktor-client-okhttp:$ktorVersion") + //implementation("io.ktor:ktor-client-okhttp-jvm:$ktorVersion") implementation("org.scilab.forge:jlatexmath:$jLatexMathVersion") + implementation("org.apache.commons:commons-text:$commonTextVersion") } \ No newline at end of file diff --git a/src/main/kotlin/JChatGPT.kt b/src/main/kotlin/JChatGPT.kt index fa4c108..3effed1 100644 --- a/src/main/kotlin/JChatGPT.kt +++ b/src/main/kotlin/JChatGPT.kt @@ -1,8 +1,6 @@ package top.jie65535.mirai -import com.aallam.openai.api.chat.ChatCompletionRequest -import com.aallam.openai.api.chat.ChatMessage -import com.aallam.openai.api.chat.ChatRole +import com.aallam.openai.api.chat.* import com.aallam.openai.api.core.Role import com.aallam.openai.api.http.Timeout import com.aallam.openai.api.model.ModelId @@ -27,6 +25,8 @@ import net.mamoe.mirai.message.data.MessageSource.Key.quote import net.mamoe.mirai.message.sourceIds import net.mamoe.mirai.utils.ExternalResource.Companion.toExternalResource import net.mamoe.mirai.utils.info +import top.jie65535.mirai.tools.BaseAgent +import top.jie65535.mirai.tools.WebSearch import java.time.OffsetDateTime import java.util.regex.Pattern import kotlin.time.Duration.Companion.milliseconds @@ -35,7 +35,7 @@ object JChatGPT : KotlinPlugin( JvmPluginDescription( id = "top.jie65535.mirai.JChatGPT", name = "J ChatGPT", - version = "1.2.1", + version = "1.3.0", ) { author("jie65535") } @@ -147,10 +147,32 @@ object JChatGPT : KotlinPlugin( subject.sendMessage(message.quote() + "再等等...") return } - val reply = chatCompletion(history) - history.add(reply) - val content = reply.content ?: "..." + var done: Boolean + var retry = 2 + var hasTools = true + do { + val reply = chatCompletion(history, hasTools) + history.add(reply) + done = true + + for (toolCall in reply.toolCalls.orEmpty()) { + require(toolCall is ToolCall.Function) { "Tool call is not a function" } + val functionResponse = toolCall.execute() + history.add( + ChatMessage( + role = ChatRole.Tool, + toolCallId = toolCall.id, + name = toolCall.function.name, + content = functionResponse + ) + ) + done = false + hasTools = false + } + } while (!done && 0 < --retry) + + val content = history.last().content ?: "..." val replyMsg = subject.sendMessage( if (content.length < 128) { message.quote() + toMessage(subject, content) @@ -200,10 +222,11 @@ object JChatGPT : KotlinPlugin( * @return 构造的消息 */ private suspend fun toMessage(contact: Contact, content: String): Message { - if (content.length < 3) { - return PlainText(content) - } - return buildMessageChain { + return if (content.isEmpty()) { + PlainText("...") + } else if (content.length < 3) { + PlainText(content) + } else buildMessageChain { // 匹配LaTeX表达式 val matcher = laTeXPattern.matcher(content) var index = 0 @@ -235,14 +258,48 @@ object JChatGPT : KotlinPlugin( } } - private suspend fun chatCompletion(messages: List): ChatMessage { + /** + * 函数映射表 + */ + private val myTools = listOf( + WebSearch() + ) + + + private suspend fun chatCompletion( + chatMessages: List, + hasTools: Boolean = true + ): ChatMessage { val openAi = this.openAi ?: throw NullPointerException("OpenAI Token 未设置,无法开始") - val request = ChatCompletionRequest(ModelId(PluginConfig.chatModel), messages) - logger.info("OpenAI API Requesting... Model=${PluginConfig.chatModel}") + val availableTools = if (hasTools) { + myTools.filter { it.isEnabled }.map { it.tool } + } else null + val request = ChatCompletionRequest( + model = ModelId(PluginConfig.chatModel), + messages = chatMessages, + tools = availableTools + ) + logger.info( + "API Requesting..." + + " Model=${PluginConfig.chatModel}" + + " Tools=${availableTools?.joinToString(prefix = "[", postfix = "]")}" + ) val response = openAi.chatCompletion(request) - logger.info("OpenAI API Usage: ${response.usage}") - return response.choices.first().message + val message = response.choices.first().message + logger.info("Response: $message ${response.usage}") + return message } private fun MessageChain.plainText() = this.filterIsInstance().joinToString().trim() + + private suspend fun ToolCall.Function.execute(): String { + val agent = + myTools.find { it.tool.function.name == function.name } ?: error("Function ${function.name} not found") + val args = function.argumentsAsJson() + logger.info("Calling ${function.name}(${args})") + val result = agent.execute(args) + logger.info("Result=$result") + return result + } + } \ No newline at end of file diff --git a/src/main/kotlin/PluginConfig.kt b/src/main/kotlin/PluginConfig.kt index 09e3d68..0b9950d 100644 --- a/src/main/kotlin/PluginConfig.kt +++ b/src/main/kotlin/PluginConfig.kt @@ -25,4 +25,7 @@ object PluginConfig : AutoSavePluginConfig("Config") { @ValueDescription("等待响应超时时间,单位毫秒,默认60秒") val timeout: Long by value(60000L) + + @ValueDescription("SearXNG 搜索引擎地址,如 http://127.0.0.1:8080/search 必须启用允许json格式返回") + val searXngUrl: String by value("") } \ No newline at end of file diff --git a/src/main/kotlin/tools/BaseAgent.kt b/src/main/kotlin/tools/BaseAgent.kt new file mode 100644 index 0000000..d1f9365 --- /dev/null +++ b/src/main/kotlin/tools/BaseAgent.kt @@ -0,0 +1,16 @@ +package top.jie65535.mirai.tools + +import com.aallam.openai.api.chat.Tool +import kotlinx.serialization.json.JsonObject + +abstract class BaseAgent( + val tool: Tool +) { + open val isEnabled: Boolean = true + + abstract suspend fun execute(args: JsonObject): String + + override fun toString(): String { + return "${tool.function.name}: ${tool.function.description}" + } +} \ No newline at end of file diff --git a/src/main/kotlin/tools/WebSearch.kt b/src/main/kotlin/tools/WebSearch.kt new file mode 100644 index 0000000..3b8c3b0 --- /dev/null +++ b/src/main/kotlin/tools/WebSearch.kt @@ -0,0 +1,50 @@ +package top.jie65535.mirai.tools + +import com.aallam.openai.api.chat.Tool +import com.aallam.openai.api.core.Parameters +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.serialization.json.* +import top.jie65535.mirai.PluginConfig +import org.apache.commons.text.StringEscapeUtils + +class WebSearch : BaseAgent( + tool = Tool.function( + name = "search", + description = "通过互联网搜索一切", + parameters = Parameters.buildJsonObject { + put("type", "object") + putJsonObject("properties") { + putJsonObject("q") { + put("type", "string") + put("description", "The search query") + } + } + putJsonArray("required") { + add("q") + } + } + ) +) { + /** + * 插件配置了 SearXNG URL 时才允许启用 + */ + override val isEnabled: Boolean + get() = PluginConfig.searXngUrl.isNotEmpty() + + private val httpClient by lazy { + HttpClient(OkHttp) + } + + override suspend fun execute(args: JsonObject): String { + val q = args.getValue("q").jsonPrimitive.content + val response = httpClient.get( + "${PluginConfig.searXngUrl}?q=${q.encodeURLParameter(true)}&format=json" + ) + val body = response.bodyAsText() + return StringEscapeUtils.unescapeJava(body) + } +} \ No newline at end of file