From 98bcd3785b3600512bb87341dd2f508f5219cab2 Mon Sep 17 00:00:00 2001 From: jie65535 Date: Sat, 21 Dec 2024 13:58:02 +0800 Subject: [PATCH] Optimize search results Add Weather Service Agent Add Crazy Thursday Agent --- build.gradle.kts | 1 - src/main/kotlin/JChatGPT.kt | 45 +++++++++--- src/main/kotlin/tools/BaseAgent.kt | 16 +++- src/main/kotlin/tools/CrazyKfc.kt | 20 +++++ src/main/kotlin/tools/WeatherService.kt | 56 ++++++++++++++ src/main/kotlin/tools/WebSearch.kt | 98 ++++++++++++++++++++++--- 6 files changed, 211 insertions(+), 25 deletions(-) create mode 100644 src/main/kotlin/tools/CrazyKfc.kt create mode 100644 src/main/kotlin/tools/WeatherService.kt diff --git a/build.gradle.kts b/build.gradle.kts index 36247df..518820a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,7 +22,6 @@ 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 3effed1..2abf108 100644 --- a/src/main/kotlin/JChatGPT.kt +++ b/src/main/kotlin/JChatGPT.kt @@ -7,6 +7,8 @@ import com.aallam.openai.api.model.ModelId import com.aallam.openai.client.OpenAI import com.aallam.openai.client.OpenAIHost import io.ktor.util.collections.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender import net.mamoe.mirai.console.permission.PermissionId @@ -25,11 +27,13 @@ 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 top.jie65535.mirai.tools.* import java.time.OffsetDateTime +import java.time.format.TextStyle +import java.util.* import java.util.regex.Pattern import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds object JChatGPT : KotlinPlugin( JvmPluginDescription( @@ -127,7 +131,11 @@ object JChatGPT : KotlinPlugin( } private fun getSystemPrompt(): String { - return PluginConfig.prompt.replace("{time}", OffsetDateTime.now().toString()) + val now = OffsetDateTime.now() + return PluginConfig.prompt.replace( + "{time}", + "$now ${now.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.CHINA)}" + ) } private suspend fun MessageEvent.startChat(context: List? = null) { @@ -158,7 +166,7 @@ object JChatGPT : KotlinPlugin( for (toolCall in reply.toolCalls.orEmpty()) { require(toolCall is ToolCall.Function) { "Tool call is not a function" } - val functionResponse = toolCall.execute() + val functionResponse = toolCall.execute(this) history.add( ChatMessage( role = ChatRole.Tool, @@ -259,10 +267,12 @@ object JChatGPT : KotlinPlugin( } /** - * 函数映射表 + * 工具列表 */ - private val myTools = listOf( - WebSearch() + private val myTools = listOf( + WebSearch(), + WeatherService(), + CrazyKfc() ) @@ -277,7 +287,8 @@ object JChatGPT : KotlinPlugin( val request = ChatCompletionRequest( model = ModelId(PluginConfig.chatModel), messages = chatMessages, - tools = availableTools + tools = availableTools, + toolChoice = ToolChoice.Auto ) logger.info( "API Requesting..." + @@ -292,13 +303,23 @@ object JChatGPT : KotlinPlugin( 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() + private suspend fun ToolCall.Function.execute(event: MessageEvent): String { + val agent = myTools.find { it.tool.function.name == function.name } + ?: return "Function ${function.name} not found" + // 提示正在执行函数 + val receipt = if (agent.loadingMessage.isNotEmpty()) { + event.subject.sendMessage(event.message.quote() + agent.loadingMessage) + } else null + // 提取参数 + val args = function.argumentsAsJsonOrNull() logger.info("Calling ${function.name}(${args})") + // 执行函数 val result = agent.execute(args) logger.info("Result=$result") + // 过会撤回加载消息 + if (receipt != null) { + launch { delay(2.seconds); receipt.recall() } + } return result } diff --git a/src/main/kotlin/tools/BaseAgent.kt b/src/main/kotlin/tools/BaseAgent.kt index d1f9365..32c15bb 100644 --- a/src/main/kotlin/tools/BaseAgent.kt +++ b/src/main/kotlin/tools/BaseAgent.kt @@ -1,14 +1,28 @@ package top.jie65535.mirai.tools import com.aallam.openai.api.chat.Tool +import io.ktor.client.* +import io.ktor.client.engine.okhttp.* import kotlinx.serialization.json.JsonObject abstract class BaseAgent( val tool: Tool ) { + /** + * 是否启用该工具 + */ open val isEnabled: Boolean = true - abstract suspend fun execute(args: JsonObject): String + /** + * 加载时消息 可用于提示用户正在执行 + */ + open val loadingMessage: String = "" + + protected val httpClient by lazy { + HttpClient(OkHttp) + } + + abstract suspend fun execute(args: JsonObject?): String override fun toString(): String { return "${tool.function.name}: ${tool.function.description}" diff --git a/src/main/kotlin/tools/CrazyKfc.kt b/src/main/kotlin/tools/CrazyKfc.kt new file mode 100644 index 0000000..84a9ea9 --- /dev/null +++ b/src/main/kotlin/tools/CrazyKfc.kt @@ -0,0 +1,20 @@ +package top.jie65535.mirai.tools + +import com.aallam.openai.api.chat.Tool +import com.aallam.openai.api.core.Parameters +import io.ktor.client.request.* +import io.ktor.client.statement.* +import kotlinx.serialization.json.* + +class CrazyKfc : BaseAgent( + tool = Tool.function( + name = "crazyThursday", + description = "获取一条KFC疯狂星期四文案", + parameters = Parameters.Empty + ) +) { + override suspend fun execute(args: JsonObject?): String { + val response = httpClient.get("https://api.52vmy.cn/api/wl/yan/kfc") + return response.bodyAsText() + } +} \ No newline at end of file diff --git a/src/main/kotlin/tools/WeatherService.kt b/src/main/kotlin/tools/WeatherService.kt new file mode 100644 index 0000000..8cd905a --- /dev/null +++ b/src/main/kotlin/tools/WeatherService.kt @@ -0,0 +1,56 @@ +package top.jie65535.mirai.tools + +import com.aallam.openai.api.chat.Tool +import com.aallam.openai.api.core.Parameters +import io.ktor.client.request.* +import io.ktor.client.statement.* +import kotlinx.serialization.json.* + +class WeatherService : BaseAgent( + tool = Tool.function( + name = "queryWeather", + description = "可用于查询某城市地区天气.", + parameters = Parameters.buildJsonObject { + put("type", "object") + putJsonObject("properties") { + putJsonObject("city") { + put("type", "string") + put("description", "城市地区,如\"深圳市\"") + } + putJsonObject("time_range") { + put("type", "string") + putJsonArray("enum") { + add("day") + add("three") + add("many") + } + put("description", "时间范围,仅当天天气可获得最详细信息,三天和更多只能获得简单信息。") + } + } + putJsonArray("required") { + add("city") + } + } + ) +) { + override val loadingMessage: String + get() = "查询天气中..." + + override suspend fun execute(args: JsonObject?): String { + requireNotNull(args) + val city = args.getValue("city").jsonPrimitive.content + val timeRange = args["time_range"]?.jsonPrimitive?.contentOrNull + val response = httpClient.get( + buildString { + append(when (timeRange) { + "many" -> "https://api.52vmy.cn/api/query/tian/many" + "three" -> "https://api.52vmy.cn/api/query/tian/three" + else -> "https://api.52vmy.cn/api/query/tian" + }) + append("?city=") + append(city) + } + ) + return response.bodyAsText() + } +} \ No newline at end of file diff --git a/src/main/kotlin/tools/WebSearch.kt b/src/main/kotlin/tools/WebSearch.kt index 3b8c3b0..b4b0cb2 100644 --- a/src/main/kotlin/tools/WebSearch.kt +++ b/src/main/kotlin/tools/WebSearch.kt @@ -2,25 +2,51 @@ 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 +import top.jie65535.mirai.PluginConfig class WebSearch : BaseAgent( tool = Tool.function( name = "search", - description = "通过互联网搜索一切", + description = "Provides meta-search functionality through SearXNG," + + " aggregating results from multiple search engines.", parameters = Parameters.buildJsonObject { put("type", "object") putJsonObject("properties") { putJsonObject("q") { put("type", "string") - put("description", "The search query") + put("description", "查询内容关键字") + } + putJsonObject("categories") { + put("type", "array") + putJsonObject("items") { + put("type", "string") + putJsonArray("enum") { + add("general") + add("images") + add("videos") + add("news") + add("music") + add("it") + add("science") + add("files") + add("social_media") + } + } + put("description", "可选择多项查询分类,通常情况下不传或用general即可。") + } + putJsonObject("time_range") { + put("type", "string") + putJsonArray("enum") { + add("day") + add("month") + add("year") + } + put("description", "可选择获取最新消息,例如day表示只查询最近一天相关信息,以此类推。") } } putJsonArray("required") { @@ -35,16 +61,66 @@ class WebSearch : BaseAgent( override val isEnabled: Boolean get() = PluginConfig.searXngUrl.isNotEmpty() - private val httpClient by lazy { - HttpClient(OkHttp) - } + override val loadingMessage: String + get() = "联网搜索中..." - override suspend fun execute(args: JsonObject): String { + override suspend fun execute(args: JsonObject?): String { + requireNotNull(args) val q = args.getValue("q").jsonPrimitive.content + val categories = args["categories"]?.jsonArray + val timeRange = args["time_range"]?.jsonPrimitive?.contentOrNull val response = httpClient.get( - "${PluginConfig.searXngUrl}?q=${q.encodeURLParameter(true)}&format=json" + buildString { + append(PluginConfig.searXngUrl) + append("?q=") + append(q.encodeURLParameter()) + append("&format=json") + if (categories != null) { + append("&") + append(categories.joinToString { it.jsonPrimitive.content }) + } + if (timeRange != null) { + append("&") + append(timeRange) + } + } ) val body = response.bodyAsText() - return StringEscapeUtils.unescapeJava(body) + val unescapedBody = StringEscapeUtils.unescapeJava(body) + val responseJsonElement = Json.parseToJsonElement(unescapedBody) + return buildJsonObject { + val root = responseJsonElement.jsonObject + // 查询内容原样转发 + root["query"]?.let { put("query", it) } + + // 过滤搜索结果 + val results = root["results"]?.jsonArray + if (results != null) { + val filteredResults = results + .filter { + // 去掉所有内容为空的结果 + !it.jsonObject.getValue("content").jsonPrimitive.contentOrNull.isNullOrEmpty() + }.sortedByDescending { + it.jsonObject.getValue("score").jsonPrimitive.double + }.take(5) // 只取得分最高的前5条结果 + .map { + // 移除掉我不想要的字段 + val item = it.jsonObject.toMutableMap() + item.remove("engine") + item.remove("parsed_url") + item.remove("template") + item.remove("engines") + item.remove("positions") + item.remove("metadata") + item.remove("thumbnail") + JsonObject(item) + } + put("results", JsonArray(filteredResults)) + } + + // 答案和信息盒子原样转发 + root["answers"]?.let { put("answers", it) } + root["infoboxes"]?.let { put("infoboxes", it) } + }.toString() } } \ No newline at end of file