fix(telegram-bot): potential empty action returned

This commit is contained in:
Neko Ayaka
2025-03-30 02:19:37 +08:00
parent fa7c41710f
commit 0c864a477f
11 changed files with 3065 additions and 136 deletions
@@ -0,0 +1,97 @@
CREATE TABLE "chat_completions_history" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"prompt" text NOT NULL,
"response" text NOT NULL,
"created_at" bigint DEFAULT 0 NOT NULL
);
--> statement-breakpoint
CREATE TABLE "memory_episodic" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"memory_id" uuid NOT NULL,
"event_type" text NOT NULL,
"participants" jsonb DEFAULT '[]'::jsonb NOT NULL,
"location" text DEFAULT '',
"created_at" bigint DEFAULT 0 NOT NULL,
"deleted_at" bigint
);
--> statement-breakpoint
CREATE TABLE "memory_fragments" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"content" text NOT NULL,
"memory_type" text NOT NULL,
"category" text NOT NULL,
"importance" integer DEFAULT 5 NOT NULL,
"emotional_impact" integer DEFAULT 0 NOT NULL,
"created_at" bigint DEFAULT 0 NOT NULL,
"last_accessed" bigint DEFAULT 0 NOT NULL,
"access_count" integer DEFAULT 1 NOT NULL,
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
"content_vector_1536" vector(1536),
"content_vector_1024" vector(1024),
"content_vector_768" vector(768),
"deleted_at" bigint
);
--> statement-breakpoint
CREATE TABLE "memory_long_term_goals" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"title" text NOT NULL,
"description" text NOT NULL,
"priority" integer DEFAULT 5 NOT NULL,
"progress" integer DEFAULT 0 NOT NULL,
"deadline" bigint DEFAULT null,
"status" text DEFAULT 'planned' NOT NULL,
"parent_goal_id" uuid,
"category" text DEFAULT 'personal' NOT NULL,
"created_at" bigint DEFAULT 0 NOT NULL,
"updated_at" bigint DEFAULT 0 NOT NULL,
"deleted_at" bigint
);
--> statement-breakpoint
CREATE TABLE "memory_short_term_ideas" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"content" text NOT NULL,
"source_type" text DEFAULT 'dream' NOT NULL,
"source_id" text DEFAULT null,
"status" text DEFAULT 'new' NOT NULL,
"excitement" integer DEFAULT 5 NOT NULL,
"created_at" bigint DEFAULT 0 NOT NULL,
"updated_at" bigint DEFAULT 0 NOT NULL,
"content_vector_1536" vector(1536),
"content_vector_1024" vector(1024),
"content_vector_768" vector(768),
"deleted_at" bigint
);
--> statement-breakpoint
CREATE TABLE "memory_tags" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"memory_id" uuid NOT NULL,
"tag" text NOT NULL,
"created_at" bigint DEFAULT 0 NOT NULL,
"deleted_at" bigint
);
--> statement-breakpoint
ALTER TABLE "memory_episodic" ADD CONSTRAINT "memory_episodic_memory_id_memory_fragments_id_fk" FOREIGN KEY ("memory_id") REFERENCES "public"."memory_fragments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "memory_long_term_goals" ADD CONSTRAINT "memory_long_term_goals_parent_goal_id_memory_long_term_goals_id_fk" FOREIGN KEY ("parent_goal_id") REFERENCES "public"."memory_long_term_goals"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "memory_tags" ADD CONSTRAINT "memory_tags_memory_id_memory_fragments_id_fk" FOREIGN KEY ("memory_id") REFERENCES "public"."memory_fragments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "memory_episodic_memory_id_index" ON "memory_episodic" USING btree ("memory_id");--> statement-breakpoint
CREATE INDEX "memory_episodic_event_type_index" ON "memory_episodic" USING btree ("event_type");--> statement-breakpoint
CREATE INDEX "memory_items_content_vector_1536_index" ON "memory_fragments" USING hnsw ("content_vector_1536" vector_cosine_ops);--> statement-breakpoint
CREATE INDEX "memory_items_content_vector_1024_index" ON "memory_fragments" USING hnsw ("content_vector_1024" vector_cosine_ops);--> statement-breakpoint
CREATE INDEX "memory_items_content_vector_768_index" ON "memory_fragments" USING hnsw ("content_vector_768" vector_cosine_ops);--> statement-breakpoint
CREATE INDEX "memory_items_memory_type_index" ON "memory_fragments" USING btree ("memory_type");--> statement-breakpoint
CREATE INDEX "memory_items_category_index" ON "memory_fragments" USING btree ("category");--> statement-breakpoint
CREATE INDEX "memory_items_importance_index" ON "memory_fragments" USING btree ("importance");--> statement-breakpoint
CREATE INDEX "memory_items_created_at_index" ON "memory_fragments" USING btree ("created_at");--> statement-breakpoint
CREATE INDEX "memory_items_last_accessed_index" ON "memory_fragments" USING btree ("last_accessed");--> statement-breakpoint
CREATE INDEX "memory_long_term_goals_priority_index" ON "memory_long_term_goals" USING btree ("priority");--> statement-breakpoint
CREATE INDEX "memory_long_term_goals_status_index" ON "memory_long_term_goals" USING btree ("status");--> statement-breakpoint
CREATE INDEX "memory_long_term_goals_deadline_index" ON "memory_long_term_goals" USING btree ("deadline");--> statement-breakpoint
CREATE INDEX "memory_long_term_goals_parent_goal_id_index" ON "memory_long_term_goals" USING btree ("parent_goal_id");--> statement-breakpoint
CREATE INDEX "memory_short_term_ideas_source_type_index" ON "memory_short_term_ideas" USING btree ("source_type");--> statement-breakpoint
CREATE INDEX "memory_short_term_ideas_status_index" ON "memory_short_term_ideas" USING btree ("status");--> statement-breakpoint
CREATE INDEX "memory_short_term_ideas_excitement_index" ON "memory_short_term_ideas" USING btree ("excitement");--> statement-breakpoint
CREATE INDEX "memory_short_term_ideas_content_vector_1536_index" ON "memory_short_term_ideas" USING hnsw ("content_vector_1536" vector_cosine_ops);--> statement-breakpoint
CREATE INDEX "memory_short_term_ideas_content_vector_1024_index" ON "memory_short_term_ideas" USING hnsw ("content_vector_1024" vector_cosine_ops);--> statement-breakpoint
CREATE INDEX "memory_short_term_ideas_content_vector_768_index" ON "memory_short_term_ideas" USING hnsw ("content_vector_768" vector_cosine_ops);--> statement-breakpoint
CREATE INDEX "memory_tags_memory_id_index" ON "memory_tags" USING btree ("memory_id");--> statement-breakpoint
CREATE INDEX "memory_tags_tag_index" ON "memory_tags" USING btree ("tag");
@@ -0,0 +1 @@
ALTER TABLE "chat_completions_history" ADD COLUMN "task" text NOT NULL;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -22,6 +22,20 @@
"when": 1742980513463,
"tag": "0002_calm_magdalene",
"breakpoints": true
},
{
"idx": 3,
"version": "7",
"when": 1743268892136,
"tag": "0003_black_warbird",
"breakpoints": true
},
{
"idx": 4,
"version": "7",
"when": 1743269104758,
"tag": "0004_complete_gravity",
"breakpoints": true
}
]
}
@@ -23,8 +23,6 @@ async function isChatIdBotAdmin(chatId: number) {
}
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()
@@ -48,94 +46,95 @@ async function handleLoop(state: BotSelf, msgs?: LLMMessage[], chatId?: string)
}
try {
try {
const action = await imagineAnAction(state.bot.botInfo.id.toString(), state.unreadMessages, currentController, msgs, state.lastInteractedNChatIds)
const action = await imagineAnAction(state.bot.botInfo.id.toString(), state.unreadMessages, currentController, msgs, state.lastInteractedNChatIds)
switch (action.action) {
case 'readMessages':
if (Object.keys(state.unreadMessages).length === 0) {
state.logger.log('No unread messages - deleting all unread messages')
state.unreadMessages = {}
break
}
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.chatId} is not an array - converting to array`)
unreadMessagesForThisChat = []
}
if (unreadMessagesForThisChat.length === 0) {
state.logger.log(`No unread messages for group ${action.chatId} - deleting`)
delete state.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 }
// }
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')}`))
await handleLoop(state, msgs)
break
case 'sendMessage':
await sendMayStructuredMessage(state, action.content, action.groupId)
break
default:
msgs.push(message.user(`The action you sent ${action.action} haven't implemented yet by developer.`))
await handleLoop(state, msgs)
break
}
// If action generation failed, don't proceed with further processing
if (!action || !action.action) {
state.logger.log('No valid action returned. Skipping further processing.')
return
}
catch (err) {
state.logger.withError(err).withField('cause', String(err.cause)).log('Error occurred')
switch (action.action) {
case 'readMessages':
if (Object.keys(state.unreadMessages).length === 0) {
state.logger.log('No unread messages - deleting all unread messages')
state.unreadMessages = {}
break
}
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.chatId} is not an array - converting to array`)
unreadMessagesForThisChat = []
}
if (unreadMessagesForThisChat.length === 0) {
state.logger.log(`No unread messages for group ${action.chatId} - deleting`)
delete state.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 }
// }
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')}`))
await handleLoop(state, msgs, chatId)
break
case 'sendMessage':
await sendMayStructuredMessage(state, action.content, action.groupId)
break
default:
msgs.push(message.user(`The action you sent ${action.action} haven't implemented yet by developer.`))
await handleLoop(state, msgs, chatId)
break
}
}
catch (err) {
@@ -256,9 +255,7 @@ async function processMessageQueue(state: BotSelf) {
}
state.unreadMessages[nextMsg.message.chat.id] = unreadMessagesForThisChat
state.logger.withField('chatId', nextMsg.message.chat.id).log('message queue processed, triggering immediate reaction')
// Trigger immediate processing when messages are ready
handleLoop(state, [], nextMsg.message.chat.id.toString())
state.messageQueue.shift()
@@ -339,7 +336,7 @@ export async function startTelegramBot() {
await bot.init()
log.withField('bot_username', bot.botInfo.username).log('bot initialized')
bot.start()
bot.start({ drop_pending_updates: true })
try {
loop(state)
@@ -8,6 +8,7 @@ import { generateText } from '@xsai/generate-text'
import { message } from '@xsai/utils-chat'
import { findLastNMessages, findRelevantMessages } from '../../../models'
import { recordChatCompletions } from '../../../models/chat-completions-history'
import { chatMessageToOneLine, telegramMessageToOneLine } from '../../../models/common'
import { systemPrompt } from '../../../prompts/system-v1'
import { sendMayStructuredMessage } from '../utils/message'
@@ -25,7 +26,7 @@ export async function readMessage(
}> {
const logger = useLogg('readMessage').useGlobalConfig()
const lastNMessages = await findLastNMessages(action.chatId, 30)
const lastNMessages = await findLastNMessages(action.chatId, 10)
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')
@@ -52,9 +53,14 @@ export async function readMessage(
const unreadHistoryMessageOneliner = unreadHistoryMessages.join('\n')
state.unreadMessages[action.chatId] = []
const relevantChatMessages = await findRelevantMessages(botId, chatId, unreadHistoryMessagesEmbedding)
const relevantChatMessagesOneliner = (await Promise.all(relevantChatMessages.map(async msgs => msgs.join('\n')))).join('\n')
const relevantChatMessages = await findRelevantMessages(
botId,
chatId,
unreadHistoryMessagesEmbedding,
[...unreadMessages.map(msg => msg.message_id.toString()), ...lastNMessages.map(msg => msg.platform_message_id)],
)
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')
const messages = message.messages(
@@ -87,26 +93,33 @@ export async function readMessage(
),
)
// eslint-disable-next-line no-console
console.log(messages)
let responseText = ''
const response = await generateText({
apiKey: env.LLM_API_KEY!,
baseURL: env.LLM_API_BASE_URL!,
model: env.LLM_MODEL!,
messages,
abortSignal: abortController.signal,
})
try {
const response = await generateText({
apiKey: env.LLM_API_KEY!,
baseURL: env.LLM_API_BASE_URL!,
model: env.LLM_MODEL!,
messages,
abortSignal: abortController.signal,
})
response.text = response.text
.replace(/^```json\s*\n/, '')
.replace(/\n```$/, '')
.replace(/^```\s*\n/, '')
.replace(/\n```$/, '')
.trim()
responseText = response.text
.replace(/^```json\s*\n/, '')
.replace(/\n```$/, '')
.replace(/^```\s*\n/, '')
.replace(/\n```$/, '')
.trim()
}
catch (err) {
logger.withField('error', err).log('Failed to generate response')
}
finally {
recordChatCompletions('readMessage', messages, responseText).then(() => {}).catch(err => logger.withField('error', err).log('Failed to record chat completions'))
}
logger.withField('response', JSON.stringify(response.text)).log('Successfully generated response')
logger.withField('response', responseText).log('Successfully generated response')
await sendMayStructuredMessage(state, responseText, action.chatId.toString())
await sendMayStructuredMessage(state, response.text, action.chatId.toString())
return { break: true }
}
+108 -1
View File
@@ -1,4 +1,4 @@
import { bigint, boolean, index, pgTable, text, uniqueIndex, uuid, vector } from 'drizzle-orm/pg-core'
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(),
@@ -74,3 +74,110 @@ export const joinedChatsTable = pgTable('joined_chats', () => {
},
]
})
export const chatCompletionsHistoryTable = pgTable('chat_completions_history', {
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 }),
content_vector_768: vector({ dimensions: 768 }),
deleted_at: bigint({ mode: 'number' }), // nullable timestamp for soft delete
}, table => [
// Vector indexes for efficient similarity search
index('memory_items_content_vector_1536_index').using('hnsw', table.content_vector_1536.op('vector_cosine_ops')),
index('memory_items_content_vector_1024_index').using('hnsw', table.content_vector_1024.op('vector_cosine_ops')),
index('memory_items_content_vector_768_index').using('hnsw', table.content_vector_768.op('vector_cosine_ops')),
// Standard indexes for common queries
index('memory_items_memory_type_index').on(table.memory_type),
index('memory_items_category_index').on(table.category),
index('memory_items_importance_index').on(table.importance),
index('memory_items_created_at_index').on(table.created_at),
index('memory_items_last_accessed_index').on(table.last_accessed),
])
// Memory Tags junction table
export const memoryTagsTable = pgTable('memory_tags', {
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),
])
// 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
}, table => [
index('memory_episodic_memory_id_index').on(table.memory_id),
index('memory_episodic_event_type_index').on(table.event_type),
])
// 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()),
deleted_at: bigint({ mode: 'number' }), // nullable timestamp for soft delete
}, table => [
index('memory_long_term_goals_priority_index').on(table.priority),
index('memory_long_term_goals_status_index').on(table.status),
index('memory_long_term_goals_deadline_index').on(table.deadline),
index('memory_long_term_goals_parent_goal_id_index').on(table.parent_goal_id),
])
// 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 }),
deleted_at: bigint({ mode: 'number' }), // nullable timestamp for soft delete
}, table => [
index('memory_short_term_ideas_source_type_index').on(table.source_type),
index('memory_short_term_ideas_status_index').on(table.status),
index('memory_short_term_ideas_excitement_index').on(table.excitement),
index('memory_short_term_ideas_content_vector_1536_index').using('hnsw', table.content_vector_1536.op('vector_cosine_ops')),
index('memory_short_term_ideas_content_vector_1024_index').using('hnsw', table.content_vector_1024.op('vector_cosine_ops')),
index('memory_short_term_ideas_content_vector_768_index').using('hnsw', table.content_vector_768.op('vector_cosine_ops')),
])
+34 -21
View File
@@ -3,11 +3,12 @@ import type { Message } from 'grammy/types'
import type { Action } from '../types'
import { env } from 'node:process'
import { useLogg } from '@guiiai/logg'
import { Format, useLogg } from '@guiiai/logg'
import { generateText } from '@xsai/generate-text'
import { message } from '@xsai/utils-chat'
import { parse } from 'best-effort-json-parser'
import { recordChatCompletions } from '../models/chat-completions-history'
import { systemPrompt } from '../prompts/system-v1'
export async function imagineAnAction(
@@ -16,7 +17,7 @@ export async function imagineAnAction(
currentAbortController: AbortController,
agentMessages: LLMMessage[],
_lastInteractedNChatIds: string[],
) {
): Promise<Action | undefined> {
const logger = useLogg('imagineAnAction').useGlobalConfig()
if (agentMessages == null) {
@@ -94,26 +95,38 @@ export async function imagineAnAction(
),
)
const res = await generateText({
apiKey: env.LLM_API_KEY!,
baseURL: env.LLM_API_BASE_URL!,
model: env.LLM_MODEL!,
messages: agentMessages,
abortSignal: currentAbortController.signal,
})
let responseText = ''
logger.withFields({
response: res.text,
unreadMessages: Object.fromEntries(Object.entries(unreadMessages).map(([key, value]) => [key, value.length])),
now: new Date().toLocaleString(),
}).log('Generated action')
try {
const res = await generateText({
apiKey: env.LLM_API_KEY!,
baseURL: env.LLM_API_BASE_URL!,
model: env.LLM_MODEL!,
messages: agentMessages,
abortSignal: currentAbortController.signal,
})
res.text = res.text
.replace(/^```json\s*\n/, '')
.replace(/\n```$/, '')
.replace(/^```\s*\n/, '')
.replace(/\n```$/, '')
.trim()
logger.withFields({
response: res.text,
unreadMessages: Object.fromEntries(Object.entries(unreadMessages).map(([key, value]) => [key, value.length])),
now: new Date().toLocaleString(),
}).log('Generated action')
return parse(res.text) as Action
responseText = res.text
.replace(/^```json\s*\n/, '')
.replace(/\n```$/, '')
.replace(/^```\s*\n/, '')
.replace(/\n```$/, '')
.trim()
return parse(responseText) as Action
}
catch (err) {
logger.withField('error', err).withFormat(Format.JSON).log('Failed to generate action')
}
finally {
recordChatCompletions('imagineAnAction', agentMessages, responseText).then(() => {}).catch(err => logger.withField('error', err).log('Failed to record chat completions'))
}
return undefined
}
@@ -0,0 +1,14 @@
import { useDrizzle } from '../db'
import { chatCompletionsHistoryTable } from '../db/schema'
export async function recordChatCompletions(task: string, content: any, response?: any) {
const db = useDrizzle()
await db
.insert(chatCompletionsHistoryTable)
.values({
prompt: JSON.stringify(content),
response: JSON.stringify(response),
task,
})
}
@@ -5,7 +5,7 @@ import type { Message, UserFromGetMe } from 'grammy/types'
import { env } from 'node:process'
import { useLogg } from '@guiiai/logg'
import { embed } from '@xsai/embed'
import { and, cosineDistance, desc, eq, gt, lt, sql } from 'drizzle-orm'
import { and, cosineDistance, desc, eq, gt, lt, notInArray, sql } from 'drizzle-orm'
import { useDrizzle } from '../db'
import { chatMessagesTable } from '../db/schema'
@@ -83,9 +83,9 @@ export async function findLastNMessages(chatId: string, n: number) {
return res.reverse()
}
export async function findRelevantMessages(botId: string, chatId: string, unreadHistoryMessagesEmbedding: { embedding: number[] }[]) {
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
const contextWindowSize = 2 // Number of messages to include before and after
const logger = useLogg('findRelevantMessages').useGlobalConfig().withField('chatId', chatId)
logger.withField('context_window_size', contextWindowSize).log('Querying relevant chat messages...')
@@ -134,6 +134,7 @@ export async function findRelevantMessages(botId: string, chatId: string, unread
eq(chatMessagesTable.platform, 'telegram'),
eq(chatMessagesTable.in_chat_id, chatId),
gt(similarity, 0.5),
notInArray(chatMessagesTable.platform_message_id, excludeMessageIds),
))
.orderBy(desc(sql`combined_score`))
.limit(3)