fix(telegram-bot): errors and bad impl, sql issue
This commit is contained in:
@@ -4,6 +4,7 @@ TELEGRAM_BOT_TOKEN=''
|
||||
LLM_API_BASE_URL=''
|
||||
LLM_API_KEY=''
|
||||
LLM_MODEL=''
|
||||
LLM_RESPONSE_LANGUAGE=''
|
||||
|
||||
LLM_VISION_API_BASE_URL=''
|
||||
LLM_VISION_API_KEY=''
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
import type { SQL } from 'drizzle-orm'
|
||||
|
||||
import { env } from 'node:process'
|
||||
import { embed } from '@xsai/embed'
|
||||
import { cosineDistance, desc, sql } from 'drizzle-orm'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
|
||||
import { initDb, useDrizzle } from '../../db'
|
||||
import { chatMessagesTable } from '../../db/schema'
|
||||
import { chatMessageToOneLine } from '../../models'
|
||||
|
||||
beforeAll(async () => {
|
||||
await initDb()
|
||||
})
|
||||
|
||||
describe.todo('telegram bot', { timeout: 30000 }, async () => {
|
||||
it('should be able to run', async () => {
|
||||
const db = useDrizzle()
|
||||
const contextWindowSize = 5 // Number of messages to include before and after
|
||||
|
||||
const embedding = await embed({
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
apiKey: env.EMBEDDING_API_KEY!,
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
input: '测试一下行不行',
|
||||
})
|
||||
.then(res => res)
|
||||
.catch((err) => {
|
||||
console.error(err, err.cause)
|
||||
return { embedding: [] }
|
||||
})
|
||||
if (embedding.embedding.length === 0) {
|
||||
throw new Error('Failed to embed the input')
|
||||
}
|
||||
|
||||
const relevantChatMessages = await Promise.all([embedding].map(async (embedding) => {
|
||||
let similarity: SQL<number>
|
||||
|
||||
switch (env.EMBEDDING_DIMENSION) {
|
||||
case '1536':
|
||||
similarity = sql<number>`(1 - (${cosineDistance(chatMessagesTable.content_vector_1536, 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)}))`
|
||||
break
|
||||
default:
|
||||
throw new Error(`Unsupported embedding dimension: ${env.EMBEDDING_DIMENSION}`)
|
||||
}
|
||||
|
||||
const timeRelevance = sql<number>`(1 - (CEIL(EXTRACT(EPOCH FROM NOW()) * 1000)::bigint - ${chatMessagesTable.created_at}) / 86400 / 30)`
|
||||
const combinedScore = sql<number>`((1.2 * ${similarity}) + (0.2 * ${timeRelevance}))`
|
||||
|
||||
// Get top messages with similarity above threshold
|
||||
const relevantMessages = await db
|
||||
.select({
|
||||
id: chatMessagesTable.id,
|
||||
platform: chatMessagesTable.platform,
|
||||
from_id: chatMessagesTable.from_id,
|
||||
from_name: chatMessagesTable.from_name,
|
||||
in_chat_id: chatMessagesTable.in_chat_id,
|
||||
content: chatMessagesTable.content,
|
||||
is_reply: chatMessagesTable.is_reply,
|
||||
reply_to_name: chatMessagesTable.reply_to_name,
|
||||
created_at: chatMessagesTable.created_at,
|
||||
updated_at: chatMessagesTable.updated_at,
|
||||
similarity: sql`${similarity} AS "similarity"`,
|
||||
time_relevance: sql`${timeRelevance} AS "time_relevance"`,
|
||||
combined_score: sql`${combinedScore} AS "combined_score"`,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
.where(sql`${similarity} > '0.5'`)
|
||||
.orderBy(desc(sql`combined_score`))
|
||||
.limit(3)
|
||||
|
||||
// Now fetch the context for each message
|
||||
return await Promise.all(
|
||||
relevantMessages.map(async (message) => {
|
||||
// Get N messages before the target message
|
||||
const messagesBefore = await db
|
||||
.select({
|
||||
id: chatMessagesTable.id,
|
||||
platform: chatMessagesTable.platform,
|
||||
from_id: chatMessagesTable.from_id,
|
||||
from_name: chatMessagesTable.from_name,
|
||||
in_chat_id: chatMessagesTable.in_chat_id,
|
||||
content: chatMessagesTable.content,
|
||||
is_reply: chatMessagesTable.is_reply,
|
||||
reply_to_name: chatMessagesTable.reply_to_name,
|
||||
created_at: chatMessagesTable.created_at,
|
||||
updated_at: chatMessagesTable.updated_at,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
.where(sql`${chatMessagesTable.in_chat_id} = ${message.in_chat_id} AND
|
||||
${chatMessagesTable.created_at} < ${message.created_at}`)
|
||||
.orderBy(desc(chatMessagesTable.created_at))
|
||||
.limit(contextWindowSize)
|
||||
|
||||
// Get N messages after the target message
|
||||
const messagesAfter = await db
|
||||
.select({
|
||||
id: chatMessagesTable.id,
|
||||
platform: chatMessagesTable.platform,
|
||||
from_id: chatMessagesTable.from_id,
|
||||
from_name: chatMessagesTable.from_name,
|
||||
in_chat_id: chatMessagesTable.in_chat_id,
|
||||
content: chatMessagesTable.content,
|
||||
is_reply: chatMessagesTable.is_reply,
|
||||
reply_to_name: chatMessagesTable.reply_to_name,
|
||||
created_at: chatMessagesTable.created_at,
|
||||
updated_at: chatMessagesTable.updated_at,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
.where(sql`${chatMessagesTable.in_chat_id} = ${message.in_chat_id} AND
|
||||
${chatMessagesTable.created_at} > ${message.created_at}`)
|
||||
.orderBy(chatMessagesTable.created_at)
|
||||
.limit(contextWindowSize)
|
||||
|
||||
// Combine all messages in chronological order
|
||||
const contextMessages = [
|
||||
...messagesBefore.reverse(), // Reverse to get chronological order
|
||||
message,
|
||||
...messagesAfter,
|
||||
]
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(contextMessages)
|
||||
|
||||
const contextMessagesOneliner = (await Promise.all(contextMessages.map(m => chatMessageToOneLine(m))))
|
||||
return `One of the relevant message along with the context:\n${contextMessagesOneliner}`
|
||||
}),
|
||||
)
|
||||
}))
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(relevantChatMessages)
|
||||
|
||||
expect(relevantChatMessages.length).toBe(1)
|
||||
})
|
||||
})
|
||||
@@ -14,6 +14,7 @@ import { interpretSticker } from '../../llm/sticker'
|
||||
import { recordMessage } from '../../models'
|
||||
import { listJoinedChats, recordJoinedChat } from '../../models/chats'
|
||||
import { readMessage } from './loop/read-message'
|
||||
import { shouldInterruptProcessing } from './utils/interruption'
|
||||
import { sendMayStructuredMessage } from './utils/message'
|
||||
|
||||
async function isChatIdBotAdmin(chatId: number) {
|
||||
@@ -21,15 +22,26 @@ async function isChatIdBotAdmin(chatId: number) {
|
||||
return admins.includes(chatId.toString())
|
||||
}
|
||||
|
||||
async function handleLoop(state: BotSelf, msgs?: LLMMessage[], forGroupId?: string) {
|
||||
async function handleLoop(state: BotSelf, msgs?: LLMMessage[], chatId?: string) {
|
||||
state.logger.log('handleLoop')
|
||||
|
||||
// Set the start time when beginning new processing
|
||||
state.currentProcessingStartTime = Date.now()
|
||||
|
||||
// Create a new abort controller for this loop execution
|
||||
if (state.currentAbortController) {
|
||||
state.currentAbortController.abort()
|
||||
}
|
||||
state.currentAbortController = new AbortController()
|
||||
const currentController = state.currentAbortController // Store reference to current controller
|
||||
const currentController = state.currentAbortController
|
||||
|
||||
// Track message processing state
|
||||
if (chatId && !state.lastInteractedNChatIds.includes(chatId)) {
|
||||
state.lastInteractedNChatIds.push(chatId)
|
||||
}
|
||||
if (state.lastInteractedNChatIds.length > 5) {
|
||||
state.lastInteractedNChatIds = state.lastInteractedNChatIds.slice(-5)
|
||||
}
|
||||
|
||||
if (msgs == null) {
|
||||
msgs = []
|
||||
@@ -37,36 +49,63 @@ async function handleLoop(state: BotSelf, msgs?: LLMMessage[], forGroupId?: stri
|
||||
|
||||
try {
|
||||
try {
|
||||
const action = await imagineAnAction(state.unreadMessages, currentController, msgs)
|
||||
const action = await imagineAnAction(state.bot.botInfo.id.toString(), state.unreadMessages, currentController, msgs, state.lastInteractedNChatIds)
|
||||
|
||||
switch (action.action) {
|
||||
case 'readMessages':
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
let unreadMessagesForThisChat: Message[] | undefined = state.unreadMessages[action.groupId]
|
||||
|
||||
if (forGroupId && forGroupId === action.groupId.toString()
|
||||
&& unreadMessagesForThisChat
|
||||
&& unreadMessagesForThisChat.length > 0) {
|
||||
state.logger.log(`Interrupting message processing for group ${action.groupId} - new messages arrived`)
|
||||
return handleLoop(state)
|
||||
}
|
||||
if (Object.keys(state.unreadMessages).length === 0) {
|
||||
state.logger.log('No unread messages - deleting all unread messages')
|
||||
state.unreadMessages = {}
|
||||
break
|
||||
}
|
||||
if (action.groupId == null) {
|
||||
if (action.chatId == null) {
|
||||
state.logger.log('No group ID - deleting all unread messages')
|
||||
state.unreadMessages = {}
|
||||
break
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
let unreadMessagesForThisChat: Message[] | undefined = state.unreadMessages[action.chatId]
|
||||
|
||||
// Modified interruption logic
|
||||
if (chatId && chatId === action.chatId
|
||||
&& unreadMessagesForThisChat
|
||||
&& unreadMessagesForThisChat.length > 0) {
|
||||
const processingTime = state.currentProcessingStartTime
|
||||
? Date.now() - state.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: msgs,
|
||||
newMessages: unreadMessagesForThisChat,
|
||||
chatId: action.chatId,
|
||||
})
|
||||
|
||||
if (shouldInterrupt) {
|
||||
state.logger.log(`Interrupting message processing for chat ${action.chatId} - new messages deemed more important`)
|
||||
return handleLoop(state)
|
||||
}
|
||||
else {
|
||||
state.logger.log(`Continuing current processing despite new messages in chat ${action.chatId}`)
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(unreadMessagesForThisChat)) {
|
||||
state.logger.log(`Unread messages for group ${action.groupId} is not an array - converting to array`)
|
||||
state.logger.log(`Unread messages for group ${action.chatId} is not an array - converting to array`)
|
||||
unreadMessagesForThisChat = []
|
||||
}
|
||||
if (unreadMessagesForThisChat.length === 0) {
|
||||
state.logger.log(`No unread messages for group ${action.groupId} - deleting`)
|
||||
delete state.unreadMessages[action.groupId]
|
||||
state.logger.log(`No unread messages for group ${action.chatId} - deleting`)
|
||||
delete state.unreadMessages[action.chatId]
|
||||
break
|
||||
}
|
||||
|
||||
@@ -80,7 +119,7 @@ async function handleLoop(state: BotSelf, msgs?: LLMMessage[], forGroupId?: stri
|
||||
// return { break: true }
|
||||
// }
|
||||
|
||||
await readMessage(state, action, unreadMessagesForThisChat, currentController)
|
||||
await readMessage(state, state.bot.botInfo.id.toString(), chatId, action, unreadMessagesForThisChat, currentController)
|
||||
break
|
||||
case 'listChats':
|
||||
msgs.push(message.user(`List of chats:${(await listJoinedChats()).map(chat => `ID:${chat.chat_id}, Name:${chat.chat_name}`).join('\n')}`))
|
||||
@@ -108,9 +147,10 @@ async function handleLoop(state: BotSelf, msgs?: LLMMessage[], forGroupId?: stri
|
||||
state.logger.withError(err).log('Error occurred')
|
||||
}
|
||||
finally {
|
||||
// Only clean up if this is still the current controller
|
||||
// Clean up timing when done
|
||||
if (state.currentAbortController === currentController) {
|
||||
state.currentAbortController = null
|
||||
state.currentProcessingStartTime = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,6 +180,8 @@ function newBotSelf(bot: Bot, logger: Logg): BotSelf {
|
||||
logger,
|
||||
processing: false,
|
||||
attentionHandler: undefined,
|
||||
lastInteractedNChatIds: [],
|
||||
currentProcessingStartTime: null,
|
||||
}
|
||||
|
||||
// botSelf.attentionHandler = createAttentionHandler(botSelf, {
|
||||
@@ -199,14 +241,16 @@ async function processMessageQueue(state: BotSelf) {
|
||||
|
||||
unreadMessagesForThisChat.push(nextMsg.message)
|
||||
|
||||
if (unreadMessagesForThisChat.length > 20) {
|
||||
unreadMessagesForThisChat = unreadMessagesForThisChat.slice(-20)
|
||||
if (unreadMessagesForThisChat.length > 100) {
|
||||
unreadMessagesForThisChat = unreadMessagesForThisChat.slice(-100)
|
||||
}
|
||||
|
||||
state.unreadMessages[nextMsg.message.chat.id] = unreadMessagesForThisChat
|
||||
|
||||
state.logger.withField('chatId', nextMsg.message.chat.id).log('message queue processed, triggering immediate reaction')
|
||||
|
||||
// Trigger immediate processing when messages are ready
|
||||
handleLoop(state, [], nextMsg.message.chat.id.toString())
|
||||
await handleLoop(state, [], nextMsg.message.chat.id.toString())
|
||||
state.messageQueue.shift()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import { sendMayStructuredMessage } from '../utils/message'
|
||||
|
||||
export async function readMessage(
|
||||
state: BotSelf,
|
||||
botId: string,
|
||||
chatId: string,
|
||||
action: ReadMessagesAction,
|
||||
unreadMessages: Message[],
|
||||
abortController: AbortController,
|
||||
@@ -23,8 +25,8 @@ export async function readMessage(
|
||||
}> {
|
||||
const logger = useLogg('readMessage').useGlobalConfig()
|
||||
|
||||
const lastNMessages = await findLastNMessages(action.groupId, 30)
|
||||
const lastNMessagesOneliner = lastNMessages.map(msg => chatMessageToOneLine(state.bot, msg)).join('\n')
|
||||
const lastNMessages = await findLastNMessages(action.chatId, 30)
|
||||
const lastNMessagesOneliner = lastNMessages.map(msg => chatMessageToOneLine(botId, msg)).join('\n')
|
||||
|
||||
logger.withField('number_of_last_n_messages', lastNMessages.length).log('Successfully found last N messages')
|
||||
|
||||
@@ -39,21 +41,18 @@ export async function readMessage(
|
||||
abortSignal: abortController.signal,
|
||||
})
|
||||
|
||||
return {
|
||||
embedding: embeddingResult.embedding,
|
||||
message: msg,
|
||||
}
|
||||
return embeddingResult
|
||||
})
|
||||
|
||||
const unreadHistoryMessagesEmbedding = await Promise.all(unreadMessagesEmbeddingPromises)
|
||||
|
||||
logger.withField('number_of_tasks', unreadMessagesEmbeddingPromises.length).log('Successfully embedded unread history messages')
|
||||
|
||||
const unreadHistoryMessages = await Promise.all(state.unreadMessages[action.groupId].map(msg => telegramMessageToOneLine(state.bot, msg)))
|
||||
const unreadHistoryMessages = await Promise.all(state.unreadMessages[action.chatId].map(msg => telegramMessageToOneLine(botId, msg)))
|
||||
const unreadHistoryMessageOneliner = unreadHistoryMessages.join('\n')
|
||||
state.unreadMessages[action.groupId] = []
|
||||
state.unreadMessages[action.chatId] = []
|
||||
|
||||
const relevantChatMessages = await findRelevantMessages(state.bot, unreadHistoryMessagesEmbedding)
|
||||
const relevantChatMessages = await findRelevantMessages(botId, chatId, unreadHistoryMessagesEmbedding)
|
||||
const relevantChatMessagesOneliner = (await Promise.all(relevantChatMessages.map(async msgs => msgs.join('\n')))).join('\n')
|
||||
|
||||
logger.withField('number_of_relevant_chat_messages', relevantChatMessages.length).log('Successfully composed relevant chat messages')
|
||||
@@ -73,10 +72,11 @@ export async function readMessage(
|
||||
+ 'All the messages you requested to read:\n'
|
||||
+ `${unreadHistoryMessageOneliner || 'No messages'}`
|
||||
+ '\n'
|
||||
+ 'Based on your personalities, imaging you have your own choice and interest over the world, '
|
||||
+ 'Based on your personalities, imaging you have your own choice and interest over different, '
|
||||
+ 'giving the above context and chat history, would you like to participate in the conversation '
|
||||
+ 'about the topic? Or will you aggressively diss or piss off about the opinions of others?\n'
|
||||
+ 'Feel free to ignore by just sending an empty array (i.e. []).'
|
||||
+ 'If you would like to participate, send me an array of messages you would like to send without telling you willing to participate.'
|
||||
+ '\n'
|
||||
+ 'Choose your action.',
|
||||
),
|
||||
@@ -102,6 +102,6 @@ export async function readMessage(
|
||||
|
||||
logger.withField('response', JSON.stringify(response.text)).log('Successfully generated response')
|
||||
|
||||
await sendMayStructuredMessage(state, response.text, action.groupId.toString())
|
||||
await sendMayStructuredMessage(state, response.text, action.chatId.toString())
|
||||
return { break: true }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
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
|
||||
}
|
||||
|
||||
export async function shouldInterruptProcessing(params: InterruptionParams): Promise<boolean> {
|
||||
// Base cases
|
||||
if (params.processingTime < 1000) {
|
||||
// Always allow very short processes to complete
|
||||
return false
|
||||
}
|
||||
|
||||
if (params.processingTime > 30000) {
|
||||
// Interrupt very long processes
|
||||
return true
|
||||
}
|
||||
|
||||
// Consider message volume
|
||||
const messageRatio = params.messageCount / 5 // normalize against baseline
|
||||
|
||||
// Consider processing time
|
||||
const timeRatio = Math.min(params.processingTime / 10000, 1) // normalize against 10s baseline
|
||||
|
||||
// Calculate interruption probability
|
||||
// More messages = higher chance to interrupt
|
||||
// Longer processing = lower chance to interrupt
|
||||
const interruptProbability = messageRatio * (1 - timeRatio)
|
||||
|
||||
// Add some randomness to prevent predictable behavior
|
||||
return Math.random() < interruptProbability
|
||||
}
|
||||
@@ -4,19 +4,63 @@ import type { Action } from '../types'
|
||||
|
||||
import { env } from 'node:process'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import { embed } from '@xsai/embed'
|
||||
import { generateText } from '@xsai/generate-text'
|
||||
import { message } from '@xsai/utils-chat'
|
||||
import { parse } from 'best-effort-json-parser'
|
||||
|
||||
import { chatMessageToOneLine, findLastNMessages, findRelevantMessages } from '../models'
|
||||
import { systemPrompt } from '../prompts/system-v1'
|
||||
import { div, ul } from '../prompts/utils'
|
||||
|
||||
export async function imagineAnAction(unreadMessages: Record<string, Message[]>, currentAbortController: AbortController, agentMessages: LLMMessage[]) {
|
||||
export async function imagineAnAction(
|
||||
botId: string,
|
||||
unreadMessages: Record<string, Message[]>,
|
||||
currentAbortController: AbortController,
|
||||
agentMessages: LLMMessage[],
|
||||
lastInteractedNChatIds: string[],
|
||||
) {
|
||||
const logger = useLogg('imagineAnAction').useGlobalConfig()
|
||||
|
||||
if (agentMessages == null) {
|
||||
agentMessages = []
|
||||
}
|
||||
|
||||
lastInteractedNChatIds.map(async (chatId) => {
|
||||
const lastNMessages = await findLastNMessages(chatId, 15)
|
||||
const lastNMessagesOneliner = lastNMessages.map(msg => chatMessageToOneLine(botId, msg))
|
||||
|
||||
const lastNMessagesEmbeddingPromises = lastNMessages
|
||||
.map(async (msg) => {
|
||||
const embeddingResult = await embed({
|
||||
baseURL: env.EMBEDDING_API_BASE_URL!,
|
||||
apiKey: env.EMBEDDING_API_KEY!,
|
||||
model: env.EMBEDDING_MODEL!,
|
||||
input: chatMessageToOneLine(botId, msg),
|
||||
abortSignal: currentAbortController.signal,
|
||||
})
|
||||
|
||||
return {
|
||||
embedding: embeddingResult.embedding,
|
||||
message: msg,
|
||||
}
|
||||
})
|
||||
|
||||
const lastNMessagesEmbedding = await Promise.all(lastNMessagesEmbeddingPromises)
|
||||
const relevantChatMessages = await findRelevantMessages(botId, chatId, lastNMessagesEmbedding)
|
||||
const relevantChatMessagesOneliner = await Promise.all(relevantChatMessages.map(async msgs => msgs.join('\n')))
|
||||
|
||||
return message.user(
|
||||
div(
|
||||
'Here are a brief list of the recent interacted chat ids\' message review',
|
||||
`Last 15 messages in chat ${chatId}:`,
|
||||
ul(...lastNMessagesOneliner),
|
||||
`Relevant messages in chat ${chatId}:`,
|
||||
ul(...relevantChatMessagesOneliner),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
agentMessages.push(
|
||||
message.system(''
|
||||
+ `${systemPrompt()}`
|
||||
@@ -37,12 +81,12 @@ export async function imagineAnAction(unreadMessages: Record<string, Message[]>,
|
||||
example: { action: 'listChats' },
|
||||
},
|
||||
{
|
||||
description: 'Send a message to a specific chat group. If you want to express anything to anyone or your friends in group, you can use this action.',
|
||||
example: { action: 'sendMessage', content: '<content>', groupId: 'id of chat to send to' },
|
||||
description: `Send a message to a specific chat group. If you want to express anything to anyone or your friends in group, you can use this action.${!!env.LLM_RESPONSE_LANGUAGE}` ? `The language of the sending message should be in ${env.LLM_RESPONSE_LANGUAGE}.` : '',
|
||||
example: { action: 'sendMessage', content: '<content>', chatId: 'id of chat to send to' },
|
||||
},
|
||||
{
|
||||
description: 'Read unread messages from a specific chat group. If you want to read the unread messages from a specific chat group, you can use this action.',
|
||||
example: { action: 'readMessages', groupId: 'id of chat to send to' },
|
||||
example: { action: 'readMessages', chatId: 'id of chat to send to' },
|
||||
},
|
||||
{
|
||||
description: 'Continue the current task, which means to keep your current state unchanged, I\'ll ask you again in next tick.',
|
||||
@@ -83,7 +127,9 @@ export async function imagineAnAction(unreadMessages: Record<string, Message[]>,
|
||||
+ `${Object.entries(unreadMessages).map(([key, value]) => `ID:${key}, Unread message count:${value.length}`).join('\n')}`
|
||||
+ '',
|
||||
),
|
||||
message.user('What do you want to do? Respond with the action and parameters you choose in JSON only, without any explanation and markups'),
|
||||
message.user(''
|
||||
+ 'What do you want to do? Respond with the action and parameters you choose in JSON only, without any explanation and markups',
|
||||
),
|
||||
)
|
||||
|
||||
const res = await generateText({
|
||||
|
||||
@@ -62,7 +62,7 @@ export async function interpretPhotos(state: BotSelf, msg: Message, photos: Phot
|
||||
input: 'Hello, world!',
|
||||
})
|
||||
|
||||
await recordPhoto(base64, msg.sticker.file_id, files[index].file_path, res.text)
|
||||
await recordPhoto(base64, msg.photo[index].file_id, files[index].file_path, res.text)
|
||||
state.logger.withField('photo', res.text).log('Interpreted photo')
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { EmbedResult } from '@xsai/embed'
|
||||
import type { SQL } from 'drizzle-orm'
|
||||
import type { Bot } from 'grammy'
|
||||
import type { Message, UserFromGetMe } from 'grammy/types'
|
||||
|
||||
import { env } from 'node:process'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import { embed } from '@xsai/embed'
|
||||
import { cosineDistance, desc, eq, sql } from 'drizzle-orm'
|
||||
import { and, cosineDistance, desc, eq, gt, lt, sql } from 'drizzle-orm'
|
||||
|
||||
import { useDrizzle } from '../db'
|
||||
import { chatMessagesTable } from '../db/schema'
|
||||
@@ -82,10 +81,10 @@ export async function findLastNMessages(chatId: string, n: number) {
|
||||
return res.reverse()
|
||||
}
|
||||
|
||||
export async function findRelevantMessages(bot: Bot, unreadHistoryMessagesEmbedding: { embedding: number[], message: Message }[]) {
|
||||
export async function findRelevantMessages(botId: string, chatId: string, unreadHistoryMessagesEmbedding: { embedding: number[] }[]) {
|
||||
const db = useDrizzle()
|
||||
const contextWindowSize = 5 // Number of messages to include before and after
|
||||
const logger = useLogg('findRelevantMessages').useGlobalConfig()
|
||||
const logger = useLogg('findRelevantMessages').useGlobalConfig().withField('chatId', chatId)
|
||||
|
||||
logger.withField('context_window_size', contextWindowSize).log('Querying relevant chat messages...')
|
||||
|
||||
@@ -127,14 +126,18 @@ export async function findRelevantMessages(bot: Bot, unreadHistoryMessagesEmbedd
|
||||
combined_score: sql`${combinedScore} AS "combined_score"`,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
.where(sql`${similarity} > '0.5' AND ${chatMessagesTable.in_chat_id} = ${embedding.message.chat.id} AND ${chatMessagesTable.platform} = 'telegram'`)
|
||||
.where(and(
|
||||
eq(chatMessagesTable.platform, 'telegram'),
|
||||
eq(chatMessagesTable.in_chat_id, chatId),
|
||||
gt(similarity, 0.5),
|
||||
))
|
||||
.orderBy(desc(sql`combined_score`))
|
||||
.limit(3)
|
||||
|
||||
logger.withField('number_of_relevant_messages', relevantMessages.length).log('Successfully found relevant chat messages')
|
||||
|
||||
// Now fetch the context for each message
|
||||
return await Promise.all(
|
||||
const relevantMessageOneliner = await Promise.all(
|
||||
relevantMessages.map(async (message) => {
|
||||
// Get N messages before the target message
|
||||
const messagesBefore = await db
|
||||
@@ -151,7 +154,11 @@ export async function findRelevantMessages(bot: Bot, unreadHistoryMessagesEmbedd
|
||||
updated_at: chatMessagesTable.updated_at,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
.where(sql`${chatMessagesTable.in_chat_id} = ${message.in_chat_id} AND ${chatMessagesTable.created_at} < ${message.created_at} AND ${chatMessagesTable.platform} = 'telegram'`)
|
||||
.where(and(
|
||||
eq(chatMessagesTable.platform, 'telegram'),
|
||||
eq(chatMessagesTable.in_chat_id, message.in_chat_id),
|
||||
lt(chatMessagesTable.created_at, message.created_at),
|
||||
))
|
||||
.orderBy(desc(chatMessagesTable.created_at))
|
||||
.limit(contextWindowSize)
|
||||
|
||||
@@ -170,7 +177,11 @@ export async function findRelevantMessages(bot: Bot, unreadHistoryMessagesEmbedd
|
||||
updated_at: chatMessagesTable.updated_at,
|
||||
})
|
||||
.from(chatMessagesTable)
|
||||
.where(sql`${chatMessagesTable.in_chat_id} = ${message.in_chat_id} AND ${chatMessagesTable.created_at} > ${message.created_at} AND ${chatMessagesTable.platform} = 'telegram'`)
|
||||
.where(and(
|
||||
eq(chatMessagesTable.platform, 'telegram'),
|
||||
eq(chatMessagesTable.in_chat_id, message.in_chat_id),
|
||||
gt(chatMessagesTable.created_at, message.created_at),
|
||||
))
|
||||
.orderBy(chatMessagesTable.created_at)
|
||||
.limit(contextWindowSize)
|
||||
|
||||
@@ -181,11 +192,13 @@ export async function findRelevantMessages(bot: Bot, unreadHistoryMessagesEmbedd
|
||||
...messagesAfter,
|
||||
]
|
||||
|
||||
logger.withField('number_of_context_messages', contextMessages.length).log('Combined context messages')
|
||||
|
||||
const contextMessagesOneliner = (await Promise.all(contextMessages.map(m => chatMessageToOneLine(bot, m))))
|
||||
const contextMessagesOneliner = (await Promise.all(contextMessages.map(m => chatMessageToOneLine(botId, m))))
|
||||
return `One of the relevant message along with the context:\n${contextMessagesOneliner}`
|
||||
}),
|
||||
)
|
||||
|
||||
logger.withField('number_of_relevant_messages', relevantMessages.length).log('processed relevant chat messages with contextual messages')
|
||||
|
||||
return relevantMessageOneliner
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { Bot } from 'grammy'
|
||||
import type { Message } from 'grammy/types'
|
||||
import type { chatMessagesTable } from '../db/schema'
|
||||
|
||||
import { findPhotoDescription } from './photos'
|
||||
import { findStickerDescription } from './stickers'
|
||||
|
||||
export function chatMessageToOneLine(bot: Bot, message: 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_1536' | 'content_vector_768' | 'content_vector_1024'>) {
|
||||
let userDisplayName = `User [${message.from_name}]`
|
||||
|
||||
if (bot.botInfo.id.toString() === message.from_id) {
|
||||
if (botId === message.from_id) {
|
||||
userDisplayName = 'Yourself'
|
||||
}
|
||||
|
||||
@@ -19,13 +18,13 @@ export function chatMessageToOneLine(bot: Bot, message: Omit<typeof chatMessages
|
||||
return `${new Date(message.created_at).toLocaleString()} ${userDisplayName} sent in same group said: ${message.content}`
|
||||
}
|
||||
|
||||
export async function telegramMessageToOneLine(bot: Bot, message: Message) {
|
||||
export async function telegramMessageToOneLine(botId: string, message: Message) {
|
||||
if (message == null) {
|
||||
return ''
|
||||
}
|
||||
|
||||
let userDisplayName = `User [${message.from.first_name} ${message.from.last_name} (${message.from.username})]`
|
||||
if (bot.botInfo.id.toString() === message.from.id.toString()) {
|
||||
if (botId === message.from.id.toString()) {
|
||||
userDisplayName = 'Yourself'
|
||||
}
|
||||
|
||||
@@ -38,7 +37,7 @@ export async function telegramMessageToOneLine(bot: Bot, message: Message) {
|
||||
return `${new Date(message.date * 1000).toLocaleString()} ${userDisplayName} sent in Group [${message.chat.title}] a photo, and description of the photo is ${description}`
|
||||
}
|
||||
if (message.reply_to_message != null) {
|
||||
if (bot.botInfo.username === message.reply_to_message.from.username) {
|
||||
if (botId === message.reply_to_message.from.id.toString()) {
|
||||
return `${new Date(message.date * 1000).toLocaleString()} ${userDisplayName} replied to your previous message ${message.reply_to_message.text || message.caption} in Group [${message.chat.title}] said: ${message.text}`
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
export function vif(condition: boolean, a: string, b = '') {
|
||||
return condition ? a : b
|
||||
}
|
||||
|
||||
export function vChoice(...args: [boolean | (() => boolean), string][]) {
|
||||
let exp
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
// eslint-disable-next-line no-cond-assign
|
||||
if (typeof (exp = args[i][0]) === '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[]) {
|
||||
return args.join('\n\n')
|
||||
}
|
||||
|
||||
// ul + li
|
||||
export function ul(...args: string[]) {
|
||||
return args.map((arg) => {
|
||||
return `- ${arg}`
|
||||
}).join('\n')
|
||||
}
|
||||
@@ -26,6 +26,8 @@ export interface BotSelf {
|
||||
logger: Logg
|
||||
processing: boolean
|
||||
attentionHandler: ReturnType<typeof createAttentionHandler>
|
||||
lastInteractedNChatIds: string[]
|
||||
currentProcessingStartTime: number | null
|
||||
}
|
||||
|
||||
export interface ContinueAction {
|
||||
@@ -95,7 +97,7 @@ export interface SearchGoogleAction {
|
||||
|
||||
export interface ReadMessagesAction {
|
||||
action: 'readMessages'
|
||||
groupId: string
|
||||
chatId: string
|
||||
}
|
||||
|
||||
export type Action = ContinueAction | BreakAction | SleepAction | LookupShortTermMemoryAction | LookupLongTermMemoryAction | MemorizeShortMemoryAction | MemorizeLongMemoryAction | ForgetShortTermMemoryAction | ForgetLongTermMemoryAction | ListChatsAction | SendMessageAction | SearchGoogleAction | ReadMessagesAction
|
||||
|
||||
Reference in New Issue
Block a user