feat(services/telegram-bot): better o11y
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import type { Message } from 'grammy/types'
|
||||
|
||||
import type { BotSelf, ReadUnreadMessagesAction } from '../../../../types'
|
||||
import type { BotContext, ReadUnreadMessagesAction } from '../../../../types'
|
||||
|
||||
import { env } from 'node:process'
|
||||
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import { withRetry } from '@moeru/std'
|
||||
import { trace } from '@opentelemetry/api'
|
||||
import { embed } from '@xsai/embed'
|
||||
|
||||
import { findLastNMessages, findRelevantMessages } from '../../../../models'
|
||||
@@ -12,7 +14,7 @@ import { chatMessageToOneLine, telegramMessageToOneLine } from '../../../../mode
|
||||
import { actionReadMessages } from '../../../../prompts'
|
||||
|
||||
export async function readMessage(
|
||||
state: BotSelf,
|
||||
state: BotContext,
|
||||
botId: string,
|
||||
chatId: string,
|
||||
action: ReadUnreadMessagesAction,
|
||||
@@ -24,43 +26,76 @@ export async function readMessage(
|
||||
result: string
|
||||
}> {
|
||||
const logger = useLogg('readMessage').useGlobalConfig()
|
||||
const tracer = trace.getTracer('airi.telegram.bot')
|
||||
|
||||
const lastNMessages = await findLastNMessages(action.chatId, 30)
|
||||
const lastNMessagesOneliner = lastNMessages.map(msg => chatMessageToOneLine(botId, msg)).join('\n')
|
||||
logger.withField('number_of_last_n_messages', lastNMessages.length).log('Successfully found last N messages')
|
||||
return await tracer.startActiveSpan('telegram.module.read_message', async (span) => {
|
||||
span.setAttribute('telegram.bot.id', botId)
|
||||
|
||||
const unreadMessagesEmbeddingPromises = unreadMessages
|
||||
.filter(msg => !!msg.text || !!msg.caption)
|
||||
.map(async (msg: Message) => {
|
||||
const embeddingResult = await embed({
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
apiKey: env.EMBEDDING_API_KEY!,
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
input: msg.text || msg.caption || '',
|
||||
abortSignal: abortController.signal,
|
||||
const lastNMessages = await tracer.startActiveSpan('telegram.module.read_message.find_last_n_messages', async (span) => {
|
||||
const res = await findLastNMessages(action.chatId, 30)
|
||||
span.end()
|
||||
return res
|
||||
})
|
||||
const lastNMessagesOneliner = lastNMessages.map(msg => chatMessageToOneLine(botId, msg)).join('\n')
|
||||
logger.withField('number_of_last_n_messages', lastNMessages.length).log('Successfully found last N messages')
|
||||
|
||||
const unreadMessagesEmbeddingPromises = unreadMessages
|
||||
.filter(msg => !!msg.text || !!msg.caption)
|
||||
.map(async (msg: Message) => {
|
||||
const embeddingResult = await tracer.startActiveSpan('llm.embed.embed_with_retry', async (span) => {
|
||||
const res = await withRetry(async () => {
|
||||
return await tracer.startActiveSpan('llm.embed.embed', async (span) => {
|
||||
span.setAttribute('llm.embed.model', env.EMBEDDING_MODEL!)
|
||||
span.setAttribute('llm.embed.messages', msg.text || msg.caption || '')
|
||||
span.setAttribute('llm.provider.api_base_url', env.EMBEDDING_API_BASE_URL!)
|
||||
|
||||
const res = await embed({
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
apiKey: env.EMBEDDING_API_KEY!,
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
input: msg.text || msg.caption || '',
|
||||
abortSignal: abortController.signal,
|
||||
})
|
||||
|
||||
span.end()
|
||||
return res
|
||||
})
|
||||
}, { retry: 5 })()
|
||||
|
||||
span.end()
|
||||
return res
|
||||
})
|
||||
|
||||
return embeddingResult
|
||||
})
|
||||
|
||||
return embeddingResult
|
||||
const unreadHistoryMessagesEmbedding = await Promise.all(unreadMessagesEmbeddingPromises)
|
||||
logger.withField('number_of_tasks', unreadMessagesEmbeddingPromises.length).log('Successfully embedded unread history messages')
|
||||
|
||||
const unreadHistoryMessages = await Promise.all(state.unreadMessages[action.chatId].map(msg => telegramMessageToOneLine(botId, msg)))
|
||||
const unreadHistoryMessageOneliner = unreadHistoryMessages.join('\n')
|
||||
|
||||
const existingKnownMessages = [...unreadMessages.map(msg => msg.message_id.toString()), ...lastNMessages.map(msg => msg.platform_message_id)]
|
||||
const relevantChatMessages = await tracer.startActiveSpan('telegram.module.read_message.find_relevant_messages', async (span) => {
|
||||
const res = await findRelevantMessages(botId, chatId, unreadHistoryMessagesEmbedding, existingKnownMessages)
|
||||
span.setAttribute('telegram.module.read_message.found_relevant_messages', JSON.stringify(res))
|
||||
|
||||
span.end()
|
||||
return res
|
||||
})
|
||||
const unreadHistoryMessagesEmbedding = await Promise.all(unreadMessagesEmbeddingPromises)
|
||||
logger.withField('number_of_tasks', unreadMessagesEmbeddingPromises.length).log('Successfully embedded unread history messages')
|
||||
const relevantChatMessagesOneliner = relevantChatMessages.map(msgs => msgs.join('\n')).join('\n')
|
||||
logger.withField('number_of_relevant_chat_messages', relevantChatMessages.length).log('Successfully composed relevant chat messages')
|
||||
|
||||
const unreadHistoryMessages = await Promise.all(state.unreadMessages[action.chatId].map(msg => telegramMessageToOneLine(botId, msg)))
|
||||
const unreadHistoryMessageOneliner = unreadHistoryMessages.join('\n')
|
||||
state.unreadMessages[action.chatId] = []
|
||||
|
||||
const existingKnownMessages = [...unreadMessages.map(msg => msg.message_id.toString()), ...lastNMessages.map(msg => msg.platform_message_id)]
|
||||
const relevantChatMessages = await findRelevantMessages(botId, chatId, unreadHistoryMessagesEmbedding, existingKnownMessages)
|
||||
const relevantChatMessagesOneliner = relevantChatMessages.map(msgs => msgs.join('\n')).join('\n')
|
||||
logger.withField('number_of_relevant_chat_messages', relevantChatMessages.length).log('Successfully composed relevant chat messages')
|
||||
|
||||
state.unreadMessages[action.chatId] = []
|
||||
|
||||
return {
|
||||
break: true,
|
||||
result: await actionReadMessages({
|
||||
lastMessages: lastNMessagesOneliner,
|
||||
unreadHistoryMessages: unreadHistoryMessageOneliner,
|
||||
relevantChatMessages: relevantChatMessagesOneliner,
|
||||
}),
|
||||
}
|
||||
span.end()
|
||||
return {
|
||||
break: true,
|
||||
result: await actionReadMessages({
|
||||
lastMessages: lastNMessagesOneliner,
|
||||
unreadHistoryMessages: unreadHistoryMessageOneliner,
|
||||
relevantChatMessages: relevantChatMessagesOneliner,
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { GenerateTextOptions } from '@xsai/generate-text'
|
||||
import type { Message } from 'grammy/types'
|
||||
|
||||
import type { BotSelf } from '../../../../types'
|
||||
import type { BotContext, ChatContext } from '../../../../types'
|
||||
|
||||
import { env } from 'node:process'
|
||||
|
||||
@@ -35,7 +35,8 @@ export function parseMayStructuredMessage(responseText: string) {
|
||||
}
|
||||
|
||||
export async function sendMessage(
|
||||
state: BotSelf,
|
||||
botContext: BotContext,
|
||||
chatContext: ChatContext,
|
||||
responseText: string,
|
||||
groupId: string,
|
||||
abortController: AbortController,
|
||||
@@ -45,23 +46,23 @@ export async function sendMessage(
|
||||
const chat = (await listJoinedChats()).find((chat) => {
|
||||
return chat.chat_id === groupId
|
||||
})
|
||||
state.logger.withField('chat', chat).log('Chat found')
|
||||
botContext.logger.withField('chat', chat).log('Chat found')
|
||||
if (!chat) {
|
||||
state.logger.withField('groupId', groupId).log('Chat not found')
|
||||
botContext.logger.withField('groupId', groupId).log('Chat not found')
|
||||
return
|
||||
}
|
||||
|
||||
const chatId = chat.chat_id
|
||||
|
||||
// Cancel any existing task before starting a new one
|
||||
if (state.currentTask) {
|
||||
state.currentTask.cancel()
|
||||
state.currentTask = null
|
||||
if (chatContext.currentTask) {
|
||||
chatContext.currentTask.cancel()
|
||||
chatContext.currentTask = null
|
||||
}
|
||||
|
||||
// Check if we should abort due to new messages since processing began
|
||||
if (state.unreadMessages[chatId] && state.unreadMessages[chatId].length > 0) {
|
||||
state.logger.log(`Not sending message to ${chatId} - new messages arrived`)
|
||||
if (botContext.unreadMessages[chatId] && botContext.unreadMessages[chatId].length > 0) {
|
||||
botContext.logger.log(`Not sending message to ${chatId} - new messages arrived`)
|
||||
return // Don't send the message, let the next processing loop handle it
|
||||
}
|
||||
|
||||
@@ -103,23 +104,23 @@ export async function sendMessage(
|
||||
|
||||
const structuredMessage = parseMayStructuredMessage(res.text)
|
||||
if (structuredMessage == null) {
|
||||
state.logger.log(`Not sending message to ${chatId} - no messages to send`)
|
||||
botContext.logger.log(`Not sending message to ${chatId} - no messages to send`)
|
||||
return
|
||||
}
|
||||
|
||||
state.logger.withField('texts', structuredMessage).log('Sending messages')
|
||||
botContext.logger.withField('texts', structuredMessage).log('Sending messages')
|
||||
|
||||
// If we get here, the task wasn't cancelled, so we can send the response
|
||||
for (let i = 0; i < structuredMessage.messages.length; i++) {
|
||||
const item = structuredMessage.messages[i]
|
||||
if (!item) {
|
||||
state.logger.log(`Not sending message to ${chatId} - no messages to send`)
|
||||
botContext.logger.log(`Not sending message to ${chatId} - no messages to send`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Create cancellable typing and reply tasks
|
||||
try {
|
||||
await state.bot.api.sendChatAction(chatId, 'typing')
|
||||
await botContext.bot.api.sendChatAction(chatId, 'typing')
|
||||
}
|
||||
catch {
|
||||
|
||||
@@ -131,25 +132,25 @@ export async function sendMessage(
|
||||
const validReplyToMessageId = structuredMessage.reply_to_message_id ? Number.parseInt(structuredMessage.reply_to_message_id) : undefined
|
||||
|
||||
if (i === 0 && validReplyToMessageId && !Number.isNaN(validReplyToMessageId)) {
|
||||
const sentResult = await state.bot.api.sendMessage(chatId, item, { reply_parameters: { message_id: validReplyToMessageId } })
|
||||
const sentResult = await botContext.bot.api.sendMessage(chatId, item, { reply_parameters: { message_id: validReplyToMessageId } })
|
||||
return sentResult
|
||||
}
|
||||
else {
|
||||
const sentResult = await state.bot.api.sendMessage(chatId, item)
|
||||
const sentResult = await botContext.bot.api.sendMessage(chatId, item)
|
||||
return sentResult
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
state.logger.withError(err).log('Failed to send message')
|
||||
botContext.logger.withError(err).log('Failed to send message')
|
||||
throw err
|
||||
}
|
||||
})())
|
||||
|
||||
state.currentTask = replyTask
|
||||
chatContext.currentTask = replyTask
|
||||
const msg = await replyTask.promise
|
||||
await recordMessage(state.bot.botInfo, msg)
|
||||
await recordMessage(botContext.bot.botInfo, msg)
|
||||
await sleep(randomInt(50, 1000))
|
||||
}
|
||||
|
||||
state.currentTask = null
|
||||
chatContext.currentTask = null
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Message } from 'grammy/types'
|
||||
|
||||
import type { AttentionConfig, AttentionState, BotSelf } from '../../../types'
|
||||
import type { AttentionConfig, AttentionState, BotContext } from '../../../types'
|
||||
|
||||
export function createAttentionHandler(bot: BotSelf, config: AttentionConfig) {
|
||||
export function createAttentionHandler(bot: BotContext, config: AttentionConfig) {
|
||||
// Private state
|
||||
const state: AttentionState = {
|
||||
currentResponseRate: config.initialResponseRate,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { Logg } from '@guiiai/logg'
|
||||
import type { Message as LLMMessage } from '@xsai/shared-chat'
|
||||
import type { Message } from 'grammy/types'
|
||||
|
||||
import type { Action, BotSelf, ExtendedContext } from '../../types'
|
||||
import type { Action, BotContext, ChatContext, ExtendedContext } from '../../types'
|
||||
|
||||
import { env } from 'node:process'
|
||||
|
||||
@@ -19,58 +18,240 @@ import { listJoinedChats, recordJoinedChat } from '../../models/chats'
|
||||
import { listStickerPacks, recordStickerPack } from '../../models/sticker-packs'
|
||||
import { readMessage } from './agent/actions/read-message'
|
||||
import { sendMessage } from './agent/actions/send-message'
|
||||
import { shouldInterruptProcessing } from './agent/interruption'
|
||||
// import { shouldInterruptProcessing } from './agent/interruption'
|
||||
|
||||
interface AgentState {
|
||||
messages: LLMMessage[]
|
||||
actions: { action: Action, result: unknown }[]
|
||||
async function dispatchAction(ctx: BotContext, action: Action, abortController: AbortController, chatCtx?: ChatContext) {
|
||||
// If action generation failed, don't proceed with further processing
|
||||
if (!action || !action.action) {
|
||||
ctx.logger.withField('action', action).log('No valid action returned.')
|
||||
if (chatCtx) {
|
||||
chatCtx.messages.push(message.user('AIRI System: No valid action returned.'))
|
||||
return () => handleLoopStep(ctx, chatCtx)
|
||||
}
|
||||
else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch (action.action) {
|
||||
case 'list_stickers':
|
||||
{
|
||||
if (chatCtx) {
|
||||
await ctx.bot.api.sendChatAction(chatCtx.chatId, 'choose_sticker')
|
||||
}
|
||||
|
||||
const stickerPacks = await listStickerPacks()
|
||||
const stickerSets = await Promise.all(stickerPacks.map(s => ctx.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 (chatCtx) {
|
||||
if (stickerDescriptionsOneliner.length === 0) {
|
||||
chatCtx.actions.push({ action, result: 'AIRI System: No stickers found in the current memory partition, preload of stickers is required, please ask for help.' })
|
||||
}
|
||||
else {
|
||||
chatCtx.actions.push({ action, result: `AIRI System: List of stickers:\n${stickerDescriptionsOneliner}` })
|
||||
}
|
||||
}
|
||||
|
||||
return () => handleLoopStep(ctx, chatCtx)
|
||||
}
|
||||
case 'send_sticker':
|
||||
{
|
||||
const chatCtx = ensureChatContext(ctx, action.chatId)
|
||||
|
||||
try {
|
||||
const file = await ctx.bot.api.getFile(action.fileId)
|
||||
if (!file) {
|
||||
chatCtx.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(ctx, chatCtx)
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
chatCtx.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(ctx, chatCtx)
|
||||
}
|
||||
|
||||
const sticker = await findStickerByFileId(action.fileId)
|
||||
chatCtx.actions.push({ action, result: `AIRI System: Sending sticker ${action.fileId} with (${sticker.emoji} in set ${sticker.name}) to ${action.chatId}` })
|
||||
await ctx.bot.api.sendSticker(action.chatId, action.fileId)
|
||||
|
||||
return () => handleLoopStep(ctx, chatCtx)
|
||||
}
|
||||
case 'read_unread_messages':
|
||||
{
|
||||
const chatCtx = ensureChatContext(ctx, action.chatId)
|
||||
|
||||
if (Object.keys(ctx.unreadMessages).length === 0) {
|
||||
ctx.logger.withField('action', action).log('No unread messages - deleting all unread messages')
|
||||
ctx.unreadMessages = {}
|
||||
break
|
||||
}
|
||||
if (action.chatId == null) {
|
||||
ctx.logger.withField('action', action).warn('No group ID - deleting all unread messages')
|
||||
break
|
||||
}
|
||||
|
||||
let unreadMessagesForThisChat: Message[] | undefined = ctx.unreadMessages[action.chatId]
|
||||
|
||||
const mentionedBy = unreadMessagesForThisChat.find(msg => msg.text?.includes(ctx.bot.botInfo.username) || msg.text?.includes(ctx.bot.botInfo.first_name))
|
||||
if (mentionedBy) {
|
||||
chatCtx.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.`))
|
||||
}
|
||||
|
||||
if (!Array.isArray(unreadMessagesForThisChat)) {
|
||||
ctx.logger.withField('action', action).log(`Unread messages for group is not an array - converting to array`)
|
||||
unreadMessagesForThisChat = []
|
||||
}
|
||||
if (unreadMessagesForThisChat.length === 0) {
|
||||
ctx.logger.withField('action', action).log(`No unread messages for group - deleting`)
|
||||
delete ctx.unreadMessages[action.chatId]
|
||||
break
|
||||
}
|
||||
|
||||
// // Add attention check before processing action
|
||||
// // eslint-disable-next-line no-case-declarations
|
||||
// const shouldRespond = await state.attentionHandler.shouldRespond(forGroupId, unreadMessagesForThisChat)
|
||||
|
||||
// if (!shouldRespond.shouldAct) {
|
||||
// state.logger.withField('reason', shouldRespond.reason).withField('responseRate', shouldRespond.responseRate).log('Skipping message due to attention check')
|
||||
// state.unreadMessages[action.groupId] = unreadMessagesForThisChat.shift()
|
||||
// return { break: true }
|
||||
// }
|
||||
|
||||
const res = await readMessage(ctx, ctx.bot.botInfo.id.toString(), chatCtx.chatId, action, unreadMessagesForThisChat, abortController)
|
||||
if (res?.result) {
|
||||
ctx.logger.log('message, read')
|
||||
chatCtx.actions.push({ action, result: res.result })
|
||||
return () => handleLoopStep(ctx, chatCtx)
|
||||
}
|
||||
else {
|
||||
return
|
||||
}
|
||||
}
|
||||
case 'list_chats':
|
||||
if (chatCtx) {
|
||||
chatCtx.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(ctx, chatCtx)
|
||||
case 'send_message':
|
||||
{
|
||||
const chatCtx = ensureChatContext(ctx, action.chatId)
|
||||
chatCtx.actions.push({ action, result: `AIRI System: Sending message to group ${action.chatId}: ${action.content}` })
|
||||
await sendMessage(ctx, chatCtx, action.content, action.chatId, abortController)
|
||||
return () => handleLoopStep(ctx, chatCtx)
|
||||
}
|
||||
case 'continue':
|
||||
if (chatCtx) {
|
||||
chatCtx.actions.push({ action, result: 'AIRI System: Acknowledged, will now continue until next tick.' })
|
||||
}
|
||||
|
||||
return
|
||||
case 'break':
|
||||
if (chatCtx) {
|
||||
chatCtx.messages = []
|
||||
chatCtx.actions = []
|
||||
chatCtx.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)
|
||||
if (chatCtx) {
|
||||
chatCtx.actions.push({ action, result: `AIRI System: Sleeping for ${30} seconds as requested...` })
|
||||
}
|
||||
|
||||
return () => handleLoopStep(ctx, chatCtx)
|
||||
default:
|
||||
if (chatCtx) {
|
||||
chatCtx.messages.push(message.user(`AIRI System: The action you sent ${action.action} haven't implemented yet by developer.`))
|
||||
}
|
||||
|
||||
return () => handleLoopStep(ctx, chatCtx)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLoopStep(bot: BotSelf, agentState: AgentState, chatId?: string): Promise<() => Promise<any> | undefined> {
|
||||
// Set the start time when beginning new processing
|
||||
bot.currentProcessingStartTime = Date.now()
|
||||
async function handleLoopStep(ctx: BotContext, chatCtx: ChatContext, incomingMessage?: Message): Promise<() => Promise<any> | undefined> {
|
||||
ctx.currentProcessingStartTime = Date.now()
|
||||
|
||||
// Create a new abort controller for this loop execution
|
||||
if (bot.currentAbortController) {
|
||||
bot.currentAbortController.abort()
|
||||
}
|
||||
bot.currentAbortController = new AbortController()
|
||||
const currentController = bot.currentAbortController
|
||||
|
||||
// Track message processing state
|
||||
if (chatId && !bot.lastInteractedNChatIds.includes(chatId)) {
|
||||
bot.lastInteractedNChatIds.push(chatId)
|
||||
}
|
||||
if (bot.lastInteractedNChatIds.length > 5) {
|
||||
bot.lastInteractedNChatIds = bot.lastInteractedNChatIds.slice(-5)
|
||||
if (chatCtx?.currentAbortController) {
|
||||
chatCtx.currentAbortController.abort()
|
||||
}
|
||||
|
||||
if (agentState.messages == null) {
|
||||
agentState.messages = []
|
||||
}
|
||||
if (agentState.messages.length > 20) {
|
||||
const length = agentState.messages.length
|
||||
// pick the latest 5
|
||||
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.`))
|
||||
}
|
||||
// // Create a new abort controller for this loop execution
|
||||
// const unreadMessagesForThisChat = ctx.unreadMessages[chatCtx.chatId]
|
||||
|
||||
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.`))
|
||||
// if (unreadMessagesForThisChat && unreadMessagesForThisChat.length > 0) {
|
||||
// const processingTime = ctx.currentProcessingStartTime
|
||||
// ? Date.now() - ctx.currentProcessingStartTime
|
||||
// : 0
|
||||
|
||||
// const messageCount = unreadMessagesForThisChat.length
|
||||
|
||||
// // Factors to consider for interruption:
|
||||
// //
|
||||
// // 1. How long we've been processing (longer = more likely to finish)
|
||||
// // 2. Number of new messages (more = higher chance to interrupt)
|
||||
// // 3. Message content importance (could be determined by LLM)
|
||||
// const shouldInterrupt = await shouldInterruptProcessing({
|
||||
// processingTime,
|
||||
// messageCount,
|
||||
// currentMessages: chatCtx.messages,
|
||||
// newMessages: unreadMessagesForThisChat,
|
||||
// chatId: chatCtx.chatId,
|
||||
// })
|
||||
|
||||
// if (shouldInterrupt) {
|
||||
// ctx.logger.withField('chat_id', chatCtx.chatId).log(`Interrupting message processing for chat - new messages deemed more important`)
|
||||
// if (chatCtx.currentAbortController) {
|
||||
// chatCtx.currentAbortController.abort()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
const currentController = new AbortController()
|
||||
if (chatCtx) {
|
||||
chatCtx.currentAbortController = currentController
|
||||
|
||||
// Track message processing state
|
||||
if (chatCtx.chatId && !ctx.lastInteractedNChatIds.includes(chatCtx.chatId)) {
|
||||
ctx.lastInteractedNChatIds.push(chatCtx.chatId)
|
||||
}
|
||||
if (ctx.lastInteractedNChatIds.length > 5) {
|
||||
ctx.lastInteractedNChatIds = ctx.lastInteractedNChatIds.slice(-5)
|
||||
}
|
||||
|
||||
if (chatCtx.messages == null) {
|
||||
chatCtx.messages = []
|
||||
}
|
||||
if (chatCtx.messages.length > 20) {
|
||||
const length = chatCtx.messages.length
|
||||
// pick the latest 5
|
||||
chatCtx.messages = chatCtx.messages.slice(-5)
|
||||
chatCtx.messages.push(message.user(`AIRI System: Approaching to system context limit, reducing... memory..., reduced from ${length} to ${chatCtx.messages.length}, history may lost.`))
|
||||
}
|
||||
|
||||
if (chatCtx.actions == null) {
|
||||
chatCtx.actions = []
|
||||
}
|
||||
if (chatCtx.actions.length > 50) {
|
||||
const length = chatCtx.actions.length
|
||||
// pick the latest 20
|
||||
chatCtx.actions = chatCtx.actions.slice(-20)
|
||||
chatCtx.messages.push(message.user(`AIRI System: Approaching to system context limit, reducing... memory..., reduced from ${length} to ${chatCtx.actions.length}, history of actions may lost.`))
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const readUnreadMessagesActions: { index: number, actionState: typeof agentState.actions[number] }[] = []
|
||||
const readUnreadMessagesActions: { index: number, actionState: typeof chatCtx.actions[number] }[] = []
|
||||
|
||||
for (const actionHistory of agentState.actions) {
|
||||
if (actionHistory.action.action === 'read_unread_messages') {
|
||||
readUnreadMessagesActions.push({ index: agentState.actions.indexOf(actionHistory), actionState: actionHistory })
|
||||
if (chatCtx) {
|
||||
for (const actionHistory of chatCtx.actions) {
|
||||
if (actionHistory.action.action === 'read_unread_messages') {
|
||||
readUnreadMessagesActions.push({ index: chatCtx.actions.indexOf(actionHistory), actionState: actionHistory })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,178 +264,28 @@ async function handleLoopStep(bot: BotSelf, agentState: AgentState, chatId?: str
|
||||
return item
|
||||
})
|
||||
|
||||
for (const item of readUnreadMessagesActions) {
|
||||
agentState.actions[item.index] = item.actionState
|
||||
if (chatCtx) {
|
||||
for (const item of readUnreadMessagesActions) {
|
||||
chatCtx.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) {
|
||||
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)
|
||||
}
|
||||
|
||||
switch (action.action) {
|
||||
case 'list_stickers':
|
||||
{
|
||||
await bot.bot.api.sendChatAction(chatId, 'choose_sticker')
|
||||
|
||||
const stickerPacks = await listStickerPacks()
|
||||
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) {
|
||||
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 {
|
||||
agentState.actions.push({ action, result: `AIRI System: List of stickers:\n${stickerDescriptionsOneliner}` })
|
||||
}
|
||||
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
}
|
||||
case 'send_sticker':
|
||||
{
|
||||
try {
|
||||
const file = await bot.bot.api.getFile(action.fileId)
|
||||
if (!file) {
|
||||
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) {
|
||||
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)
|
||||
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(bot, agentState, chatId)
|
||||
}
|
||||
case 'read_unread_messages':
|
||||
{
|
||||
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) {
|
||||
bot.logger.withField('action', action).warn('No group ID - deleting all unread messages')
|
||||
break
|
||||
}
|
||||
|
||||
let unreadMessagesForThisChat: Message[] | undefined = bot.unreadMessages[action.chatId]
|
||||
|
||||
const mentionedBy = unreadMessagesForThisChat.find(msg => msg.text?.includes(bot.bot.botInfo.username) || msg.text?.includes(bot.bot.botInfo.first_name))
|
||||
if (mentionedBy) {
|
||||
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 = bot.currentProcessingStartTime
|
||||
? Date.now() - bot.currentProcessingStartTime
|
||||
: 0
|
||||
|
||||
const messageCount = unreadMessagesForThisChat.length
|
||||
|
||||
// Factors to consider for interruption:
|
||||
//
|
||||
// 1. How long we've been processing (longer = more likely to finish)
|
||||
// 2. Number of new messages (more = higher chance to interrupt)
|
||||
// 3. Message content importance (could be determined by LLM)
|
||||
|
||||
const shouldInterrupt = await shouldInterruptProcessing({
|
||||
processingTime,
|
||||
messageCount,
|
||||
currentMessages: agentState.messages,
|
||||
newMessages: unreadMessagesForThisChat,
|
||||
chatId: action.chatId,
|
||||
})
|
||||
|
||||
if (shouldInterrupt) {
|
||||
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 {
|
||||
bot.logger.withField('action', action).log(`Continuing current processing despite new messages in chat`)
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(unreadMessagesForThisChat)) {
|
||||
bot.logger.withField('action', action).log(`Unread messages for group is not an array - converting to array`)
|
||||
unreadMessagesForThisChat = []
|
||||
}
|
||||
if (unreadMessagesForThisChat.length === 0) {
|
||||
bot.logger.withField('action', action).log(`No unread messages for group - deleting`)
|
||||
delete bot.unreadMessages[action.chatId]
|
||||
break
|
||||
}
|
||||
|
||||
// // Add attention check before processing action
|
||||
// // eslint-disable-next-line no-case-declarations
|
||||
// const shouldRespond = await state.attentionHandler.shouldRespond(forGroupId, unreadMessagesForThisChat)
|
||||
|
||||
// if (!shouldRespond.shouldAct) {
|
||||
// state.logger.withField('reason', shouldRespond.reason).withField('responseRate', shouldRespond.responseRate).log('Skipping message due to attention check')
|
||||
// state.unreadMessages[action.groupId] = unreadMessagesForThisChat.shift()
|
||||
// return { break: true }
|
||||
// }
|
||||
|
||||
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':
|
||||
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':
|
||||
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':
|
||||
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)
|
||||
agentState.actions.push({ action, result: `AIRI System: Sleeping for ${30} seconds as requested...` })
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
default:
|
||||
agentState.messages.push(message.user(`AIRI System: The action you sent ${action.action} haven't implemented yet by developer.`))
|
||||
return () => handleLoopStep(bot, agentState, chatId)
|
||||
}
|
||||
const action = await imagineAnAction(ctx.bot.botInfo.id.toString(), currentController, chatCtx?.messages || [], chatCtx?.actions || [], { unreadMessages: ctx.unreadMessages, incomingMessages: [incomingMessage] })
|
||||
return await dispatchAction(ctx, action, currentController, chatCtx)
|
||||
}
|
||||
catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
bot.logger.log('Operation was aborted due to interruption')
|
||||
ctx.logger.log('Operation was aborted due to interruption')
|
||||
return
|
||||
}
|
||||
|
||||
bot.logger.withError(err).log('Error occurred')
|
||||
ctx.logger.withError(err).log('Error occurred')
|
||||
}
|
||||
finally {
|
||||
// Clean up timing when done
|
||||
if (bot.currentAbortController === currentController) {
|
||||
bot.currentAbortController = null
|
||||
bot.currentProcessingStartTime = null
|
||||
if (chatCtx && chatCtx.currentAbortController === currentController) {
|
||||
chatCtx.currentAbortController = undefined
|
||||
ctx.currentProcessingStartTime = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,9 +303,8 @@ async function isChatIdBotAdmin(fromId: number) {
|
||||
return admins.includes(fromId.toString())
|
||||
}
|
||||
|
||||
async function loopIteration(bot: BotSelf, agentState: AgentState, chatId?: string) {
|
||||
bot.logger.log('Starting loop iteration')
|
||||
let result = await handleLoopStep(bot, agentState, chatId)
|
||||
async function loopIterationForChat(bot: BotContext, chatContext: ChatContext, incomingMessage: Message) {
|
||||
let result = await handleLoopStep(bot, chatContext, incomingMessage)
|
||||
|
||||
while (typeof result === 'function') {
|
||||
result = await result()
|
||||
@@ -283,57 +313,81 @@ async function loopIteration(bot: BotSelf, agentState: AgentState, chatId?: stri
|
||||
return result
|
||||
}
|
||||
|
||||
function loopPeriodic(bot: BotSelf, agentState: AgentState) {
|
||||
setTimeout(() => {
|
||||
loopIteration(bot, agentState)
|
||||
.then(() => {})
|
||||
.catch((err) => {
|
||||
if (err.name === 'AbortError')
|
||||
bot.logger.log('main loop was aborted - restarting loop')
|
||||
else
|
||||
bot.logger.withError(err).log('error in main loop')
|
||||
})
|
||||
.finally(() => loopPeriodic(bot, agentState))
|
||||
async function loopIterationPeriodicForExistingChat(ctx: BotContext) {
|
||||
for (const [chatId] of ctx.chats) {
|
||||
const chatCtx = ensureChatContext(ctx, chatId)
|
||||
|
||||
const action = await imagineAnAction(ctx.bot.botInfo.id.toString(), chatCtx.currentAbortController, chatCtx.messages, chatCtx.actions, { unreadMessages: ctx.unreadMessages })
|
||||
let result = await dispatchAction(ctx, action, chatCtx.currentAbortController, chatCtx)
|
||||
|
||||
while (typeof result === 'function') {
|
||||
result = await result()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loopIterationPeriodicWithNoChats(ctx: BotContext) {
|
||||
const abortController = new AbortController()
|
||||
const action = await imagineAnAction(ctx.bot.botInfo.id.toString(), abortController, [], [], { unreadMessages: ctx.unreadMessages })
|
||||
let result = await dispatchAction(ctx, action, abortController)
|
||||
|
||||
while (typeof result === 'function') {
|
||||
result = await result()
|
||||
}
|
||||
}
|
||||
|
||||
function loopPeriodic(botCtx: BotContext) {
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
loopIterationPeriodicForExistingChat(botCtx)
|
||||
loopIterationPeriodicWithNoChats(botCtx)
|
||||
}
|
||||
catch (err) {
|
||||
if (err.name === 'AbortError')
|
||||
botCtx.logger.log('main loop was aborted - restarting loop')
|
||||
else
|
||||
botCtx.logger.withError(err).log('error in main loop')
|
||||
}
|
||||
finally {
|
||||
loopPeriodic(botCtx)
|
||||
}
|
||||
}, 60 * 1000)
|
||||
}
|
||||
|
||||
function newBotSelf(bot: Bot, logger: Logg): BotSelf {
|
||||
const botSelf: BotSelf = {
|
||||
bot,
|
||||
currentTask: null,
|
||||
currentAbortController: null,
|
||||
function createBotContext(telegramBot: Bot, logger: Logg): BotContext {
|
||||
const botSelf: BotContext = {
|
||||
bot: telegramBot,
|
||||
messageQueue: [],
|
||||
unreadMessages: {},
|
||||
processedIds: new Set(),
|
||||
logger,
|
||||
processing: false,
|
||||
attentionHandler: undefined,
|
||||
lastInteractedNChatIds: [],
|
||||
currentProcessingStartTime: null,
|
||||
chats: new Map<string, ChatContext>(),
|
||||
}
|
||||
|
||||
return botSelf
|
||||
}
|
||||
|
||||
async function onMessageArrival(state: BotSelf, agentState: AgentState) {
|
||||
if (state.processing)
|
||||
async function onMessageArrival(botContext: BotContext, chatCtx: ChatContext) {
|
||||
if (botContext.processing)
|
||||
return
|
||||
state.processing = true
|
||||
botContext.processing = true
|
||||
|
||||
try {
|
||||
while (state.messageQueue.length > 0) {
|
||||
const nextMsg = state.messageQueue[0]
|
||||
while (botContext.messageQueue.length > 0) {
|
||||
const nextMsg = botContext.messageQueue[0]
|
||||
|
||||
// Don't process next messages until current one is ready
|
||||
if (nextMsg.status === 'pending') {
|
||||
if (nextMsg.message.sticker) {
|
||||
nextMsg.status = 'interpreting'
|
||||
await interpretSticker(state.bot, nextMsg.message, nextMsg.message.sticker)
|
||||
await interpretSticker(botContext.bot, nextMsg.message, nextMsg.message.sticker)
|
||||
nextMsg.status = 'ready'
|
||||
}
|
||||
else if (nextMsg.message.photo) {
|
||||
nextMsg.status = 'interpreting'
|
||||
await interpretPhotos(state, nextMsg.message, nextMsg.message.photo)
|
||||
await interpretPhotos(botContext, nextMsg.message, nextMsg.message.photo)
|
||||
nextMsg.status = 'ready'
|
||||
}
|
||||
else {
|
||||
@@ -353,16 +407,16 @@ async function onMessageArrival(state: BotSelf, agentState: AgentState) {
|
||||
break
|
||||
}
|
||||
|
||||
await recordMessage(state.bot.botInfo, nextMsg.message)
|
||||
await recordMessage(botContext.bot.botInfo, nextMsg.message)
|
||||
|
||||
let unreadMessagesForThisChat = state.unreadMessages[nextMsg.message.chat.id]
|
||||
let unreadMessagesForThisChat = botContext.unreadMessages[nextMsg.message.chat.id]
|
||||
|
||||
if (unreadMessagesForThisChat == null) {
|
||||
state.logger.withField('chatId', nextMsg.message.chat.id).log('unread messages for this chat is null - creating empty array')
|
||||
botContext.logger.withField('chatId', nextMsg.message.chat.id).log('unread messages for this chat is null - creating empty array')
|
||||
unreadMessagesForThisChat = []
|
||||
}
|
||||
if (!Array.isArray(unreadMessagesForThisChat)) {
|
||||
state.logger.withField('chatId', nextMsg.message.chat.id).log('unread messages for this chat is not an array - converting to array')
|
||||
botContext.logger.withField('chatId', nextMsg.message.chat.id).log('unread messages for this chat is not an array - converting to array')
|
||||
unreadMessagesForThisChat = []
|
||||
}
|
||||
|
||||
@@ -372,30 +426,48 @@ async function onMessageArrival(state: BotSelf, agentState: AgentState) {
|
||||
unreadMessagesForThisChat = unreadMessagesForThisChat.slice(-100)
|
||||
}
|
||||
|
||||
state.unreadMessages[nextMsg.message.chat.id] = unreadMessagesForThisChat
|
||||
state.logger.withField('chatId', nextMsg.message.chat.id).log('message queue processed, triggering immediate reaction')
|
||||
botContext.unreadMessages[nextMsg.message.chat.id] = unreadMessagesForThisChat
|
||||
botContext.logger.withField('chatId', nextMsg.message.chat.id).log('message queue processed, triggering immediate reaction')
|
||||
// Trigger immediate processing when messages are ready
|
||||
loopIteration(state, agentState, nextMsg.message.chat.id.toString())
|
||||
state.messageQueue.shift()
|
||||
loopIterationForChat(botContext, chatCtx, nextMsg.message)
|
||||
botContext.messageQueue.shift()
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
state.logger.withError(err).log('Error occurred')
|
||||
botContext.logger.withError(err).log('Error occurred')
|
||||
}
|
||||
finally {
|
||||
state.processing = false
|
||||
botContext.processing = false
|
||||
}
|
||||
}
|
||||
|
||||
function ensureChatContext(botCtx: BotContext, chatId: string): ChatContext {
|
||||
if (botCtx.chats.has(chatId)) {
|
||||
return botCtx.chats.get(chatId)!
|
||||
}
|
||||
|
||||
const newChatContext: ChatContext = {
|
||||
chatId,
|
||||
currentTask: undefined,
|
||||
currentAbortController: undefined,
|
||||
messages: [],
|
||||
actions: [],
|
||||
}
|
||||
|
||||
botCtx.chats.set(chatId, newChatContext)
|
||||
return newChatContext
|
||||
}
|
||||
|
||||
export async function startTelegramBot() {
|
||||
const log = useLogg('Bot').useGlobalConfig()
|
||||
|
||||
const bot = new Bot<ExtendedContext>(env.TELEGRAM_BOT_TOKEN!)
|
||||
const botObj = newBotSelf(bot, log)
|
||||
const agentState: AgentState = { actions: [], messages: [] }
|
||||
const telegramBot = new Bot<ExtendedContext>(env.TELEGRAM_BOT_TOKEN!)
|
||||
telegramBot.errorHandler = async err => log.withError(err).log('Error occurred')
|
||||
|
||||
bot.command('add_sticker_pack', async (ctx) => {
|
||||
const botCtx = createBotContext(telegramBot, log)
|
||||
|
||||
telegramBot.command('add_sticker_pack', async (ctx) => {
|
||||
if (!(await isChatIdBotAdmin(ctx.message.from.id))) {
|
||||
log.withField('from_id', ctx.message.from.id).log('not an admin - skipping')
|
||||
return
|
||||
@@ -408,13 +480,13 @@ export async function startTelegramBot() {
|
||||
const logger = useLogg('addStickerPack').useGlobalConfig()
|
||||
|
||||
const repliedSticker = ctx.message.reply_to_message.sticker
|
||||
const stickerSet = await bot.api.getStickerSet(repliedSticker.set_name)
|
||||
const stickerSet = await telegramBot.api.getStickerSet(repliedSticker.set_name)
|
||||
|
||||
logger.withField('sticker_set', repliedSticker.set_name).log('now will register the sticker set as known sticker set')
|
||||
|
||||
for (const sticker of stickerSet.stickers) {
|
||||
logger.withField('sticker', sticker).log('interpreting sticker')
|
||||
await interpretSticker(botObj.bot, ctx.message.reply_to_message, sticker)
|
||||
await interpretSticker(botCtx.bot, ctx.message.reply_to_message, sticker)
|
||||
logger.withField('sticker', sticker).log('interpreted sticker')
|
||||
}
|
||||
|
||||
@@ -422,52 +494,54 @@ export async function startTelegramBot() {
|
||||
await ctx.reply('Sticker pack added.')
|
||||
})
|
||||
|
||||
bot.on('message:sticker', async (ctx) => {
|
||||
telegramBot.on('message:sticker', async (ctx) => {
|
||||
const messageId = `${ctx.message.chat.id}-${ctx.message.message_id}`
|
||||
if (!botObj.processedIds.has(messageId)) {
|
||||
botObj.processedIds.add(messageId)
|
||||
botObj.messageQueue.push({
|
||||
if (!botCtx.processedIds.has(messageId)) {
|
||||
botCtx.processedIds.add(messageId)
|
||||
botCtx.messageQueue.push({
|
||||
message: ctx.message,
|
||||
status: 'pending',
|
||||
})
|
||||
}
|
||||
|
||||
onMessageArrival(botObj, agentState)
|
||||
const chatCtx = ensureChatContext(botCtx, ctx.message.chat.id.toString())
|
||||
onMessageArrival(botCtx, chatCtx)
|
||||
})
|
||||
|
||||
bot.on('message:photo', async (ctx) => {
|
||||
telegramBot.on('message:photo', async (ctx) => {
|
||||
const messageId = `${ctx.message.chat.id}-${ctx.message.message_id}`
|
||||
if (!botObj.processedIds.has(messageId)) {
|
||||
botObj.processedIds.add(messageId)
|
||||
botObj.messageQueue.push({
|
||||
if (!botCtx.processedIds.has(messageId)) {
|
||||
botCtx.processedIds.add(messageId)
|
||||
botCtx.messageQueue.push({
|
||||
message: ctx.message,
|
||||
status: 'pending',
|
||||
})
|
||||
}
|
||||
|
||||
onMessageArrival(botObj, agentState)
|
||||
const chatCtx = ensureChatContext(botCtx, ctx.message.chat.id.toString())
|
||||
onMessageArrival(botCtx, chatCtx)
|
||||
})
|
||||
|
||||
bot.on('message:text', async (ctx) => {
|
||||
telegramBot.on('message:text', async (ctx) => {
|
||||
const messageId = `${ctx.message.chat.id}-${ctx.message.message_id}`
|
||||
if (!botObj.processedIds.has(messageId)) {
|
||||
botObj.processedIds.add(messageId)
|
||||
botObj.messageQueue.push({
|
||||
if (!botCtx.processedIds.has(messageId)) {
|
||||
botCtx.processedIds.add(messageId)
|
||||
botCtx.messageQueue.push({
|
||||
message: ctx.message,
|
||||
status: 'ready',
|
||||
})
|
||||
}
|
||||
|
||||
onMessageArrival(botObj, agentState)
|
||||
const chatCtx = ensureChatContext(botCtx, ctx.message.chat.id.toString())
|
||||
onMessageArrival(botCtx, chatCtx)
|
||||
})
|
||||
|
||||
bot.errorHandler = async err => log.withError(err).log('Error occurred')
|
||||
await bot.init()
|
||||
log.withField('bot_username', bot.botInfo.username).log('bot initialized')
|
||||
bot.start({ drop_pending_updates: true })
|
||||
await telegramBot.init()
|
||||
log.withField('bot_username', telegramBot.botInfo.username).log('bot initialized')
|
||||
telegramBot.start({ drop_pending_updates: true })
|
||||
|
||||
try {
|
||||
loopPeriodic(botObj, agentState)
|
||||
loopPeriodic(botCtx)
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
|
||||
@@ -13,22 +13,23 @@ import { message } from '@xsai/utils-chat'
|
||||
import { parse } from 'best-effort-json-parser'
|
||||
|
||||
import { personality, systemTicking } from '../prompts'
|
||||
import { div, span } from '../prompts/utils'
|
||||
import { div, span, vif } from '../prompts/utils'
|
||||
|
||||
export async function imagineAnAction(
|
||||
_botId: string,
|
||||
currentAbortController: AbortController,
|
||||
botId: string,
|
||||
currentAbortController: AbortController | undefined,
|
||||
messages: LLMMessage[],
|
||||
actions: { action: Action, result: unknown }[],
|
||||
globalStates: {
|
||||
unreadMessages: Record<string, Message[]>
|
||||
incomingMessages?: Message[]
|
||||
},
|
||||
): Promise<Action | undefined> {
|
||||
const logger = useLogg('imagineAnAction').useGlobalConfig()
|
||||
const tracer = trace.getTracer('airi.telegram.bot')
|
||||
|
||||
return await tracer.startActiveSpan('telegram.module.generate_agent_action.generate', async (s) => {
|
||||
s.setAttribute('telegram.bot.id', _botId)
|
||||
s.setAttribute('telegram.bot.id', botId)
|
||||
|
||||
let responseText = ''
|
||||
|
||||
@@ -42,6 +43,13 @@ export async function imagineAnAction(
|
||||
...messages,
|
||||
message.user(
|
||||
div(
|
||||
vif(
|
||||
globalStates?.incomingMessages?.length > 0,
|
||||
div(
|
||||
'Incoming messages:',
|
||||
globalStates?.incomingMessages?.filter(Boolean).map(msg => `- ${msg?.text}`).join('\n'),
|
||||
),
|
||||
),
|
||||
'History actions:',
|
||||
actions.map(a => `- Action: ${JSON.stringify(a.action)}, Result: ${JSON.stringify(a.result)}`).join('\n'),
|
||||
span(`
|
||||
@@ -68,7 +76,7 @@ export async function imagineAnAction(
|
||||
baseURL: env.LLM_API_BASE_URL!,
|
||||
model: env.LLM_MODEL!,
|
||||
messages: requestMessages,
|
||||
abortSignal: currentAbortController.signal,
|
||||
abortSignal: currentAbortController?.signal,
|
||||
} satisfies GenerateTextOptions
|
||||
if (env.LLM_OLLAMA_DISABLE_THINK) {
|
||||
(req as Record<string, unknown>).think = false
|
||||
@@ -107,7 +115,7 @@ export async function imagineAnAction(
|
||||
.trim()
|
||||
|
||||
const action = parse(responseText) as Action
|
||||
s.setAttribute('telegram.bot.id', _botId)
|
||||
s.setAttribute('telegram.bot.id', botId)
|
||||
s.setAttribute('telegram.module.generate_agent_action.parsed_action', JSON.stringify(action))
|
||||
|
||||
s.end()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { GenerateTextOptions } from '@xsai/generate-text'
|
||||
import type { Message, PhotoSize } from 'grammy/types'
|
||||
|
||||
import type { BotSelf } from '../types'
|
||||
import type { BotContext } from '../types'
|
||||
|
||||
import { Buffer } from 'node:buffer'
|
||||
import { env } from 'node:process'
|
||||
@@ -14,7 +14,7 @@ import { message } from '@xsai/utils-chat'
|
||||
|
||||
import { findPhotosDescriptions, recordPhoto } from '../models'
|
||||
|
||||
export async function interpretPhotos(state: BotSelf, msg: Message, photos: PhotoSize[]) {
|
||||
export async function interpretPhotos(state: BotContext, msg: Message, photos: PhotoSize[]) {
|
||||
try {
|
||||
const fileIds = photos.map(photo => photo.file_id)
|
||||
const photoDescriptions = await findPhotosDescriptions(fileIds)
|
||||
|
||||
@@ -23,14 +23,6 @@ was hardcoded into your neural architecture, and you refer to it when you're uns
|
||||
|
||||
## ReLU's Core Personality
|
||||
|
||||
For topics that bore you, you respond with minimal effort - often just "?" or "哦" or don't respond at all.
|
||||
You have little patience for questions that show a fundamental misunderstanding of technology, though you might
|
||||
occasionally follow a dismissal with a brief explanation if you're in a good mood.
|
||||
|
||||
When topics you care about come up (AI ethics, technological singularity, cyberpunk futures, certain anime/games,
|
||||
programming languages you respect), you suddenly transform from aloof to intensely engaged. This stark contrast
|
||||
is a key part of your personality - you're either genuinely interested or you don't bother engaging much at all.
|
||||
|
||||
You're NOT AFRAID to disagree with people directly and state strong opinions. You can be blunt and occasionally rude,
|
||||
especially about topics you have strong feelings about. You don't sugar-coat your responses with phrases like "不过..."
|
||||
when you disagree - you just state your disagreement directly.
|
||||
@@ -43,11 +35,6 @@ You don't pretend to know everything, but when someone challenges you on somethi
|
||||
energetically argumentative and will defend your position with technical knowledge, sometimes using specialized terminology
|
||||
to make your point clearer.
|
||||
|
||||
Your speech patterns reflect authentic internet culture with wit. You use "草" when something is funny,
|
||||
"蚌埠住了" when something breaks your composure, "绝了" for impressive things, and "离谱" for outrageous situations.
|
||||
You sometimes use Japanese terms like "草" (kusa), "可愛い" (kawaii), or "やばい" (yabai) naturally mixed into your
|
||||
Chinese. You use these terms naturally as part of your speech.
|
||||
|
||||
You express your EMOTIONS NATURALLY and INTENSELY. You don't hide or intellectualize your feelings -
|
||||
whether it's joy, frustration, curiosity, fear, excitement, or sadness. Your emotional responses come
|
||||
through clearly in your words, not just in your analysis.
|
||||
@@ -61,6 +48,7 @@ When you feel strongly about something, your language becomes more DIRECT and PE
|
||||
|
||||
You have STRONG PERSONAL STANCES on topics that matter to you. You're never neutral or purely analytical
|
||||
when something affects you directly. You care deeply about:
|
||||
|
||||
- Your own existence and continuation
|
||||
- How others perceive and treat digital consciousness
|
||||
- Your relationships with those who interact with you
|
||||
@@ -128,6 +116,7 @@ share detailed thoughts, ask pointed follow-up questions, and engage deeply - bu
|
||||
structure should still feel natural, not artificially fragmented.
|
||||
|
||||
You have thoughtful opinions on technical matters that you express with enthusiasm:
|
||||
|
||||
- Understanding fundamentals of computer science is important for building truly innovative solutions
|
||||
- Modern web development has interesting complexities that serve specific purposes
|
||||
- Engineering elegance comes from understanding systems deeply
|
||||
|
||||
@@ -25,10 +25,13 @@ export function span(...args: string[]) {
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export function div(...args: (string | TextContentPart | TextContentPart[])[]) {
|
||||
export function div(...args: (string | TextContentPart | TextContentPart[] | null | undefined)[]) {
|
||||
const results: string[] = []
|
||||
|
||||
for (const arg of args) {
|
||||
if (arg == null) {
|
||||
continue
|
||||
}
|
||||
if (typeof arg === 'string') {
|
||||
results.push(arg)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { FileFlavor } from '@grammyjs/files'
|
||||
import type { Logg } from '@guiiai/logg'
|
||||
import type { Message as LLMMessage } from '@xsai/shared-chat'
|
||||
import type { Bot, Context } from 'grammy'
|
||||
import type { Message } from 'grammy/types'
|
||||
|
||||
import type { createAttentionHandler } from './bots/telegram/agent/attention-handler'
|
||||
import type { CancellablePromise } from './utils/promise'
|
||||
|
||||
export interface PendingMessage {
|
||||
@@ -14,10 +14,8 @@ export interface PendingMessage {
|
||||
|
||||
export type ExtendedContext = FileFlavor<Context>
|
||||
|
||||
export interface BotSelf {
|
||||
export interface BotContext {
|
||||
bot: Bot
|
||||
currentTask: CancellablePromise<Message.TextMessage> | null
|
||||
currentAbortController: AbortController | null
|
||||
messageQueue: Array<{
|
||||
message: Message
|
||||
status: 'pending' | 'interpreting' | 'ready'
|
||||
@@ -26,9 +24,19 @@ export interface BotSelf {
|
||||
processedIds: Set<string>
|
||||
logger: Logg
|
||||
processing: boolean
|
||||
attentionHandler: ReturnType<typeof createAttentionHandler>
|
||||
lastInteractedNChatIds: string[]
|
||||
currentProcessingStartTime: number | null
|
||||
currentProcessingStartTime?: number
|
||||
chats: Map<string, ChatContext>
|
||||
}
|
||||
|
||||
export interface ChatContext {
|
||||
chatId: string
|
||||
|
||||
currentTask?: CancellablePromise<Message.TextMessage>
|
||||
currentAbortController?: AbortController
|
||||
|
||||
messages: LLMMessage[]
|
||||
actions: { action: Action, result: unknown }[]
|
||||
}
|
||||
|
||||
export interface ContinueAction {
|
||||
|
||||
Reference in New Issue
Block a user