style: lint
This commit is contained in:
@@ -9,10 +9,10 @@ try {
|
||||
catch {}
|
||||
|
||||
export default defineConfig({
|
||||
out: './drizzle',
|
||||
schema: './src/db/schema.ts',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: env.DATABASE_URL!,
|
||||
},
|
||||
dialect: 'postgresql',
|
||||
out: './drizzle',
|
||||
schema: './src/db/schema.ts',
|
||||
})
|
||||
|
||||
@@ -25,10 +25,10 @@ async function main() {
|
||||
let messages: typeof chatMessagesTable.$inferSelect[] = []
|
||||
|
||||
switch (env.EMBEDDING_DIMENSION) {
|
||||
case '1536':
|
||||
case '768':
|
||||
messages = await db.query.chatMessagesTable.findMany({
|
||||
where(fields) {
|
||||
return isNull(fields.content_vector_1536)
|
||||
return isNull(fields.content_vector_768)
|
||||
},
|
||||
})
|
||||
break
|
||||
@@ -39,10 +39,10 @@ async function main() {
|
||||
},
|
||||
})
|
||||
break
|
||||
case '768':
|
||||
case '1536':
|
||||
messages = await db.query.chatMessagesTable.findMany({
|
||||
where(fields) {
|
||||
return isNull(fields.content_vector_768)
|
||||
return isNull(fields.content_vector_1536)
|
||||
},
|
||||
})
|
||||
break
|
||||
@@ -53,24 +53,24 @@ async function main() {
|
||||
// Split messages into batches
|
||||
const batches = chunk(messages, BATCH_SIZE)
|
||||
// Process each batch with worker pool
|
||||
const processedCount = { success: 0, error: 0 }
|
||||
const processedCount = { error: 0, success: 0 }
|
||||
|
||||
for (const batch of batches) {
|
||||
await limit(async () => {
|
||||
const embedPromises = batch.map(async (message) => {
|
||||
try {
|
||||
const embeddingRes = await embed({
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
apiKey: env.EMBEDDING_API_KEY!,
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
input: message.content,
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
})
|
||||
|
||||
switch (env.EMBEDDING_DIMENSION) {
|
||||
case '1536':
|
||||
case '768':
|
||||
await db
|
||||
.update(chatMessagesTable)
|
||||
.set({ content_vector_1536: embeddingRes.embedding })
|
||||
.set({ content_vector_768: embeddingRes.embedding })
|
||||
.where(eq(chatMessagesTable.id, message.id))
|
||||
break
|
||||
case '1024':
|
||||
@@ -79,10 +79,10 @@ async function main() {
|
||||
.set({ content_vector_1024: embeddingRes.embedding })
|
||||
.where(eq(chatMessagesTable.id, message.id))
|
||||
break
|
||||
case '768':
|
||||
case '1536':
|
||||
await db
|
||||
.update(chatMessagesTable)
|
||||
.set({ content_vector_768: embeddingRes.embedding })
|
||||
.set({ content_vector_1536: embeddingRes.embedding })
|
||||
.where(eq(chatMessagesTable.id, message.id))
|
||||
break
|
||||
default:
|
||||
|
||||
@@ -21,8 +21,8 @@ export async function readMessage(
|
||||
unreadMessages: Message[],
|
||||
abortController: AbortController,
|
||||
): Promise<{
|
||||
loop?: boolean
|
||||
break?: boolean
|
||||
loop?: boolean
|
||||
result: string
|
||||
}> {
|
||||
const logger = useLogg('readMessage').useGlobalConfig()
|
||||
@@ -50,11 +50,11 @@ export async function readMessage(
|
||||
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,
|
||||
apiKey: env.EMBEDDING_API_KEY!,
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
input: msg.text || msg.caption || '',
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
})
|
||||
|
||||
span.end()
|
||||
@@ -93,8 +93,8 @@ export async function readMessage(
|
||||
break: true,
|
||||
result: await actionReadMessages({
|
||||
lastMessages: lastNMessagesOneliner,
|
||||
unreadHistoryMessages: unreadHistoryMessageOneliner,
|
||||
relevantChatMessages: relevantChatMessagesOneliner,
|
||||
unreadHistoryMessages: unreadHistoryMessageOneliner,
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -25,7 +25,7 @@ export function parseMayStructuredMessage(responseText: string) {
|
||||
if (result) {
|
||||
logger.withField('text', JSON.stringify(responseText)).withField('result', result).log('Multiple messages detected')
|
||||
|
||||
const parsedResponse = parse(result[0]) as ({ messages?: unknown, reply_to_message_id?: unknown } | undefined)
|
||||
const parsedResponse = parse(result[0]) as (undefined | { messages?: unknown, reply_to_message_id?: unknown })
|
||||
const hasMessagesArray = Array.isArray(parsedResponse?.messages)
|
||||
const messages = Array.isArray(parsedResponse?.messages)
|
||||
? parsedResponse.messages.filter((message): message is string => typeof message === 'string' && message.trim() !== '')
|
||||
@@ -79,15 +79,15 @@ export async function sendMessage(
|
||||
|
||||
const systemContent = String(await messageSplit())
|
||||
const req = {
|
||||
abortSignal: abortController.signal,
|
||||
apiKey: env.LLM_API_KEY!,
|
||||
baseURL: env.LLM_API_BASE_URL!,
|
||||
model: env.LLM_MODEL!,
|
||||
messages: message.messages(
|
||||
{ role: 'system' as const, content: systemContent },
|
||||
{ role: 'user' as const, content: 'This is the input message:' },
|
||||
{ role: 'user' as const, content: String(responseText) },
|
||||
{ content: systemContent, role: 'system' as const },
|
||||
{ content: 'This is the input message:', role: 'user' as const },
|
||||
{ content: String(responseText), role: 'user' as const },
|
||||
),
|
||||
abortSignal: abortController.signal,
|
||||
model: env.LLM_MODEL!,
|
||||
} satisfies GenerateTextOptions
|
||||
if (env.LLM_OLLAMA_DISABLE_THINK) {
|
||||
(req as Record<string, unknown>).think = false
|
||||
@@ -106,12 +106,12 @@ export async function sendMessage(
|
||||
}
|
||||
|
||||
logger.withFields({
|
||||
messages: responseText,
|
||||
response: res.text,
|
||||
now: new Date().toLocaleString(),
|
||||
totalTokens: res.usage.totalTokens,
|
||||
promptTokens: res.usage.inputTokens,
|
||||
completion_tokens: res.usage.outputTokens,
|
||||
messages: responseText,
|
||||
now: new Date().toLocaleString(),
|
||||
promptTokens: res.usage.inputTokens,
|
||||
response: res.text,
|
||||
totalTokens: res.usage.totalTokens,
|
||||
}).log('Message split')
|
||||
|
||||
const structuredMessage = parseMayStructuredMessage(res.text)
|
||||
|
||||
@@ -8,9 +8,9 @@ export function createAttentionHandler(bot: BotContext, config: AttentionConfig)
|
||||
currentResponseRate: config.initialResponseRate,
|
||||
lastResponseTimes: new Map(),
|
||||
stats: {
|
||||
lastInteractionTime: Date.now(),
|
||||
mentionCount: 0,
|
||||
triggerWordCount: 0,
|
||||
lastInteractionTime: Date.now(),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export function createAttentionHandler(bot: BotContext, config: AttentionConfig)
|
||||
return now - lastResponse >= config.cooldownMs
|
||||
}
|
||||
|
||||
const checkTriggerWords = (text?: string): string | false => {
|
||||
const checkTriggerWords = (text?: string): false | string => {
|
||||
if (!text || !config.triggerWords.length)
|
||||
return false
|
||||
return config.triggerWords.find(word => text.includes(word)) || false
|
||||
@@ -66,65 +66,6 @@ export function createAttentionHandler(bot: BotContext, config: AttentionConfig)
|
||||
|
||||
// Public interface
|
||||
const handler = {
|
||||
async shouldRespond(chatId: string, messages: Message[]): Promise<{ shouldAct: boolean, reason: string, responseRate?: number }> {
|
||||
const fromPrivate = messages.every(message => message.chat.type === 'private')
|
||||
const mentioned = messages.some(message => message.text?.includes(`@${bot.bot.botInfo.username}`))
|
||||
const reply = messages.some(message => message.reply_to_message?.from?.id.toString() === bot.bot.botInfo.id.toString())
|
||||
|
||||
try {
|
||||
// Always respond to private messages
|
||||
if (fromPrivate) {
|
||||
state.stats.mentionCount++
|
||||
state.stats.lastInteractionTime = Date.now()
|
||||
return { shouldAct: true, reason: 'private_message' }
|
||||
}
|
||||
|
||||
if (mentioned || reply) {
|
||||
state.stats.mentionCount++
|
||||
state.stats.lastInteractionTime = Date.now()
|
||||
return { shouldAct: true, reason: 'mention_or_reply' }
|
||||
}
|
||||
|
||||
// Check trigger words
|
||||
const matchedTrigger = checkTriggerWords(messages.map(message => message.text).join(' '))
|
||||
if (matchedTrigger) {
|
||||
state.stats.triggerWordCount++
|
||||
state.stats.lastInteractionTime = Date.now()
|
||||
return { shouldAct: true, reason: `trigger_word:${matchedTrigger}` }
|
||||
}
|
||||
|
||||
// Check cooldown
|
||||
if (!checkCooldown(chatId)) {
|
||||
return { shouldAct: false, reason: 'cooldown' }
|
||||
}
|
||||
|
||||
// Check ignore words
|
||||
if (checkIgnoreWords(messages.map(message => message.text).join(' '))) {
|
||||
return { shouldAct: false, reason: 'ignore_word' }
|
||||
}
|
||||
|
||||
// Random response based on current rate
|
||||
if (Math.random() < state.currentResponseRate) {
|
||||
state.lastResponseTimes.set(chatId, Date.now())
|
||||
return {
|
||||
shouldAct: true,
|
||||
reason: 'random',
|
||||
responseRate: state.currentResponseRate,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
shouldAct: false,
|
||||
reason: 'rate_check_failed',
|
||||
responseRate: state.currentResponseRate,
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
bot.logger.withError(error).log('Error in attention handler')
|
||||
return { shouldAct: false, reason: 'error' }
|
||||
}
|
||||
},
|
||||
|
||||
// Cleanup function
|
||||
destroy() {
|
||||
clearInterval(decayInterval)
|
||||
@@ -134,6 +75,65 @@ export function createAttentionHandler(bot: BotContext, config: AttentionConfig)
|
||||
getState() {
|
||||
return { ...state }
|
||||
},
|
||||
|
||||
async shouldRespond(chatId: string, messages: Message[]): Promise<{ reason: string, responseRate?: number, shouldAct: boolean }> {
|
||||
const fromPrivate = messages.every(message => message.chat.type === 'private')
|
||||
const mentioned = messages.some(message => message.text?.includes(`@${bot.bot.botInfo.username}`))
|
||||
const reply = messages.some(message => message.reply_to_message?.from?.id.toString() === bot.bot.botInfo.id.toString())
|
||||
|
||||
try {
|
||||
// Always respond to private messages
|
||||
if (fromPrivate) {
|
||||
state.stats.mentionCount++
|
||||
state.stats.lastInteractionTime = Date.now()
|
||||
return { reason: 'private_message', shouldAct: true }
|
||||
}
|
||||
|
||||
if (mentioned || reply) {
|
||||
state.stats.mentionCount++
|
||||
state.stats.lastInteractionTime = Date.now()
|
||||
return { reason: 'mention_or_reply', shouldAct: true }
|
||||
}
|
||||
|
||||
// Check trigger words
|
||||
const matchedTrigger = checkTriggerWords(messages.map(message => message.text).join(' '))
|
||||
if (matchedTrigger) {
|
||||
state.stats.triggerWordCount++
|
||||
state.stats.lastInteractionTime = Date.now()
|
||||
return { reason: `trigger_word:${matchedTrigger}`, shouldAct: true }
|
||||
}
|
||||
|
||||
// Check cooldown
|
||||
if (!checkCooldown(chatId)) {
|
||||
return { reason: 'cooldown', shouldAct: false }
|
||||
}
|
||||
|
||||
// Check ignore words
|
||||
if (checkIgnoreWords(messages.map(message => message.text).join(' '))) {
|
||||
return { reason: 'ignore_word', shouldAct: false }
|
||||
}
|
||||
|
||||
// Random response based on current rate
|
||||
if (Math.random() < state.currentResponseRate) {
|
||||
state.lastResponseTimes.set(chatId, Date.now())
|
||||
return {
|
||||
reason: 'random',
|
||||
responseRate: state.currentResponseRate,
|
||||
shouldAct: true,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
reason: 'rate_check_failed',
|
||||
responseRate: state.currentResponseRate,
|
||||
shouldAct: false,
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
bot.logger.withError(error).log('Error in attention handler')
|
||||
return { reason: 'error', shouldAct: false }
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return handler
|
||||
|
||||
@@ -2,11 +2,11 @@ import type { Message as LLMMessage } from '@xsai/shared-chat'
|
||||
import type { Message } from 'grammy/types'
|
||||
|
||||
interface InterruptionParams {
|
||||
processingTime: number
|
||||
messageCount: number
|
||||
currentMessages: LLMMessage[]
|
||||
newMessages: Message[]
|
||||
chatId: string
|
||||
currentMessages: LLMMessage[]
|
||||
messageCount: number
|
||||
newMessages: Message[]
|
||||
processingTime: number
|
||||
}
|
||||
|
||||
export async function shouldInterruptProcessing(params: InterruptionParams): Promise<boolean> {
|
||||
|
||||
@@ -20,439 +20,6 @@ import { readMessage } from './agent/actions/read-message'
|
||||
import { sendMessage } from './agent/actions/send-message'
|
||||
// import { shouldInterruptProcessing } from './agent/interruption'
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
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.`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLoopStep(ctx: BotContext, chatCtx: ChatContext, incomingMessage?: Message): Promise<() => Promise<any> | undefined> {
|
||||
ctx.currentProcessingStartTime = Date.now()
|
||||
|
||||
if (chatCtx?.currentAbortController) {
|
||||
chatCtx.currentAbortController.abort()
|
||||
}
|
||||
|
||||
// // Create a new abort controller for this loop execution
|
||||
// const unreadMessagesForThisChat = ctx.unreadMessages[chatCtx.chatId]
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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.`))
|
||||
}
|
||||
|
||||
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 chatCtx.actions[number] }[] = []
|
||||
|
||||
if (chatCtx) {
|
||||
for (const actionHistory of chatCtx.actions) {
|
||||
if (actionHistory.action.action === 'read_unread_messages') {
|
||||
readUnreadMessagesActions.push({ index: chatCtx.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
|
||||
})
|
||||
|
||||
if (chatCtx) {
|
||||
for (const item of readUnreadMessagesActions) {
|
||||
chatCtx.actions[item.index] = item.actionState
|
||||
}
|
||||
}
|
||||
|
||||
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') {
|
||||
ctx.logger.log('Operation was aborted due to interruption')
|
||||
return
|
||||
}
|
||||
|
||||
ctx.logger.withError(err).log('Error occurred')
|
||||
}
|
||||
finally {
|
||||
// Clean up timing when done
|
||||
if (chatCtx && chatCtx.currentAbortController === currentController) {
|
||||
chatCtx.currentAbortController = undefined
|
||||
ctx.currentProcessingStartTime = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function isChatIdBotAdmin(fromId: number) {
|
||||
if (!env.ADMIN_USER_IDS) {
|
||||
return false
|
||||
}
|
||||
|
||||
const admins = env.ADMIN_USER_IDS.split(',')
|
||||
if (admins.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return admins.includes(fromId.toString())
|
||||
}
|
||||
|
||||
async function loopIterationForChat(bot: BotContext, chatContext: ChatContext, incomingMessage: Message) {
|
||||
let result = await handleLoopStep(bot, chatContext, incomingMessage)
|
||||
|
||||
while (typeof result === 'function') {
|
||||
result = await result()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
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 createBotContext(telegramBot: Bot, logger: Logg): BotContext {
|
||||
const botSelf: BotContext = {
|
||||
bot: telegramBot,
|
||||
messageQueue: [],
|
||||
unreadMessages: {},
|
||||
processedIds: new Set(),
|
||||
logger,
|
||||
processing: false,
|
||||
lastInteractedNChatIds: [],
|
||||
chats: new Map<string, ChatContext>(),
|
||||
}
|
||||
|
||||
return botSelf
|
||||
}
|
||||
|
||||
async function onMessageArrival(botContext: BotContext, chatCtx: ChatContext) {
|
||||
if (botContext.processing)
|
||||
return
|
||||
botContext.processing = true
|
||||
|
||||
try {
|
||||
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(botContext.bot, nextMsg.message, nextMsg.message.sticker)
|
||||
nextMsg.status = 'ready'
|
||||
}
|
||||
else if (nextMsg.message.photo) {
|
||||
nextMsg.status = 'interpreting'
|
||||
await interpretPhotos(botContext, nextMsg.message, nextMsg.message.photo)
|
||||
nextMsg.status = 'ready'
|
||||
}
|
||||
else {
|
||||
nextMsg.status = 'ready'
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMsg.status === 'ready') {
|
||||
switch (nextMsg.message.chat.type) {
|
||||
case 'private':
|
||||
await recordJoinedChat(nextMsg.message.chat.id.toString(), `${nextMsg.message.from.first_name} ${nextMsg.message.from.last_name}`)
|
||||
break
|
||||
case 'channel':
|
||||
case 'group':
|
||||
case 'supergroup':
|
||||
await recordJoinedChat(nextMsg.message.chat.id.toString(), nextMsg.message.chat.title)
|
||||
break
|
||||
}
|
||||
|
||||
await recordMessage(botContext.bot.botInfo, nextMsg.message)
|
||||
|
||||
let unreadMessagesForThisChat = botContext.unreadMessages[nextMsg.message.chat.id]
|
||||
|
||||
if (unreadMessagesForThisChat == null) {
|
||||
botContext.logger.withField('chatId', nextMsg.message.chat.id).log('unread messages for this chat is null - creating empty array')
|
||||
unreadMessagesForThisChat = []
|
||||
}
|
||||
if (!Array.isArray(unreadMessagesForThisChat)) {
|
||||
botContext.logger.withField('chatId', nextMsg.message.chat.id).log('unread messages for this chat is not an array - converting to array')
|
||||
unreadMessagesForThisChat = []
|
||||
}
|
||||
|
||||
unreadMessagesForThisChat.push(nextMsg.message)
|
||||
|
||||
if (unreadMessagesForThisChat.length > 100) {
|
||||
unreadMessagesForThisChat = unreadMessagesForThisChat.slice(-100)
|
||||
}
|
||||
|
||||
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
|
||||
loopIterationForChat(botContext, chatCtx, nextMsg.message)
|
||||
botContext.messageQueue.shift()
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
botContext.logger.withError(err).log('Error occurred')
|
||||
}
|
||||
finally {
|
||||
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()
|
||||
|
||||
@@ -541,3 +108,436 @@ export async function startTelegramBot() {
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
function createBotContext(telegramBot: Bot, logger: Logg): BotContext {
|
||||
const botSelf: BotContext = {
|
||||
bot: telegramBot,
|
||||
chats: new Map<string, ChatContext>(),
|
||||
lastInteractedNChatIds: [],
|
||||
logger,
|
||||
messageQueue: [],
|
||||
processedIds: new Set(),
|
||||
processing: false,
|
||||
unreadMessages: {},
|
||||
}
|
||||
|
||||
return botSelf
|
||||
}
|
||||
|
||||
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 '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 'continue':
|
||||
if (chatCtx) {
|
||||
chatCtx.actions.push({ action, result: 'AIRI System: Acknowledged, will now continue until next tick.' })
|
||||
}
|
||||
|
||||
return
|
||||
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
|
||||
}
|
||||
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
|
||||
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 '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 '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.`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ensureChatContext(botCtx: BotContext, chatId: string): ChatContext {
|
||||
if (botCtx.chats.has(chatId)) {
|
||||
return botCtx.chats.get(chatId)!
|
||||
}
|
||||
|
||||
const newChatContext: ChatContext = {
|
||||
actions: [],
|
||||
chatId,
|
||||
currentAbortController: undefined,
|
||||
currentTask: undefined,
|
||||
messages: [],
|
||||
}
|
||||
|
||||
botCtx.chats.set(chatId, newChatContext)
|
||||
return newChatContext
|
||||
}
|
||||
|
||||
async function handleLoopStep(ctx: BotContext, chatCtx: ChatContext, incomingMessage?: Message): Promise<() => Promise<any> | undefined> {
|
||||
ctx.currentProcessingStartTime = Date.now()
|
||||
|
||||
if (chatCtx?.currentAbortController) {
|
||||
chatCtx.currentAbortController.abort()
|
||||
}
|
||||
|
||||
// // Create a new abort controller for this loop execution
|
||||
// const unreadMessagesForThisChat = ctx.unreadMessages[chatCtx.chatId]
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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.`))
|
||||
}
|
||||
|
||||
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: { actionState: typeof chatCtx.actions[number], index: number }[] = []
|
||||
|
||||
if (chatCtx) {
|
||||
for (const actionHistory of chatCtx.actions) {
|
||||
if (actionHistory.action.action === 'read_unread_messages') {
|
||||
readUnreadMessagesActions.push({ actionState: actionHistory, index: chatCtx.actions.indexOf(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
|
||||
})
|
||||
|
||||
if (chatCtx) {
|
||||
for (const item of readUnreadMessagesActions) {
|
||||
chatCtx.actions[item.index] = item.actionState
|
||||
}
|
||||
}
|
||||
|
||||
const action = await imagineAnAction(ctx.bot.botInfo.id.toString(), currentController, chatCtx?.messages || [], chatCtx?.actions || [], { incomingMessages: [incomingMessage], unreadMessages: ctx.unreadMessages })
|
||||
return await dispatchAction(ctx, action, currentController, chatCtx)
|
||||
}
|
||||
catch (err) {
|
||||
if (err.name === 'AbortError') {
|
||||
ctx.logger.log('Operation was aborted due to interruption')
|
||||
return
|
||||
}
|
||||
|
||||
ctx.logger.withError(err).log('Error occurred')
|
||||
}
|
||||
finally {
|
||||
// Clean up timing when done
|
||||
if (chatCtx && chatCtx.currentAbortController === currentController) {
|
||||
chatCtx.currentAbortController = undefined
|
||||
ctx.currentProcessingStartTime = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function isChatIdBotAdmin(fromId: number) {
|
||||
if (!env.ADMIN_USER_IDS) {
|
||||
return false
|
||||
}
|
||||
|
||||
const admins = env.ADMIN_USER_IDS.split(',')
|
||||
if (admins.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return admins.includes(fromId.toString())
|
||||
}
|
||||
|
||||
async function loopIterationForChat(bot: BotContext, chatContext: ChatContext, incomingMessage: Message) {
|
||||
let result = await handleLoopStep(bot, chatContext, incomingMessage)
|
||||
|
||||
while (typeof result === 'function') {
|
||||
result = await result()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
async function onMessageArrival(botContext: BotContext, chatCtx: ChatContext) {
|
||||
if (botContext.processing)
|
||||
return
|
||||
botContext.processing = true
|
||||
|
||||
try {
|
||||
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(botContext.bot, nextMsg.message, nextMsg.message.sticker)
|
||||
nextMsg.status = 'ready'
|
||||
}
|
||||
else if (nextMsg.message.photo) {
|
||||
nextMsg.status = 'interpreting'
|
||||
await interpretPhotos(botContext, nextMsg.message, nextMsg.message.photo)
|
||||
nextMsg.status = 'ready'
|
||||
}
|
||||
else {
|
||||
nextMsg.status = 'ready'
|
||||
}
|
||||
}
|
||||
|
||||
if (nextMsg.status === 'ready') {
|
||||
switch (nextMsg.message.chat.type) {
|
||||
case 'channel':
|
||||
case 'group':
|
||||
case 'supergroup':
|
||||
await recordJoinedChat(nextMsg.message.chat.id.toString(), nextMsg.message.chat.title)
|
||||
break
|
||||
case 'private':
|
||||
await recordJoinedChat(nextMsg.message.chat.id.toString(), `${nextMsg.message.from.first_name} ${nextMsg.message.from.last_name}`)
|
||||
break
|
||||
}
|
||||
|
||||
await recordMessage(botContext.bot.botInfo, nextMsg.message)
|
||||
|
||||
let unreadMessagesForThisChat = botContext.unreadMessages[nextMsg.message.chat.id]
|
||||
|
||||
if (unreadMessagesForThisChat == null) {
|
||||
botContext.logger.withField('chatId', nextMsg.message.chat.id).log('unread messages for this chat is null - creating empty array')
|
||||
unreadMessagesForThisChat = []
|
||||
}
|
||||
if (!Array.isArray(unreadMessagesForThisChat)) {
|
||||
botContext.logger.withField('chatId', nextMsg.message.chat.id).log('unread messages for this chat is not an array - converting to array')
|
||||
unreadMessagesForThisChat = []
|
||||
}
|
||||
|
||||
unreadMessagesForThisChat.push(nextMsg.message)
|
||||
|
||||
if (unreadMessagesForThisChat.length > 100) {
|
||||
unreadMessagesForThisChat = unreadMessagesForThisChat.slice(-100)
|
||||
}
|
||||
|
||||
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
|
||||
loopIterationForChat(botContext, chatCtx, nextMsg.message)
|
||||
botContext.messageQueue.shift()
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
botContext.logger.withError(err).log('Error occurred')
|
||||
}
|
||||
finally {
|
||||
botContext.processing = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { bigint, boolean, index, integer, jsonb, pgTable, text, uniqueIndex, uuid, vector } from 'drizzle-orm/pg-core'
|
||||
|
||||
export const chatMessagesTable = pgTable('chat_messages', {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
platform: text().notNull().default(''),
|
||||
platform_message_id: text().notNull().default(''),
|
||||
content: text().notNull().default(''),
|
||||
content_vector_768: vector({ dimensions: 768 }),
|
||||
content_vector_1024: vector({ dimensions: 1024 }),
|
||||
content_vector_1536: vector({ dimensions: 1536 }),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
from_id: text().notNull().default(''),
|
||||
from_name: text().notNull().default(''),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
in_chat_id: text().notNull().default(''),
|
||||
content: text().notNull().default(''),
|
||||
is_reply: boolean().notNull().default(false),
|
||||
reply_to_name: text().notNull().default(''),
|
||||
platform: text().notNull().default(''),
|
||||
platform_message_id: text().notNull().default(''),
|
||||
reply_to_id: text().notNull().default(''),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
reply_to_name: text().notNull().default(''),
|
||||
updated_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
content_vector_1536: vector({ dimensions: 1536 }),
|
||||
content_vector_1024: vector({ dimensions: 1024 }),
|
||||
content_vector_768: vector({ dimensions: 768 }),
|
||||
}, table => [
|
||||
index('chat_messages_content_vector_1536_index').using('hnsw', table.content_vector_1536.op('vector_cosine_ops')),
|
||||
index('chat_messages_content_vector_1024_index').using('hnsw', table.content_vector_1024.op('vector_cosine_ops')),
|
||||
@@ -23,20 +23,20 @@ export const chatMessagesTable = pgTable('chat_messages', {
|
||||
])
|
||||
|
||||
export const stickersTable = pgTable('stickers', {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
platform: text().notNull().default(''),
|
||||
name: text().notNull().default(''),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
description: text().notNull().default(''),
|
||||
description_vector_768: vector({ dimensions: 768 }),
|
||||
description_vector_1024: vector({ dimensions: 1024 }),
|
||||
description_vector_1536: vector({ dimensions: 1536 }),
|
||||
emoji: text().notNull().default(''),
|
||||
label: text().notNull().default(''),
|
||||
file_id: text().notNull().default(''),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
image_base64: text().notNull().default(''),
|
||||
image_path: text().notNull().default(''),
|
||||
description: text().notNull().default(''),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
label: text().notNull().default(''),
|
||||
name: text().notNull().default(''),
|
||||
platform: text().notNull().default(''),
|
||||
updated_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
description_vector_1536: vector({ dimensions: 1536 }),
|
||||
description_vector_1024: vector({ dimensions: 1024 }),
|
||||
description_vector_768: vector({ dimensions: 768 }),
|
||||
}, table => [
|
||||
index('stickers_description_vector_1536_index').using('hnsw', table.description_vector_1536.op('vector_cosine_ops')),
|
||||
index('stickers_description_vector_1024_index').using('hnsw', table.description_vector_1024.op('vector_cosine_ops')),
|
||||
@@ -44,37 +44,37 @@ export const stickersTable = pgTable('stickers', {
|
||||
])
|
||||
|
||||
export const stickerPacksTable = pgTable('sticker_packs', {
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
description: text().notNull().default(''),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
name: text().notNull().default(''),
|
||||
platform: text().notNull().default(''),
|
||||
platform_id: text().notNull().default(''),
|
||||
name: text().notNull().default(''),
|
||||
description: text().notNull().default(''),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
updated_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
}, table => [
|
||||
uniqueIndex('sticker_packs_platform_platform_id_unique_index').on(table.platform, table.platform_id),
|
||||
])
|
||||
|
||||
export const recentSentStickersTable = pgTable('recent_sent_stickers', {
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
sticker_id: uuid().notNull().references(() => stickersTable.id, { onDelete: 'cascade' }),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
updated_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
})
|
||||
|
||||
export const photosTable = pgTable('photos', {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
platform: text().notNull().default(''),
|
||||
caption: text().notNull().default(''),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
description: text().notNull().default(''),
|
||||
description_vector_768: vector({ dimensions: 768 }),
|
||||
description_vector_1024: vector({ dimensions: 1024 }),
|
||||
description_vector_1536: vector({ dimensions: 1536 }),
|
||||
file_id: text().notNull().default(''),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
image_base64: text().notNull().default(''),
|
||||
image_path: text().notNull().default(''),
|
||||
caption: text().notNull().default(''),
|
||||
description: text().notNull().default(''),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
platform: text().notNull().default(''),
|
||||
updated_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
description_vector_1536: vector({ dimensions: 1536 }),
|
||||
description_vector_1024: vector({ dimensions: 1024 }),
|
||||
description_vector_768: vector({ dimensions: 768 }),
|
||||
}, table => [
|
||||
index('photos_description_vector_1536_index').using('hnsw', table.description_vector_1536.op('vector_cosine_ops')),
|
||||
index('photos_description_vector_1024_index').using('hnsw', table.description_vector_1024.op('vector_cosine_ops')),
|
||||
@@ -83,11 +83,11 @@ export const photosTable = pgTable('photos', {
|
||||
|
||||
export const joinedChatsTable = pgTable('joined_chats', () => {
|
||||
return {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
platform: text().notNull().default(''),
|
||||
chat_id: text().notNull().default('').unique(),
|
||||
chat_name: text().notNull().default(''),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
platform: text().notNull().default(''),
|
||||
updated_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
}
|
||||
}, (table) => {
|
||||
@@ -99,29 +99,29 @@ export const joinedChatsTable = pgTable('joined_chats', () => {
|
||||
})
|
||||
|
||||
export const chatCompletionsHistoryTable = pgTable('chat_completions_history', {
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
prompt: text().notNull(),
|
||||
response: text().notNull(),
|
||||
task: text().notNull(),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
})
|
||||
|
||||
// Memory Item table - base table for all memories
|
||||
export const memoryFragmentsTable = pgTable('memory_fragments', {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
content: text().notNull(),
|
||||
memory_type: text().notNull(), // 'working', 'short_term', 'long_term', 'muscle'
|
||||
category: text().notNull(), // 'chat', 'relationships', 'people', 'life', etc.
|
||||
importance: integer().notNull().default(5), // 1-10 scale
|
||||
emotional_impact: integer().notNull().default(0), // -10 to 10 scale
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
last_accessed: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
access_count: integer().notNull().default(1),
|
||||
metadata: jsonb().notNull().default({}),
|
||||
content_vector_1536: vector({ dimensions: 1536 }),
|
||||
content_vector_1024: vector({ dimensions: 1024 }),
|
||||
category: text().notNull(), // 'chat', 'relationships', 'people', 'life', etc.
|
||||
content: text().notNull(),
|
||||
content_vector_768: vector({ dimensions: 768 }),
|
||||
content_vector_1024: vector({ dimensions: 1024 }),
|
||||
content_vector_1536: vector({ dimensions: 1536 }),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
deleted_at: bigint({ mode: 'number' }), // nullable timestamp for soft delete
|
||||
emotional_impact: integer().notNull().default(0), // -10 to 10 scale
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
importance: integer().notNull().default(5), // 1-10 scale
|
||||
last_accessed: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
memory_type: text().notNull(), // 'working', 'short_term', 'long_term', 'muscle'
|
||||
metadata: jsonb().notNull().default({}),
|
||||
}, table => [
|
||||
// Vector indexes for efficient similarity search
|
||||
index('memory_items_content_vector_1536_index').using('hnsw', table.content_vector_1536.op('vector_cosine_ops')),
|
||||
@@ -137,11 +137,11 @@ export const memoryFragmentsTable = pgTable('memory_fragments', {
|
||||
|
||||
// Memory Tags junction table
|
||||
export const memoryTagsTable = pgTable('memory_tags', {
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
deleted_at: bigint({ mode: 'number' }), // nullable timestamp for soft delete
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
memory_id: uuid().notNull().references(() => memoryFragmentsTable.id, { onDelete: 'cascade' }),
|
||||
tag: text().notNull(),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
deleted_at: bigint({ mode: 'number' }), // nullable timestamp for soft delete
|
||||
}, table => [
|
||||
index('memory_tags_memory_id_index').on(table.memory_id),
|
||||
index('memory_tags_tag_index').on(table.tag),
|
||||
@@ -149,13 +149,13 @@ export const memoryTagsTable = pgTable('memory_tags', {
|
||||
|
||||
// Episodic Memory (specific events)
|
||||
export const memoryEpisodicTable = pgTable('memory_episodic', {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
memory_id: uuid().notNull().references(() => memoryFragmentsTable.id, { onDelete: 'cascade' }),
|
||||
event_type: text().notNull(), // 'conversation', 'introduction', 'argument', etc.
|
||||
participants: jsonb().notNull().default([]), // Array of participant IDs
|
||||
location: text().default(''),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
deleted_at: bigint({ mode: 'number' }), // nullable timestamp for soft delete
|
||||
event_type: text().notNull(), // 'conversation', 'introduction', 'argument', etc.
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
location: text().default(''),
|
||||
memory_id: uuid().notNull().references(() => memoryFragmentsTable.id, { onDelete: 'cascade' }),
|
||||
participants: jsonb().notNull().default([]), // Array of participant IDs
|
||||
}, table => [
|
||||
index('memory_episodic_memory_id_index').on(table.memory_id),
|
||||
index('memory_episodic_event_type_index').on(table.event_type),
|
||||
@@ -163,18 +163,18 @@ export const memoryEpisodicTable = pgTable('memory_episodic', {
|
||||
|
||||
// Goals table
|
||||
export const memoryLongTermGoalsTable = pgTable('memory_long_term_goals', {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
title: text().notNull(),
|
||||
description: text().notNull(),
|
||||
priority: integer().notNull().default(5), // 1-10 scale
|
||||
progress: integer().notNull().default(0), // 0-100 percentage
|
||||
deadline: bigint({ mode: 'number' }).default(null),
|
||||
status: text().notNull().default('planned'), // 'planned', 'in_progress', 'completed', 'abandoned'
|
||||
parent_goal_id: uuid().references(() => memoryLongTermGoalsTable.id),
|
||||
category: text().notNull().default('personal'),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
updated_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
deadline: bigint({ mode: 'number' }).default(null),
|
||||
deleted_at: bigint({ mode: 'number' }), // nullable timestamp for soft delete
|
||||
description: text().notNull(),
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
parent_goal_id: uuid().references(() => memoryLongTermGoalsTable.id),
|
||||
priority: integer().notNull().default(5), // 1-10 scale
|
||||
progress: integer().notNull().default(0), // 0-100 percentage
|
||||
status: text().notNull().default('planned'), // 'planned', 'in_progress', 'completed', 'abandoned'
|
||||
title: text().notNull(),
|
||||
updated_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
}, table => [
|
||||
index('memory_long_term_goals_priority_index').on(table.priority),
|
||||
index('memory_long_term_goals_status_index').on(table.status),
|
||||
@@ -184,18 +184,18 @@ export const memoryLongTermGoalsTable = pgTable('memory_long_term_goals', {
|
||||
|
||||
// Ideas generated from dreams or normal thinking
|
||||
export const memoryShortTermIdeas = pgTable('memory_short_term_ideas', {
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
content: text().notNull(),
|
||||
source_type: text().notNull().default('dream'), // 'dream', 'conversation', 'reflection'
|
||||
source_id: text().default(null), // ID of source (dream ID, conversation ID, etc.)
|
||||
status: text().notNull().default('new'), // 'new', 'developing', 'implemented', 'abandoned'
|
||||
excitement: integer().notNull().default(5), // 1-10 scale
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
updated_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
content_vector_1536: vector({ dimensions: 1536 }),
|
||||
content_vector_1024: vector({ dimensions: 1024 }),
|
||||
content_vector_768: vector({ dimensions: 768 }),
|
||||
content_vector_1024: vector({ dimensions: 1024 }),
|
||||
content_vector_1536: vector({ dimensions: 1536 }),
|
||||
created_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
deleted_at: bigint({ mode: 'number' }), // nullable timestamp for soft delete
|
||||
excitement: integer().notNull().default(5), // 1-10 scale
|
||||
id: uuid().primaryKey().defaultRandom(),
|
||||
source_id: text().default(null), // ID of source (dream ID, conversation ID, etc.)
|
||||
source_type: text().notNull().default('dream'), // 'dream', 'conversation', 'reflection'
|
||||
status: text().notNull().default('new'), // 'new', 'developing', 'implemented', 'abandoned'
|
||||
updated_at: bigint({ mode: 'number' }).notNull().default(0).$defaultFn(() => Date.now()),
|
||||
}, table => [
|
||||
index('memory_short_term_ideas_source_type_index').on(table.source_type),
|
||||
index('memory_short_term_ideas_status_index').on(table.status),
|
||||
|
||||
@@ -16,6 +16,12 @@ setGlobalLogLevel(LogLevel.Debug)
|
||||
|
||||
async function main() {
|
||||
const sdk = new NodeSDK({
|
||||
metricReader: new PeriodicExportingMetricReader({
|
||||
exporter: new OTLPMetricExporter({
|
||||
url: env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT || 'http://localhost:4318/v1/metrics',
|
||||
}),
|
||||
exportIntervalMillis: 5000,
|
||||
}),
|
||||
resource: resourceFromAttributes({
|
||||
[ATTR_SERVICE_NAME]: 'moeru_ai.airi.telegram_bot',
|
||||
[ATTR_SERVICE_VERSION]: '1.0.0',
|
||||
@@ -23,12 +29,6 @@ async function main() {
|
||||
traceExporter: new OTLPTraceExporter({
|
||||
url: env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || 'http://localhost:4318/v1/traces',
|
||||
}),
|
||||
metricReader: new PeriodicExportingMetricReader({
|
||||
exporter: new OTLPMetricExporter({
|
||||
url: env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT || 'http://localhost:4318/v1/metrics',
|
||||
}),
|
||||
exportIntervalMillis: 5000,
|
||||
}),
|
||||
})
|
||||
|
||||
sdk.start()
|
||||
|
||||
@@ -21,8 +21,8 @@ export async function imagineAnAction(
|
||||
messages: LLMMessage[],
|
||||
actions: { action: Action, result: unknown }[],
|
||||
globalStates: {
|
||||
unreadMessages: Record<string, Message[]>
|
||||
incomingMessages?: Message[]
|
||||
unreadMessages: Record<string, Message[]>
|
||||
},
|
||||
): Promise<Action | undefined> {
|
||||
const logger = useLogg('imagineAnAction').useGlobalConfig()
|
||||
@@ -58,9 +58,9 @@ export async function imagineAnAction(
|
||||
'Respond with the action and parameters you choose in JSON only, without any explanation and markups.',
|
||||
))
|
||||
const requestMessages = message.messages(
|
||||
{ role: 'system' as const, content: systemContent },
|
||||
{ content: systemContent, role: 'system' as const },
|
||||
...messages,
|
||||
{ role: 'user' as const, content: userContent },
|
||||
{ content: userContent, role: 'user' as const },
|
||||
)
|
||||
|
||||
try {
|
||||
@@ -70,11 +70,11 @@ export async function imagineAnAction(
|
||||
s.setAttribute('llm.provider.api_base_url', env.LLM_API_BASE_URL!)
|
||||
|
||||
const req = {
|
||||
abortSignal: currentAbortController?.signal,
|
||||
apiKey: env.LLM_API_KEY!,
|
||||
baseURL: env.LLM_API_BASE_URL!,
|
||||
model: env.LLM_MODEL!,
|
||||
messages: requestMessages,
|
||||
abortSignal: currentAbortController?.signal,
|
||||
model: env.LLM_MODEL!,
|
||||
} satisfies GenerateTextOptions
|
||||
if (env.LLM_OLLAMA_DISABLE_THINK) {
|
||||
(req as Record<string, unknown>).think = false
|
||||
@@ -96,12 +96,12 @@ export async function imagineAnAction(
|
||||
})
|
||||
|
||||
logger.withFields({
|
||||
response: res.text,
|
||||
unreadMessages: Object.fromEntries(Object.entries(globalStates.unreadMessages).map(([key, value]) => [key, value.length])),
|
||||
now: new Date().toLocaleString(),
|
||||
totalTokens: res.usage.totalTokens,
|
||||
promptTokens: res.usage.inputTokens,
|
||||
completion_tokens: res.usage.outputTokens,
|
||||
now: new Date().toLocaleString(),
|
||||
promptTokens: res.usage.inputTokens,
|
||||
response: res.text,
|
||||
totalTokens: res.usage.totalTokens,
|
||||
unreadMessages: Object.fromEntries(Object.entries(globalStates.unreadMessages).map(([key, value]) => [key, value.length])),
|
||||
}).log('Generated action')
|
||||
|
||||
const action = tracer.startActiveSpan('telegram.module.generate_agent_action.parse', (s) => {
|
||||
@@ -126,13 +126,13 @@ export async function imagineAnAction(
|
||||
|
||||
// Normalize action name aliases
|
||||
const actionAliases: Record<string, string> = {
|
||||
read_messages: 'read_unread_messages',
|
||||
get_unread_messages: 'read_unread_messages',
|
||||
check_messages: 'read_unread_messages',
|
||||
reply_to_a_message_from_a_chat: 'send_message',
|
||||
reply_message: 'send_message',
|
||||
get_messages_from_chat: 'read_unread_messages',
|
||||
get_unread_messages: 'read_unread_messages',
|
||||
read_messages: 'read_unread_messages',
|
||||
Read_unread_messages: 'read_unread_messages',
|
||||
reply_message: 'send_message',
|
||||
reply_to_a_message_from_a_chat: 'send_message',
|
||||
}
|
||||
if (typeof raw.action === 'string' && actionAliases[raw.action])
|
||||
raw.action = actionAliases[raw.action]
|
||||
|
||||
@@ -23,30 +23,6 @@ import { toPngBase64FromFile } from './image'
|
||||
// Set path to FFmpeg binaries
|
||||
ffmpeg.setFfmpegPath(ffmpegInstaller.path)
|
||||
|
||||
async function extractFrames(inputFilePath, outputDir, frameRate = 5) {
|
||||
await fs.mkdir(outputDir, { recursive: true })
|
||||
const outputPattern = path.join(outputDir, 'frame-%03d.png')
|
||||
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
ffmpeg(inputFilePath)
|
||||
.outputOptions(`-vf fps=${frameRate}`)
|
||||
.output(outputPattern)
|
||||
.on('end', async () => {
|
||||
const files = await fs.readdir(outputDir)
|
||||
const sortedFiles = files
|
||||
.filter(file => file.match(/frame-\d+\.png/))
|
||||
.sort((a, b) => {
|
||||
const numA = Number.parseInt(a.match(/frame-(\d+)\.png/)[1])
|
||||
const numB = Number.parseInt(b.match(/frame-(\d+)\.png/)[1])
|
||||
return numA - numB
|
||||
})
|
||||
resolve(sortedFiles.map(file => path.join(outputDir, file)))
|
||||
})
|
||||
.on('error', err => reject(err))
|
||||
.run()
|
||||
})
|
||||
}
|
||||
|
||||
export async function interpretAnimatedSticker(bot: Bot, msg: Message, sticker: Sticker) {
|
||||
const logger = useLogg('interpretAnimatedSticker')
|
||||
.useGlobalConfig()
|
||||
@@ -89,8 +65,8 @@ export async function interpretAnimatedSticker(bot: Bot, msg: Message, sticker:
|
||||
|
||||
// Process frames with Sharp
|
||||
const frames = await Promise.all(sampled.map(async (framePath, index) => ({
|
||||
index,
|
||||
base64: await toPngBase64FromFile(framePath),
|
||||
index,
|
||||
})))
|
||||
logger.withField('sampled_frames', sampled).log('Normalized the frames')
|
||||
|
||||
@@ -101,7 +77,6 @@ export async function interpretAnimatedSticker(bot: Bot, msg: Message, sticker:
|
||||
const req = {
|
||||
apiKey: env.LLM_VISION_API_KEY!,
|
||||
baseURL: env.LLM_VISION_API_BASE_URL!,
|
||||
model: env.LLM_VISION_MODEL!,
|
||||
messages: message.messages(
|
||||
message.system(div(
|
||||
span(`
|
||||
@@ -124,6 +99,7 @@ export async function interpretAnimatedSticker(bot: Bot, msg: Message, sticker:
|
||||
)),
|
||||
message.user([message.imagePart(`data:image/png;base64,${frame.base64}`)]),
|
||||
),
|
||||
model: env.LLM_VISION_MODEL!,
|
||||
} satisfies GenerateTextOptions
|
||||
if (env.LLM_OLLAMA_DISABLE_THINK) {
|
||||
(req as Record<string, unknown>).think = false
|
||||
@@ -136,8 +112,8 @@ export async function interpretAnimatedSticker(bot: Bot, msg: Message, sticker:
|
||||
}
|
||||
|
||||
frameDescriptions.push({
|
||||
frameNumber: frame.index + 1,
|
||||
description: res.text,
|
||||
frameNumber: frame.index + 1,
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
@@ -159,7 +135,6 @@ export async function interpretAnimatedSticker(bot: Bot, msg: Message, sticker:
|
||||
const req = {
|
||||
apiKey: env.LLM_API_KEY!, // Using text-only LLM API
|
||||
baseURL: env.LLM_API_BASE_URL!,
|
||||
model: env.LLM_MODEL!,
|
||||
messages: message.messages(
|
||||
message.system(
|
||||
div(
|
||||
@@ -191,6 +166,7 @@ export async function interpretAnimatedSticker(bot: Bot, msg: Message, sticker:
|
||||
),
|
||||
),
|
||||
),
|
||||
model: env.LLM_MODEL!,
|
||||
} satisfies GenerateTextOptions
|
||||
if (env.LLM_OLLAMA_DISABLE_THINK) {
|
||||
(req as Record<string, unknown>).think = false
|
||||
@@ -199,7 +175,7 @@ export async function interpretAnimatedSticker(bot: Bot, msg: Message, sticker:
|
||||
const consolidatedResult = await generateText(req)
|
||||
|
||||
// Clean up temp files
|
||||
await fs.rm(tempDir, { recursive: true, force: true })
|
||||
await fs.rm(tempDir, { force: true, recursive: true })
|
||||
|
||||
logger.withField('consolidated_result', consolidatedResult.text).log('Animated sticker interpreted')
|
||||
|
||||
@@ -213,3 +189,27 @@ export async function interpretAnimatedSticker(bot: Bot, msg: Message, sticker:
|
||||
logger.withError(err).log('Error interpreting animated sticker')
|
||||
}
|
||||
}
|
||||
|
||||
async function extractFrames(inputFilePath, outputDir, frameRate = 5) {
|
||||
await fs.mkdir(outputDir, { recursive: true })
|
||||
const outputPattern = path.join(outputDir, 'frame-%03d.png')
|
||||
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
ffmpeg(inputFilePath)
|
||||
.outputOptions(`-vf fps=${frameRate}`)
|
||||
.output(outputPattern)
|
||||
.on('end', async () => {
|
||||
const files = await fs.readdir(outputDir)
|
||||
const sortedFiles = files
|
||||
.filter(file => file.match(/frame-\d+\.png/))
|
||||
.sort((a, b) => {
|
||||
const numA = Number.parseInt(a.match(/frame-(\d+)\.png/)[1])
|
||||
const numB = Number.parseInt(b.match(/frame-(\d+)\.png/)[1])
|
||||
return numA - numB
|
||||
})
|
||||
resolve(sortedFiles.map(file => path.join(outputDir, file)))
|
||||
})
|
||||
.on('error', err => reject(err))
|
||||
.run()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,10 +5,6 @@ import { ResizeFilterType, Transformer } from '@napi-rs/image'
|
||||
|
||||
type ImageInput = ArrayBuffer | Buffer | Uint8Array
|
||||
|
||||
function toUint8Array(input: ImageInput): Uint8Array {
|
||||
return input instanceof Uint8Array ? input : new Uint8Array(input)
|
||||
}
|
||||
|
||||
export async function toPngBase64(input: ImageInput) {
|
||||
const transformer = new Transformer(toUint8Array(input))
|
||||
transformer.resize(512, 512, ResizeFilterType.Lanczos3)
|
||||
@@ -20,3 +16,7 @@ export async function toPngBase64FromFile(filePath: string) {
|
||||
const buffer = await readFile(filePath)
|
||||
return toPngBase64(buffer)
|
||||
}
|
||||
|
||||
function toUint8Array(input: ImageInput): Uint8Array {
|
||||
return input instanceof Uint8Array ? input : new Uint8Array(input)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ export async function interpretPhotos(state: BotContext, msg: Message, photos: P
|
||||
const req = {
|
||||
apiKey: env.LLM_VISION_API_KEY!,
|
||||
baseURL: env.LLM_VISION_API_BASE_URL!,
|
||||
model: env.LLM_VISION_MODEL!,
|
||||
messages: message.messages(
|
||||
message.system(''
|
||||
+ 'You are a helpful assistant on visual content description work for blindness disability '
|
||||
@@ -53,6 +52,7 @@ export async function interpretPhotos(state: BotContext, msg: Message, photos: P
|
||||
),
|
||||
message.user([message.imagePart(`data:image/png;base64,${base64}`)]),
|
||||
),
|
||||
model: env.LLM_VISION_MODEL!,
|
||||
} satisfies GenerateTextOptions
|
||||
if (env.LLM_OLLAMA_DISABLE_THINK) {
|
||||
(req as Record<string, unknown>).think = false
|
||||
@@ -66,10 +66,10 @@ export async function interpretPhotos(state: BotContext, msg: Message, photos: P
|
||||
|
||||
// TODO: implement this for photo searching
|
||||
const _embedRes = await embed({
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
apiKey: env.EMBEDDING_API_KEY!,
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
input: 'Hello, world!',
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
})
|
||||
|
||||
await recordPhoto(base64, msg.photo[index].file_id, files[index].file_path, res.text)
|
||||
|
||||
@@ -42,7 +42,6 @@ export async function interpretSticker(bot: Bot, msg: Message, sticker: Sticker)
|
||||
const req = {
|
||||
apiKey: env.LLM_VISION_API_KEY!,
|
||||
baseURL: env.LLM_VISION_API_BASE_URL!,
|
||||
model: env.LLM_VISION_MODEL!,
|
||||
messages: message.messages(
|
||||
message.system(div(
|
||||
span(`
|
||||
@@ -66,6 +65,7 @@ export async function interpretSticker(bot: Bot, msg: Message, sticker: Sticker)
|
||||
)),
|
||||
message.user([message.imagePart(`data:image/png;base64,${stickerBase64}`)]),
|
||||
),
|
||||
model: env.LLM_VISION_MODEL!,
|
||||
} satisfies GenerateTextOptions
|
||||
if (env.LLM_OLLAMA_DISABLE_THINK) {
|
||||
(req as Record<string, unknown>).think = false
|
||||
@@ -79,10 +79,10 @@ export async function interpretSticker(bot: Bot, msg: Message, sticker: Sticker)
|
||||
|
||||
// TODO: implement this for sticker searching
|
||||
const _embedRes = await embed({
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
apiKey: env.EMBEDDING_API_KEY!,
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
input: 'Hello, world!',
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
})
|
||||
|
||||
await recordSticker(stickerBase64, sticker.file_id, file.file_path, res.text, sticker.set_name, sticker.emoji, sticker.set_name)
|
||||
|
||||
@@ -14,65 +14,6 @@ import { chatMessageToOneLine } from './common'
|
||||
import { findPhotoDescription } from './photos'
|
||||
import { findStickerDescription } from './stickers'
|
||||
|
||||
export async function recordMessage(botInfo: UserFromGetMe, message: Message) {
|
||||
const replyToName = message.reply_to_message?.from.first_name || ''
|
||||
|
||||
let embedding: EmbedResult
|
||||
let text: string
|
||||
|
||||
if (message.sticker != null) {
|
||||
text = `A sticker sent by user ${await findStickerDescription(message.sticker.file_id)}, sticker set named ${message.sticker.set_name}`
|
||||
}
|
||||
else if (message.photo != null) {
|
||||
text = `A set of photo, descriptions are: ${(await Promise.all(message.photo.map(photo => findPhotoDescription(photo.file_id)))).join('\n')}`
|
||||
}
|
||||
else if (message.text) {
|
||||
text = message.text || message.caption || ''
|
||||
}
|
||||
|
||||
if (text === '') {
|
||||
return
|
||||
}
|
||||
else {
|
||||
embedding = await embed({
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
apiKey: env.EMBEDDING_API_KEY!,
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
input: text,
|
||||
})
|
||||
}
|
||||
|
||||
const values: Partial<Omit<typeof chatMessagesTable.$inferSelect, 'id' | 'created_at' | 'updated_at'>> = {
|
||||
platform: 'telegram',
|
||||
from_id: message.from.id.toString(),
|
||||
platform_message_id: message.message_id.toString(),
|
||||
from_name: message.from.first_name,
|
||||
in_chat_id: message.chat.id.toString(),
|
||||
content: text,
|
||||
is_reply: !!message.reply_to_message,
|
||||
reply_to_name: replyToName === botInfo.first_name ? 'Yourself' : replyToName,
|
||||
reply_to_id: message.reply_to_message?.message_id.toString() || '',
|
||||
}
|
||||
|
||||
switch (env.EMBEDDING_DIMENSION) {
|
||||
case '1536':
|
||||
values.content_vector_1536 = embedding.embedding
|
||||
break
|
||||
case '1024':
|
||||
values.content_vector_1024 = embedding.embedding
|
||||
break
|
||||
case '768':
|
||||
values.content_vector_768 = embedding.embedding
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported embedding dimension: ${env.EMBEDDING_DIMENSION}`)
|
||||
}
|
||||
|
||||
await useDrizzle()
|
||||
.insert(chatMessagesTable)
|
||||
.values(values)
|
||||
}
|
||||
|
||||
export async function findLastNMessages(chatId: string, n: number) {
|
||||
const res = await useDrizzle()
|
||||
.select()
|
||||
@@ -84,6 +25,21 @@ export async function findLastNMessages(chatId: string, n: number) {
|
||||
return res.reverse()
|
||||
}
|
||||
|
||||
export async function findMessagesByIDs(messageIds: string[]) {
|
||||
const db = useDrizzle()
|
||||
|
||||
return await db
|
||||
.select()
|
||||
.from(chatMessagesTable)
|
||||
.where(
|
||||
and(
|
||||
inArray(chatMessagesTable.platform_message_id, messageIds),
|
||||
eq(chatMessagesTable.platform, 'telegram'),
|
||||
ne(chatMessagesTable.platform_message_id, ''),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export async function findRelevantMessages(botId: string, chatId: string, unreadHistoryMessagesEmbedding: { embedding: number[] }[], excludeMessageIds: string[] = []) {
|
||||
const db = useDrizzle()
|
||||
const contextWindowSize = 5 // Number of messages to include before and after
|
||||
@@ -95,14 +51,14 @@ export async function findRelevantMessages(botId: string, chatId: string, unread
|
||||
let similarity: SQL<number>
|
||||
|
||||
switch (env.EMBEDDING_DIMENSION) {
|
||||
case '1536':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1536, embedding.embedding)}))`
|
||||
case '768':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_768, embedding.embedding)}))`
|
||||
break
|
||||
case '1024':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1024, embedding.embedding)}))`
|
||||
break
|
||||
case '768':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_768, embedding.embedding)}))`
|
||||
case '1536':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1536, embedding.embedding)}))`
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported embedding dimension: ${env.EMBEDDING_DIMENSION}`)
|
||||
@@ -114,21 +70,21 @@ export async function findRelevantMessages(botId: string, chatId: string, unread
|
||||
// Get top messages with similarity above threshold
|
||||
const relevantMessages = await db
|
||||
.select({
|
||||
id: chatMessagesTable.id,
|
||||
platform: chatMessagesTable.platform,
|
||||
platform_message_id: chatMessagesTable.platform_message_id,
|
||||
combined_score: sql`${combinedScore} AS "combined_score"`,
|
||||
content: chatMessagesTable.content,
|
||||
created_at: chatMessagesTable.created_at,
|
||||
from_id: chatMessagesTable.from_id,
|
||||
from_name: chatMessagesTable.from_name,
|
||||
id: chatMessagesTable.id,
|
||||
in_chat_id: chatMessagesTable.in_chat_id,
|
||||
content: chatMessagesTable.content,
|
||||
is_reply: chatMessagesTable.is_reply,
|
||||
reply_to_name: chatMessagesTable.reply_to_name,
|
||||
platform: chatMessagesTable.platform,
|
||||
platform_message_id: chatMessagesTable.platform_message_id,
|
||||
reply_to_id: chatMessagesTable.reply_to_id,
|
||||
created_at: chatMessagesTable.created_at,
|
||||
updated_at: chatMessagesTable.updated_at,
|
||||
reply_to_name: chatMessagesTable.reply_to_name,
|
||||
similarity: sql`${similarity} AS "similarity"`,
|
||||
time_relevance: sql`${timeRelevance} AS "time_relevance"`,
|
||||
combined_score: sql`${combinedScore} AS "combined_score"`,
|
||||
updated_at: chatMessagesTable.updated_at,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
.where(and(
|
||||
@@ -148,17 +104,17 @@ export async function findRelevantMessages(botId: string, chatId: string, unread
|
||||
// Get N messages before the target message
|
||||
const messagesBefore = await db
|
||||
.select({
|
||||
id: chatMessagesTable.id,
|
||||
platform: chatMessagesTable.platform,
|
||||
platform_message_id: chatMessagesTable.platform_message_id,
|
||||
content: chatMessagesTable.content,
|
||||
created_at: chatMessagesTable.created_at,
|
||||
from_id: chatMessagesTable.from_id,
|
||||
from_name: chatMessagesTable.from_name,
|
||||
id: chatMessagesTable.id,
|
||||
in_chat_id: chatMessagesTable.in_chat_id,
|
||||
content: chatMessagesTable.content,
|
||||
is_reply: chatMessagesTable.is_reply,
|
||||
reply_to_name: chatMessagesTable.reply_to_name,
|
||||
platform: chatMessagesTable.platform,
|
||||
platform_message_id: chatMessagesTable.platform_message_id,
|
||||
reply_to_id: chatMessagesTable.reply_to_id,
|
||||
created_at: chatMessagesTable.created_at,
|
||||
reply_to_name: chatMessagesTable.reply_to_name,
|
||||
updated_at: chatMessagesTable.updated_at,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
@@ -174,17 +130,17 @@ export async function findRelevantMessages(botId: string, chatId: string, unread
|
||||
// Get N messages after the target message
|
||||
const messagesAfter = await db
|
||||
.select({
|
||||
id: chatMessagesTable.id,
|
||||
platform: chatMessagesTable.platform,
|
||||
platform_message_id: chatMessagesTable.platform_message_id,
|
||||
content: chatMessagesTable.content,
|
||||
created_at: chatMessagesTable.created_at,
|
||||
from_id: chatMessagesTable.from_id,
|
||||
from_name: chatMessagesTable.from_name,
|
||||
id: chatMessagesTable.id,
|
||||
in_chat_id: chatMessagesTable.in_chat_id,
|
||||
content: chatMessagesTable.content,
|
||||
is_reply: chatMessagesTable.is_reply,
|
||||
reply_to_name: chatMessagesTable.reply_to_name,
|
||||
platform: chatMessagesTable.platform,
|
||||
platform_message_id: chatMessagesTable.platform_message_id,
|
||||
reply_to_id: chatMessagesTable.reply_to_id,
|
||||
created_at: chatMessagesTable.created_at,
|
||||
reply_to_name: chatMessagesTable.reply_to_name,
|
||||
updated_at: chatMessagesTable.updated_at,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
@@ -244,17 +200,61 @@ export async function findRelevantMessages(botId: string, chatId: string, unread
|
||||
}))
|
||||
}
|
||||
|
||||
export async function findMessagesByIDs(messageIds: string[]) {
|
||||
const db = useDrizzle()
|
||||
export async function recordMessage(botInfo: UserFromGetMe, message: Message) {
|
||||
const replyToName = message.reply_to_message?.from.first_name || ''
|
||||
|
||||
return await db
|
||||
.select()
|
||||
.from(chatMessagesTable)
|
||||
.where(
|
||||
and(
|
||||
inArray(chatMessagesTable.platform_message_id, messageIds),
|
||||
eq(chatMessagesTable.platform, 'telegram'),
|
||||
ne(chatMessagesTable.platform_message_id, ''),
|
||||
),
|
||||
)
|
||||
let embedding: EmbedResult
|
||||
let text: string
|
||||
|
||||
if (message.sticker != null) {
|
||||
text = `A sticker sent by user ${await findStickerDescription(message.sticker.file_id)}, sticker set named ${message.sticker.set_name}`
|
||||
}
|
||||
else if (message.photo != null) {
|
||||
text = `A set of photo, descriptions are: ${(await Promise.all(message.photo.map(photo => findPhotoDescription(photo.file_id)))).join('\n')}`
|
||||
}
|
||||
else if (message.text) {
|
||||
text = message.text || message.caption || ''
|
||||
}
|
||||
|
||||
if (text === '') {
|
||||
return
|
||||
}
|
||||
else {
|
||||
embedding = await embed({
|
||||
apiKey: env.EMBEDDING_API_KEY!,
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
input: text,
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
})
|
||||
}
|
||||
|
||||
const values: Partial<Omit<typeof chatMessagesTable.$inferSelect, 'created_at' | 'id' | 'updated_at'>> = {
|
||||
content: text,
|
||||
from_id: message.from.id.toString(),
|
||||
from_name: message.from.first_name,
|
||||
in_chat_id: message.chat.id.toString(),
|
||||
is_reply: !!message.reply_to_message,
|
||||
platform: 'telegram',
|
||||
platform_message_id: message.message_id.toString(),
|
||||
reply_to_id: message.reply_to_message?.message_id.toString() || '',
|
||||
reply_to_name: replyToName === botInfo.first_name ? 'Yourself' : replyToName,
|
||||
}
|
||||
|
||||
switch (env.EMBEDDING_DIMENSION) {
|
||||
case '768':
|
||||
values.content_vector_768 = embedding.embedding
|
||||
break
|
||||
case '1024':
|
||||
values.content_vector_1024 = embedding.embedding
|
||||
break
|
||||
case '1536':
|
||||
values.content_vector_1536 = embedding.embedding
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported embedding dimension: ${env.EMBEDDING_DIMENSION}`)
|
||||
}
|
||||
|
||||
await useDrizzle()
|
||||
.insert(chatMessagesTable)
|
||||
.values(values)
|
||||
}
|
||||
|
||||
@@ -15,15 +15,15 @@ export async function recordJoinedChat(chatId: string, chatName: string) {
|
||||
return useDrizzle()
|
||||
.insert(joinedChatsTable)
|
||||
.values({
|
||||
platform: 'telegram',
|
||||
chat_id: chatId,
|
||||
chat_name: chatName,
|
||||
platform: 'telegram',
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: joinedChatsTable.chat_id,
|
||||
set: {
|
||||
chat_name: chatName,
|
||||
updated_at: Date.now(),
|
||||
},
|
||||
target: joinedChatsTable.chat_id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { chatMessagesTable } from '../db/schema'
|
||||
import { findPhotoDescription } from './photos'
|
||||
import { findStickerDescription } from './stickers'
|
||||
|
||||
export function chatMessageToOneLine(botId: string, message: Omit<typeof chatMessagesTable.$inferSelect, 'content_vector_1536' | 'content_vector_768' | 'content_vector_1024'>, repliedToMessage?: Omit<typeof chatMessagesTable.$inferSelect, 'content_vector_1536' | 'content_vector_768' | 'content_vector_1024'>) {
|
||||
export function chatMessageToOneLine(botId: string, message: Omit<typeof chatMessagesTable.$inferSelect, 'content_vector_768' | 'content_vector_1024' | 'content_vector_1536'>, repliedToMessage?: Omit<typeof chatMessagesTable.$inferSelect, 'content_vector_768' | 'content_vector_1024' | 'content_vector_1536'>) {
|
||||
let userDisplayName = `User [${message.from_name}]`
|
||||
|
||||
if (botId === message.from_id) {
|
||||
|
||||
@@ -17,21 +17,21 @@ export async function findPhotoDescription(fileId: string) {
|
||||
return photo[0].description
|
||||
}
|
||||
|
||||
export async function recordPhoto(photoBase64: string, fileId: string, filePath: string, description: string) {
|
||||
await useDrizzle()
|
||||
.insert(photosTable)
|
||||
.values({
|
||||
platform: 'telegram',
|
||||
file_id: fileId,
|
||||
image_base64: photoBase64,
|
||||
image_path: filePath,
|
||||
description,
|
||||
})
|
||||
}
|
||||
|
||||
export async function findPhotosDescriptions(fileIds: string[]) {
|
||||
return await useDrizzle()
|
||||
.select()
|
||||
.from(photosTable)
|
||||
.where(inArray(photosTable.file_id, fileIds))
|
||||
}
|
||||
|
||||
export async function recordPhoto(photoBase64: string, fileId: string, filePath: string, description: string) {
|
||||
await useDrizzle()
|
||||
.insert(photosTable)
|
||||
.values({
|
||||
description,
|
||||
file_id: fileId,
|
||||
image_base64: photoBase64,
|
||||
image_path: filePath,
|
||||
platform: 'telegram',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,20 +3,20 @@ import { desc } from 'drizzle-orm'
|
||||
import { useDrizzle } from '../db'
|
||||
import { stickerPacksTable } from '../db/schema'
|
||||
|
||||
export async function recordStickerPack(platformId: string, name: string, platform = 'telegram') {
|
||||
await useDrizzle()
|
||||
.insert(stickerPacksTable)
|
||||
.values({
|
||||
platform,
|
||||
platform_id: platformId,
|
||||
name,
|
||||
description: '',
|
||||
})
|
||||
}
|
||||
|
||||
export async function listStickerPacks() {
|
||||
return await useDrizzle()
|
||||
.select()
|
||||
.from(stickerPacksTable)
|
||||
.orderBy(desc(stickerPacksTable.created_at))
|
||||
}
|
||||
|
||||
export async function recordStickerPack(platformId: string, name: string, platform = 'telegram') {
|
||||
await useDrizzle()
|
||||
.insert(stickerPacksTable)
|
||||
.values({
|
||||
description: '',
|
||||
name,
|
||||
platform,
|
||||
platform_id: platformId,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,15 +3,6 @@ import { desc, eq, inArray } from 'drizzle-orm'
|
||||
import { useDrizzle } from '../db'
|
||||
import { recentSentStickersTable, stickersTable } from '../db/schema'
|
||||
|
||||
export async function findStickerDescription(fileId: string) {
|
||||
const sticker = await findStickerByFileId(fileId)
|
||||
if (sticker == null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return sticker.description
|
||||
}
|
||||
|
||||
export async function findStickerByFileId(fileId: string) {
|
||||
const sticker = await useDrizzle()
|
||||
.select()
|
||||
@@ -26,6 +17,15 @@ export async function findStickerByFileId(fileId: string) {
|
||||
return sticker[0]
|
||||
}
|
||||
|
||||
export async function findStickerDescription(fileId: string) {
|
||||
const sticker = await findStickerByFileId(fileId)
|
||||
if (sticker == null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return sticker.description
|
||||
}
|
||||
|
||||
export async function findStickersByFileIds(fileIds: string[]) {
|
||||
const stickers = await useDrizzle()
|
||||
.select()
|
||||
@@ -35,24 +35,24 @@ export async function findStickersByFileIds(fileIds: string[]) {
|
||||
return stickers
|
||||
}
|
||||
|
||||
export async function recordSticker(stickerBase64: string, fileId: string, filePath: string, description: string, name: string, emoji: string, label: string) {
|
||||
await useDrizzle()
|
||||
.insert(stickersTable)
|
||||
.values({
|
||||
platform: 'telegram',
|
||||
file_id: fileId,
|
||||
image_base64: stickerBase64,
|
||||
image_path: filePath,
|
||||
description,
|
||||
name,
|
||||
emoji,
|
||||
label,
|
||||
})
|
||||
}
|
||||
|
||||
export async function listRecentSentStickers() {
|
||||
return await useDrizzle()
|
||||
.select()
|
||||
.from(recentSentStickersTable)
|
||||
.orderBy(desc(recentSentStickersTable.created_at))
|
||||
}
|
||||
|
||||
export async function recordSticker(stickerBase64: string, fileId: string, filePath: string, description: string, name: string, emoji: string, label: string) {
|
||||
await useDrizzle()
|
||||
.insert(stickersTable)
|
||||
.values({
|
||||
description,
|
||||
emoji,
|
||||
file_id: fileId,
|
||||
image_base64: stickerBase64,
|
||||
image_path: filePath,
|
||||
label,
|
||||
name,
|
||||
platform: 'telegram',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,14 @@ import { env } from 'node:process'
|
||||
|
||||
import { velin } from '../utils/velin'
|
||||
|
||||
export async function actionReadMessages(props: { lastMessages?: string, relevantChatMessages?: string, unreadHistoryMessages?: string }) {
|
||||
return await (velin<{ lastMessages?: string, relevantChatMessages?: string, unreadHistoryMessages?: string }>('action-read-messages.velin.md', import.meta.url))(props)
|
||||
}
|
||||
|
||||
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 personality() {
|
||||
return await (velin('personality-v1.velin.md', import.meta.url))()
|
||||
}
|
||||
@@ -9,11 +17,3 @@ export async function personality() {
|
||||
export async function systemTicking() {
|
||||
return await (velin<{ responseLanguage: string }>('system-ticking-v1.velin.md', import.meta.url))({ responseLanguage: env.LLM_RESPONSE_LANGUAGE })
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -1,30 +1,6 @@
|
||||
import type { TextContentPart } from '@xsai/shared-chat'
|
||||
|
||||
export function vif(condition: boolean, a: string, b = '') {
|
||||
return condition ? a : b
|
||||
}
|
||||
|
||||
export function vChoice(...args: [boolean | (() => boolean), string][]) {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const exp = args[i][0]
|
||||
|
||||
if (typeof exp === 'function' ? exp() : exp) {
|
||||
return args[i][1]
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export function span(...args: string[]) {
|
||||
return args
|
||||
.map(arg => arg.trim())
|
||||
.map(arg => arg.replaceAll(/\n\s+/g, ''))
|
||||
.map(arg => arg.replaceAll(/\r\s+/g, ' '))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export function div(...args: (string | TextContentPart | TextContentPart[] | null | undefined)[]) {
|
||||
export function div(...args: (null | string | TextContentPart | TextContentPart[] | undefined)[]) {
|
||||
const results: string[] = []
|
||||
|
||||
for (const arg of args) {
|
||||
@@ -45,9 +21,33 @@ export function div(...args: (string | TextContentPart | TextContentPart[] | nul
|
||||
return results.join('\n\n')
|
||||
}
|
||||
|
||||
export function span(...args: string[]) {
|
||||
return args
|
||||
.map(arg => arg.trim())
|
||||
.map(arg => arg.replaceAll(/\n\s+/g, ''))
|
||||
.map(arg => arg.replaceAll(/\r\s+/g, ' '))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
// ul + li
|
||||
export function ul(...args: string[]) {
|
||||
return args.map((arg) => {
|
||||
return `- ${arg}`
|
||||
}).join('\n')
|
||||
}
|
||||
|
||||
export function vChoice(...args: [(() => boolean) | boolean, string][]) {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const exp = args[i][0]
|
||||
|
||||
if (typeof exp === 'function' ? exp() : exp) {
|
||||
return args[i][1]
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export function vif(condition: boolean, a: string, b = '') {
|
||||
return condition ? a : b
|
||||
}
|
||||
|
||||
@@ -6,115 +6,33 @@ import type { Message } from 'grammy/types'
|
||||
|
||||
import type { CancellablePromise } from './utils/promise'
|
||||
|
||||
export interface PendingMessage {
|
||||
message: Message
|
||||
interpretationPromise?: Promise<void>
|
||||
status: 'pending' | 'interpreting' | 'ready'
|
||||
}
|
||||
|
||||
export type ExtendedContext = FileFlavor<Context>
|
||||
|
||||
export interface BotContext {
|
||||
bot: Bot
|
||||
messageQueue: Array<{
|
||||
message: Message
|
||||
status: 'pending' | 'interpreting' | 'ready'
|
||||
}>
|
||||
unreadMessages: Record<number, Message[]>
|
||||
processedIds: Set<string>
|
||||
logger: Logg
|
||||
processing: boolean
|
||||
lastInteractedNChatIds: string[]
|
||||
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 {
|
||||
action: 'continue'
|
||||
}
|
||||
|
||||
export interface BreakAction {
|
||||
action: 'break'
|
||||
}
|
||||
|
||||
export interface SleepAction {
|
||||
action: 'sleep'
|
||||
}
|
||||
|
||||
export interface ListChatsAction {
|
||||
action: 'list_chats'
|
||||
}
|
||||
|
||||
export interface SendMessageAction {
|
||||
action: 'send_message'
|
||||
content: string
|
||||
chatId: string
|
||||
}
|
||||
|
||||
export interface SendStickerAction {
|
||||
action: 'send_sticker'
|
||||
fileId: string
|
||||
chatId: string
|
||||
}
|
||||
|
||||
export interface SearchGoogleAction {
|
||||
action: 'search_google'
|
||||
query: string
|
||||
}
|
||||
|
||||
export interface ReadHistoryMessagesAction {
|
||||
action: 'read_history_messages'
|
||||
beforeMessageId?: string
|
||||
afterMessageId?: string
|
||||
chatId: string
|
||||
}
|
||||
|
||||
export interface ReadUnreadMessagesAction {
|
||||
action: 'read_unread_messages'
|
||||
chatId: string
|
||||
}
|
||||
|
||||
export interface ListStickersAction {
|
||||
action: 'list_stickers'
|
||||
}
|
||||
|
||||
export type Action
|
||||
= | ContinueAction
|
||||
| BreakAction
|
||||
| SleepAction
|
||||
= | BreakAction
|
||||
| ContinueAction
|
||||
| ListChatsAction
|
||||
| SendMessageAction
|
||||
| SendStickerAction
|
||||
| SearchGoogleAction
|
||||
| ListStickersAction
|
||||
| ReadHistoryMessagesAction
|
||||
| ReadUnreadMessagesAction
|
||||
| ListStickersAction
|
||||
| SearchGoogleAction
|
||||
| SendMessageAction
|
||||
| SendStickerAction
|
||||
| SleepAction
|
||||
|
||||
export interface AttentionConfig {
|
||||
initialResponseRate: number
|
||||
responseRateMin: number
|
||||
responseRateMax: number
|
||||
cooldownMs: number
|
||||
triggerWords: string[]
|
||||
ignoreWords: string[]
|
||||
decayRatePerMinute: number
|
||||
decayCheckIntervalMs: number
|
||||
decayRatePerMinute: number
|
||||
ignoreWords: string[]
|
||||
initialResponseRate: number
|
||||
responseRateMax: number
|
||||
responseRateMin: number
|
||||
triggerWords: string[]
|
||||
}
|
||||
|
||||
export interface AttentionStats {
|
||||
mentionCount: number
|
||||
triggerWordCount: number
|
||||
lastInteractionTime: number
|
||||
export interface AttentionResponse {
|
||||
reason: string
|
||||
responseRate?: number
|
||||
shouldAct: boolean
|
||||
}
|
||||
|
||||
export interface AttentionState {
|
||||
@@ -123,8 +41,90 @@ export interface AttentionState {
|
||||
stats: AttentionStats
|
||||
}
|
||||
|
||||
export interface AttentionResponse {
|
||||
shouldAct: boolean
|
||||
reason: string
|
||||
responseRate?: number
|
||||
export interface AttentionStats {
|
||||
lastInteractionTime: number
|
||||
mentionCount: number
|
||||
triggerWordCount: number
|
||||
}
|
||||
|
||||
export interface BotContext {
|
||||
bot: Bot
|
||||
chats: Map<string, ChatContext>
|
||||
currentProcessingStartTime?: number
|
||||
lastInteractedNChatIds: string[]
|
||||
logger: Logg
|
||||
messageQueue: Array<{
|
||||
message: Message
|
||||
status: 'interpreting' | 'pending' | 'ready'
|
||||
}>
|
||||
processedIds: Set<string>
|
||||
processing: boolean
|
||||
unreadMessages: Record<number, Message[]>
|
||||
}
|
||||
|
||||
export interface BreakAction {
|
||||
action: 'break'
|
||||
}
|
||||
|
||||
export interface ChatContext {
|
||||
actions: { action: Action, result: unknown }[]
|
||||
|
||||
chatId: string
|
||||
currentAbortController?: AbortController
|
||||
|
||||
currentTask?: CancellablePromise<Message.TextMessage>
|
||||
messages: LLMMessage[]
|
||||
}
|
||||
|
||||
export interface ContinueAction {
|
||||
action: 'continue'
|
||||
}
|
||||
|
||||
export type ExtendedContext = FileFlavor<Context>
|
||||
|
||||
export interface ListChatsAction {
|
||||
action: 'list_chats'
|
||||
}
|
||||
|
||||
export interface ListStickersAction {
|
||||
action: 'list_stickers'
|
||||
}
|
||||
|
||||
export interface PendingMessage {
|
||||
interpretationPromise?: Promise<void>
|
||||
message: Message
|
||||
status: 'interpreting' | 'pending' | 'ready'
|
||||
}
|
||||
|
||||
export interface ReadHistoryMessagesAction {
|
||||
action: 'read_history_messages'
|
||||
afterMessageId?: string
|
||||
beforeMessageId?: string
|
||||
chatId: string
|
||||
}
|
||||
|
||||
export interface ReadUnreadMessagesAction {
|
||||
action: 'read_unread_messages'
|
||||
chatId: string
|
||||
}
|
||||
|
||||
export interface SearchGoogleAction {
|
||||
action: 'search_google'
|
||||
query: string
|
||||
}
|
||||
|
||||
export interface SendMessageAction {
|
||||
action: 'send_message'
|
||||
chatId: string
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface SendStickerAction {
|
||||
action: 'send_sticker'
|
||||
chatId: string
|
||||
fileId: string
|
||||
}
|
||||
|
||||
export interface SleepAction {
|
||||
action: 'sleep'
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface CancellablePromise<T> {
|
||||
promise: Promise<T>
|
||||
cancel: () => void
|
||||
promise: Promise<T>
|
||||
}
|
||||
|
||||
export function cancellable<T>(promise: Promise<T>): CancellablePromise<T> {
|
||||
@@ -12,7 +12,7 @@ export function cancellable<T>(promise: Promise<T>): CancellablePromise<T> {
|
||||
})
|
||||
|
||||
return {
|
||||
promise: wrappedPromise,
|
||||
cancel: () => cancel?.(),
|
||||
promise: wrappedPromise,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,6 @@ export interface VelinModule {
|
||||
render: <P>(data: P) => Promise<string>
|
||||
}
|
||||
|
||||
function isMarkdown(module: string) {
|
||||
return module.endsWith('.md') || module.endsWith('.velin.md')
|
||||
}
|
||||
|
||||
export function importVelin(module: string, base: string): VelinModule {
|
||||
return {
|
||||
render: async (data) => {
|
||||
@@ -30,3 +26,7 @@ export function importVelin(module: string, base: string): VelinModule {
|
||||
export function velin<P = undefined>(module: string, base: string): (data?: P) => Promise<string> {
|
||||
return importVelin(module, base).render
|
||||
}
|
||||
|
||||
function isMarkdown(module: string) {
|
||||
return module.endsWith('.md') || module.endsWith('.velin.md')
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ export default defineConfig(({ mode }) => {
|
||||
{
|
||||
extends: true,
|
||||
test: {
|
||||
name: 'node',
|
||||
environment: 'node',
|
||||
include: ['**/*.{spec,test}.ts'],
|
||||
exclude: ['**/*.browser.{spec,test}.ts', '**/node_modules/**'],
|
||||
include: ['**/*.{spec,test}.ts'],
|
||||
name: 'node',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user