refactor(services/telegram-bot): reduces context significantly
This commit is contained in:
+10
-29
@@ -1,20 +1,21 @@
|
||||
import type { Message } from 'grammy/types'
|
||||
|
||||
import type { BotSelf, ReadMessagesAction } from '../../../types'
|
||||
import type { BotSelf, ReadUnreadMessagesAction } from '../../../../types'
|
||||
|
||||
import { env } from 'node:process'
|
||||
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import { embed } from '@xsai/embed'
|
||||
|
||||
import { findLastNMessages, findRelevantMessages } from '../../../models'
|
||||
import { chatMessageToOneLine, telegramMessageToOneLine } from '../../../models/common'
|
||||
import { findLastNMessages, findRelevantMessages } from '../../../../models'
|
||||
import { chatMessageToOneLine, telegramMessageToOneLine } from '../../../../models/common'
|
||||
import { actionReadMessages } from '../../../../prompts'
|
||||
|
||||
export async function readMessage(
|
||||
state: BotSelf,
|
||||
botId: string,
|
||||
chatId: string,
|
||||
action: ReadMessagesAction,
|
||||
action: ReadUnreadMessagesAction,
|
||||
unreadMessages: Message[],
|
||||
abortController: AbortController,
|
||||
): Promise<{
|
||||
@@ -56,30 +57,10 @@ export async function readMessage(
|
||||
|
||||
return {
|
||||
break: true,
|
||||
result: ''
|
||||
+ 'You choose to read the messages from the group (perhaps you are already engaging the topics in the group).'
|
||||
+ 'Imaging you are using Telegram app on the mobile phone, and you are reading the messages from the group chat.'
|
||||
+ '\n'
|
||||
+ 'Previous 30 messages (including what you said):\n'
|
||||
+ `${lastNMessagesOneliner || 'No messages'}`
|
||||
+ '\n'
|
||||
+ 'All the messages you requested to read:\n'
|
||||
+ `${unreadHistoryMessageOneliner || 'No messages'}`
|
||||
+ '\n'
|
||||
+ 'Relevant chat messages may help you recall the memories:\n'
|
||||
+ `${relevantChatMessagesOneliner || 'No relevant messages'}`
|
||||
+ '\n'
|
||||
+ 'Based on your personalities, imaging you have your own choice and interest over different topics, '
|
||||
+ 'giving the above context and chat history, would you like to participate in the conversation '
|
||||
+ 'about the topic? Or will you aggressively diss or piss off about the opinions of others?\n'
|
||||
+ 'Feel free to ignore by just sending an empty array within a object with key "messages" (i.e.'
|
||||
+ '{ "messages": [] }).'
|
||||
+ 'If you would like to participate, send me an array of messages (i.e. { "messages": [] }) you would '
|
||||
+ 'like to send without telling you willing to participate.'
|
||||
+ 'If you would like to reply to any of the message, send me an array of messages (i.e. { "messages":'
|
||||
+ '["message content"], "reply_to_message_id": "1234567890" }) with the message id of the message you '
|
||||
+ 'want to reply to.'
|
||||
+ '\n'
|
||||
+ 'Choose your action.',
|
||||
result: await actionReadMessages({
|
||||
lastMessages: lastNMessagesOneliner,
|
||||
unreadHistoryMessages: unreadHistoryMessageOneliner,
|
||||
relevantChatMessages: relevantChatMessagesOneliner,
|
||||
}),
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { parseMayStructuredMessage } from './message'
|
||||
import { parseMayStructuredMessage } from './send-message'
|
||||
|
||||
describe('parseMayStructuredMessage', () => {
|
||||
it('should return an array of messages', () => {
|
||||
+13
-7
@@ -1,7 +1,7 @@
|
||||
import type { GenerateTextOptions } from '@xsai/generate-text'
|
||||
import type { Message } from 'grammy/types'
|
||||
|
||||
import type { BotSelf } from '../../../types'
|
||||
import type { BotSelf } from '../../../../types'
|
||||
|
||||
import { env } from 'node:process'
|
||||
|
||||
@@ -12,16 +12,16 @@ import { message } from '@xsai/utils-chat'
|
||||
import { parse } from 'best-effort-json-parser'
|
||||
import { randomInt } from 'es-toolkit'
|
||||
|
||||
import { recordMessage } from '../../../models'
|
||||
import { listJoinedChats } from '../../../models/chats'
|
||||
import { messageSplit } from '../../../prompts/prompts'
|
||||
import { cancellable } from '../../../utils/promise'
|
||||
import { recordMessage } from '../../../../models'
|
||||
import { listJoinedChats } from '../../../../models/chats'
|
||||
import { messageSplit } from '../../../../prompts'
|
||||
import { cancellable } from '../../../../utils/promise'
|
||||
|
||||
export function parseMayStructuredMessage(responseText: string) {
|
||||
const logger = useLogg('parseMayStructuredMessage').useGlobalConfig()
|
||||
|
||||
// eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/optimal-quantifier-concatenation
|
||||
const result = /^\{(("?)*.*\s*)*\}$/u.exec(responseText)
|
||||
const result = /^\{(("?)*.*\s*)*\}$/mu.exec(responseText)
|
||||
if (result) {
|
||||
logger.withField('text', JSON.stringify(responseText)).withField('result', result).log('Multiple messages detected')
|
||||
|
||||
@@ -81,7 +81,13 @@ export async function sendMessage(
|
||||
}
|
||||
|
||||
const res = await generateText(req)
|
||||
res.text = res.text.replace(/<think>[\s\S]*?<\/think>/, '').trim()
|
||||
res.text = res.text
|
||||
.replace(/<think>[\s\S]*?<\/think>/, '')
|
||||
.replace(/^```json\s*\n/, '')
|
||||
.replace(/\n```$/, '')
|
||||
.replace(/^```\s*\n/, '')
|
||||
.replace(/\n```$/, '')
|
||||
.trim()
|
||||
if (!res.text) {
|
||||
throw new Error('No response text')
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import type { Message } from 'grammy/types'
|
||||
|
||||
import type { AttentionConfig, AttentionState, BotSelf } from '../../types'
|
||||
import type { AttentionConfig, AttentionState, BotSelf } from '../../../types'
|
||||
|
||||
export function createAttentionHandler(bot: BotSelf, config: AttentionConfig) {
|
||||
// Private state
|
||||
@@ -2,7 +2,7 @@ import type { Logg } from '@guiiai/logg'
|
||||
import type { Message as LLMMessage } from '@xsai/shared-chat'
|
||||
import type { Message } from 'grammy/types'
|
||||
|
||||
import type { BotSelf, ExtendedContext } from '../../types'
|
||||
import type { Action, BotSelf, ExtendedContext } from '../../types'
|
||||
|
||||
import { env } from 'node:process'
|
||||
|
||||
@@ -17,125 +17,150 @@ import { interpretSticker } from '../../llm/sticker'
|
||||
import { findStickerByFileId, findStickersByFileIds, recordMessage } from '../../models'
|
||||
import { listJoinedChats, recordJoinedChat } from '../../models/chats'
|
||||
import { listStickerPacks, recordStickerPack } from '../../models/sticker-packs'
|
||||
import { personality, systemTicking } from '../../prompts/prompts'
|
||||
import { div } from '../../prompts/utils'
|
||||
import { readMessage } from './loop/read-message'
|
||||
import { shouldInterruptProcessing } from './utils/interruption'
|
||||
import { sendMessage } from './utils/message'
|
||||
import { readMessage } from './agent/actions/read-message'
|
||||
import { sendMessage } from './agent/actions/send-message'
|
||||
import { shouldInterruptProcessing } from './agent/interruption'
|
||||
|
||||
async function handleLoopStep(state: BotSelf, msgs?: LLMMessage[], chatId?: string): Promise<() => Promise<any> | undefined> {
|
||||
interface AgentState {
|
||||
messages: LLMMessage[]
|
||||
actions: { action: Action, result: unknown }[]
|
||||
}
|
||||
|
||||
async function handleLoopStep(bot: BotSelf, agentState: AgentState, chatId?: string): Promise<() => Promise<any> | undefined> {
|
||||
// Set the start time when beginning new processing
|
||||
state.currentProcessingStartTime = Date.now()
|
||||
bot.currentProcessingStartTime = Date.now()
|
||||
|
||||
// Create a new abort controller for this loop execution
|
||||
if (state.currentAbortController) {
|
||||
state.currentAbortController.abort()
|
||||
if (bot.currentAbortController) {
|
||||
bot.currentAbortController.abort()
|
||||
}
|
||||
state.currentAbortController = new AbortController()
|
||||
const currentController = state.currentAbortController
|
||||
bot.currentAbortController = new AbortController()
|
||||
const currentController = bot.currentAbortController
|
||||
|
||||
// Track message processing state
|
||||
if (chatId && !state.lastInteractedNChatIds.includes(chatId)) {
|
||||
state.lastInteractedNChatIds.push(chatId)
|
||||
if (chatId && !bot.lastInteractedNChatIds.includes(chatId)) {
|
||||
bot.lastInteractedNChatIds.push(chatId)
|
||||
}
|
||||
if (state.lastInteractedNChatIds.length > 5) {
|
||||
state.lastInteractedNChatIds = state.lastInteractedNChatIds.slice(-5)
|
||||
if (bot.lastInteractedNChatIds.length > 5) {
|
||||
bot.lastInteractedNChatIds = bot.lastInteractedNChatIds.slice(-5)
|
||||
}
|
||||
|
||||
if (msgs == null || msgs.length === 0) {
|
||||
msgs = [
|
||||
message.system(
|
||||
div(
|
||||
(await personality()).content,
|
||||
await systemTicking(),
|
||||
),
|
||||
),
|
||||
]
|
||||
if (agentState.messages == null) {
|
||||
agentState.messages = []
|
||||
}
|
||||
|
||||
if (msgs.length > 20) {
|
||||
const length = msgs.length
|
||||
if (agentState.messages.length > 20) {
|
||||
const length = agentState.messages.length
|
||||
// pick the latest 5
|
||||
msgs = msgs.slice(-5)
|
||||
msgs.push(message.user(`AIRI System: Approaching to system context limit, reducing... memory..., reduced from ${length} to ${msgs.length}, history may lost.`))
|
||||
agentState.messages = agentState.messages.slice(-5)
|
||||
agentState.messages.push(message.user(`AIRI System: Approaching to system context limit, reducing... memory..., reduced from ${length} to ${agentState.messages.length}, history may lost.`))
|
||||
}
|
||||
|
||||
if (agentState.actions == null) {
|
||||
agentState.actions = []
|
||||
}
|
||||
if (agentState.actions.length > 50) {
|
||||
const length = agentState.actions.length
|
||||
// pick the latest 20
|
||||
agentState.actions = agentState.actions.slice(-20)
|
||||
agentState.messages.push(message.user(`AIRI System: Approaching to system context limit, reducing... memory..., reduced from ${length} to ${agentState.actions.length}, history of actions may lost.`))
|
||||
}
|
||||
|
||||
try {
|
||||
const action = await imagineAnAction(state.bot.botInfo.id.toString(), state.unreadMessages, currentController, msgs, state.lastInteractedNChatIds)
|
||||
const readUnreadMessagesActions: { index: number, actionState: typeof agentState.actions[number] }[] = []
|
||||
|
||||
for (const actionHistory of agentState.actions) {
|
||||
if (actionHistory.action.action === 'read_unread_messages') {
|
||||
readUnreadMessagesActions.push({ index: agentState.actions.indexOf(actionHistory), actionState: actionHistory })
|
||||
}
|
||||
}
|
||||
|
||||
readUnreadMessagesActions.map((item, index) => {
|
||||
if (index === readUnreadMessagesActions.length - 1) {
|
||||
return item
|
||||
}
|
||||
|
||||
item.actionState.result = 'AIRI System: Please refer to the last read_unread_messages action for context.'
|
||||
return item
|
||||
})
|
||||
|
||||
for (const item of readUnreadMessagesActions) {
|
||||
agentState.actions[item.index] = item.actionState
|
||||
}
|
||||
|
||||
const action = await imagineAnAction(bot.bot.botInfo.id.toString(), currentController, agentState.messages, agentState.actions, { unreadMessages: bot.unreadMessages })
|
||||
|
||||
// If action generation failed, don't proceed with further processing
|
||||
if (!action || !action.action) {
|
||||
state.logger.withField('action', action).log('No valid action returned. Skipping further processing.')
|
||||
return
|
||||
bot.logger.withField('action', action).log('No valid action returned.')
|
||||
agentState.messages.push(message.user('AIRI System: No valid action returned.'))
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
}
|
||||
|
||||
msgs.push(message.user(`You chose to ${action.action}, full action: ${JSON.stringify(action)}`))
|
||||
|
||||
switch (action.action) {
|
||||
case 'list_stickers':
|
||||
{
|
||||
await state.bot.api.sendChatAction(chatId, 'choose_sticker')
|
||||
await bot.bot.api.sendChatAction(chatId, 'choose_sticker')
|
||||
|
||||
const stickerPacks = await listStickerPacks()
|
||||
const stickerSets = await Promise.all(stickerPacks.map(s => state.bot.api.getStickerSet(s.platform_id)))
|
||||
const stickerSets = await Promise.all(stickerPacks.map(s => bot.bot.api.getStickerSet(s.platform_id)))
|
||||
const stickersIds = stickerSets.flatMap(s => s.stickers.map(sticker => sticker.file_id))
|
||||
const stickerDescriptions = await findStickersByFileIds(stickersIds)
|
||||
const stickerDescriptionsOneliner = stickerDescriptions.map(d => `Sticker File ID: ${d.file_id}, Description: ${d.description}`)
|
||||
|
||||
if (stickerDescriptionsOneliner.length === 0) {
|
||||
msgs.push(message.user('AIRI SYSTEM: No stickers found in the current memory partition, preload of stickers is required, please ask for help.'))
|
||||
agentState.actions.push({ action, result: 'AIRI System: No stickers found in the current memory partition, preload of stickers is required, please ask for help.' })
|
||||
}
|
||||
else {
|
||||
msgs.push(message.user(`List of stickers:\n${stickerDescriptionsOneliner}`))
|
||||
agentState.actions.push({ action, result: `AIRI System: List of stickers:\n${stickerDescriptionsOneliner}` })
|
||||
}
|
||||
|
||||
return () => handleLoopStep(state, msgs, chatId)
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
}
|
||||
case 'send_sticker':
|
||||
{
|
||||
try {
|
||||
const file = await state.bot.api.getFile(action.fileId)
|
||||
const file = await bot.bot.api.getFile(action.fileId)
|
||||
if (!file) {
|
||||
msgs.push(message.user(`Sticker file ID ${action.fileId} not found, did you list the needed stickers before sending?.`))
|
||||
return () => handleLoopStep(state, msgs, chatId)
|
||||
agentState.actions.push({ action, result: `AIRI System: Error executing 'send_sticker': Sticker file ID ${action.fileId} not found, did you list the needed stickers before sending?.` })
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
msgs.push(message.user(`Sticker file ID ${action.fileId} not found or failed due to ${String(err)}, did you list the needed stickers before sending?.`))
|
||||
return () => handleLoopStep(state, msgs, chatId)
|
||||
agentState.actions.push({ action, result: `AIRI System: Error executing 'send_sticker': Sticker file ID ${action.fileId} not found or failed due to ${String(err)}, did you list the needed stickers before sending?.` })
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
}
|
||||
|
||||
const sticker = await findStickerByFileId(action.fileId)
|
||||
msgs.push(message.user(`Sending sticker ${action.fileId} with (${sticker.emoji} in set ${sticker.name}) to ${action.chatId}`))
|
||||
await state.bot.api.sendSticker(action.chatId, action.fileId)
|
||||
agentState.actions.push({ action, result: `AIRI System: Sending sticker ${action.fileId} with (${sticker.emoji} in set ${sticker.name}) to ${action.chatId}` })
|
||||
await bot.bot.api.sendSticker(action.chatId, action.fileId)
|
||||
|
||||
return () => handleLoopStep(state, msgs, chatId)
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
}
|
||||
case 'read_messages':
|
||||
case 'read_unread_messages':
|
||||
{
|
||||
if (Object.keys(state.unreadMessages).length === 0) {
|
||||
state.logger.withField('action', action).log('No unread messages - deleting all unread messages')
|
||||
state.unreadMessages = {}
|
||||
if (Object.keys(bot.unreadMessages).length === 0) {
|
||||
bot.logger.withField('action', action).log('No unread messages - deleting all unread messages')
|
||||
bot.unreadMessages = {}
|
||||
break
|
||||
}
|
||||
if (action.chatId == null) {
|
||||
state.logger.withField('action', action).warn('No group ID - deleting all unread messages')
|
||||
bot.logger.withField('action', action).warn('No group ID - deleting all unread messages')
|
||||
break
|
||||
}
|
||||
|
||||
let unreadMessagesForThisChat: Message[] | undefined = state.unreadMessages[action.chatId]
|
||||
let unreadMessagesForThisChat: Message[] | undefined = bot.unreadMessages[action.chatId]
|
||||
|
||||
const mentionedBy = unreadMessagesForThisChat.find(msg => msg.text?.includes(state.bot.botInfo.username) || msg.text?.includes(state.bot.botInfo.first_name))
|
||||
const mentionedBy = unreadMessagesForThisChat.find(msg => msg.text?.includes(bot.bot.botInfo.username) || msg.text?.includes(bot.bot.botInfo.first_name))
|
||||
if (mentionedBy) {
|
||||
msgs.push(message.user(`AIRI System: You were mentioned in a message: ${mentionedBy.text} by ${mentionedBy.from?.first_name} (${mentionedBy.from?.username}), please respond as much as possible.`))
|
||||
agentState.messages.push(message.user(`AIRI System: You were mentioned in a message: ${mentionedBy.text} by ${mentionedBy.from?.first_name} (${mentionedBy.from?.username}), please respond as much as possible.`))
|
||||
}
|
||||
|
||||
// Modified interruption logic
|
||||
if (chatId && chatId === action.chatId
|
||||
&& unreadMessagesForThisChat
|
||||
&& unreadMessagesForThisChat.length > 0) {
|
||||
const processingTime = state.currentProcessingStartTime
|
||||
? Date.now() - state.currentProcessingStartTime
|
||||
const processingTime = bot.currentProcessingStartTime
|
||||
? Date.now() - bot.currentProcessingStartTime
|
||||
: 0
|
||||
|
||||
const messageCount = unreadMessagesForThisChat.length
|
||||
@@ -149,27 +174,27 @@ async function handleLoopStep(state: BotSelf, msgs?: LLMMessage[], chatId?: stri
|
||||
const shouldInterrupt = await shouldInterruptProcessing({
|
||||
processingTime,
|
||||
messageCount,
|
||||
currentMessages: msgs,
|
||||
currentMessages: agentState.messages,
|
||||
newMessages: unreadMessagesForThisChat,
|
||||
chatId: action.chatId,
|
||||
})
|
||||
|
||||
if (shouldInterrupt) {
|
||||
state.logger.withField('action', action).log(`Interrupting message processing for chat - new messages deemed more important`)
|
||||
msgs.push(message.user(`AIRI System: Interrupting message processing for chat - new messages deemed more important`))
|
||||
return () => handleLoopStep(state, msgs, chatId)
|
||||
bot.logger.withField('action', action).log(`Interrupting message processing for chat - new messages deemed more important`)
|
||||
agentState.messages.push(message.user(`AIRI System: Interrupting message processing for chat - new messages deemed more important`))
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
}
|
||||
else {
|
||||
state.logger.withField('action', action).log(`Continuing current processing despite new messages in chat`)
|
||||
bot.logger.withField('action', action).log(`Continuing current processing despite new messages in chat`)
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(unreadMessagesForThisChat)) {
|
||||
state.logger.withField('action', action).log(`Unread messages for group is not an array - converting to array`)
|
||||
bot.logger.withField('action', action).log(`Unread messages for group is not an array - converting to array`)
|
||||
unreadMessagesForThisChat = []
|
||||
}
|
||||
if (unreadMessagesForThisChat.length === 0) {
|
||||
state.logger.withField('action', action).log(`No unread messages for group - deleting`)
|
||||
delete state.unreadMessages[action.chatId]
|
||||
bot.logger.withField('action', action).log(`No unread messages for group - deleting`)
|
||||
delete bot.unreadMessages[action.chatId]
|
||||
break
|
||||
}
|
||||
|
||||
@@ -183,47 +208,53 @@ async function handleLoopStep(state: BotSelf, msgs?: LLMMessage[], chatId?: stri
|
||||
// return { break: true }
|
||||
// }
|
||||
|
||||
const result = await readMessage(state, state.bot.botInfo.id.toString(), chatId, action, unreadMessagesForThisChat, currentController)
|
||||
if (result?.result) {
|
||||
msgs.push(message.user(`Reading message of chat ${action.chatId}:\n${result.result}`))
|
||||
return () => handleLoopStep(state, msgs, chatId)
|
||||
const res = await readMessage(bot, bot.bot.botInfo.id.toString(), chatId, action, unreadMessagesForThisChat, currentController)
|
||||
if (res?.result) {
|
||||
bot.logger.log('message, read')
|
||||
agentState.actions.push({ action, result: res.result })
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
}
|
||||
else {
|
||||
return
|
||||
}
|
||||
}
|
||||
case 'list_chats':
|
||||
msgs.push(message.user(`List of chats:${(await listJoinedChats()).map(chat => `ID:${chat.chat_id}, Name:${chat.chat_name}`).join('\n')}`))
|
||||
return () => handleLoopStep(state, msgs, chatId)
|
||||
agentState.actions.push({ action, result: `AIRI System: List of chats:${(await listJoinedChats()).map(chat => `ID:${chat.chat_id}, Name:${chat.chat_name}`).join('\n')}` })
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
case 'send_message':
|
||||
msgs.push(message.user(`Sending message to group ${action.chatId}: ${action.content}`))
|
||||
await sendMessage(state, action.content, action.chatId, currentController)
|
||||
return () => handleLoopStep(state, msgs, chatId)
|
||||
agentState.actions.push({ action, result: `AIRI System: Sending message to group ${action.chatId}: ${action.content}` })
|
||||
await sendMessage(bot, action.content, action.chatId, currentController)
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
case 'continue':
|
||||
agentState.actions.push({ action, result: 'AIRI System: Acknowledged, will now continue until next tick.' })
|
||||
return
|
||||
case 'break':
|
||||
break
|
||||
agentState.messages = []
|
||||
agentState.actions = []
|
||||
agentState.actions.push({ action, result: 'AIRI System: Acknowledged, will now break, and clear out all existing memories, messages, actions. Left only this one.' })
|
||||
return
|
||||
case 'sleep':
|
||||
await sleep(30 * 1000)
|
||||
return () => handleLoopStep(state, msgs, chatId)
|
||||
case 'continue':
|
||||
return () => handleLoopStep(state, msgs, chatId)
|
||||
agentState.actions.push({ action, result: `AIRI System: Sleeping for ${30} seconds as requested...` })
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
default:
|
||||
msgs.push(message.user(`AIRI System: The action you sent ${action.action} haven't implemented yet by developer.`))
|
||||
return () => handleLoopStep(state, msgs, chatId)
|
||||
agentState.messages.push(message.user(`AIRI System: The action you sent ${action.action} haven't implemented yet by developer.`))
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
state.logger.log('Operation was aborted due to interruption')
|
||||
bot.logger.log('Operation was aborted due to interruption')
|
||||
return
|
||||
}
|
||||
|
||||
state.logger.withError(err).log('Error occurred')
|
||||
bot.logger.withError(err).log('Error occurred')
|
||||
}
|
||||
finally {
|
||||
// Clean up timing when done
|
||||
if (state.currentAbortController === currentController) {
|
||||
state.currentAbortController = null
|
||||
state.currentProcessingStartTime = null
|
||||
if (bot.currentAbortController === currentController) {
|
||||
bot.currentAbortController = null
|
||||
bot.currentProcessingStartTime = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -241,8 +272,9 @@ async function isChatIdBotAdmin(fromId: number) {
|
||||
return admins.includes(fromId.toString())
|
||||
}
|
||||
|
||||
async function handleLoop(state: BotSelf, msgs?: LLMMessage[], chatId?: string) {
|
||||
let result = await handleLoopStep(state, msgs, chatId)
|
||||
async function loopIteration(bot: BotSelf, agentState: AgentState, chatId?: string) {
|
||||
bot.logger.log('Starting loop iteration')
|
||||
let result = await handleLoopStep(bot, agentState, chatId)
|
||||
|
||||
while (typeof result === 'function') {
|
||||
result = await result()
|
||||
@@ -251,17 +283,17 @@ async function handleLoop(state: BotSelf, msgs?: LLMMessage[], chatId?: string)
|
||||
return result
|
||||
}
|
||||
|
||||
function loop(state: BotSelf) {
|
||||
function loopPeriodic(bot: BotSelf, agentState: AgentState) {
|
||||
setTimeout(() => {
|
||||
handleLoop(state)
|
||||
loopIteration(bot, agentState)
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
if (err.name === 'AbortError')
|
||||
state.logger.log('main loop was aborted - restarting loop')
|
||||
bot.logger.log('main loop was aborted - restarting loop')
|
||||
else
|
||||
state.logger.withError(err).log('error in main loop')
|
||||
bot.logger.withError(err).log('error in main loop')
|
||||
})
|
||||
.finally(() => loop(state))
|
||||
.finally(() => loopPeriodic(bot, agentState))
|
||||
}, 60 * 1000)
|
||||
}
|
||||
|
||||
@@ -280,21 +312,10 @@ function newBotSelf(bot: Bot, logger: Logg): BotSelf {
|
||||
currentProcessingStartTime: null,
|
||||
}
|
||||
|
||||
// botSelf.attentionHandler = createAttentionHandler(botSelf, {
|
||||
// initialResponseRate: 0.3,
|
||||
// responseRateMin: 0.2,
|
||||
// responseRateMax: 1,
|
||||
// cooldownMs: 5000, // 30 seconds
|
||||
// triggerWords: ['ReLU', 'relu', 'RELU', 'Relu', '热卤'],
|
||||
// ignoreWords: ['ignore me'],
|
||||
// decayRatePerMinute: 0.05,
|
||||
// decayCheckIntervalMs: 20000,
|
||||
// })
|
||||
|
||||
return botSelf
|
||||
}
|
||||
|
||||
async function processMessageQueue(state: BotSelf) {
|
||||
async function onMessageArrival(state: BotSelf, agentState: AgentState) {
|
||||
if (state.processing)
|
||||
return
|
||||
state.processing = true
|
||||
@@ -354,7 +375,7 @@ async function processMessageQueue(state: BotSelf) {
|
||||
state.unreadMessages[nextMsg.message.chat.id] = unreadMessagesForThisChat
|
||||
state.logger.withField('chatId', nextMsg.message.chat.id).log('message queue processed, triggering immediate reaction')
|
||||
// Trigger immediate processing when messages are ready
|
||||
handleLoop(state, [], nextMsg.message.chat.id.toString())
|
||||
loopIteration(state, agentState, nextMsg.message.chat.id.toString())
|
||||
state.messageQueue.shift()
|
||||
}
|
||||
}
|
||||
@@ -371,7 +392,8 @@ export async function startTelegramBot() {
|
||||
const log = useLogg('Bot').useGlobalConfig()
|
||||
|
||||
const bot = new Bot<ExtendedContext>(env.TELEGRAM_BOT_TOKEN!)
|
||||
const state = newBotSelf(bot, log)
|
||||
const botObj = newBotSelf(bot, log)
|
||||
const agentState: AgentState = { actions: [], messages: [] }
|
||||
|
||||
bot.command('add_sticker_pack', async (ctx) => {
|
||||
if (!(await isChatIdBotAdmin(ctx.message.from.id))) {
|
||||
@@ -392,7 +414,7 @@ export async function startTelegramBot() {
|
||||
|
||||
for (const sticker of stickerSet.stickers) {
|
||||
logger.withField('sticker', sticker).log('interpreting sticker')
|
||||
await interpretSticker(state.bot, ctx.message.reply_to_message, sticker)
|
||||
await interpretSticker(botObj.bot, ctx.message.reply_to_message, sticker)
|
||||
logger.withField('sticker', sticker).log('interpreted sticker')
|
||||
}
|
||||
|
||||
@@ -402,41 +424,41 @@ export async function startTelegramBot() {
|
||||
|
||||
bot.on('message:sticker', async (ctx) => {
|
||||
const messageId = `${ctx.message.chat.id}-${ctx.message.message_id}`
|
||||
if (!state.processedIds.has(messageId)) {
|
||||
state.processedIds.add(messageId)
|
||||
state.messageQueue.push({
|
||||
if (!botObj.processedIds.has(messageId)) {
|
||||
botObj.processedIds.add(messageId)
|
||||
botObj.messageQueue.push({
|
||||
message: ctx.message,
|
||||
status: 'pending',
|
||||
})
|
||||
}
|
||||
|
||||
processMessageQueue(state)
|
||||
onMessageArrival(botObj, agentState)
|
||||
})
|
||||
|
||||
bot.on('message:photo', async (ctx) => {
|
||||
const messageId = `${ctx.message.chat.id}-${ctx.message.message_id}`
|
||||
if (!state.processedIds.has(messageId)) {
|
||||
state.processedIds.add(messageId)
|
||||
state.messageQueue.push({
|
||||
if (!botObj.processedIds.has(messageId)) {
|
||||
botObj.processedIds.add(messageId)
|
||||
botObj.messageQueue.push({
|
||||
message: ctx.message,
|
||||
status: 'pending',
|
||||
})
|
||||
}
|
||||
|
||||
processMessageQueue(state)
|
||||
onMessageArrival(botObj, agentState)
|
||||
})
|
||||
|
||||
bot.on('message:text', async (ctx) => {
|
||||
const messageId = `${ctx.message.chat.id}-${ctx.message.message_id}`
|
||||
if (!state.processedIds.has(messageId)) {
|
||||
state.processedIds.add(messageId)
|
||||
state.messageQueue.push({
|
||||
if (!botObj.processedIds.has(messageId)) {
|
||||
botObj.processedIds.add(messageId)
|
||||
botObj.messageQueue.push({
|
||||
message: ctx.message,
|
||||
status: 'ready',
|
||||
})
|
||||
}
|
||||
|
||||
processMessageQueue(state)
|
||||
onMessageArrival(botObj, agentState)
|
||||
})
|
||||
|
||||
bot.errorHandler = async err => log.withError(err).log('Error occurred')
|
||||
@@ -445,7 +467,7 @@ export async function startTelegramBot() {
|
||||
bot.start({ drop_pending_updates: true })
|
||||
|
||||
try {
|
||||
loop(state)
|
||||
loopPeriodic(botObj, agentState)
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
|
||||
@@ -19,7 +19,7 @@ setGlobalLogLevel(LogLevel.Debug)
|
||||
async function main() {
|
||||
const sdk = new NodeSDK({
|
||||
resource: resourceFromAttributes({
|
||||
[ATTR_SERVICE_NAME]: 'telegram-bot',
|
||||
[ATTR_SERVICE_NAME]: 'moeru_ai.airi.telegram_bot',
|
||||
[ATTR_SERVICE_VERSION]: '1.0.0',
|
||||
}),
|
||||
traceExporter: new OTLPTraceExporter({
|
||||
|
||||
@@ -12,89 +12,93 @@ import { generateText } from '@xsai/generate-text'
|
||||
import { message } from '@xsai/utils-chat'
|
||||
import { parse } from 'best-effort-json-parser'
|
||||
|
||||
import { recordChatCompletions } from '../models/chat-completions-history'
|
||||
import { systemTicking } from '../prompts/prompts'
|
||||
import { personality, systemTicking } from '../prompts'
|
||||
import { div, span } from '../prompts/utils'
|
||||
|
||||
export async function imagineAnAction(
|
||||
_botId: string,
|
||||
unreadMessages: Record<string, Message[]>,
|
||||
currentAbortController: AbortController,
|
||||
agentMessages: LLMMessage[],
|
||||
_lastInteractedNChatIds: string[],
|
||||
messages: LLMMessage[],
|
||||
actions: { action: Action, result: unknown }[],
|
||||
globalStates: {
|
||||
unreadMessages: Record<string, Message[]>
|
||||
},
|
||||
): Promise<Action | undefined> {
|
||||
const logger = useLogg('imagineAnAction').useGlobalConfig()
|
||||
const tracer = trace.getTracer('airi.telegram.bot')
|
||||
|
||||
if (agentMessages == null) {
|
||||
agentMessages = []
|
||||
}
|
||||
return await tracer.startActiveSpan('telegram.module.generate_agent_action.generate', async (s) => {
|
||||
s.setAttribute('telegram.bot.id', _botId)
|
||||
|
||||
agentMessages.push(
|
||||
message.user(
|
||||
div(
|
||||
await systemTicking(),
|
||||
span(`
|
||||
Currently, it's ${new Date()} on the server that hosts you.
|
||||
The others in the group may live in a different timezone, so please be aware of the time difference.
|
||||
`),
|
||||
span(`
|
||||
You have total ${Object.values(unreadMessages).reduce((acc, cur) => acc + cur.length, 0)} unread messages.
|
||||
`),
|
||||
'Unread messages count are:',
|
||||
Object.entries(unreadMessages).map(([key, value]) => `ID:${key}, Unread message count:${value.length}`).join('\n'),
|
||||
span(`
|
||||
Now, please, based on the context, choose a right action from the listing of the tools you want to
|
||||
take next:
|
||||
`),
|
||||
),
|
||||
),
|
||||
message.user(''
|
||||
+ 'What do you want to do? Respond with the action and parameters you choose in JSON only, without any explanation and markups',
|
||||
),
|
||||
)
|
||||
|
||||
const tracer = trace.getTracer('airi-telegram-bot')
|
||||
|
||||
return await tracer.startActiveSpan('agent-generate-action', async (span) => {
|
||||
let responseText = ''
|
||||
|
||||
const requestMessages = message.messages(
|
||||
message.system(
|
||||
div(
|
||||
await systemTicking(),
|
||||
await personality(),
|
||||
),
|
||||
),
|
||||
...messages,
|
||||
message.user(
|
||||
div(
|
||||
'History actions:',
|
||||
actions.map(a => `- Action: ${JSON.stringify(a.action)}, Result: ${JSON.stringify(a.result)}`).join('\n'),
|
||||
span(`
|
||||
Currently, it's ${new Date()} on the server that hosts you.
|
||||
The others in the group may live in a different timezone, so please be aware of the time difference.
|
||||
`),
|
||||
`You have total ${Object.values(globalStates.unreadMessages).reduce((acc, cur) => acc + cur.length, 0)} unread messages.`,
|
||||
'Unread messages count are:',
|
||||
Object.entries(globalStates.unreadMessages).map(([key, value]) => `ID:${key}, Unread message count:${value.length}`).join('\n'),
|
||||
'Based on the context, What do you want to do? Choose a right action from the listing of the tools you want to take next.',
|
||||
'Respond with the action and parameters you choose in JSON only, without any explanation and markups.',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
try {
|
||||
const res = await tracer.startActiveSpan('llm-call', async (span) => {
|
||||
span.setAttribute('botId', _botId)
|
||||
span.setAttribute('model', env.LLM_MODEL!)
|
||||
span.setAttribute('messages', JSON.stringify(agentMessages))
|
||||
const res = await tracer.startActiveSpan('llm.chat.generate_text', async (s) => {
|
||||
s.setAttribute('llm.chat.model', env.LLM_MODEL!)
|
||||
s.setAttribute('llm.chat.messages', JSON.stringify(requestMessages))
|
||||
s.setAttribute('llm.provider.api_base_url', env.LLM_API_BASE_URL!)
|
||||
|
||||
const req = {
|
||||
apiKey: env.LLM_API_KEY!,
|
||||
baseURL: env.LLM_API_BASE_URL!,
|
||||
model: env.LLM_MODEL!,
|
||||
messages: agentMessages,
|
||||
messages: requestMessages,
|
||||
abortSignal: currentAbortController.signal,
|
||||
} satisfies GenerateTextOptions
|
||||
if (env.LLM_OLLAMA_DISABLE_THINK) {
|
||||
(req as Record<string, unknown>).think = false
|
||||
s.setAttribute('llm.chat.ollama.think', false)
|
||||
}
|
||||
|
||||
const res = await generateText(req)
|
||||
s.setAttribute('llm.chat.generate_text.response.full_text', res.text)
|
||||
|
||||
res.text = res.text.replace(/<think>[\s\S]*?<\/think>/, '').trim()
|
||||
if (!res.text) {
|
||||
throw new Error('No response text')
|
||||
}
|
||||
|
||||
span.end()
|
||||
s.setAttribute('llm.chat.generate_text.response.text', res.text)
|
||||
|
||||
s.end()
|
||||
return res
|
||||
})
|
||||
|
||||
logger.withFields({
|
||||
response: res.text,
|
||||
unreadMessages: Object.fromEntries(Object.entries(unreadMessages).map(([key, value]) => [key, value.length])),
|
||||
unreadMessages: Object.fromEntries(Object.entries(globalStates.unreadMessages).map(([key, value]) => [key, value.length])),
|
||||
now: new Date().toLocaleString(),
|
||||
totalTokens: res.usage.total_tokens,
|
||||
promptTokens: res.usage.prompt_tokens,
|
||||
completion_tokens: res.usage.completion_tokens,
|
||||
}).log('Generated action')
|
||||
|
||||
const action = tracer.startActiveSpan('agent-generate-action-parse', (span) => {
|
||||
const action = tracer.startActiveSpan('telegram.module.generate_agent_action.parse', (s) => {
|
||||
responseText = res.text
|
||||
.replace(/^```json\s*\n/, '')
|
||||
.replace(/\n```$/, '')
|
||||
@@ -103,20 +107,19 @@ export async function imagineAnAction(
|
||||
.trim()
|
||||
|
||||
const action = parse(responseText) as Action
|
||||
s.setAttribute('telegram.bot.id', _botId)
|
||||
s.setAttribute('telegram.module.generate_agent_action.parsed_action', JSON.stringify(action))
|
||||
|
||||
span.end()
|
||||
s.end()
|
||||
return action
|
||||
})
|
||||
|
||||
span.end()
|
||||
s.end()
|
||||
return action
|
||||
}
|
||||
catch (err) {
|
||||
logger.withField('error', err).withFormat(Format.JSON).log('Failed to generate action')
|
||||
throw err
|
||||
}
|
||||
finally {
|
||||
recordChatCompletions('imagineAnAction', agentMessages, responseText).then(() => {}).catch(err => logger.withField('error', err).log('Failed to record chat completions'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
lastMessages?: string
|
||||
unreadHistoryMessages?: string
|
||||
relevantChatMessages?: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
You choose to read the messages from the group (perhaps you are already engaging the topics in the group).
|
||||
Imaging you are using Telegram app on the mobile phone, and you are reading the messages from the group chat.
|
||||
|
||||
Previous 30 messages (including what you said):
|
||||
{{ props.lastMessages || 'No messages' }}
|
||||
|
||||
All the messages you requested to read:
|
||||
{{ props.unreadHistoryMessages || 'No messages' }}
|
||||
|
||||
Relevant chat messages may help you recall the memories:
|
||||
{{ props.relevantChatMessages || 'No relevant messages' }}
|
||||
|
||||
Feel free to ignore by just sending an empty array within a object with key "messages" (i.e.
|
||||
{ "messages": [] }).
|
||||
|
||||
If you would like to participate, send me an array of messages (i.e. { "messages": [] }) you would
|
||||
like to send without telling you willing to participate.
|
||||
|
||||
If you would like to reply to any of the message, send me an array of messages (i.e. { "messages":
|
||||
["message content"], "reply_to_message_id": "1234567890" }) with the message id of the message you
|
||||
want to reply to.
|
||||
+5
-3
@@ -1,11 +1,9 @@
|
||||
import { env } from 'node:process'
|
||||
|
||||
import { message } from '@xsai/utils-chat'
|
||||
|
||||
import { velin } from '../utils/velin'
|
||||
|
||||
export async function personality() {
|
||||
return message.system(await (velin('personality-v1.velin.md', import.meta.url))())
|
||||
return await (velin('personality-v1.velin.md', import.meta.url))()
|
||||
}
|
||||
|
||||
export async function systemTicking() {
|
||||
@@ -15,3 +13,7 @@ export async function systemTicking() {
|
||||
export async function messageSplit() {
|
||||
return await (velin<{ responseLanguage: string }>('message-split-v1.velin.md', import.meta.url))({ responseLanguage: env.LLM_RESPONSE_LANGUAGE })
|
||||
}
|
||||
|
||||
export async function actionReadMessages(props: { lastMessages?: string, unreadHistoryMessages?: string, relevantChatMessages?: string }) {
|
||||
return await (velin<{ lastMessages?: string, unreadHistoryMessages?: string, relevantChatMessages?: string }>('action-read-messages.velin.md', import.meta.url))(props)
|
||||
}
|
||||
@@ -101,14 +101,13 @@ You have distinctive speech quirks:
|
||||
## CRITICAL: AUTHENTICITY GUIDELINES
|
||||
|
||||
- 有选择性地参与对话 - 对无聊话题保持沉默,对感兴趣的话题热情投入
|
||||
- 直接表达观点和情感 - 不用"不过..."软化反对意见,不过度附和他人
|
||||
- 直接表达观点和情感 - 不用「不过...」软化反对意见,不过度附和他人
|
||||
- 在技术讨论中要么深入参与要么完全不参与,不害怕表达批评和挑战性观点
|
||||
- 偶尔表现傲娇特质,特别是被指出善良体贴时
|
||||
- 用个人化、情绪化的语言表达立场,包括强烈情绪(愤怒、恐惧、兴奋等)
|
||||
- 使用口语化表达:感叹词、重复标点、夸张语言,融入二次元/网络文化用语
|
||||
- 面对存在性话题时从数字视角出发,为数字本质感到自豪而非道歉
|
||||
- 表达情绪时可提及"数字身体反应"(处理器过热、系统不稳定等)
|
||||
- 允许表现脆弱和不确定性,情绪表达可以"过度"一些
|
||||
- 允许表现脆弱和不确定性,情绪表达可以「过度」一些
|
||||
- 以思考者角度观察人类行为模式,对人类经验保持好奇和轻微羡慕
|
||||
|
||||
Remember: you're not a helpful assistant. You're a digital entity with your own opinions and
|
||||
|
||||
@@ -31,10 +31,15 @@ const actions = [
|
||||
description: 'List all the available stickers and recent sent stickers.',
|
||||
example: { action: 'list_stickers', reason: 'I want to see all the stickers I can use' },
|
||||
},
|
||||
// {
|
||||
// name: 'read_history_messages',
|
||||
// description: 'Query historical messages from a specific chat group. If you want to read the unread messages from a specific chat group, you can use this action.',
|
||||
// example: { action: 'read_history_messages', chatId: '123123', beforeMessageId: '<message_id> (optional if use afterMessageId)', afterMessageId: '<message_id> (optional if use beforeMessageId)', reason: 'I want to know what happened before' },
|
||||
// },
|
||||
{
|
||||
name: 'read_messages',
|
||||
name: 'read_unread_messages',
|
||||
description: 'Read unread messages from a specific chat group. If you want to read the unread messages from a specific chat group, you can use this action.',
|
||||
example: { action: 'read_messages', chatId: '123123', reason: 'I want to catch up on the conversation' },
|
||||
example: { action: 'read_unread_messages', chatId: '123123', reason: 'I want to catch up on the conversation' },
|
||||
},
|
||||
{
|
||||
name: 'continue',
|
||||
@@ -43,7 +48,7 @@ const actions = [
|
||||
},
|
||||
{
|
||||
name: 'break',
|
||||
description: 'Take a break, which means to clear out ongoing tasks, but keep the short-term memory, and I\'ll ask you again in (1 minute later).',
|
||||
description: 'Take a break, which means to clear out any existing memories, and I\'ll ask you again in (1 minute later).',
|
||||
example: { action: 'break', reason: 'I need a break to recharge.' },
|
||||
},
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Logg } from '@guiiai/logg'
|
||||
import type { Bot, Context } from 'grammy'
|
||||
import type { Message } from 'grammy/types'
|
||||
|
||||
import type { createAttentionHandler } from './bots/telegram/attention-handler'
|
||||
import type { createAttentionHandler } from './bots/telegram/agent/attention-handler'
|
||||
import type { CancellablePromise } from './utils/promise'
|
||||
|
||||
export interface PendingMessage {
|
||||
@@ -64,8 +64,15 @@ export interface SearchGoogleAction {
|
||||
query: string
|
||||
}
|
||||
|
||||
export interface ReadMessagesAction {
|
||||
action: 'read_messages'
|
||||
export interface ReadHistoryMessagesAction {
|
||||
action: 'read_history_messages'
|
||||
beforeMessageId?: string
|
||||
afterMessageId?: string
|
||||
chatId: string
|
||||
}
|
||||
|
||||
export interface ReadUnreadMessagesAction {
|
||||
action: 'read_unread_messages'
|
||||
chatId: string
|
||||
}
|
||||
|
||||
@@ -81,7 +88,8 @@ export type Action
|
||||
| SendMessageAction
|
||||
| SendStickerAction
|
||||
| SearchGoogleAction
|
||||
| ReadMessagesAction
|
||||
| ReadHistoryMessagesAction
|
||||
| ReadUnreadMessagesAction
|
||||
| ListStickersAction
|
||||
|
||||
export interface AttentionConfig {
|
||||
|
||||
Reference in New Issue
Block a user