Compare commits

...

3 Commits

Author SHA1 Message Date
f822999ab4 Add HTTP Client timeout config 2025-07-13 18:20:24 +08:00
3c4373e1ff Add Visual Agent 2025-07-11 16:50:59 +08:00
89794b587e Add visit web tool
Add send message tools
Update version to v1.7.0
2025-07-11 13:08:06 +08:00
14 changed files with 889 additions and 361 deletions

View File

@ -7,7 +7,7 @@ plugins {
} }
group = "top.jie65535.mirai" group = "top.jie65535.mirai"
version = "1.5.0" version = "1.7.0"
mirai { mirai {
jvmTarget = JavaVersion.VERSION_11 jvmTarget = JavaVersion.VERSION_11

View File

@ -4,14 +4,11 @@ import com.aallam.openai.api.chat.ChatCompletionRequest
import com.aallam.openai.api.chat.ChatMessage import com.aallam.openai.api.chat.ChatMessage
import com.aallam.openai.api.chat.ChatRole import com.aallam.openai.api.chat.ChatRole
import com.aallam.openai.api.chat.ToolCall import com.aallam.openai.api.chat.ToolCall
import com.aallam.openai.api.http.Timeout
import com.aallam.openai.api.model.ModelId import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.Chat
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIHost
import io.ktor.util.collections.* import io.ktor.util.collections.*
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register
import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender import net.mamoe.mirai.console.command.CommandSender.Companion.toCommandSender
import net.mamoe.mirai.console.permission.PermissionId import net.mamoe.mirai.console.permission.PermissionId
@ -26,34 +23,31 @@ import net.mamoe.mirai.event.events.FriendMessageEvent
import net.mamoe.mirai.event.events.GroupMessageEvent import net.mamoe.mirai.event.events.GroupMessageEvent
import net.mamoe.mirai.event.events.MessageEvent import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.data.* import net.mamoe.mirai.message.data.*
import net.mamoe.mirai.message.data.Image.Key.queryUrl
import net.mamoe.mirai.message.data.MessageSource.Key.quote 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.ExternalResource.Companion.toExternalResource
import net.mamoe.mirai.utils.info import net.mamoe.mirai.utils.info
import top.jie65535.mirai.tools.* import top.jie65535.mirai.tools.*
import xyz.cssxsh.mirai.hibernate.MiraiHibernateRecorder import xyz.cssxsh.mirai.hibernate.MiraiHibernateRecorder
import xyz.cssxsh.mirai.hibernate.entry.MessageRecord
import java.time.Instant import java.time.Instant
import java.time.OffsetDateTime import java.time.OffsetDateTime
import java.time.ZoneOffset import java.time.ZoneOffset
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.util.regex.Pattern
import kotlin.collections.* import kotlin.collections.*
import kotlin.math.max import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
object JChatGPT : KotlinPlugin( object JChatGPT : KotlinPlugin(
JvmPluginDescription( JvmPluginDescription(
id = "top.jie65535.mirai.JChatGPT", id = "top.jie65535.mirai.JChatGPT",
name = "J ChatGPT", name = "J ChatGPT",
version = "1.5.0", version = "1.7.0",
) { ) {
author("jie65535") author("jie65535")
// dependsOn("xyz.cssxsh.mirai.plugin.mirai-hibernate-plugin", true) // dependsOn("xyz.cssxsh.mirai.plugin.mirai-hibernate-plugin", true)
} }
) { ) {
private var llm: Chat? = null
/** /**
* 是否包含历史对话 * 是否包含历史对话
*/ */
@ -61,15 +55,15 @@ object JChatGPT : KotlinPlugin(
val chatPermission = PermissionId("JChatGPT", "Chat") val chatPermission = PermissionId("JChatGPT", "Chat")
private var keyword: Regex? = null
override fun onEnable() { override fun onEnable() {
// 注册聊天权限 // 注册聊天权限
PermissionService.INSTANCE.register(chatPermission, "JChatGPT Chat Permission") PermissionService.INSTANCE.register(chatPermission, "JChatGPT Chat Permission")
PluginConfig.reload() PluginConfig.reload()
// 设置Token // 设置Token
if (PluginConfig.openAiToken.isNotEmpty()) { LargeLanguageModels.reload()
updateOpenAiToken(PluginConfig.openAiToken)
}
// 注册插件命令 // 注册插件命令
PluginCommands.register() PluginCommands.register()
@ -82,36 +76,25 @@ object JChatGPT : KotlinPlugin(
false false
} }
if (PluginConfig.callKeyword.isNotEmpty()) {
keyword = Regex(PluginConfig.callKeyword)
}
GlobalEventChannel.parentScope(this) GlobalEventChannel.parentScope(this)
.subscribeAlways<MessageEvent> { event -> onMessage(event) } .subscribeAlways<MessageEvent> { event -> onMessage(event) }
logger.info { "Plugin loaded" } logger.info { "Plugin loaded" }
} }
fun updateOpenAiToken(token: String) {
val timeout = PluginConfig.timeout.milliseconds
llm = OpenAI(
token,
host = OpenAIHost(baseUrl = PluginConfig.openAiApi),
timeout = Timeout(request = timeout, connect = timeout, socket = timeout),
// logging = LoggingConfig(LogLevel.All)
)
reasoningAgent.llm = llm
}
private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss") private val timeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss")
.withZone(ZoneOffset.systemDefault()) .withZone(ZoneOffset.systemDefault())
private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd E HH:mm:ss") private val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy年MM月dd E HH:mm:ss")
// private val userContext = ConcurrentMap<Long, MutableList<ChatMessage>>()
private const val REPLAY_QUEUE_MAX = 10
private val replyMap = ConcurrentMap<Int, MutableList<ChatMessage>>(REPLAY_QUEUE_MAX)
private val replyQueue = mutableListOf<Int>()
private val requestMap = ConcurrentSet<Long>() private val requestMap = ConcurrentSet<Long>()
private suspend fun onMessage(event: MessageEvent) { private suspend fun onMessage(event: MessageEvent) {
// 检查Token是否设置 // 检查Token是否设置
if (llm == null) return if (LargeLanguageModels.chat == null) return
// 发送者是否有权限 // 发送者是否有权限
if (!event.toCommandSender().hasPermission(chatPermission)) { if (!event.toCommandSender().hasPermission(chatPermission)) {
if (event is GroupMessageEvent) { if (event is GroupMessageEvent) {
@ -132,34 +115,12 @@ object JChatGPT : KotlinPlugin(
} }
} }
// 是否@bot // 如果没有@bot或者触发关键字则直接结束
val isAtBot = event.message.contains(At(event.bot)) if (!event.message.contains(At(event.bot))
// 是否包含引用消息 && keyword?.let { event.message.content.contains(it) } != true)
val quote = event.message[QuoteReply]
// 如果没有@bot或者引用消息则直接结束
if (!isAtBot && quote == null)
return return
// 如果有引用消息,则尝试从回复记录中找到对应消息 startChat(event)
var context: List<ChatMessage>? = if (quote != null) {
replyMap[quote.source.ids[0]]
} else null
// 如果没有At机器人同时上下文是空的直接忽略
if (!isAtBot && context == null) return
if (context == null) {
// 如果没有上下文但是引用了消息并且at了机器人则用引用的消息内容作为上下文
if (quote != null) {
val msg = quote.source.originalMessage.plainText()
if (msg.isNotEmpty()) {
context = listOf(ChatMessage(ChatRole.User, msg))
}
}
}
startChat(event, context)
} }
private fun getSystemPrompt(event: MessageEvent): String { private fun getSystemPrompt(event: MessageEvent): String {
@ -178,126 +139,206 @@ object JChatGPT : KotlinPlugin(
replace("{subject}") { replace("{subject}") {
if (event is GroupMessageEvent) { if (event is GroupMessageEvent) {
"\"${event.subject.name}\" 群聊中" "\"${event.subject.name}\" 群聊中,你在本群的名片是:${getNameCard(event.subject.botAsMember)}"
} else { } else {
"私聊中" "\"${event.senderName}\" 私聊中"
} }
} }
return prompt.toString()
}
// replace("{sender}") { // region - 历史消息相关 -
// if (event is GroupMessageEvent) {
// event.sender.specialTitle
// val permissionName = when (event.sender.permission) {
// MEMBER -> "普通群员"
// ADMINISTRATOR -> "管理员"
// OWNER -> "群主"
// }
// "\"${event.senderName}\" 身份:$permissionName"
// } else {
// "\"${event.senderName}\""
// }
// }
replace("{history}") { /**
* 获取历史消息
* @param event 消息事件
* @return 如果未获取到则返回空字符串
*/
private fun getHistory(event: MessageEvent): String {
if (!includeHistory) { if (!includeHistory) {
return@replace "暂无内容" return event.message.content
} }
val now = OffsetDateTime.now()
// 一段时间内的消息 // 一段时间内的消息
val beforeTimestamp = now.minusMinutes(PluginConfig.historyWindowMin.toLong()).toEpochSecond().toInt() val beforeTimestamp = now.minusMinutes(PluginConfig.historyWindowMin.toLong()).toEpochSecond().toInt()
val nowTimestamp = now.toEpochSecond().toInt() return getAfterHistory(beforeTimestamp, event)
}
/**
* 获取指定时间后的历史消息
* @param time Epoch时间戳
* @param event 消息事件
* @return 如果未获取到则返回空字符串
*/
private fun getAfterHistory(time: Int, event: MessageEvent): String {
if (!includeHistory) {
return ""
}
// 现在时间
val nowTimestamp = OffsetDateTime.now().toEpochSecond().toInt()
// 最近这段时间的历史对话 // 最近这段时间的历史对话
val history = MiraiHibernateRecorder[event.subject, beforeTimestamp, nowTimestamp] val history = MiraiHibernateRecorder[event.subject, time, nowTimestamp]
.take(PluginConfig.historyMessageLimit) // 只取最近的部分消息,避免上下文过长 .take(PluginConfig.historyMessageLimit) // 只取最近的部分消息,避免上下文过长
.sortedBy { it.time } // 按时间排序 .sortedBy { it.time } // 按时间排序
// 构造历史消息 // 构造历史消息
val historyText = StringBuilder() val historyText = StringBuilder()
if (event is GroupMessageEvent) { if (event is GroupMessageEvent) {
for (record in history) { for (record in history) {
appendGroupMessageRecord(historyText, record, event)
}
} else {
for (record in history) {
appendMessageRecord(historyText, record, event)
}
}
return historyText.toString()
}
/**
* 添加群消息记录到历史上下文中
* @param historyText 历史消息构造器
* @param record 群消息记录
* @param event 群消息事件
*/
fun appendGroupMessageRecord(
historyText: StringBuilder,
record: MessageRecord,
event: GroupMessageEvent
) {
if (event.bot.id == record.fromId) { if (event.bot.id == record.fromId) {
historyText.append("") historyText.append("**** " + getNameCard(event.subject.botAsMember))
} else { } else {
val recordSender = event.subject[record.fromId] historyText.append(getNameCard(event.subject, record.fromId))
if (recordSender != null) {
// 群活跃等级
historyText.append(getNameCard(recordSender))
} else {
// 未知群员
historyText.append("未知群员(").append(record.fromId).append(")")
} }
}
historyText
.append(" ")
// 发言时间 // 发言时间
historyText.append(' ')
.append(timeFormatter.format(Instant.ofEpochSecond(record.time.toLong()))) .append(timeFormatter.format(Instant.ofEpochSecond(record.time.toLong())))
val recordMessage = record.toMessageChain()
recordMessage[QuoteReply.Key]?.let {
historyText.append(" 引用 ${getNameCard(event.subject, it.source.fromId)} 说的\n > ")
.appendLine(it.source.originalMessage.content.replace("\n", "\n > "))
}
// 消息内容 // 消息内容
.append(" 说:").appendLine(record.toMessageChain().joinToString("") { historyText.append(" 说:").appendLine(record.toMessageChain().joinToString("") {
when (it) { when (it) {
is At -> { is At -> {
it.getDisplay(event.subject) it.getDisplay(event.subject)
} }
is ForwardMessage -> { else -> singleMessageToText(it)
it.title + "\n" + it.preview
}
is QuoteReply -> {
">" + it.source.originalMessage.contentToString().replace("\n", "\n> ") + "\n"
}
else -> {
it.contentToString()
}
} }
}) })
} }
private fun getNameCard(group: Group, qq: Long): String {
val member = group[qq]
return if (member == null) {
"未知群员($qq)"
} else { } else {
// TODO 私聊 getNameCard(member)
}
} }
historyText.toString() /**
* 添加消息记录到历史上下文中
* @param historyText 历史消息构造器
* @param record 消息记录
* @param event 消息事件
*/
fun appendMessageRecord(
historyText: StringBuilder,
record: MessageRecord,
event: MessageEvent
) {
if (event.bot.id == record.fromId) {
historyText.append("**你** " + event.bot.nameCardOrNick)
} else {
historyText.append(event.senderName)
}
historyText
.append(" ")
// 发言时间
.append(timeFormatter.format(Instant.ofEpochSecond(record.time.toLong())))
val recordMessage = record.toMessageChain()
recordMessage[QuoteReply.Key]?.let {
historyText.append(" 引用\n > ")
.appendLine(it.source.originalMessage
.joinToString("", transform = ::singleMessageToText)
.replace("\n", "\n > "))
}
// 消息内容
historyText.append(" 说:").appendLine(
record.toMessageChain().joinToString("", transform = ::singleMessageToText))
} }
return prompt.toString() private fun singleMessageToText(it: SingleMessage): String {
return when (it) {
is ForwardMessage -> {
it.title + "\n " + it.preview
}
// 图片格式化
is Image -> {
try {
val imageUrl = runBlocking {
it.queryUrl()
}
"![图片]($imageUrl)"
} catch (e: Throwable) {
logger.warning("图片地址获取失败", e)
it.content
}
}
else -> it.content
}
}
// endregion - 历史消息相关 -
private val thinkRegex = Regex("<think>[\\s\\S]*?</think>")
private suspend fun startChat(event: MessageEvent) {
if (!requestMap.add(event.sender.id)) {
event.subject.sendMessage("再等等...")
return
} }
private suspend fun startChat(event: MessageEvent, context: List<ChatMessage>? = null) {
val history = mutableListOf<ChatMessage>() val history = mutableListOf<ChatMessage>()
if (!context.isNullOrEmpty()) { if (PluginConfig.prompt.isNotEmpty()) {
history.addAll(context)
} else if (PluginConfig.prompt.isNotEmpty()) {
val prompt = getSystemPrompt(event) val prompt = getSystemPrompt(event)
if (PluginConfig.logPrompt) { if (PluginConfig.logPrompt) {
logger.info("Prompt: $prompt") logger.info("Prompt: $prompt")
} }
history.add(ChatMessage(ChatRole.System, prompt)) history.add(ChatMessage(ChatRole.System, prompt))
} }
val msg = event.message.plainText() val historyText = getHistory(event)
if (msg.isNotEmpty()) { logger.info("History: $historyText")
history.add(ChatMessage(ChatRole.User, if (event is GroupMessageEvent) { history.add(ChatMessage.User(historyText))
"${getNameCard(event.sender)} 说:$msg"
} else {
msg
}))
}
try { try {
if (!requestMap.add(event.sender.id)) { var done: Boolean
event.subject.sendMessage(event.message.quote() + "再等等...") // 至少循环3次
return var retry = max(PluginConfig.retryMax, 3)
}
var done = true
// 至少重试两次
var retry = max(PluginConfig.retryMax, 2)
do { do {
try { try {
val reply = chatCompletion(history, retry > 1) val startedAt = OffsetDateTime.now().toEpochSecond().toInt()
history.add(reply) val response = chatCompletion(history)
done = true // 移除思考内容
val responseContent = response.content?.replace(thinkRegex, "")?.trim()
history.add(ChatMessage.Assistant(
content = responseContent,
name = response.name,
toolCalls = response.toolCalls
))
for (toolCall in reply.toolCalls.orEmpty()) { if (response.toolCalls.isNullOrEmpty()) {
done = true
} else {
done = false
// 处理函数调用
for (toolCall in response.toolCalls) {
require(toolCall is ToolCall.Function) { "Tool call is not a function" } require(toolCall is ToolCall.Function) { "Tool call is not a function" }
val functionResponse = toolCall.execute(event) val functionResponse = toolCall.execute(event)
history.add( history.add(
@ -308,102 +349,255 @@ object JChatGPT : KotlinPlugin(
content = functionResponse content = functionResponse
) )
) )
done = false if (toolCall.function.name == "endConversation") {
done = true
}
}
}
if (!done) {
history.add(ChatMessage.User(
buildString {
append("系统提示:本次运行还剩${retry-1}")
// if (response.toolCalls.isNullOrEmpty()) {
// append("\n在上一轮对话中未检测到调用任何工具请检查工具调用语法是否正确")
// append("\n如果你确实不需要调用其它工具比如发送消息请调用`endConversation`来结束对话。")
// }
val newMessages = getAfterHistory(startedAt, event)
if (newMessages.isNotEmpty()) {
append("\n以下是上次运行至今的新消息\n\n$newMessages")
}
}
))
} }
} catch (e: Exception) { } catch (e: Exception) {
if (retry <= 1) { if (retry <= 1) {
throw e throw e
} else { } else {
done = false
logger.warning("调用llm时发生异常重试中", e) logger.warning("调用llm时发生异常重试中", e)
event.subject.sendMessage(event.message.quote() + "出错了...正在重试...") event.subject.sendMessage(event.message.quote() + "出错了...正在重试...")
} }
} }
} while (!done && 0 <-- retry) } while (!done && 0 < --retry)
val content = history.last().content ?: "..."
val replyMsg = event.subject.sendMessage(
if (content.length < PluginConfig.messageMergeThreshold) {
event.message.quote() + toMessage(event.subject, content)
} else {
// 消息内容太长则转为转发消息避免刷屏
event.buildForwardMessage {
event.bot says toMessage(event.subject, content)
}
// 不再将历史对话记录加入其中
// event.buildForwardMessage {
// for (item in history) {
// if (item.content.isNullOrEmpty())
// continue
// val temp = toMessage(event.subject, item.content!!)
// when (item.role) {
// Role.User -> event.sender says temp
// Role.Assistant -> event.bot says temp
// }
// }
//
// // 检查并移除超出转发消息上限的消息
// var isOverflow = false
// var count = 0
// for (i in size - 1 downTo 0) {
// if (count > 4900) {
// isOverflow = true
// // 删除早期上下文消息
// removeAt(i)
// } else {
// for (text in this[i].messageChain.filterIsInstance<PlainText>()) {
// count += text.content.length
// }
// }
// }
// if (count > 5000) {
// removeAt(0)
// }
// if (isOverflow) {
// // 如果溢出了,插入一条提示到最开始
// add(
// 0, ForwardMessage.Node(
// senderId = event.bot.id,
// time = this[0].time - 1,
// senderName = event.bot.nameCardOrNick,
// message = PlainText("更早的消息已隐藏,避免超出转发消息上限。")
// )
// )
// }
// }
}
)
// 将回复的消息和对话历史保存到队列
if (replyMsg.sourceIds.isNotEmpty()) {
val msgId = replyMsg.sourceIds[0]
replyMap[msgId] = history
replyQueue.add(msgId)
}
// 移除超出队列的对话
if (replyQueue.size > REPLAY_QUEUE_MAX) {
replyMap.remove(replyQueue.removeAt(0))
}
} catch (ex: Throwable) { } catch (ex: Throwable) {
logger.warning(ex) logger.warning(ex)
event.subject.sendMessage(event.message.quote() + "很抱歉,发生异常,请稍后重试") event.subject.sendMessage(event.message.quote() + "很抱歉,发生异常,请稍后重试")
} finally { } finally {
// 一段时间后才允许再次提问,防止高频对话
launch {
delay(5.seconds)
requestMap.remove(event.sender.id) requestMap.remove(event.sender.id)
} }
// catch (ex: OpenAITimeoutException) { }
// event.subject.sendMessage(event.message.quote() + "很抱歉,服务器没响应,请稍后重试")
// }
} }
private val laTeXPattern = Pattern.compile( // private suspend fun startChat(event: MessageEvent) {
// if (!requestMap.add(event.sender.id)) {
// // CD中不再引用消息否则可能导致和机器人无限循环对话
// event.subject.sendMessage("再等等...")
// return
// }
//
// val history = mutableListOf<ChatMessage>()
// if (PluginConfig.prompt.isNotEmpty()) {
// val prompt = getSystemPrompt(event)
// if (PluginConfig.logPrompt) {
// logger.info("Prompt: $prompt")
// }
// history.add(ChatMessage(ChatRole.System, prompt))
// }
// val historyText = getHistory(event)
// logger.info("History: $historyText")
// history.add(ChatMessage.User(historyText))
//
// try {
// var done = true
// // 至少重试两次
// var retry = max(PluginConfig.retryMax, 3)
// val finalToolCalls = mutableMapOf<Int, ToolCall.Function>()
// val contentBuilder = StringBuilder()
// do {
// finalToolCalls.clear()
// contentBuilder.setLength(0)
//
// try {
// var sent = false
// // 流式处理响应
// withTimeout(PluginConfig.timeout) {
// chatCompletions(history, retry > 1).collect { chunk ->
// val delta = chunk.choices[0].delta
// if (delta == null) return@collect
//
// // 处理工具调用
// val toolCalls = delta.toolCalls
// if (toolCalls != null) {
// for (toolCall in toolCalls) {
// val index = toolCall.index
// val toolId = toolCall.id
// val function = toolCall.function
// // 取出未完成的函数调用
// val incompleteCall = finalToolCalls[index]
// // 如果是新的函数调用,保存起来
// if (incompleteCall == null && toolId != null && function != null) {
// // 添加函数调用
// finalToolCalls[index] = ToolCall.Function(toolId, function)
// } else if (incompleteCall != null && function != null && function.argumentsOrNull != null) {
// // 更新参数内容
// finalToolCalls[index] = incompleteCall.copy(
// function = incompleteCall.function.copy(
// argumentsOrNull = incompleteCall.function.arguments + function.arguments
// )
// )
// }
// }
// }
//
// // 处理响应内容
// val contentChunk = delta.content
// // 避免连续发送多次,只拆分第一次进行发送
// if (contentChunk != null && !sent) {
// // 填入内容
// contentBuilder.append(contentChunk)
// sent = parseStreamingContent(contentBuilder, event)
// }
// }
// }
//
// val lastBlock = contentBuilder.toString().trim()
// if (lastBlock.isNotEmpty()) {
// event.subject.sendMessage(
// if (lastBlock.length > PluginConfig.messageMergeThreshold) {
// event.buildForwardMessage {
// event.bot says toMessage(event.subject, lastBlock)
// }
// } else {
// toMessage(event.subject, lastBlock)
// }
// )
// }
//
// if (finalToolCalls.isNotEmpty()) {
// val toolCalls = finalToolCalls.values.toList()
// history.add(ChatMessage.Assistant(toolCalls = toolCalls))
// for (toolCall in toolCalls) {
// val functionResponse = toolCall.execute(event)
// history.add(
// ChatMessage(
// role = ChatRole.Tool,
// toolCallId = toolCall.id,
// name = toolCall.function.name,
// content = functionResponse
// )
// )
// done = false
// }
// } else {
// done = true
// }
// } catch (e: Exception) {
// if (retry <= 1) {
// throw e
// } else {
// done = false
// logger.warning("调用llm时发生异常重试中", e)
// event.subject.sendMessage(event.message.quote() + "出错了...正在重试...")
// }
// }
// } while (!done && 0 < --retry)
// } catch (ex: Throwable) {
// logger.warning(ex)
// event.subject.sendMessage(event.message.quote() + "很抱歉,发生异常,请稍后重试")
// } finally {
// // 一段时间后才允许再次提问,防止高频对话
// launch {
// delay(10.seconds)
// requestMap.remove(event.sender.id)
// }
// }
// }
// /**
// * 解析流消息
// */
// private fun parseStreamingContent(contentBuilder: StringBuilder, event: MessageEvent): Boolean {
// // 处理推理内容
// val thinkBeginAt = contentBuilder.indexOf("<think")
// if (thinkBeginAt >= 0) {
// val thinkEndAt = contentBuilder.indexOf("</think>")
// if (thinkEndAt > 0) {
// // 去除思考内容
// contentBuilder.delete(thinkBeginAt, thinkEndAt + "</think>".length)
// }
// // 跳过本轮处理
// return false
// }
//
// // 处理代码块
// val codeBlockBeginAt = contentBuilder.indexOf("```")
// if (codeBlockBeginAt >= 0) {
// val codeBlockEndAt = contentBuilder.indexOf("```", codeBlockBeginAt + 3)
// if (codeBlockEndAt >= 0) {
// val codeBlockContentBegin = contentBuilder.indexOf("\n", codeBlockBeginAt + 3)
// if (codeBlockContentBegin in codeBlockBeginAt..codeBlockEndAt) {
// val codeBlockContent = contentBuilder.substring(codeBlockContentBegin, codeBlockEndAt).trim()
// contentBuilder.delete(codeBlockBeginAt, codeBlockEndAt + 3)
// launch {
// // 发送代码块内容
// event.subject.sendMessage(
// if (codeBlockContent.length < PluginConfig.messageMergeThreshold) {
// toMessage(event.subject, codeBlockContent)
// } else {
// // 消息内容太长则转为转发消息避免刷屏
// event.buildForwardMessage {
// event.bot says toMessage(event.subject, codeBlockContent)
// }
// }
// )
// }
// }
// }
// // 跳过本轮处理
// return true
// }
//
// // 徒手trimStart
// var contentBeginAt = 0
// while (contentBeginAt < contentBuilder.length) {
// if (contentBuilder[contentBeginAt].isWhitespace()) {
// contentBeginAt++
// } else {
// break
// }
// }
//
// // 对空行进行分割输出
// val emptyLineAt = contentBuilder.indexOf("\n\n", contentBeginAt)
// if (emptyLineAt > 0) {
// val lineContent = contentBuilder.substring(contentBeginAt, emptyLineAt)
// contentBuilder.delete(0, emptyLineAt + 2)
// launch {
// // 发送消息内容
// event.subject.sendMessage(toMessage(event.subject, lineContent))
// }
// return true
// }
// return false
// }
private val regexAtQq = Regex("@(\\d+)")
private val regexLaTeX = Regex(
"\\\\\\((.+?)\\\\\\)|" + // 匹配行内公式 \(...\) "\\\\\\((.+?)\\\\\\)|" + // 匹配行内公式 \(...\)
"\\\\\\[(.+?)\\\\\\]|" + // 匹配独立公式 \[...\] "\\\\\\[(.+?)\\\\\\]|" + // 匹配独立公式 \[...\]
"\\$\\$([^$]+?)\\$\\$|" + // 匹配独立公式 $$...$$ "\\$\\s(.+?)\\s\\$|" // 匹配行内公式 $...$
"\\$\\s(.+?)\\s\\$|" + // 匹配行内公式 $...$
"```latex\\s*([^`]+?)\\s*```" // 匹配 ```latex ... ```
, Pattern.DOTALL
) )
private data class MessageChunk(val range: IntRange, val content: Message)
/** /**
* 将聊天内容转为聊天消息如果聊天中包含LaTeX表达式将会转为图片拼接到消息中 * 将聊天内容转为聊天消息如果聊天中包含LaTeX表达式将会转为图片拼接到消息中
* *
@ -411,74 +605,112 @@ object JChatGPT : KotlinPlugin(
* @param content 文本内容 * @param content 文本内容
* @return 构造的消息 * @return 构造的消息
*/ */
private suspend fun toMessage(contact: Contact, content: String): Message { suspend fun toMessage(contact: Contact, content: String): Message {
return if (content.isEmpty()) { return if (content.isEmpty()) {
PlainText("...") PlainText("...")
} else if (content.length < 3) { } else if (content.length < 3) {
PlainText(content) PlainText(content)
} else buildMessageChain { } else {
// 匹配LaTeX表达式 val t = mutableListOf<MessageChunk>()
val matcher = laTeXPattern.matcher(content) regexAtQq.findAll(content).forEach {
var index = 0 val qq = it.groups[1]?.value?.toLongOrNull()
while (matcher.find()) { if (qq != null && contact is Group) {
for (i in 1..matcher.groupCount()) { contact[qq]?.let { member -> t.add(MessageChunk(it.range, At(member))) }
if (matcher.group(i) == null) {
continue
} }
}
regexLaTeX.findAll(content).forEach {
it.groups.forEach { group ->
if (group == null || group.value.isEmpty()) return@forEach
try { try {
// 将所有匹配的LaTeX公式转为图片拼接到消息中 // 将所有匹配的LaTeX公式转为图片拼接到消息中
val formula = matcher.group(i) val formula = group.value
val imageByteArray = LaTeXConverter.convertToImage(formula, "png") val imageByteArray = LaTeXConverter.convertToImage(formula, "png")
val resource = imageByteArray.toExternalResource("png") val resource = imageByteArray.toExternalResource("png")
val image = contact.uploadImage(resource) val image = contact.uploadImage(resource)
// 拼接公式前的文本 t.add(MessageChunk(group.range, image))
append(content, index, matcher.start())
// 插入图片
append(image)
// 移动索引
index = matcher.end()
} catch (ex: Throwable) { } catch (ex: Throwable) {
logger.warning("处理LaTeX表达式时异常", ex) logger.warning("处理LaTeX表达式时异常", ex)
} }
} }
} }
buildMessageChain {
var index = 0
for ((range, msg) in t.sortedBy { it.range.start }) {
if (index < range.start) {
append(content, index, range.start)
}
append(msg)
index = range.endInclusive + 1
}
// 拼接后续消息 // 拼接后续消息
if (index < content.length) {
append(content, index, content.length) append(content, index, content.length)
} }
} }
}
private val reasoningAgent = ReasoningAgent() }
/** /**
* 工具列表 * 工具列表
*/ */
private val myTools = listOf( private val myTools = listOf(
// 发送单条消息
SendSingleMessageAgent(),
// 发送组合消息
SendCompositeMessage(),
// 结束循环
StopLoopAgent(),
// 网页搜索 // 网页搜索
WebSearch(), WebSearch(),
// 访问网页
VisitWeb(),
// 运行代码 // 运行代码
RunCode(), RunCode(),
// 推理代理 // 推理代理
reasoningAgent, ReasoningAgent(),
// 视觉代理
VisualAgent(),
// 天气服务 // 天气服务
WeatherService(), WeatherService(),
// IP所在地查询 暂时取消,几乎不会用到
// IpAddressQuery(),
// Epic 免费游戏 // Epic 免费游戏
EpicFreeGame(), EpicFreeGame(),
) )
// private fun chatCompletions(
// chatMessages: List<ChatMessage>,
// hasTools: Boolean = true
// ): Flow<ChatCompletionChunk> {
// val llm = this.llm ?: throw NullPointerException("OpenAI Token 未设置,无法开始")
// 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}")
// return llm.chatCompletions(request)
// }
private suspend fun chatCompletion( private suspend fun chatCompletion(
chatMessages: List<ChatMessage>, chatMessages: List<ChatMessage>,
hasTools: Boolean = true hasTools: Boolean = true
): ChatMessage { ): ChatMessage {
val llm = this.llm ?: throw NullPointerException("OpenAI Token 未设置,无法开始") val llm = LargeLanguageModels.chat ?: throw NullPointerException("OpenAI Token 未设置,无法开始")
val availableTools = if (hasTools) { val availableTools = if (hasTools) {
myTools.filter { it.isEnabled }.map { it.tool } myTools.filter { it.isEnabled }.map { it.tool }
} else null } else null
@ -487,9 +719,7 @@ object JChatGPT : KotlinPlugin(
messages = chatMessages, messages = chatMessages,
tools = availableTools, tools = availableTools,
) )
logger.info("API Requesting... Model=${PluginConfig.chatModel}" logger.info("API Requesting... Model=${PluginConfig.chatModel}")
// " Tools=${availableTools?.joinToString(prefix = "[", postfix = "]")}"
)
val response = llm.chatCompletion(request) val response = llm.chatCompletion(request)
val message = response.choices.first().message val message = response.choices.first().message
logger.info("Response: $message ${response.usage}") logger.info("Response: $message ${response.usage}")
@ -500,6 +730,7 @@ object JChatGPT : KotlinPlugin(
val nameCard = StringBuilder() val nameCard = StringBuilder()
// 群活跃等级 // 群活跃等级
nameCard.append("【lv").append(member.active.temperature).append(" ") nameCard.append("【lv").append(member.active.temperature).append(" ")
try {
// 群头衔 // 群头衔
if (member.specialTitle.isNotEmpty()) { if (member.specialTitle.isNotEmpty()) {
nameCard.append(member.specialTitle) nameCard.append(member.specialTitle)
@ -512,27 +743,28 @@ object JChatGPT : KotlinPlugin(
} }
) )
} }
} catch (e: Throwable) {
logger.warning("获取群头衔失败", e)
}
// 群名片 // 群名片
nameCard.append("").append(member.nameCardOrNick) nameCard.append("").append(member.nameCardOrNick).append("(").append(member.id).append(")")
// .append(" (").append(recordSender.id).append(")")
return nameCard.toString() return nameCard.toString()
} }
private fun MessageChain.plainText() = this.filterIsInstance<PlainText>().joinToString().trim()
private suspend fun ToolCall.Function.execute(event: MessageEvent): String { private suspend fun ToolCall.Function.execute(event: MessageEvent): String {
val agent = myTools.find { it.tool.function.name == function.name } val agent = myTools.find { it.tool.function.name == function.name }
?: return "Function ${function.name} not found" ?: return "Function ${function.name} not found"
// 提示正在执行函数 // 提示正在执行函数
val receipt = if (agent.loadingMessage.isNotEmpty()) { val receipt = if (agent.loadingMessage.isNotEmpty()) {
event.subject.sendMessage(event.message.quote() + agent.loadingMessage) event.subject.sendMessage(agent.loadingMessage)
} else null } else null
// 提取参数 // 提取参数
val args = function.argumentsAsJsonOrNull() val args = function.argumentsAsJsonOrNull()
logger.info("Calling ${function.name}(${args})") logger.info("Calling ${function.name}(${args})")
// 执行函数 // 执行函数
val result = try { val result = try {
agent.execute(args) agent.execute(args, event)
} catch (e: Throwable) { } catch (e: Throwable) {
logger.error("Failed to call ${function.name}", e) logger.error("Failed to call ${function.name}", e)
"工具调用失败,请尝试自行回答用户,或如实告知。" "工具调用失败,请尝试自行回答用户,或如实告知。"
@ -545,9 +777,11 @@ object JChatGPT : KotlinPlugin(
try { try {
receipt.recall() receipt.recall()
} catch (e: Throwable) { } catch (e: Throwable) {
logger.error("消息撤回失败,调试信息:" + logger.error(
"消息撤回失败,调试信息:" +
"source.internalIds=${receipt.source.internalIds.joinToString()} " + "source.internalIds=${receipt.source.internalIds.joinToString()} " +
"source.ids= ${receipt.source.ids.joinToString()}", e) "source.ids= ${receipt.source.ids.joinToString()}", e
)
} }
} }
} }

View File

@ -23,7 +23,7 @@ object LaTeXConverter {
g2.color = Color.white g2.color = Color.white
g2.fillRect(0, 0, icon.iconWidth, icon.iconHeight) g2.fillRect(0, 0, icon.iconWidth, icon.iconHeight)
val jl = JLabel() val jl = JLabel()
jl.setForeground(Color(0, 0, 0)) jl.setForeground(Color.black)
icon.paintIcon(jl, g2, 0, 0) icon.paintIcon(jl, g2, 0, 0)
val stream = ByteArrayOutputStream() val stream = ByteArrayOutputStream()
ImageIO.write(image, format, stream) ImageIO.write(image, format, stream)

View File

@ -0,0 +1,40 @@
package top.jie65535.mirai
import com.aallam.openai.api.http.Timeout
import com.aallam.openai.client.Chat
import com.aallam.openai.client.OpenAI
import com.aallam.openai.client.OpenAIHost
import kotlin.time.Duration.Companion.milliseconds
object LargeLanguageModels {
var chat: Chat? = null
var reasoning: Chat? = null
var visual: Chat? = null
fun reload() {
val timeout = PluginConfig.timeout.milliseconds
if (PluginConfig.openAiApi.isNotBlank() && PluginConfig.openAiToken.isNotBlank()) {
chat = OpenAI(
token = PluginConfig.openAiToken,
host = OpenAIHost(baseUrl = PluginConfig.openAiApi),
timeout = Timeout(request = timeout, connect = timeout, socket = timeout)
)
}
if (PluginConfig.reasoningModelApi.isNotBlank() && PluginConfig.reasoningModelToken.isNotBlank()) {
reasoning = OpenAI(
token = PluginConfig.reasoningModelToken,
host = OpenAIHost(baseUrl = PluginConfig.reasoningModelApi),
timeout = Timeout(request = timeout, connect = timeout, socket = timeout)
)
}
if (PluginConfig.visualModelApi.isNotBlank() && PluginConfig.visualModelToken.isNotBlank()) {
visual = OpenAI(
token = PluginConfig.visualModelToken,
host = OpenAIHost(baseUrl = PluginConfig.visualModelApi),
timeout = Timeout(request = timeout, connect = timeout, socket = timeout)
)
}
}
}

View File

@ -18,7 +18,7 @@ object PluginCommands : CompositeCommand(
suspend fun CommandSender.setToken(token: String) { suspend fun CommandSender.setToken(token: String) {
PluginConfig.openAiToken = token PluginConfig.openAiToken = token
PluginConfig.save() PluginConfig.save()
JChatGPT.updateOpenAiToken(token) LargeLanguageModels.reload()
sendMessage("OK") sendMessage("OK")
} }
@ -45,7 +45,7 @@ object PluginCommands : CompositeCommand(
@SubCommand @SubCommand
suspend fun CommandSender.reload() { suspend fun CommandSender.reload() {
PluginConfig.reload() PluginConfig.reload()
JChatGPT.updateOpenAiToken(PluginConfig.openAiToken) LargeLanguageModels.reload()
sendMessage("OK") sendMessage("OK")
} }
} }

View File

@ -6,7 +6,7 @@ import net.mamoe.mirai.console.data.value
object PluginConfig : AutoSavePluginConfig("Config") { object PluginConfig : AutoSavePluginConfig("Config") {
@ValueDescription("OpenAI API base url") @ValueDescription("OpenAI API base url")
val openAiApi: String by value("https://api.openai.com/v1/") val openAiApi: String by value("https://dashscope.aliyuncs.com/compatible-mode/v1/")
@ValueDescription("OpenAI API Token") @ValueDescription("OpenAI API Token")
var openAiToken: String by value("") var openAiToken: String by value("")
@ -14,11 +14,32 @@ object PluginConfig : AutoSavePluginConfig("Config") {
@ValueDescription("Chat模型") @ValueDescription("Chat模型")
var chatModel: String by value("qwen-max") var chatModel: String by value("qwen-max")
@ValueDescription("推理模型API")
var reasoningModelApi: String by value("https://dashscope.aliyuncs.com/compatible-mode/v1/")
@ValueDescription("推理模型Token")
var reasoningModelToken: String by value("")
@ValueDescription("推理模型") @ValueDescription("推理模型")
var reasoningModel: String by value("qwq-plus") var reasoningModel: String by value("qwq-plus")
@ValueDescription("Chat默认提示") @ValueDescription("视觉模型API")
var prompt: String by value("") var visualModelApi: String by value("https://dashscope.aliyuncs.com/compatible-mode/v1/")
@ValueDescription("视觉模型Token")
var visualModelToken: String by value("")
@ValueDescription("视觉模型")
var visualModel: String by value("qwen-vl-plus")
@ValueDescription("Jina API Key")
val jinaApiKey by value("")
@ValueDescription("SearXNG 搜索引擎地址,如 http://127.0.0.1:8080/search 必须启用允许json格式返回")
val searXngUrl: String by value("")
@ValueDescription("在线运行代码 glot.io 的 api token在官网注册账号即可获取。")
val glotToken: String by value("")
@ValueDescription("群管理是否自动拥有对话权限,默认是") @ValueDescription("群管理是否自动拥有对话权限,默认是")
val groupOpHasChatPermission: Boolean by value(true) val groupOpHasChatPermission: Boolean by value(true)
@ -26,17 +47,14 @@ object PluginConfig : AutoSavePluginConfig("Config") {
@ValueDescription("好友是否自动拥有对话权限,默认是") @ValueDescription("好友是否自动拥有对话权限,默认是")
val friendHasChatPermission: Boolean by value(true) val friendHasChatPermission: Boolean by value(true)
@ValueDescription("群荣誉等级权限门槛,达到这个等级相当于自动拥有权限。") @ValueDescription("群荣誉等级权限门槛,达到这个等级相当于自动拥有对话权限。")
val temperaturePermission: Int by value(60) val temperaturePermission: Int by value(50)
@ValueDescription("等待响应超时时间单位毫秒默认60秒") @ValueDescription("等待响应超时时间单位毫秒默认60秒")
val timeout: Long by value(60000L) val timeout: Long by value(60000L)
@ValueDescription("SearXNG 搜索引擎地址,如 http://127.0.0.1:8080/search 必须启用允许json格式返回") @ValueDescription("系统提示词")
val searXngUrl: String by value("") var prompt: String by value("你是一个乐于助人的助手")
@ValueDescription("在线运行代码 glot.io 的 api token在官网注册账号即可获取。")
val glotToken: String by value("")
@ValueDescription("创建Prompt时取最近多少分钟内的消息") @ValueDescription("创建Prompt时取最近多少分钟内的消息")
val historyWindowMin: Int by value(10) val historyWindowMin: Int by value(10)
@ -50,6 +68,9 @@ object PluginConfig : AutoSavePluginConfig("Config") {
@ValueDescription("达到需要合并转发消息的阈值") @ValueDescription("达到需要合并转发消息的阈值")
val messageMergeThreshold by value(150) val messageMergeThreshold by value(150)
@ValueDescription("最大重试次数至少2次最后一次请求不会带工具非工具调用相当于正常回复") @ValueDescription("最大循环次数至少2次")
val retryMax: Int by value(3) val retryMax: Int by value(5)
@ValueDescription("关键字呼叫,支持正则表达式")
val callKeyword by value("[小筱][林淋月玥]")
} }

View File

@ -3,7 +3,12 @@ package top.jie65535.mirai.tools
import com.aallam.openai.api.chat.Tool import com.aallam.openai.api.chat.Tool
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.okhttp.* import io.ktor.client.engine.okhttp.*
import io.ktor.client.plugins.HttpTimeout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import net.mamoe.mirai.event.events.MessageEvent
abstract class BaseAgent( abstract class BaseAgent(
val tool: Tool val tool: Tool
@ -18,11 +23,33 @@ abstract class BaseAgent(
*/ */
open val loadingMessage: String = "" open val loadingMessage: String = ""
/**
* HTTP客户端
*/
protected val httpClient by lazy { protected val httpClient by lazy {
HttpClient(OkHttp) HttpClient(OkHttp) {
install(HttpTimeout) {
requestTimeoutMillis = 60000
connectTimeoutMillis = 5000
socketTimeoutMillis = 15000
}
}
} }
abstract suspend fun execute(args: JsonObject?): String /**
* 协程作用域
*/
protected val scope by lazy {
CoroutineScope(Dispatchers.IO + SupervisorJob())
}
open suspend fun execute(args: JsonObject?): String {
return "OK"
}
open suspend fun execute(args: JsonObject?, event: MessageEvent): String {
return execute(args)
}
override fun toString(): String { override fun toString(): String {
return "${tool.function.name}: ${tool.function.description}" return "${tool.function.name}: ${tool.function.description}"

View File

@ -5,8 +5,8 @@ import com.aallam.openai.api.chat.ChatMessage
import com.aallam.openai.api.chat.Tool import com.aallam.openai.api.chat.Tool
import com.aallam.openai.api.core.Parameters import com.aallam.openai.api.core.Parameters
import com.aallam.openai.api.model.ModelId import com.aallam.openai.api.model.ModelId
import com.aallam.openai.client.Chat
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import top.jie65535.mirai.LargeLanguageModels
import top.jie65535.mirai.PluginConfig import top.jie65535.mirai.PluginConfig
class ReasoningAgent : BaseAgent( class ReasoningAgent : BaseAgent(
@ -18,32 +18,30 @@ class ReasoningAgent : BaseAgent(
putJsonObject("properties") { putJsonObject("properties") {
putJsonObject("prompt") { putJsonObject("prompt") {
put("type", "string") put("type", "string")
put("description", "用于调用推理模型的提示") put("description", "用于调用推理模型的提示")
} }
} }
putJsonArray("required") { putJsonArray("required") {
add("question") add("prompt")
} }
}, },
) )
) { ) {
var llm: Chat? = null
override val loadingMessage: String override val loadingMessage: String
get() = "深度思考中..." get() = "深度思考中..."
override val isEnabled: Boolean override val isEnabled: Boolean
get() = llm != null get() = LargeLanguageModels.reasoning != null
override suspend fun execute(args: JsonObject?): String { override suspend fun execute(args: JsonObject?): String {
requireNotNull(args) requireNotNull(args)
val llm = llm ?: return "未配置llm无法进行推理。" val llm = LargeLanguageModels.reasoning ?: return "未配置llm无法进行推理。"
val prompt = args.getValue("prompt").jsonPrimitive.content val prompt = args.getValue("prompt").jsonPrimitive.content
val answerContent = StringBuilder() val answerContent = StringBuilder()
llm.chatCompletions(ChatCompletionRequest( llm.chatCompletions(ChatCompletionRequest(
model = ModelId(PluginConfig.reasoningModel), model = ModelId(PluginConfig.reasoningModel),
messages = listOf(ChatMessage.Companion.User(prompt)) messages = listOf(ChatMessage.User(prompt))
)).collect { )).collect {
if (it.choices.isNotEmpty()) { if (it.choices.isNotEmpty()) {
val delta = it.choices[0].delta ?: return@collect val delta = it.choices[0].delta ?: return@collect

View File

@ -0,0 +1,50 @@
package top.jie65535.mirai.tools
import com.aallam.openai.api.chat.Tool
import com.aallam.openai.api.core.Parameters
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.add
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import net.mamoe.mirai.event.events.MessageEvent
import net.mamoe.mirai.message.data.buildForwardMessage
import top.jie65535.mirai.JChatGPT
import top.jie65535.mirai.PluginConfig
import kotlin.collections.getValue
class SendCompositeMessage : BaseAgent(
tool = Tool.function(
name = "sendCompositeMessage",
description = "发送组合消息适合发送较长消息而避免刷屏不支持Markdown",
parameters = Parameters.buildJsonObject {
put("type", "object")
putJsonObject("properties") {
putJsonObject("content") {
put("type", "string")
put("description", "消息内容")
}
}
putJsonArray("required") {
add("content")
}
}
)
) {
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
requireNotNull(args)
val content = args.getValue("content").jsonPrimitive.content
val msg = JChatGPT.toMessage(event.subject, content)
event.subject.sendMessage(
if (content.length > PluginConfig.messageMergeThreshold) {
event.buildForwardMessage {
event.bot says msg
}
} else {
msg
}
)
return "OK"
}
}

View File

@ -0,0 +1,33 @@
package top.jie65535.mirai.tools
import com.aallam.openai.api.chat.Tool
import com.aallam.openai.api.core.Parameters
import kotlinx.serialization.json.*
import net.mamoe.mirai.event.events.MessageEvent
import top.jie65535.mirai.JChatGPT
class SendSingleMessageAgent : BaseAgent(
tool = Tool.function(
name = "sendSingleMessage",
description = "发送一条消息适合发送一行以内的短句不支持Markdown",
parameters = Parameters.buildJsonObject {
put("type", "object")
putJsonObject("properties") {
putJsonObject("content") {
put("type", "string")
put("description", "消息内容")
}
}
putJsonArray("required") {
add("content")
}
}
)
) {
override suspend fun execute(args: JsonObject?, event: MessageEvent): String {
requireNotNull(args)
val content = args.getValue("content").jsonPrimitive.content
event.subject.sendMessage(JChatGPT.toMessage(event.subject, content))
return "OK"
}
}

View File

@ -0,0 +1,12 @@
package top.jie65535.mirai.tools
import com.aallam.openai.api.chat.Tool
import com.aallam.openai.api.core.Parameters
class StopLoopAgent : BaseAgent(
tool = Tool.function(
name = "endConversation",
description = "结束本轮对话",
parameters = Parameters.Empty
)
)

View File

@ -0,0 +1,71 @@
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.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.serialization.json.*
import top.jie65535.mirai.PluginConfig
class VisitWeb : BaseAgent(
tool = Tool.function(
name = "visit",
description = "Visit webpage(s) and return the summary of the content.",
parameters = Parameters.buildJsonObject {
put("type", "object")
putJsonObject("properties") {
putJsonObject("url") {
putJsonArray("type") {
add("string")
add("array")
}
putJsonObject("items") {
put("type", "string")
}
put("minItems", 1)
put("description", "The URL(s) of the webpage(s) to visit. Can be a single URL or an array of URLs.")
}
}
putJsonArray("required") {
add("url")
}
}
)
) {
companion object {
// Visit Tool (Using Jina Reader)
const val JINA_READER_URL_PREFIX = "https://r.jina.ai/"
}
override val isEnabled: Boolean
get() = PluginConfig.jinaApiKey.isNotEmpty()
override val loadingMessage: String
get() = "访问网页中..."
override suspend fun execute(args: JsonObject?): String {
requireNotNull(args)
val urlJson = args.getValue("url")
if (urlJson is JsonPrimitive) {
return jinaReadPage(urlJson.content)
} else if (urlJson is JsonArray) {
return urlJson.map {
scope.async { jinaReadPage(it.jsonPrimitive.content) }
}.awaitAll().joinToString()
}
return ""
}
private suspend fun jinaReadPage(url: String): String {
return try {
httpClient.get(JINA_READER_URL_PREFIX + url) {
header("Authorization", "Bearer ${PluginConfig.jinaApiKey}")
}.bodyAsText()
} catch (e: Throwable) {
"Error fetching \"$url\": ${e.message}"
}
}
}

View File

@ -0,0 +1,76 @@
package top.jie65535.mirai.tools
import com.aallam.openai.api.chat.ChatCompletionRequest
import com.aallam.openai.api.chat.ChatMessage
import com.aallam.openai.api.chat.ImagePart
import com.aallam.openai.api.chat.TextPart
import com.aallam.openai.api.chat.Tool
import com.aallam.openai.api.core.Parameters
import com.aallam.openai.api.model.ModelId
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.add
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonArray
import kotlinx.serialization.json.putJsonObject
import top.jie65535.mirai.LargeLanguageModels
import top.jie65535.mirai.PluginConfig
class VisualAgent : BaseAgent(
tool = Tool.function(
name = "visualAgent",
description = "可通过调用视觉模型识别图片。",
parameters = Parameters.buildJsonObject {
put("type", "object")
putJsonObject("properties") {
putJsonObject("image_url") {
put("type", "string")
put("description", "图片地址")
}
putJsonObject("prompt") {
put("type", "string")
put("description", "用于调用视觉模型的提示词")
}
}
putJsonArray("required") {
add("image_url")
add("prompt")
}
}
)
) {
override val loadingMessage: String
get() = "图片识别中..."
override val isEnabled: Boolean
get() = LargeLanguageModels.visual != null
override suspend fun execute(args: JsonObject?): String {
requireNotNull(args)
val llm = LargeLanguageModels.visual ?: return "未配置llm无法进行识别。"
val imageUrl = args.getValue("image_url").jsonPrimitive.content
val prompt = args.getValue("prompt").jsonPrimitive.content
val answerContent = StringBuilder()
llm.chatCompletions(ChatCompletionRequest(
model = ModelId(PluginConfig.visualModel),
messages = listOf(
ChatMessage.System("You are a helpful assistant."),
ChatMessage.User(
content = listOf(
ImagePart(imageUrl),
TextPart(prompt)
)
)
)
)).collect {
if (it.choices.isNotEmpty()) {
val delta = it.choices[0].delta ?: return@collect
if (!delta.content.isNullOrEmpty()) {
answerContent.append(delta.content)
}
}
}
return answerContent.toString().ifEmpty { "识图异常,结果为空" }
}
}

View File

@ -7,6 +7,7 @@ import io.ktor.client.statement.*
import io.ktor.http.* import io.ktor.http.*
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.apache.commons.text.StringEscapeUtils import org.apache.commons.text.StringEscapeUtils
import top.jie65535.mirai.JChatGPT
import top.jie65535.mirai.PluginConfig import top.jie65535.mirai.PluginConfig
class WebSearch : BaseAgent( class WebSearch : BaseAgent(
@ -21,33 +22,6 @@ class WebSearch : BaseAgent(
put("type", "string") put("type", "string")
put("description", "查询内容关键字") 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") { putJsonArray("required") {
add("q") add("q")
@ -67,25 +41,17 @@ class WebSearch : BaseAgent(
override suspend fun execute(args: JsonObject?): String { override suspend fun execute(args: JsonObject?): String {
requireNotNull(args) requireNotNull(args)
val q = args.getValue("q").jsonPrimitive.content val q = args.getValue("q").jsonPrimitive.content
val categories = args["categories"]?.jsonArray val url = buildString {
val timeRange = args["time_range"]?.jsonPrimitive?.contentOrNull
val response = httpClient.get(
buildString {
append(PluginConfig.searXngUrl) append(PluginConfig.searXngUrl)
append("?q=") append("?q=")
append(q.encodeURLParameter()) append(q.encodeURLParameter())
append("&format=json") append("&format=json")
if (categories != null) {
append("&")
append(categories.joinToString { it.jsonPrimitive.content })
} }
if (timeRange != null) {
append("&") val response = httpClient.get(url)
append(timeRange) JChatGPT.logger.info("Request: $url")
}
}
)
val body = response.bodyAsText() val body = response.bodyAsText()
JChatGPT.logger.info("Response: $body")
val responseJsonElement = Json.parseToJsonElement(body) val responseJsonElement = Json.parseToJsonElement(body)
val filteredResponse = buildJsonObject { val filteredResponse = buildJsonObject {
val root = responseJsonElement.jsonObject val root = responseJsonElement.jsonObject