mirror of
https://github.com/jie65535/JChatGPT.git
synced 2025-08-04 19:09:20 +08:00
Compare commits
No commits in common. "f822999ab4238453888fdda689ba62eedf1efdb4" and "6c034ab2a7e404d73082782019d1c6a6fa6b5a56" have entirely different histories.
f822999ab4
...
6c034ab2a7
@ -7,7 +7,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "top.jie65535.mirai"
|
group = "top.jie65535.mirai"
|
||||||
version = "1.7.0"
|
version = "1.5.0"
|
||||||
|
|
||||||
mirai {
|
mirai {
|
||||||
jvmTarget = JavaVersion.VERSION_11
|
jvmTarget = JavaVersion.VERSION_11
|
||||||
|
@ -4,11 +4,14 @@ 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
|
||||||
@ -23,31 +26,34 @@ 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.7.0",
|
version = "1.5.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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否包含历史对话
|
* 是否包含历史对话
|
||||||
*/
|
*/
|
||||||
@ -55,15 +61,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
|
||||||
LargeLanguageModels.reload()
|
if (PluginConfig.openAiToken.isNotEmpty()) {
|
||||||
|
updateOpenAiToken(PluginConfig.openAiToken)
|
||||||
|
}
|
||||||
|
|
||||||
// 注册插件命令
|
// 注册插件命令
|
||||||
PluginCommands.register()
|
PluginCommands.register()
|
||||||
@ -76,25 +82,36 @@ 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 (LargeLanguageModels.chat == null) return
|
if (llm == null) return
|
||||||
// 发送者是否有权限
|
// 发送者是否有权限
|
||||||
if (!event.toCommandSender().hasPermission(chatPermission)) {
|
if (!event.toCommandSender().hasPermission(chatPermission)) {
|
||||||
if (event is GroupMessageEvent) {
|
if (event is GroupMessageEvent) {
|
||||||
@ -115,12 +132,34 @@ object JChatGPT : KotlinPlugin(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有@bot或者触发关键字则直接结束
|
// 是否@bot
|
||||||
if (!event.message.contains(At(event.bot))
|
val isAtBot = 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 {
|
||||||
@ -139,206 +178,126 @@ object JChatGPT : KotlinPlugin(
|
|||||||
|
|
||||||
replace("{subject}") {
|
replace("{subject}") {
|
||||||
if (event is GroupMessageEvent) {
|
if (event is GroupMessageEvent) {
|
||||||
"\"${event.subject.name}\" 群聊中,你在本群的名片是:${getNameCard(event.subject.botAsMember)}"
|
"\"${event.subject.name}\" 群聊中"
|
||||||
} else {
|
} else {
|
||||||
"与 \"${event.senderName}\" 私聊中"
|
"私聊中"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return prompt.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// region - 历史消息相关 -
|
// replace("{sender}") {
|
||||||
|
// 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 event.message.content
|
return@replace "暂无内容"
|
||||||
}
|
}
|
||||||
val now = OffsetDateTime.now()
|
|
||||||
// 一段时间内的消息
|
// 一段时间内的消息
|
||||||
val beforeTimestamp = now.minusMinutes(PluginConfig.historyWindowMin.toLong()).toEpochSecond().toInt()
|
val beforeTimestamp = now.minusMinutes(PluginConfig.historyWindowMin.toLong()).toEpochSecond().toInt()
|
||||||
return getAfterHistory(beforeTimestamp, event)
|
val nowTimestamp = now.toEpochSecond().toInt()
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定时间后的历史消息
|
|
||||||
* @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, time, nowTimestamp]
|
val history = MiraiHibernateRecorder[event.subject, beforeTimestamp, 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("**你** " + getNameCard(event.subject.botAsMember))
|
historyText.append("你")
|
||||||
} else {
|
} else {
|
||||||
historyText.append(getNameCard(event.subject, record.fromId))
|
val recordSender = event.subject[record.fromId]
|
||||||
}
|
if (recordSender != null) {
|
||||||
// 发言时间
|
// 群活跃等级
|
||||||
historyText.append(' ')
|
historyText.append(getNameCard(recordSender))
|
||||||
.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 > "))
|
|
||||||
}
|
|
||||||
// 消息内容
|
|
||||||
historyText.append(" 说:").appendLine(record.toMessageChain().joinToString("") {
|
|
||||||
when (it) {
|
|
||||||
is At -> {
|
|
||||||
it.getDisplay(event.subject)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> singleMessageToText(it)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNameCard(group: Group, qq: Long): String {
|
|
||||||
val member = group[qq]
|
|
||||||
return if (member == null) {
|
|
||||||
"未知群员($qq)"
|
|
||||||
} else {
|
} else {
|
||||||
getNameCard(member)
|
// 未知群员
|
||||||
|
historyText.append("未知群员(").append(record.fromId).append(")")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加消息记录到历史上下文中
|
|
||||||
* @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
|
historyText
|
||||||
.append(" ")
|
.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(" 引用\n > ")
|
|
||||||
.appendLine(it.source.originalMessage
|
|
||||||
.joinToString("", transform = ::singleMessageToText)
|
|
||||||
.replace("\n", "\n > "))
|
|
||||||
}
|
|
||||||
// 消息内容
|
// 消息内容
|
||||||
historyText.append(" 说:").appendLine(
|
.append(" 说:").appendLine(record.toMessageChain().joinToString("") {
|
||||||
record.toMessageChain().joinToString("", transform = ::singleMessageToText))
|
when (it) {
|
||||||
|
is At -> {
|
||||||
|
it.getDisplay(event.subject)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun singleMessageToText(it: SingleMessage): String {
|
|
||||||
return when (it) {
|
|
||||||
is ForwardMessage -> {
|
is ForwardMessage -> {
|
||||||
it.title + "\n" + it.preview
|
it.title + "\n" + it.preview
|
||||||
}
|
}
|
||||||
|
|
||||||
// 图片格式化
|
is QuoteReply -> {
|
||||||
is Image -> {
|
">" + it.source.originalMessage.contentToString().replace("\n", "\n> ") + "\n"
|
||||||
try {
|
|
||||||
val imageUrl = runBlocking {
|
|
||||||
it.queryUrl()
|
|
||||||
}
|
|
||||||
""
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
logger.warning("图片地址获取失败", e)
|
|
||||||
it.content
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> it.content
|
else -> {
|
||||||
|
it.contentToString()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// endregion - 历史消息相关 -
|
}
|
||||||
|
} else {
|
||||||
private val thinkRegex = Regex("<think>[\\s\\S]*?</think>")
|
// TODO 私聊
|
||||||
|
|
||||||
private suspend fun startChat(event: MessageEvent) {
|
|
||||||
if (!requestMap.add(event.sender.id)) {
|
|
||||||
event.subject.sendMessage("再等等...")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
historyText.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return prompt.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun startChat(event: MessageEvent, context: List<ChatMessage>? = null) {
|
||||||
val history = mutableListOf<ChatMessage>()
|
val history = mutableListOf<ChatMessage>()
|
||||||
if (PluginConfig.prompt.isNotEmpty()) {
|
if (!context.isNullOrEmpty()) {
|
||||||
|
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 historyText = getHistory(event)
|
val msg = event.message.plainText()
|
||||||
logger.info("History: $historyText")
|
if (msg.isNotEmpty()) {
|
||||||
history.add(ChatMessage.User(historyText))
|
history.add(ChatMessage(ChatRole.User, if (event is GroupMessageEvent) {
|
||||||
|
"${getNameCard(event.sender)} 说:$msg"
|
||||||
|
} else {
|
||||||
|
msg
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var done: Boolean
|
if (!requestMap.add(event.sender.id)) {
|
||||||
// 至少循环3次
|
event.subject.sendMessage(event.message.quote() + "再等等...")
|
||||||
var retry = max(PluginConfig.retryMax, 3)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var done = true
|
||||||
|
// 至少重试两次
|
||||||
|
var retry = max(PluginConfig.retryMax, 2)
|
||||||
do {
|
do {
|
||||||
try {
|
try {
|
||||||
val startedAt = OffsetDateTime.now().toEpochSecond().toInt()
|
val reply = chatCompletion(history, retry > 1)
|
||||||
val response = chatCompletion(history)
|
history.add(reply)
|
||||||
// 移除思考内容
|
|
||||||
val responseContent = response.content?.replace(thinkRegex, "")?.trim()
|
|
||||||
history.add(ChatMessage.Assistant(
|
|
||||||
content = responseContent,
|
|
||||||
name = response.name,
|
|
||||||
toolCalls = response.toolCalls
|
|
||||||
))
|
|
||||||
|
|
||||||
if (response.toolCalls.isNullOrEmpty()) {
|
|
||||||
done = true
|
done = true
|
||||||
} else {
|
|
||||||
done = false
|
for (toolCall in reply.toolCalls.orEmpty()) {
|
||||||
// 处理函数调用
|
|
||||||
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(
|
||||||
@ -349,255 +308,102 @@ object JChatGPT : KotlinPlugin(
|
|||||||
content = functionResponse
|
content = functionResponse
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (toolCall.function.name == "endConversation") {
|
done = false
|
||||||
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 suspend fun startChat(event: MessageEvent) {
|
private val laTeXPattern = Pattern.compile(
|
||||||
// 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表达式,将会转为图片拼接到消息中。
|
||||||
*
|
*
|
||||||
@ -605,112 +411,74 @@ object JChatGPT : KotlinPlugin(
|
|||||||
* @param content 文本内容
|
* @param content 文本内容
|
||||||
* @return 构造的消息
|
* @return 构造的消息
|
||||||
*/
|
*/
|
||||||
suspend fun toMessage(contact: Contact, content: String): Message {
|
private 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 {
|
} else buildMessageChain {
|
||||||
val t = mutableListOf<MessageChunk>()
|
// 匹配LaTeX表达式
|
||||||
regexAtQq.findAll(content).forEach {
|
val matcher = laTeXPattern.matcher(content)
|
||||||
val qq = it.groups[1]?.value?.toLongOrNull()
|
var index = 0
|
||||||
if (qq != null && contact is Group) {
|
while (matcher.find()) {
|
||||||
contact[qq]?.let { member -> t.add(MessageChunk(it.range, At(member))) }
|
for (i in 1..matcher.groupCount()) {
|
||||||
|
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 = group.value
|
val formula = matcher.group(i)
|
||||||
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 = LargeLanguageModels.chat ?: throw NullPointerException("OpenAI Token 未设置,无法开始")
|
val llm = this.llm ?: 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
|
||||||
@ -719,7 +487,9 @@ 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}")
|
||||||
@ -730,7 +500,6 @@ 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)
|
||||||
@ -743,28 +512,27 @@ object JChatGPT : KotlinPlugin(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
|
||||||
logger.warning("获取群头衔失败", e)
|
|
||||||
}
|
|
||||||
// 群名片
|
// 群名片
|
||||||
nameCard.append("】 ").append(member.nameCardOrNick).append("(").append(member.id).append(")")
|
nameCard.append("】 ").append(member.nameCardOrNick)
|
||||||
|
// .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(agent.loadingMessage)
|
event.subject.sendMessage(event.message.quote() + 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, event)
|
agent.execute(args)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
logger.error("Failed to call ${function.name}", e)
|
logger.error("Failed to call ${function.name}", e)
|
||||||
"工具调用失败,请尝试自行回答用户,或如实告知。"
|
"工具调用失败,请尝试自行回答用户,或如实告知。"
|
||||||
@ -777,11 +545,9 @@ 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)
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.black)
|
jl.setForeground(Color(0, 0, 0))
|
||||||
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)
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
||||||
LargeLanguageModels.reload()
|
JChatGPT.updateOpenAiToken(token)
|
||||||
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()
|
||||||
LargeLanguageModels.reload()
|
JChatGPT.updateOpenAiToken(PluginConfig.openAiToken)
|
||||||
sendMessage("OK")
|
sendMessage("OK")
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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://dashscope.aliyuncs.com/compatible-mode/v1/")
|
val openAiApi: String by value("https://api.openai.com/v1/")
|
||||||
|
|
||||||
@ValueDescription("OpenAI API Token")
|
@ValueDescription("OpenAI API Token")
|
||||||
var openAiToken: String by value("")
|
var openAiToken: String by value("")
|
||||||
@ -14,32 +14,11 @@ 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("视觉模型API")
|
@ValueDescription("Chat默认提示")
|
||||||
var visualModelApi: String by value("https://dashscope.aliyuncs.com/compatible-mode/v1/")
|
var prompt: String by value("")
|
||||||
|
|
||||||
@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)
|
||||||
@ -47,14 +26,17 @@ 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(50)
|
val temperaturePermission: Int by value(60)
|
||||||
|
|
||||||
@ValueDescription("等待响应超时时间,单位毫秒,默认60秒")
|
@ValueDescription("等待响应超时时间,单位毫秒,默认60秒")
|
||||||
val timeout: Long by value(60000L)
|
val timeout: Long by value(60000L)
|
||||||
|
|
||||||
@ValueDescription("系统提示词")
|
@ValueDescription("SearXNG 搜索引擎地址,如 http://127.0.0.1:8080/search 必须启用允许json格式返回")
|
||||||
var prompt: String by value("你是一个乐于助人的助手")
|
val searXngUrl: 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)
|
||||||
@ -68,9 +50,6 @@ 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(5)
|
val retryMax: Int by value(3)
|
||||||
|
|
||||||
@ValueDescription("关键字呼叫,支持正则表达式")
|
|
||||||
val callKeyword by value("[小筱][林淋月玥]")
|
|
||||||
}
|
}
|
@ -3,12 +3,7 @@ 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
|
||||||
@ -23,33 +18,11 @@ 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}"
|
||||||
|
@ -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,30 +18,32 @@ 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("prompt")
|
add("question")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
|
var llm: Chat? = null
|
||||||
|
|
||||||
override val loadingMessage: String
|
override val loadingMessage: String
|
||||||
get() = "深度思考中..."
|
get() = "深度思考中..."
|
||||||
|
|
||||||
override val isEnabled: Boolean
|
override val isEnabled: Boolean
|
||||||
get() = LargeLanguageModels.reasoning != null
|
get() = llm != null
|
||||||
|
|
||||||
override suspend fun execute(args: JsonObject?): String {
|
override suspend fun execute(args: JsonObject?): String {
|
||||||
requireNotNull(args)
|
requireNotNull(args)
|
||||||
val llm = LargeLanguageModels.reasoning ?: return "未配置llm,无法进行推理。"
|
val llm = llm ?: 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.User(prompt))
|
messages = listOf(ChatMessage.Companion.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
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
@ -1,71 +0,0 @@
|
|||||||
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}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
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 { "识图异常,结果为空" }
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,7 +7,6 @@ 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(
|
||||||
@ -22,6 +21,33 @@ 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")
|
||||||
@ -41,17 +67,25 @@ 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 url = buildString {
|
val categories = args["categories"]?.jsonArray
|
||||||
|
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) {
|
||||||
val response = httpClient.get(url)
|
append("&")
|
||||||
JChatGPT.logger.info("Request: $url")
|
append(timeRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
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
|
||||||
|
Loading…
Reference in New Issue
Block a user