diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 00000000000..d20478e4f61 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json index 84ecd9e0788..f8d53dcabcb 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "build:cli": "turbo run build --filter=./packages/cli && cd packages/cli && bun link", "start": "turbo run start --filter=!./packages/docs", "agent": "turbo run start --filter=@elizaos/agent", - "dev": "turbo run dev --filter=!./packages/docs", + "dev": "turbo run dev --filter=!./packages/docs --concurrency=20", "release": "bun run build && bun format && npx lerna publish --no-private --force-publish", "docker:build": "bash ./scripts/docker.sh build", "docker:run": "bash ./scripts/docker.sh run", diff --git a/packages/agent/src/api.ts b/packages/agent/src/api.ts index 5b63dcbdf7e..b425b93792b 100644 --- a/packages/agent/src/api.ts +++ b/packages/agent/src/api.ts @@ -1,11 +1,11 @@ import { - type AgentRuntime, type Character, - logger, getEnvVariable, + IAgentRuntime, + logger, type UUID, validateCharacterConfig, - validateUuid, + validateUuid } from "@elizaos/core"; import bodyParser from "body-parser"; import cors from "cors"; @@ -46,7 +46,7 @@ function validateUUIDParams( } export function createApiRouter( - agents: Map, + agents: Map, directClient: CharacterServer ): express.Router { const router = express.Router(); @@ -117,7 +117,7 @@ export function createApiRouter( }; if (!agentId) return; - const agent: AgentRuntime = agents.get(agentId); + const agent: IAgentRuntime = agents.get(agentId); if (agent) { agent.stop(); @@ -134,7 +134,7 @@ export function createApiRouter( }; if (!agentId) return; - let agent: AgentRuntime = agents.get(agentId); + let agent: IAgentRuntime = agents.get(agentId); // update character if (agent) { @@ -338,7 +338,7 @@ export function createApiRouter( router.post("/agents/:agentId/stop", async (req, res) => { const agentId = req.params.agentId; console.log("agentId", agentId); - const agent: AgentRuntime = agents.get(agentId); + const agent: IAgentRuntime = agents.get(agentId); // update character if (agent) { diff --git a/packages/agent/src/defaultCharacter.ts b/packages/agent/src/defaultCharacter.ts index 6cb5d550f3f..9f34b1659dc 100644 --- a/packages/agent/src/defaultCharacter.ts +++ b/packages/agent/src/defaultCharacter.ts @@ -4,11 +4,11 @@ export const defaultCharacter: Character = { name: "Eliza", username: "eliza", plugins: [ - "@elizaos/plugin-node", - "@elizaos/plugin-bootstrap", "@elizaos/plugin-anthropic", "@elizaos/plugin-openai", - "@elizaos/plugin-local-ai", + "@elizaos/plugin-discord", + "@elizaos/plugin-node", + "elizaos/plugin-telegram", ], settings: { secrets: {}, diff --git a/packages/agent/src/server.ts b/packages/agent/src/server.ts index 31c014290bc..77840a4055b 100644 --- a/packages/agent/src/server.ts +++ b/packages/agent/src/server.ts @@ -1,8 +1,6 @@ import { composeContext, logger, - generateCaption, - generateImage, generateMessageResponse, generateObject, messageCompletionFooter, @@ -166,10 +164,7 @@ export class CharacterServer { return; } - const transcription = await runtime.call(ModelClass.TRANSCRIPTION, { - file: fs.createReadStream(audioFile.path), - model: "whisper-1", - }); + const transcription = await runtime.useModel(ModelClass.TRANSCRIPTION, fs.createReadStream(audioFile.path)); res.json(transcription); } @@ -591,15 +586,11 @@ export class CharacterServer { res.status(404).send("Agent not found"); return; } - - const images = await generateImage({ ...req.body }, agent); + const images = await agent.useModel(ModelClass.IMAGE, { ...req.body }); const imagesRes: { image: string; caption: string }[] = []; if (images.data && images.data.length > 0) { for (let i = 0; i < images.data.length; i++) { - const caption = await generateCaption( - { imageUrl: images.data[i] }, - agent - ); + const caption = await agent.useModel(ModelClass.IMAGE_DESCRIPTION, images.data[i]); imagesRes.push({ image: images.data[i], caption: caption.title, @@ -823,10 +814,7 @@ export class CharacterServer { // Get the text to convert to speech const textToSpeak = response.text; - const speechResponse = await runtime.call(ModelClass.TRANSCRIPTION, { - text: textToSpeak, - runtime, - }); + const speechResponse = await runtime.useModel(ModelClass.TEXT_TO_SPEECH, textToSpeak); if (!speechResponse.ok) { throw new Error( diff --git a/packages/core/src/context.ts b/packages/core/src/context.ts index d85b2857f32..80e4814359d 100644 --- a/packages/core/src/context.ts +++ b/packages/core/src/context.ts @@ -39,7 +39,7 @@ export const composeContext = ({ template: TemplateType; }) => { const templateStr = - typeof template === "function" ? template({ state }) : template; + composeRandomUser(typeof template === "function" ? template({ state }) : template, 10); const templateFunction = handlebars.compile(templateStr); return templateFunction(state); diff --git a/packages/core/src/generation.ts b/packages/core/src/generation.ts index 8a262006e82..fb43f779ae7 100644 --- a/packages/core/src/generation.ts +++ b/packages/core/src/generation.ts @@ -91,11 +91,15 @@ export async function trimTokens( maxTokens: number, runtime: IAgentRuntime ) { - if (!context) return ""; + if (!context) throw new Error("Trim tokens received a null context"); + + // if context is less than of maxtokens / 5, skip + if (context.length < (maxTokens / 5)) return context; + if (maxTokens <= 0) throw new Error("maxTokens must be positive"); try { - const tokens = await runtime.call(ModelClass.TEXT_TOKENIZER_ENCODE, context); + const tokens = await runtime.useModel(ModelClass.TEXT_TOKENIZER_ENCODE, { context }); // If already within limits, return unchanged if (tokens.length <= maxTokens) { @@ -106,7 +110,7 @@ export async function trimTokens( const truncatedTokens = tokens.slice(-maxTokens); // Decode back to text - js-tiktoken decode() returns a string directly - return await runtime.call(ModelClass.TEXT_TOKENIZER_DECODE, truncatedTokens); + return await runtime.useModel(ModelClass.TEXT_TOKENIZER_DECODE, { tokens: truncatedTokens }); } catch (error) { logger.error("Error in trimTokens:", error); // Return truncated string if tokenization fails @@ -127,7 +131,7 @@ export async function generateText({ stopSequences?: string[]; customSystemPrompt?: string; }): Promise { - const text = await runtime.call(modelClass, { + const text = await runtime.useModel(modelClass, { runtime, context, stopSequences, @@ -262,7 +266,7 @@ export const generateObject = async ({ throw new Error(errorMessage); } - const { object } = await runtime.call(modelClass, { + const { object } = await runtime.useModel(modelClass, { runtime, context, modelClass, @@ -320,7 +324,7 @@ export async function generateMessageResponse({ logger.debug("Context:", context); return await withRetry(async () => { - const text = await runtime.call(modelClass, { + const text = await runtime.useModel(modelClass, { runtime, context, stop: stopSequences, @@ -337,60 +341,4 @@ export async function generateMessageResponse({ return parsedContent; }); -} - -// ================ IMAGE-RELATED FUNCTIONS ================ -export const generateImage = async ( - data: { - prompt: string; - width: number; - height: number; - count?: number; - negativePrompt?: string; - numIterations?: number; - guidanceScale?: number; - seed?: number; - modelId?: string; - jobId?: string; - stylePreset?: string; - hideWatermark?: boolean; - safeMode?: boolean; - cfgScale?: number; - }, - runtime: IAgentRuntime -): Promise<{ - success: boolean; - data?: string[]; - error?: any; -}> => { - return await withRetry( - async () => { - const result = await runtime.call(ModelClass.IMAGE, data); - return { - success: true, - data: result.images, - error: undefined, - }; - }, - { - maxRetries: 2, - initialDelay: 2000, - } - ); -}; - -export const generateCaption = async ( - data: { imageUrl: string }, - runtime: IAgentRuntime -): Promise<{ - title: string; - description: string; -}> => { - const { imageUrl } = data; - const resp = await runtime.call(ModelClass.IMAGE_DESCRIPTION, imageUrl); - - return { - title: resp.title.trim(), - description: resp.description.trim(), - }; -}; \ No newline at end of file +} \ No newline at end of file diff --git a/packages/core/src/knowledge.ts b/packages/core/src/knowledge.ts index e7df1c797e0..9a149d7455d 100644 --- a/packages/core/src/knowledge.ts +++ b/packages/core/src/knowledge.ts @@ -31,7 +31,7 @@ async function get( return []; } - const embedding = await runtime.call(ModelClass.TEXT_EMBEDDING, processed); + const embedding = await runtime.useModel(ModelClass.TEXT_EMBEDDING, processed); const fragments = await runtime.knowledgeManager.searchMemories( { embedding, @@ -69,7 +69,7 @@ async function set( chunkSize = 512, bleed = 20 ) { - const embedding = await runtime.call(ModelClass.TEXT_EMBEDDING, null); + const embedding = await runtime.useModel(ModelClass.TEXT_EMBEDDING, null); await runtime.documentsManager.createMemory({ id: item.id, agentId: runtime.agentId, @@ -84,7 +84,7 @@ async function set( const fragments = await splitChunks(preprocessed, chunkSize, bleed); for (const fragment of fragments) { - const embedding = await runtime.call(ModelClass.TEXT_EMBEDDING, fragment); + const embedding = await runtime.useModel(ModelClass.TEXT_EMBEDDING, fragment); await runtime.knowledgeManager.createMemory({ // We namespace the knowledge base uuid to avoid id // collision with the document above. diff --git a/packages/core/src/memory.ts b/packages/core/src/memory.ts index d85fe471917..08ea3e13e6e 100644 --- a/packages/core/src/memory.ts +++ b/packages/core/src/memory.ts @@ -66,11 +66,11 @@ export class MemoryManager implements IMemoryManager { try { // Generate embedding from text content - memory.embedding = await this.runtime.call(ModelClass.TEXT_EMBEDDING, memoryText); + memory.embedding = await this.runtime.useModel(ModelClass.TEXT_EMBEDDING, memoryText); } catch (error) { logger.error("Failed to generate embedding:", error); // Fallback to zero vector if embedding fails - memory.embedding = await this.runtime.call(ModelClass.TEXT_EMBEDDING, null); + memory.embedding = await this.runtime.useModel(ModelClass.TEXT_EMBEDDING, null); } return memory; @@ -185,7 +185,7 @@ export class MemoryManager implements IMemoryManager { logger.log("Creating Memory", memory.id, memory.content.text); if(!memory.embedding){ - const embedding = await this.runtime.call(ModelClass.TEXT_EMBEDDING, null); + const embedding = await this.runtime.useModel(ModelClass.TEXT_EMBEDDING, null); memory.embedding = embedding; } diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index fa8cb5ec429..958ccd7a542 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -248,7 +248,7 @@ export class AgentRuntime implements IAgentRuntime { private readonly knowledgeRoot: string; private readonly memoryManagerService: MemoryManagerService; - handlers = new Map Promise)[]>(); + models = new Map Promise)[]>(); constructor(opts: { conversationLength?: number; @@ -317,6 +317,10 @@ export class AgentRuntime implements IAgentRuntime { for (const manager of (plugin.memoryManagers ?? [])) { this.registerMemoryManager(manager) } + + for(const service of plugin.services){ + this.registerService(service); + } } this.plugins = plugins; @@ -328,7 +332,7 @@ export class AgentRuntime implements IAgentRuntime { async initialize() { // load the character plugins dymamically from string if(this.character.plugins){ - const plugins = await handlePluginImporting(this.character.plugins); + const plugins = await handlePluginImporting(this.character.plugins) as Plugin[]; if (plugins?.length > 0) { for (const plugin of plugins) { if(!plugin) { @@ -343,15 +347,26 @@ export class AgentRuntime implements IAgentRuntime { this.clients.push(startedClient); } } - if (plugin.handlers) { - for (const [modelClass, handler] of Object.entries(plugin.handlers)) { - this.registerHandler(modelClass as ModelClass, handler as (params: any) => Promise); + if (plugin.models) { + for (const [modelClass, handler] of Object.entries(plugin.models)) { + this.registerModel(modelClass as ModelClass, handler as (params: any) => Promise); + } + } + if (plugin.services) { + for(const service of plugin.services){ + this.services.set(service.serviceType, service); } } this.plugins.push(plugin); } } } + + if (this.services) { + for(const [_, service] of this.services.entries()) { + await service.initialize(this); + } + } await this.ensureRoomExists(this.agentId); await this.ensureUserExists( @@ -361,22 +376,6 @@ export class AgentRuntime implements IAgentRuntime { ); await this.ensureParticipantExists(this.agentId, this.agentId); - for (const [serviceType, service] of this.services.entries()) { - try { - await service.initialize(this); - this.services.set(serviceType, service); - logger.success( - `${this.character.name}(${this.agentId}) - Service ${serviceType} initialized successfully` - ); - } catch (error) { - logger.error( - `${this.character.name}(${this.agentId}) - Failed to initialize service ${serviceType}:`, - error - ); - throw error; - } - } - if (this.character?.knowledge && this.character.knowledge.length > 0) { // Non-RAG mode: only process string knowledge const stringKnowledge = this.character.knowledge.filter( @@ -1272,25 +1271,25 @@ Text: ${attachment.text} return this.memoryManagerService.getKnowledgeManager(); } - registerHandler(handlerType: ModelClass, handler: (params: any) => Promise) { - if (!this.handlers.has(handlerType)) { - this.handlers.set(handlerType, []); + registerModel(modelClass: ModelClass, handler: (params: any) => Promise) { + if (!this.models.has(modelClass)) { + this.models.set(modelClass, []); } - this.handlers.get(handlerType)?.push(handler); + this.models.get(modelClass)?.push(handler); } - getHandler(handlerType: ModelClass): ((params: any) => Promise) | undefined { - const handlers = this.handlers.get(handlerType); - if (!handlers?.length) { + getModel(modelClass: ModelClass): ((params: any) => Promise) | undefined { + const models = this.models.get(modelClass); + if (!models?.length) { return undefined; } - return handlers[0]; + return models[0]; } - async call(handlerType: ModelClass, params: any): Promise { - const handler = this.getHandler(handlerType); + async useModel(modelClass: ModelClass, params: any): Promise { + const handler = this.getModel(modelClass); if (!handler) { - throw new Error(`No handler found for delegate type: ${handlerType}`); + throw new Error(`No handler found for delegate type: ${modelClass}`); } return await handler(params); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 9b4af94bf89..b57a5d70412 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,5 +1,3 @@ -import type { Readable } from "stream"; - /** * Represents a UUID string in the format "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" */ @@ -592,8 +590,8 @@ export type Plugin = { /** Optional memory managers */ memoryManagers?: IMemoryManager[]; - /** Optional handlers */ - handlers?: { + /** Optional models */ + models?: { [key: string]: (...args: any[]) => Promise; }; @@ -1023,9 +1021,11 @@ export interface IAgentRuntime { updateRecentMessageState(state: State): Promise; - call(modelClass: ModelClass, params: T): Promise; - registerHandler(modelClass: ModelClass, handler: (params: any) => Promise): void; - getHandler(modelClass: ModelClass): ((params: any) => Promise) | undefined; + useModel(modelClass: ModelClass, params: T): Promise; + registerModel(modelClass: ModelClass, handler: (params: any) => Promise): void; + getModel(modelClass: ModelClass): ((params: any) => Promise) | undefined; + + stop(): Promise; } export enum LoggingLevel { @@ -1095,7 +1095,7 @@ export type InventoryAction = { export type InventoryProvider = { name: string description: string - items: InventoryItem[] + providers: (runtime: IAgentRuntime, params: any) => Promise actions: InventoryAction[] } diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index cc6f73411c2..84b4c5672cc 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -21,6 +21,6 @@ "moduleDetection": "force", "allowArbitraryExtensions": true }, - "include": ["src/**/*"], + "include": ["src/**/*", "../plugin-bootstrap/src/relationships.ts", "../plugin-bootstrap/src/goals.ts", "../plugin-bootstrap/src/knowledge.ts"], "exclude": ["node_modules", "dist", "src/**/*.d.ts", "types/**/*.test.ts"] } diff --git a/packages/plugin-anthropic/src/index.ts b/packages/plugin-anthropic/src/index.ts index a619d7ccf0c..8080fd580e1 100644 --- a/packages/plugin-anthropic/src/index.ts +++ b/packages/plugin-anthropic/src/index.ts @@ -37,7 +37,7 @@ export const anthropicPlugin: Plugin = { throw error; } }, - handlers: { + models: { [ModelClass.TEXT_SMALL]: async ({ runtime, context, diff --git a/packages/plugin-bootstrap/src/providers/facts.ts b/packages/plugin-bootstrap/src/providers/facts.ts index cb4d120a477..4e6aa0f3831 100644 --- a/packages/plugin-bootstrap/src/providers/facts.ts +++ b/packages/plugin-bootstrap/src/providers/facts.ts @@ -16,7 +16,7 @@ const factsProvider: Provider = { actors: state?.actorsData, }); - const embedding = await runtime.call(ModelClass.TEXT_EMBEDDING, recentMessages); + const embedding = await runtime.useModel(ModelClass.TEXT_EMBEDDING, recentMessages); const memoryManager = new MemoryManager({ runtime, diff --git a/packages/plugin-discord/__tests__/discord-client.test.ts b/packages/plugin-discord/__tests__/discord-client.test.ts index b47b26d0313..ee1cb0b8030 100644 --- a/packages/plugin-discord/__tests__/discord-client.test.ts +++ b/packages/plugin-discord/__tests__/discord-client.test.ts @@ -15,7 +15,6 @@ vi.mock('@elizaos/core', () => ({ generateMessageResponse: vi.fn(), generateShouldRespond: vi.fn(), composeContext: vi.fn(), - composeRandomUser: vi.fn(), })); // Mock discord.js Client diff --git a/packages/plugin-discord/package.json b/packages/plugin-discord/package.json index e6a84469103..f11d87387d2 100644 --- a/packages/plugin-discord/package.json +++ b/packages/plugin-discord/package.json @@ -1,5 +1,5 @@ { - "name": "@elizaos-plugins/discord", + "name": "@elizaos/plugin-discord", "version": "0.25.6-alpha.1", "type": "module", "main": "dist/index.js", @@ -37,6 +37,10 @@ "dev": "tsup --format esm --dts --watch", "test": "vitest run" }, + "trustedDependencies": [ + "@discordjs/opus", + "@discordjs/voice" + ], "peerDependencies": { "whatwg-url": "7.1.0" }, diff --git a/packages/plugin-discord/src/attachments.ts b/packages/plugin-discord/src/attachments.ts index f7a22505d97..3be7d39b284 100644 --- a/packages/plugin-discord/src/attachments.ts +++ b/packages/plugin-discord/src/attachments.ts @@ -1,4 +1,4 @@ -import { generateCaption, generateText, trimTokens } from "@elizaos/core"; +import { generateText, trimTokens } from "@elizaos/core"; import { parseJSONObjectFromText } from "@elizaos/core"; import { type IAgentRuntime, @@ -133,7 +133,7 @@ export class AttachmentManager { throw new Error("Unsupported audio/video format"); } - const transcription = await this.runtime.call(ModelClass.TRANSCRIPTION, audioBuffer); + const transcription = await this.runtime.useModel(ModelClass.TRANSCRIPTION, audioBuffer); const { title, description } = await generateSummary( this.runtime, transcription @@ -212,8 +212,12 @@ export class AttachmentManager { try { const response = await fetch(attachment.url); const pdfBuffer = await response.arrayBuffer(); + console.log("service") + console.log(this.runtime + .getService(ServiceType.PDF)) const text = await this.runtime .getService(ServiceType.PDF) + .getInstance() .convertPdfToText(Buffer.from(pdfBuffer)); const { title, description } = await generateSummary( this.runtime, @@ -280,10 +284,8 @@ export class AttachmentManager { attachment: Attachment ): Promise { try { - const { description, title } = await generateCaption( - { imageUrl: attachment.url }, - this.runtime - ); + const { description, title } = await + this.runtime.useModel(ModelClass.IMAGE_DESCRIPTION, attachment.url); return { id: attachment.id, url: attachment.url, diff --git a/packages/plugin-discord/src/index.ts b/packages/plugin-discord/src/index.ts index f82fae50f33..9f102154bef 100644 --- a/packages/plugin-discord/src/index.ts +++ b/packages/plugin-discord/src/index.ts @@ -1,11 +1,10 @@ import { logger, - ModelClass, stringToUuid, type Character, type Client as ElizaClient, type IAgentRuntime, - type Plugin, + type Plugin } from "@elizaos/core"; import { Client, @@ -27,8 +26,8 @@ import transcribe_media from "./actions/transcribe_media.ts"; import { MessageManager } from "./messages.ts"; import channelStateProvider from "./providers/channelState.ts"; import voiceStateProvider from "./providers/voiceState.ts"; -import { VoiceManager } from "./voice.ts"; import { IDiscordClient } from "./types.ts"; +import { VoiceManager } from "./voice.ts"; export class DiscordClient extends EventEmitter implements IDiscordClient { apiToken: string; @@ -69,16 +68,6 @@ export class DiscordClient extends EventEmitter implements IDiscordClient { this.client.login(this.apiToken); this.setupEventListeners(); - - this.runtime.registerAction(joinvoice); - this.runtime.registerAction(leavevoice); - this.runtime.registerAction(summarize); - this.runtime.registerAction(chat_with_attachments); - this.runtime.registerAction(transcribe_media); - this.runtime.registerAction(download_media); - - this.runtime.providers.push(channelStateProvider); - this.runtime.providers.push(voiceStateProvider); } private setupEventListeners() { @@ -405,5 +394,17 @@ const discordPlugin: Plugin = { name: "discord", description: "Discord client plugin", clients: [DiscordClientInterface], + actions: [ + chat_with_attachments, + download_media, + joinvoice, + leavevoice, + summarize, + transcribe_media, + ], + providers: [ + channelStateProvider, + voiceStateProvider, + ] }; export default discordPlugin; \ No newline at end of file diff --git a/packages/plugin-discord/src/messages.ts b/packages/plugin-discord/src/messages.ts index c8e1d4d29bd..0afe95281be 100644 --- a/packages/plugin-discord/src/messages.ts +++ b/packages/plugin-discord/src/messages.ts @@ -1,5 +1,5 @@ import { - composeContext, composeRandomUser, type Content, generateMessageResponse, generateShouldRespond, type HandlerCallback, + composeContext, type Content, generateMessageResponse, generateShouldRespond, type HandlerCallback, type IAgentRuntime, type IBrowserService, type IVideoService, logger, type Media, type Memory, ModelClass, ServiceType, @@ -35,16 +35,6 @@ interface MessageContext { timestamp: number; } -interface AutoPostConfig { - enabled: boolean; - monitorTime: number; - inactivityThreshold: number; // milliseconds - mainChannelId: string; - announcementChannelIds: string[]; - lastAutoPost?: number; - minTimeBetweenPosts?: number; // minimum time between auto posts -} - export type InterestChannels = { [key: string]: { currentHandler: string | undefined; @@ -63,7 +53,6 @@ export class MessageManager { private discordClient: any; private voiceManager: VoiceManager; //Auto post - private autoPostConfig: AutoPostConfig; private lastChannelActivity: { [channelId: string]: number } = {}; private autoPostInterval: NodeJS.Timeout; @@ -73,23 +62,9 @@ export class MessageManager { this.discordClient = discordClient; this.runtime = discordClient.runtime; this.attachmentManager = new AttachmentManager(this.runtime); - - this.autoPostConfig = { - enabled: this.runtime.character.clientConfig?.discord?.autoPost?.enabled || false, - monitorTime: this.runtime.character.clientConfig?.discord?.autoPost?.monitorTime || 300000, - inactivityThreshold: this.runtime.character.clientConfig?.discord?.autoPost?.inactivityThreshold || 3600000, // 1 hour default - mainChannelId: this.runtime.character.clientConfig?.discord?.autoPost?.mainChannelId, - announcementChannelIds: this.runtime.character.clientConfig?.discord?.autoPost?.announcementChannelIds || [], - minTimeBetweenPosts: this.runtime.character.clientConfig?.discord?.autoPost?.minTimeBetweenPosts || 7200000, // 2 hours default - }; - - if (this.autoPostConfig.enabled) { - this._startAutoPostMonitoring(); - } } async handleMessage(message: DiscordMessage) { - if (this.runtime.character.clientConfig?.discord?.allowedChannelIds && !this.runtime.character.clientConfig.discord.allowedChannelIds.includes(message.channelId)) { return; @@ -114,16 +89,6 @@ export class MessageManager { return; } - // Check for mentions-only mode setting - if ( - this.runtime.character.clientConfig?.discord - ?.shouldRespondOnlyToMentions - ) { - if (!this._isMessageForMe(message)) { - return; - } - } - if ( this.runtime.character.clientConfig?.discord ?.shouldIgnoreDirectMessages && @@ -397,7 +362,7 @@ export class MessageManager { // For voice channels, use text-to-speech for the error message const errorMessage = "Sorry, I had a glitch. What was that?"; - const audioStream = await this.runtime.call(ModelClass.TEXT_TO_SPEECH, errorMessage) + const audioStream = await this.runtime.useModel(ModelClass.TEXT_TO_SPEECH, errorMessage) await this.voiceManager.playAudioStream(userId, audioStream); } else { @@ -416,245 +381,6 @@ export class MessageManager { } } - private _startAutoPostMonitoring(): void { - // Wait for client to be ready - if (!this.client.isReady()) { - logger.info('[AutoPost Discord] Client not ready, waiting for ready event') - this.client.once('ready', () => { - logger.info('[AutoPost Discord] Client ready, starting monitoring') - this._initializeAutoPost(); - }); - } else { - logger.info('[AutoPost Discord] Client already ready, starting monitoring') - this._initializeAutoPost(); - } - } - - private _initializeAutoPost(): void { - // Give the client a moment to fully load its cache - setTimeout(() => { - // Monitor with random intervals between 2-6 hours - this.autoPostInterval = setInterval(() => { - this._checkChannelActivity(); - }, Math.floor(Math.random() * (4 * 60 * 60 * 1000) + 2 * 60 * 60 * 1000)); - - // Start monitoring announcement channels - this._monitorAnnouncementChannels(); - }, 5000); // 5 second delay to ensure everything is loaded - } - - private async _checkChannelActivity(): Promise { - if (!this.autoPostConfig.enabled || !this.autoPostConfig.mainChannelId) return; - - const channel = this.client.channels.cache.get(this.autoPostConfig.mainChannelId) as TextChannel; - if (!channel) return; - - try { - // Get last message time - const messages = await channel.messages.fetch({ limit: 1 }); - const lastMessage = messages.first(); - const lastMessageTime = lastMessage ? lastMessage.createdTimestamp : 0; - - const now = Date.now(); - const timeSinceLastMessage = now - lastMessageTime; - const timeSinceLastAutoPost = now - (this.autoPostConfig.lastAutoPost || 0); - - // Add some randomness to the inactivity threshold (±30 minutes) - const randomThreshold = this.autoPostConfig.inactivityThreshold + - (Math.random() * 1800000 - 900000); - - // Check if we should post - if ((timeSinceLastMessage > randomThreshold) && - timeSinceLastAutoPost > (this.autoPostConfig.minTimeBetweenPosts || 0)) { - - try { - // Create memory and generate response - const roomId = stringToUuid(channel.id + "-" + this.runtime.agentId); - - const memory = { - id: stringToUuid(`autopost-${Date.now()}`), - userId: this.runtime.agentId, - agentId: this.runtime.agentId, - roomId, - content: { text: "AUTO_POST_ENGAGEMENT", source: "discord" }, - createdAt: Date.now() - }; - - let state = await this.runtime.composeState(memory, { - discordClient: this.client, - discordMessage: null, - agentName: this.runtime.character.name || this.client.user?.displayName - }); - - // Generate response using template - const context = composeContext({ - state, - template: this.runtime.character.templates?.discordAutoPostTemplate || discordAutoPostTemplate - }); - - const responseContent = await this._generateResponse(memory, state, context); - if (!responseContent?.text) return; - - // Send message and update memory - const messages = await sendMessageInChunks(channel, responseContent.text.trim(), null, []); - - // Create and store memories - const memories = messages.map(m => ({ - id: stringToUuid(m.id + "-" + this.runtime.agentId), - userId: this.runtime.agentId, - agentId: this.runtime.agentId, - content: { - ...responseContent, - url: m.url, - }, - roomId, - createdAt: m.createdTimestamp, - })); - - for (const m of memories) { - await this.runtime.messageManager.createMemory(m); - } - - // Update state and last post time - this.autoPostConfig.lastAutoPost = Date.now(); - state = await this.runtime.updateRecentMessageState(state); - await this.runtime.evaluate(memory, state, true); - } catch (error) { - logger.warn("[AutoPost Discord] Error:", error); - } - } else { - logger.warn("[AutoPost Discord] Activity within threshold. Not posting."); - } - } catch (error) { - logger.warn("[AutoPost Discord] Error checking last message:", error); - } - } - - private async _monitorAnnouncementChannels(): Promise { - if (!this.autoPostConfig.enabled || !this.autoPostConfig.announcementChannelIds.length) { - logger.warn('[AutoPost Discord] Auto post config disabled or no announcement channels') - return; - } - - for (const announcementChannelId of this.autoPostConfig.announcementChannelIds) { - const channel = this.client.channels.cache.get(announcementChannelId); - - if (channel) { - // Check if it's either a text channel or announcement channel - // ChannelType.GuildAnnouncement is 5 - // ChannelType.GuildText is 0 - if (channel instanceof TextChannel || channel.type === ChannelType.GuildAnnouncement) { - const newsChannel = channel as TextChannel; - try { - newsChannel.createMessageCollector().on('collect', async (message: DiscordMessage) => { - if (message.author.bot || Date.now() - message.createdTimestamp > 300000) return; - - const mainChannel = this.client.channels.cache.get(this.autoPostConfig.mainChannelId) as TextChannel; - if (!mainChannel) return; - - try { - // Create memory and generate response - const roomId = stringToUuid(mainChannel.id + "-" + this.runtime.agentId); - const memory = { - id: stringToUuid(`announcement-${Date.now()}`), - userId: this.runtime.agentId, - agentId: this.runtime.agentId, - roomId, - content: { - text: message.content, - source: "discord", - metadata: { announcementUrl: message.url } - }, - createdAt: Date.now() - }; - - let state = await this.runtime.composeState(memory, { - discordClient: this.client, - discordMessage: message, - announcementContent: message?.content, - announcementChannelId: channel.id, - agentName: this.runtime.character.name || this.client.user?.displayName - }); - - // Generate response using template - const context = composeContext({ - state, - template: this.runtime.character.templates?.discordAnnouncementHypeTemplate || discordAnnouncementHypeTemplate - - }); - - const responseContent = await this._generateResponse(memory, state, context); - if (!responseContent?.text) return; - - // Send message and update memory - const messages = await sendMessageInChunks(mainChannel, responseContent.text.trim(), null, []); - - // Create and store memories - const memories = messages.map(m => ({ - id: stringToUuid(m.id + "-" + this.runtime.agentId), - userId: this.runtime.agentId, - agentId: this.runtime.agentId, - content: { - ...responseContent, - url: m.url, - }, - roomId, - createdAt: m.createdTimestamp, - })); - - for (const m of memories) { - await this.runtime.messageManager.createMemory(m); - } - - // Update state - state = await this.runtime.updateRecentMessageState(state); - await this.runtime.evaluate(memory, state, true); - } catch (error) { - logger.warn("[AutoPost Discord] Announcement Error:", error); - } - }); - logger.info(`[AutoPost Discord] Successfully set up collector for announcement channel: ${newsChannel.name}`); - } catch (error) { - logger.warn(`[AutoPost Discord] Error setting up announcement channel collector:`, error); - } - } else { - logger.warn(`[AutoPost Discord] Channel ${announcementChannelId} is not a valid announcement or text channel, type:`, channel.type); - } - } else { - logger.warn(`[AutoPost Discord] Could not find channel ${announcementChannelId} directly`); - } - } - } - - private _isMessageForMe(message: DiscordMessage): boolean { - const isMentioned = message.mentions.users?.has( - this.client.user?.id as string - ); - const guild = message.guild; - const member = guild?.members.cache.get(this.client.user?.id as string); - const nickname = member?.nickname; - - return ( - isMentioned || - (!this.runtime.character.clientConfig?.discord - ?.shouldRespondOnlyToMentions && - (message.content - .toLowerCase() - .includes( - this.client.user?.username.toLowerCase() as string - ) || - message.content - .toLowerCase() - .includes( - this.client.user?.tag.toLowerCase() as string - ) || - (nickname && - message.content - .toLowerCase() - .includes(nickname.toLowerCase())))) - ); - } - async processMessageMedia( message: DiscordMessage ): Promise<{ processedContent: string; attachments: Media[] }> { @@ -778,14 +504,6 @@ export class MessageManager { // if the message is from us, ignore if (message.author.id === this.client.user?.id) return true; - // Honor mentions-only mode - if ( - this.runtime.character.clientConfig?.discord - ?.shouldRespondOnlyToMentions - ) { - return !this._isMessageForMe(message); - } - let messageContent = message.content.toLowerCase(); // Replace the bot's @ping with the character name @@ -870,14 +588,6 @@ export class MessageManager { if (message.author.id === this.client.user?.id) return false; // if (message.author.bot) return false; - // Honor mentions-only mode - if ( - this.runtime.character.clientConfig?.discord - ?.shouldRespondOnlyToMentions - ) { - return this._isMessageForMe(message); - } - const channelState = this.interestChannels[message.channelId]; if (message.mentions.has(this.client.user?.id as string)) return true; @@ -906,7 +616,7 @@ export class MessageManager { this.runtime.character.templates ?.discordShouldRespondTemplate || this.runtime.character.templates?.shouldRespondTemplate || - composeRandomUser(discordShouldRespondTemplate, 2), + discordShouldRespondTemplate, }); const response = await generateShouldRespond({ diff --git a/packages/plugin-discord/src/voice.ts b/packages/plugin-discord/src/voice.ts index 4383a471d5c..79033ebf8d4 100644 --- a/packages/plugin-discord/src/voice.ts +++ b/packages/plugin-discord/src/voice.ts @@ -20,7 +20,6 @@ import { type State, type UUID, composeContext, - composeRandomUser, generateMessageResponse, generateShouldRespond, logger, @@ -587,7 +586,7 @@ export class VoiceManager extends EventEmitter { const wavBuffer = await this.convertOpusToWav(inputBuffer); console.log("Starting transcription..."); - const transcriptionText = await this.runtime.call(ModelClass.TRANSCRIPTION, wavBuffer) + const transcriptionText = await this.runtime.useModel(ModelClass.TRANSCRIPTION, wavBuffer) function isValidTranscription(text: string): boolean { if (!text || text.includes("[BLANK_AUDIO]")) return false; return true; @@ -733,7 +732,7 @@ export class VoiceManager extends EventEmitter { ); state = await this.runtime.updateRecentMessageState(state); - const responseStream = await this.runtime.call(ModelClass.TEXT_TO_SPEECH, content.text) + const responseStream = await this.runtime.useModel(ModelClass.TEXT_TO_SPEECH, content.text) if (responseStream) { await this.playAudioStream( @@ -828,7 +827,7 @@ export class VoiceManager extends EventEmitter { this.runtime.character.templates ?.discordShouldRespondTemplate || this.runtime.character.templates?.shouldRespondTemplate || - composeRandomUser(discordShouldRespondTemplate, 2), + discordShouldRespondTemplate, }); const response = await generateShouldRespond({ diff --git a/packages/plugin-discord/tsconfig.json b/packages/plugin-discord/tsconfig.json index 2153cf41345..dc4f0b96053 100644 --- a/packages/plugin-discord/tsconfig.json +++ b/packages/plugin-discord/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "outDir": "dist", "rootDir": "src", - "lib": ["ESNext"], + "lib": ["ESNext", "dom"], "target": "ESNext", "module": "Preserve", "moduleResolution": "Bundler", diff --git a/packages/plugin-drizzle/.gitignore b/packages/plugin-drizzle/.gitignore new file mode 100644 index 00000000000..7a2e48a9e76 --- /dev/null +++ b/packages/plugin-drizzle/.gitignore @@ -0,0 +1,176 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local +.env.test + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/packages/plugin-postgres/.npmignore b/packages/plugin-drizzle/.npmignore similarity index 100% rename from packages/plugin-postgres/.npmignore rename to packages/plugin-drizzle/.npmignore diff --git a/packages/plugin-postgres/config.toml b/packages/plugin-drizzle/config.toml similarity index 100% rename from packages/plugin-postgres/config.toml rename to packages/plugin-drizzle/config.toml diff --git a/packages/plugin-drizzle/drizzle.config.ts b/packages/plugin-drizzle/drizzle.config.ts new file mode 100644 index 00000000000..fba4a2f22fe --- /dev/null +++ b/packages/plugin-drizzle/drizzle.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "drizzle-kit"; + +// TODO: read URL from env. + +export default defineConfig({ + dialect: "postgresql", + schema: "./src/schema.ts", + out: "./drizzle/migrations", + dbCredentials: { + url: "postgres://postgres:postgres@localhost:5432/eliza", + }, + migrations: { + table: "__drizzle_migrations", + schema: "public", + prefix: "timestamp", + }, + breakpoints: true, +}); diff --git a/packages/plugin-postgres/schema.sql b/packages/plugin-drizzle/drizzle/migrations/20250201002018_init.sql similarity index 71% rename from packages/plugin-postgres/schema.sql rename to packages/plugin-drizzle/drizzle/migrations/20250201002018_init.sql index a09b2e6ad3e..45dfe2db39d 100644 --- a/packages/plugin-postgres/schema.sql +++ b/packages/plugin-drizzle/drizzle/migrations/20250201002018_init.sql @@ -1,3 +1,4 @@ +-- Custom SQL migration file, put your code below! -- -- Enable pgvector extension -- -- Drop existing tables and extensions @@ -23,7 +24,8 @@ CREATE TABLE IF NOT EXISTS accounts ( "name" TEXT, "username" TEXT, "email" TEXT NOT NULL, - "avatarUrl" TEXT + "avatarUrl" TEXT, + "details" JSONB DEFAULT '{}'::jsonb ); CREATE TABLE IF NOT EXISTS rooms ( @@ -31,28 +33,21 @@ CREATE TABLE IF NOT EXISTS rooms ( "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -DO $$ -DECLARE - vector_dim INTEGER; -BEGIN - vector_dim := get_embedding_dimension(); - - EXECUTE format(' - CREATE TABLE IF NOT EXISTS memories ( - "id" UUID PRIMARY KEY, - "type" TEXT NOT NULL, - "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - "content" JSONB NOT NULL, - "embedding" vector(%s), - "userId" UUID REFERENCES accounts("id"), - "agentId" UUID REFERENCES accounts("id"), - "roomId" UUID REFERENCES rooms("id"), - "unique" BOOLEAN DEFAULT true NOT NULL, - CONSTRAINT fk_room FOREIGN KEY ("roomId") REFERENCES rooms("id") ON DELETE CASCADE, - CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES accounts("id") ON DELETE CASCADE, - CONSTRAINT fk_agent FOREIGN KEY ("agentId") REFERENCES accounts("id") ON DELETE CASCADE - )', vector_dim); -END $$; + +CREATE TABLE IF NOT EXISTS memories ( + "id" UUID PRIMARY KEY, + "type" TEXT NOT NULL, + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "content" JSONB NOT NULL, + "embedding" vector(1536), + "userId" UUID REFERENCES accounts("id"), + "agentId" UUID REFERENCES accounts("id"), + "roomId" UUID REFERENCES rooms("id"), + "unique" BOOLEAN DEFAULT true NOT NULL, + CONSTRAINT fk_room FOREIGN KEY ("roomId") REFERENCES rooms("id") ON DELETE CASCADE, + CONSTRAINT fk_user FOREIGN KEY ("userId") REFERENCES accounts("id") ON DELETE CASCADE, + CONSTRAINT fk_agent FOREIGN KEY ("agentId") REFERENCES accounts("id") ON DELETE CASCADE +) CREATE TABLE IF NOT EXISTS goals ( "id" UUID PRIMARY KEY, @@ -111,26 +106,18 @@ CREATE TABLE IF NOT EXISTS cache ( PRIMARY KEY ("key", "agentId") ); -DO $$ -DECLARE - vector_dim INTEGER; -BEGIN - vector_dim := get_embedding_dimension(); - - EXECUTE format(' - CREATE TABLE IF NOT EXISTS knowledge ( - "id" UUID PRIMARY KEY, - "agentId" UUID REFERENCES accounts("id"), - "content" JSONB NOT NULL, - "embedding" vector(%s), - "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - "isMain" BOOLEAN DEFAULT FALSE, - "originalId" UUID REFERENCES knowledge("id"), - "chunkIndex" INTEGER, - "isShared" BOOLEAN DEFAULT FALSE, - CHECK(("isShared" = true AND "agentId" IS NULL) OR ("isShared" = false AND "agentId" IS NOT NULL)) - )', vector_dim); -END $$; +CREATE TABLE IF NOT EXISTS knowledge ( + "id" UUID PRIMARY KEY, + "agentId" UUID REFERENCES accounts("id"), + "content" JSONB NOT NULL, + "embedding" vector(1536), + "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, + "isMain" BOOLEAN DEFAULT FALSE, + "originalId" UUID REFERENCES knowledge("id"), + "chunkIndex" INTEGER, + "isShared" BOOLEAN DEFAULT FALSE, + CHECK(("isShared" = true AND "agentId" IS NULL) OR ("isShared" = false AND "agentId" IS NOT NULL)) +) -- Indexes CREATE INDEX IF NOT EXISTS idx_memories_embedding ON memories USING hnsw ("embedding" vector_cosine_ops); diff --git a/packages/plugin-drizzle/drizzle/migrations/meta/20250201002018_snapshot.json b/packages/plugin-drizzle/drizzle/migrations/meta/20250201002018_snapshot.json new file mode 100644 index 00000000000..6453ebcdf85 --- /dev/null +++ b/packages/plugin-drizzle/drizzle/migrations/meta/20250201002018_snapshot.json @@ -0,0 +1,18 @@ +{ + "id": "1011a59b-2c21-47a9-bf57-cc3585fcc660", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": {}, + "enums": {}, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/plugin-drizzle/drizzle/migrations/meta/_journal.json b/packages/plugin-drizzle/drizzle/migrations/meta/_journal.json new file mode 100644 index 00000000000..47e5d13dc92 --- /dev/null +++ b/packages/plugin-drizzle/drizzle/migrations/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1738369218670, + "tag": "20250201002018_init", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/packages/plugin-drizzle/package.json b/packages/plugin-drizzle/package.json new file mode 100644 index 00000000000..b5d5eecd099 --- /dev/null +++ b/packages/plugin-drizzle/package.json @@ -0,0 +1,41 @@ +{ + "name": "@elizaos/plugin-drizzle", + "version": "0.25.6-alpha.1", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "@elizaos/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist", + "schema.sql", + "seed.sql" + ], + "dependencies": { + "@elizaos/core": "workspace:*", + "@types/pg": "8.11.10", + "drizzle-kit": "^0.30.4", + "drizzle-orm": "^0.39.1", + "pg": "8.13.1" + }, + "devDependencies": { + "dockerode": "^4.0.4", + "tsup": "8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/packages/plugin-drizzle/src/index.ts b/packages/plugin-drizzle/src/index.ts new file mode 100644 index 00000000000..ef0134a9e4d --- /dev/null +++ b/packages/plugin-drizzle/src/index.ts @@ -0,0 +1,1465 @@ +import { + type Account, + type Actor, + DatabaseAdapter, + type GoalStatus, + type Participant, + elizaLogger, + type Goal, + type IDatabaseCacheAdapter, + type Memory, + type Relationship, + type UUID, +} from "@elizaos/core"; +import { + and, + eq, + gte, + lte, + sql, + desc, + inArray, + or, + cosineDistance, +} from "drizzle-orm"; +import { + accountTable, + goalTable, + logTable, + memoryTable, + participantTable, + relationshipTable, + roomTable, + knowledgeTable, + cacheTable, +} from "./schema"; +import { drizzle, NodePgDatabase } from "drizzle-orm/node-postgres"; +import { v4 } from "uuid"; +import { runMigrations } from "./migrations"; +import pg, { ConnectionConfig, PoolConfig } from "pg"; +type Pool = pg.Pool; + +export class DrizzleDatabaseAdapter + extends DatabaseAdapter + implements IDatabaseCacheAdapter +{ + private pool: Pool; + private readonly maxRetries: number = 3; + private readonly baseDelay: number = 1000; // 1 second + private readonly maxDelay: number = 10000; // 10 seconds + private readonly jitterMax: number = 1000; // 1 second + private readonly connectionTimeout: number = 5000; // 5 seconds + + constructor( + connectionConfig: any, + ) { + super(); + const defaultConfig = { + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: this.connectionTimeout, + }; + + const { poolConfig } = this.parseConnectionConfig( + connectionConfig, + defaultConfig + ); + this.pool = new pg.Pool(poolConfig); + + this.pool.on("error", (err) => { + elizaLogger.error("Unexpected pool error", err); + this.handlePoolError(err); + }); + + this.setupPoolErrorHandling(); + this.db = drizzle({ client: this.pool }); + } + + private setupPoolErrorHandling() { + process.on("SIGINT", async () => { + await this.cleanup(); + process.exit(0); + }); + + process.on("SIGTERM", async () => { + await this.cleanup(); + process.exit(0); + }); + + process.on("beforeExit", async () => { + await this.cleanup(); + }); + } + + private async handlePoolError(error: Error) { + elizaLogger.error("Pool error occurred, attempting to reconnect", { + error: error.message, + }); + + try { + // Close existing pool + await this.pool.end(); + + // Create new pool + this.pool = new pg.Pool({ + ...this.pool.options, + connectionTimeoutMillis: this.connectionTimeout, + }); + + await this.testConnection(); + elizaLogger.success("Pool reconnection successful"); + } catch (reconnectError) { + elizaLogger.error("Failed to reconnect pool", { + error: + reconnectError instanceof Error + ? reconnectError.message + : String(reconnectError), + }); + throw reconnectError; + } + } + + private async withDatabase( + operation: () => Promise, + context: string + ): Promise { + return this.withRetry(operation); + } + + private async withRetry(operation: () => Promise): Promise { + let lastError: Error = new Error("Unknown error"); // Initialize with default + + for (let attempt = 1; attempt <= this.maxRetries; attempt++) { + try { + return await operation(); + } catch (error) { + lastError = error as Error; + + if (attempt < this.maxRetries) { + // Calculate delay with exponential backoff + const backoffDelay = Math.min( + this.baseDelay * Math.pow(2, attempt - 1), + this.maxDelay + ); + + // Add jitter to prevent thundering herd + const jitter = Math.random() * this.jitterMax; + const delay = backoffDelay + jitter; + + elizaLogger.warn( + `Database operation failed (attempt ${attempt}/${this.maxRetries}):`, + { + error: + error instanceof Error + ? error.message + : String(error), + nextRetryIn: `${(delay / 1000).toFixed(1)}s`, + } + ); + + await new Promise((resolve) => setTimeout(resolve, delay)); + } else { + elizaLogger.error("Max retry attempts reached:", { + error: + error instanceof Error + ? error.message + : String(error), + totalAttempts: attempt, + }); + throw error instanceof Error + ? error + : new Error(String(error)); + } + } + } + + throw lastError; + } + + async cleanup(): Promise { + try { + await this.pool.end(); + elizaLogger.info("Database pool closed"); + } catch (error) { + elizaLogger.error("Error closing database pool:", error); + } + } + + private parseConnectionConfig( + config: ConnectionConfig, + defaults: Partial + ): { poolConfig: PoolConfig; databaseName: string } { + if (typeof config === "string") { + try { + const url = new URL(config); + const databaseName = url.pathname.split("/")[1] || "postgres"; + return { + poolConfig: { ...defaults, connectionString: config }, + databaseName, + }; + } catch (error) { + throw new Error( + `Invalid connection string: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + } else { + return { + poolConfig: { ...defaults, ...config }, + databaseName: config.database || "postgres", + }; + } + } + + private async validateVectorSetup(): Promise { + try { + const vectorExt = await this.db.execute(sql` + SELECT * FROM pg_extension WHERE extname = 'vector' + `); + + const hasVector = vectorExt?.rows.length > 0; + + if (!hasVector) { + elizaLogger.warn("Vector extension not found"); + return false; + } + + return true; + } catch (error) { + elizaLogger.error("Error validating vector setup:", error); + return false; + } + } + + async init(): Promise { + try { + // TODO: Get the null embedding from provider, if no provider is set for embeddings, throw an error + // Store the embedding dimension on this class so we can use elsewhere + + const { rows } = await this.db.execute(sql` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = 'rooms' + ); + `); + + if (!rows[0].exists || !(await this.validateVectorSetup())) { + await runMigrations(this.pool); + } + } catch (error) { + elizaLogger.error("Failed to initialize database:", error); + throw error; + } + } + + async close(): Promise { + try { + if (this.db && (this.db as any).client) { + await (this.db as any).client.close(); + } + } catch (error) { + elizaLogger.error("Failed to close database connection:", { + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } + } + + async testConnection(): Promise { + try { + const result = await this.db.execute(sql`SELECT NOW()`); + elizaLogger.success( + "Database connection test successful:", + result.rows[0] + ); + return true; + } catch (error) { + elizaLogger.error("Database connection test failed:", error); + throw new Error( + `Failed to connect to database: ${(error as Error).message}` + ); + } + } + + async getAccountById(userId: UUID): Promise { + return this.withDatabase(async () => { + const result = await this.db + .select() + .from(accountTable) + .where(eq(accountTable.id, userId)) + .limit(1); + + if (result.length === 0) return null; + + const account = result[0]; + + return { + id: account.id as UUID, + name: account.name ?? "", + username: account.username ?? "", + email: account.email ?? "", + avatarUrl: account.avatarUrl ?? "", + details: account.details ?? {}, + }; + }, "getAccountById"); + } + + async createAccount(account: Account): Promise { + return this.withDatabase(async () => { + try { + const accountId = account.id ?? v4(); + + await this.db.insert(accountTable).values({ + id: accountId, + name: account.name ?? null, + username: account.username ?? null, + email: account.email ?? "", + avatarUrl: account.avatarUrl ?? null, + details: sql`${account.details}::jsonb` || {}, + }); + + elizaLogger.debug("Account created successfully:", { + accountId, + }); + + return true; + } catch (error) { + elizaLogger.error("Error creating account:", { + error: + error instanceof Error ? error.message : String(error), + accountId: account.id, + name: account.name, + }); + return false; + } + }, "createAccount"); + } + + async getMemories(params: { + roomId: UUID; + count?: number; + unique?: boolean; + tableName: string; + agentId?: UUID; + start?: number; + end?: number; + }): Promise { + if (!params.tableName) throw new Error("tableName is required"); + if (!params.roomId) throw new Error("roomId is required"); + + return this.withDatabase(async () => { + const conditions = [ + eq(memoryTable.type, params.tableName), + eq(memoryTable.roomId, params.roomId), + ]; + + if (params.start) { + conditions.push(gte(memoryTable.createdAt, params.start)); + } + + if (params.end) { + conditions.push(lte(memoryTable.createdAt, params.end)); + } + + if (params.unique) { + conditions.push(eq(memoryTable.unique, true)); + } + + if (params.agentId) { + conditions.push(eq(memoryTable.agentId, params.agentId)); + } + + const query = this.db + .select() + .from(memoryTable) + .where(and(...conditions)) + .orderBy(desc(memoryTable.createdAt)); + + const rows = params.count + ? await query.limit(params.count) + : await query; + + return rows.map((row) => ({ + id: row.id as UUID, + type: row.type, + createdAt: row.createdAt, + content: + typeof row.content === "string" + ? JSON.parse(row.content) + : row.content, + embedding: row.embedding ?? undefined, + userId: row.userId as UUID, + agentId: row.agentId as UUID, + roomId: row.roomId as UUID, + unique: row.unique, + })); + }, "getMemories"); + } + + async getMemoriesByRoomIds(params: { + roomIds: UUID[]; + agentId?: UUID; + tableName: string; + limit?: number; + }): Promise { + return this.withDatabase(async () => { + if (params.roomIds.length === 0) return []; + + const conditions = [ + eq(memoryTable.type, params.tableName), + inArray(memoryTable.roomId, params.roomIds), + ]; + + if (params.agentId) { + conditions.push(eq(memoryTable.agentId, params.agentId)); + } + + const query = this.db + .select() + .from(memoryTable) + .where(and(...conditions)) + .orderBy(desc(memoryTable.createdAt)); + + const rows = params.limit + ? await query.limit(params.limit) + : await query; + + return rows.map((row) => ({ + id: row.id as UUID, + createdAt: row.createdAt, + content: + typeof row.content === "string" + ? JSON.parse(row.content) + : row.content, + embedding: row.embedding, + userId: row.userId as UUID, + agentId: row.agentId as UUID, + roomId: row.roomId as UUID, + unique: row.unique, + })) as Memory[]; + }, "getMemoriesByRoomIds"); + } + + async getMemoryById(id: UUID): Promise { + return this.withDatabase(async () => { + const result = await this.db + .select() + .from(memoryTable) + .where(eq(memoryTable.id, id)) + .limit(1); + + if (result.length === 0) return null; + + const row = result[0]; + return { + id: row.id as UUID, + createdAt: row.createdAt, + content: + typeof row.content === "string" + ? JSON.parse(row.content) + : row.content, + embedding: row.embedding ?? undefined, + userId: row.userId as UUID, + agentId: row.agentId as UUID, + roomId: row.roomId as UUID, + unique: row.unique, + }; + }, "getMemoryById"); + } + + async getMemoriesByIds( + memoryIds: UUID[], + tableName?: string + ): Promise { + return this.withDatabase(async () => { + if (memoryIds.length === 0) return []; + const conditions = [inArray(memoryTable.id, memoryIds)]; + + if (tableName) { + conditions.push(eq(memoryTable.type, tableName)); + } + + const rows = await this.db + .select() + .from(memoryTable) + .where(and(...conditions)) + .orderBy(desc(memoryTable.createdAt)); + + return rows.map((row) => ({ + id: row.id as UUID, + createdAt: row.createdAt, + content: + typeof row.content === "string" + ? JSON.parse(row.content) + : row.content, + embedding: row.embedding ?? undefined, + userId: row.userId as UUID, + agentId: row.agentId as UUID, + roomId: row.roomId as UUID, + unique: row.unique, + })); + }, "getMemoriesByIds"); + } + + async getCachedEmbeddings(opts: { + query_table_name: string; + query_threshold: number; + query_input: string; + query_field_name: string; + query_field_sub_name: string; + query_match_count: number; + }): Promise<{ embedding: number[]; levenshtein_score: number }[]> { + return this.withDatabase(async () => { + try { + const results = await this.db.execute<{ + embedding: number[]; + levenshtein_score: number; + }>(sql` + WITH content_text AS ( + SELECT + embedding, + COALESCE( + content->>${opts.query_field_sub_name}, + '' + ) as content_text + FROM memories + WHERE type = ${opts.query_table_name} + AND content->>${opts.query_field_sub_name} IS NOT NULL + ) + SELECT + embedding, + levenshtein(${opts.query_input}, content_text) as levenshtein_score + FROM content_text + WHERE levenshtein(${opts.query_input}, content_text) <= ${opts.query_threshold} + ORDER BY levenshtein_score + LIMIT ${opts.query_match_count} + `); + + return results.rows + .map((row) => ({ + embedding: Array.isArray(row.embedding) + ? row.embedding + : typeof row.embedding === "string" + ? JSON.parse(row.embedding) + : [], + levenshtein_score: Number(row.levenshtein_score), + })) + .filter((row) => Array.isArray(row.embedding)); + } catch (error) { + elizaLogger.error("Error in getCachedEmbeddings:", { + error: + error instanceof Error ? error.message : String(error), + tableName: opts.query_table_name, + fieldName: opts.query_field_name, + }); + if ( + error instanceof Error && + error.message === + "levenshtein argument exceeds maximum length of 255 characters" + ) { + return []; + } + throw error; + } + }, "getCachedEmbeddings"); + } + + async log(params: { + body: { [key: string]: unknown }; + userId: UUID; + roomId: UUID; + type: string; + }): Promise { + return this.withDatabase(async () => { + try { + await this.db.insert(logTable).values({ + body: sql`${params.body}::jsonb`, + userId: params.userId, + roomId: params.roomId, + type: params.type, + }); + } catch (error) { + elizaLogger.error("Failed to create log entry:", { + error: + error instanceof Error ? error.message : String(error), + type: params.type, + roomId: params.roomId, + userId: params.userId, + }); + throw error; + } + }, "log"); + } + + async getActorDetails(params: { roomId: string }): Promise { + if (!params.roomId) { + throw new Error("roomId is required"); + } + + return this.withDatabase(async () => { + try { + const result = await this.db + .select({ + id: accountTable.id, + name: accountTable.name, + username: accountTable.username, + details: accountTable.details, + }) + .from(participantTable) + .leftJoin( + accountTable, + eq(participantTable.userId, accountTable.id) + ) + .where(eq(participantTable.roomId, params.roomId)) + .orderBy(accountTable.name); + + elizaLogger.debug("Retrieved actor details:", { + roomId: params.roomId, + actorCount: result.length, + }); + + return result.map((row) => { + try { + const details = + typeof row.details === "string" + ? JSON.parse(row.details) + : row.details || {}; + + return { + id: row.id as UUID, + name: row.name ?? "", + username: row.username ?? "", + details: { + tagline: details.tagline ?? "", + summary: details.summary ?? "", + quote: details.quote ?? "", + }, + }; + } catch (error) { + elizaLogger.warn("Failed to parse actor details:", { + actorId: row.id, + error: + error instanceof Error + ? error.message + : String(error), + }); + + return { + id: row.id as UUID, + name: row.name ?? "", + username: row.username ?? "", + details: { + tagline: "", + summary: "", + quote: "", + }, + }; + } + }); + } catch (error) { + elizaLogger.error("Failed to fetch actor details:", { + roomId: params.roomId, + error: + error instanceof Error ? error.message : String(error), + }); + throw new Error( + `Failed to fetch actor details: ${ + error instanceof Error ? error.message : String(error) + }` + ); + } + }, "getActorDetails"); + } + + async searchMemories(params: { + tableName: string; + agentId: UUID; + roomId: UUID; + embedding: number[]; + match_threshold: number; + count: number; + unique: boolean; + }): Promise { + return await this.searchMemoriesByEmbedding(params.embedding, { + match_threshold: params.match_threshold, + count: params.count, + agentId: params.agentId, + roomId: params.roomId, + unique: params.unique, + tableName: params.tableName, + }); + } + + async updateGoalStatus(params: { + goalId: UUID; + status: GoalStatus; + }): Promise { + return this.withDatabase(async () => { + await this.db + .update(goalTable) + .set({ status: params.status }) + .where(eq(goalTable.id, params.goalId)); + }, "updateGoalStatus"); + } + + async searchMemoriesByEmbedding( + embedding: number[], + params: { + match_threshold?: number; + count?: number; + roomId?: UUID; + agentId?: UUID; + unique?: boolean; + tableName: string; + } + ): Promise { + return this.withDatabase(async () => { + const cleanVector = embedding.map((n) => + Number.isFinite(n) ? Number(n.toFixed(6)) : 0 + ); + + const similarity = sql`1 - (${cosineDistance( + memoryTable.embedding, + cleanVector + )})`; + + const conditions = [eq(memoryTable.type, params.tableName)]; + + if (params.unique) { + conditions.push(eq(memoryTable.unique, true)); + } + if (params.agentId) { + conditions.push(eq(memoryTable.agentId, params.agentId)); + } + if (params.roomId) { + conditions.push(eq(memoryTable.roomId, params.roomId)); + } + + if (params.match_threshold) { + conditions.push(gte(similarity, params.match_threshold)); + } + + const results = await this.db + .select({ + id: memoryTable.id, + type: memoryTable.type, + createdAt: memoryTable.createdAt, + content: memoryTable.content, + embedding: memoryTable.embedding, + userId: memoryTable.userId, + agentId: memoryTable.agentId, + roomId: memoryTable.roomId, + unique: memoryTable.unique, + similarity: similarity, + }) + .from(memoryTable) + .where(and(...conditions)) + .orderBy(desc(similarity)) + .limit(params.count ?? 10); + + return results.map((row) => ({ + id: row.id as UUID, + type: row.type, + createdAt: row.createdAt, + content: + typeof row.content === "string" + ? JSON.parse(row.content) + : row.content, + embedding: row.embedding ?? undefined, + userId: row.userId as UUID, + agentId: row.agentId as UUID, + roomId: row.roomId as UUID, + unique: row.unique, + similarity: row.similarity, + })); + }, "searchMemoriesByEmbedding"); + } + + async createMemory(memory: Memory, tableName: string): Promise { + return this.withDatabase(async () => { + elizaLogger.debug("DrizzleAdapter createMemory:", { + memoryId: memory.id, + embeddingLength: memory.embedding?.length, + contentLength: memory.content?.text?.length, + }); + + let isUnique = true; + if (memory.embedding) { + elizaLogger.info("Searching for similar memories:"); + const similarMemories = await this.searchMemoriesByEmbedding( + memory.embedding, + { + tableName, + roomId: memory.roomId, + match_threshold: 0.95, + count: 1, + } + ); + isUnique = similarMemories.length === 0; + } + + const contentToInsert = + typeof memory.content === "string" + ? JSON.parse(memory.content) + : memory.content; + + await this.db.insert(memoryTable).values([ + { + id: memory.id ?? v4(), + type: tableName, + content: sql`${contentToInsert}::jsonb`, + embedding: memory.embedding, + userId: memory.userId, + roomId: memory.roomId, + agentId: memory.agentId, + unique: memory.unique ?? isUnique, + createdAt: memory.createdAt, + }, + ]); + }, "createMemory"); + } + + async removeMemory(memoryId: UUID, tableName: string): Promise { + return this.withDatabase(async () => { + await this.db + .delete(memoryTable) + .where( + and( + eq(memoryTable.id, memoryId), + eq(memoryTable.type, tableName) + ) + ); + }, "removeMemory"); + } + + async removeAllMemories(roomId: UUID, tableName: string): Promise { + return this.withDatabase(async () => { + await this.db + .delete(memoryTable) + .where( + and( + eq(memoryTable.roomId, roomId), + eq(memoryTable.type, tableName) + ) + ); + + elizaLogger.debug("All memories removed successfully:", { + roomId, + tableName, + }); + }, "removeAllMemories"); + } + + async countMemories( + roomId: UUID, + unique = true, + tableName = "" + ): Promise { + if (!tableName) throw new Error("tableName is required"); + + return this.withDatabase(async () => { + const conditions = [ + eq(memoryTable.roomId, roomId), + eq(memoryTable.type, tableName), + ]; + + if (unique) { + conditions.push(eq(memoryTable.unique, true)); + } + + const result = await this.db + .select({ count: sql`count(*)` }) + .from(memoryTable) + .where(and(...conditions)); + + return Number(result[0]?.count ?? 0); + }, "countMemories"); + } + + async getGoals(params: { + roomId: UUID; + userId?: UUID | null; + onlyInProgress?: boolean; + count?: number; + }): Promise { + return this.withDatabase(async () => { + const conditions = [eq(goalTable.roomId, params.roomId)]; + + if (params.userId) { + conditions.push(eq(goalTable.userId, params.userId)); + } + + if (params.onlyInProgress) { + conditions.push( + eq(goalTable.status, "IN_PROGRESS" as GoalStatus) + ); + } + + const query = this.db + .select() + .from(goalTable) + .where(and(...conditions)) + .orderBy(desc(goalTable.createdAt)); + + const result = await (params.count + ? query.limit(params.count) + : query); + + return result.map((row) => ({ + id: row.id as UUID, + roomId: row.roomId as UUID, + userId: row.userId as UUID, + name: row.name ?? "", + status: (row.status ?? "NOT_STARTED") as GoalStatus, + description: row.description ?? "", + objectives: row.objectives as any[], + createdAt: row.createdAt, + })); + }, "getGoals"); + } + + async updateGoal(goal: Goal): Promise { + return this.withDatabase(async () => { + try { + await this.db + .update(goalTable) + .set({ + name: goal.name, + status: goal.status, + objectives: goal.objectives, + }) + .where(eq(goalTable.id, goal.id as string)); + } catch (error) { + elizaLogger.error("Failed to update goal:", { + error: + error instanceof Error ? error.message : String(error), + goalId: goal.id, + status: goal.status, + }); + throw error; + } + }, "updateGoal"); + } + + async createGoal(goal: Goal): Promise { + try { + await this.db.insert(goalTable).values({ + id: goal.id ?? v4(), + roomId: goal.roomId, + userId: goal.userId, + name: goal.name, + status: goal.status, + objectives: sql`${goal.objectives}::jsonb`, + }); + } catch (error) { + elizaLogger.error("Failed to update goal:", { + goalId: goal.id, + error: error instanceof Error ? error.message : String(error), + status: goal.status, + }); + throw error; + } + } + + async removeGoal(goalId: UUID): Promise { + if (!goalId) throw new Error("Goal ID is required"); + + return this.withDatabase(async () => { + try { + await this.db.delete(goalTable).where(eq(goalTable.id, goalId)); + + elizaLogger.debug("Goal removal attempt:", { + goalId, + removed: true, + }); + } catch (error) { + elizaLogger.error("Failed to remove goal:", { + error: + error instanceof Error ? error.message : String(error), + goalId, + }); + throw error; + } + }, "removeGoal"); + } + + async removeAllGoals(roomId: UUID): Promise { + return this.withDatabase(async () => { + await this.db.delete(goalTable).where(eq(goalTable.roomId, roomId)); + }, "removeAllGoals"); + } + + async getRoom(roomId: UUID): Promise { + return this.withDatabase(async () => { + const result = await this.db + .select({ + id: roomTable.id, + }) + .from(roomTable) + .where(eq(roomTable.id, roomId)) + .limit(1); + + return (result[0]?.id as UUID) ?? null; + }, "getRoom"); + } + + async createRoom(roomId?: UUID): Promise { + return this.withDatabase(async () => { + const newRoomId = roomId || v4(); + await this.db.insert(roomTable).values([ + { + id: newRoomId, + }, + ]); + return newRoomId as UUID; + }, "createRoom"); + } + + async removeRoom(roomId: UUID): Promise { + if (!roomId) throw new Error("Room ID is required"); + return this.withDatabase(async () => { + await this.db.delete(roomTable).where(eq(roomTable.id, roomId)); + }, "removeRoom"); + } + + async getRoomsForParticipant(userId: UUID): Promise { + return this.withDatabase(async () => { + const result = await this.db + .select({ roomId: participantTable.roomId }) + .from(participantTable) + .where(eq(participantTable.userId, userId)); + + return result.map((row) => row.roomId as UUID); + }, "getRoomsForParticipant"); + } + + async getRoomsForParticipants(userIds: UUID[]): Promise { + return this.withDatabase(async () => { + const result = await this.db + .selectDistinct({ roomId: participantTable.roomId }) + .from(participantTable) + .where(inArray(participantTable.userId, userIds)); + + return result.map((row) => row.roomId as UUID); + }, "getRoomsForParticipants"); + } + + async addParticipant(userId: UUID, roomId: UUID): Promise { + return this.withDatabase(async () => { + try { + await this.db.insert(participantTable).values({ + id: v4(), + userId, + roomId, + }); + return true; + } catch (error) { + elizaLogger.error("Failed to add participant:", { + error: + error instanceof Error ? error.message : String(error), + userId, + roomId, + }); + return false; + } + }, "addParticipant"); + } + + async removeParticipant(userId: UUID, roomId: UUID): Promise { + return this.withDatabase(async () => { + try { + const result = await this.db + .delete(participantTable) + .where( + and( + eq(participantTable.userId, userId), + eq(participantTable.roomId, roomId) + ) + ) + .returning(); + + return result.length > 0; + } catch (error) { + elizaLogger.error("Failed to remove participant:", { + error: + error instanceof Error ? error.message : String(error), + userId, + roomId, + }); + return false; + } + }, "removeParticipant"); + } + + async getParticipantsForAccount(userId: UUID): Promise { + return this.withDatabase(async () => { + const result = await this.db + .select({ + id: participantTable.id, + userId: participantTable.userId, + roomId: participantTable.roomId, + lastMessageRead: participantTable.lastMessageRead, + }) + .from(participantTable) + .where(eq(participantTable.userId, userId)); + + const account = await this.getAccountById(userId); + + return result.map((row) => ({ + id: row.id as UUID, + account: account!, + })); + }, "getParticipantsForAccount"); + } + + async getParticipantsForRoom(roomId: UUID): Promise { + return this.withDatabase(async () => { + const result = await this.db + .select({ userId: participantTable.userId }) + .from(participantTable) + .where(eq(participantTable.roomId, roomId)); + + return result.map((row) => row.userId as UUID); + }, "getParticipantsForRoom"); + } + + async getParticipantUserState( + roomId: UUID, + userId: UUID + ): Promise<"FOLLOWED" | "MUTED" | null> { + return this.withDatabase(async () => { + const result = await this.db + .select({ userState: participantTable.userState }) + .from(participantTable) + .where( + and( + eq(participantTable.roomId, roomId), + eq(participantTable.userId, userId) + ) + ) + .limit(1); + + return ( + (result[0]?.userState as "FOLLOWED" | "MUTED" | null) ?? null + ); + }, "getParticipantUserState"); + } + + async setParticipantUserState( + roomId: UUID, + userId: UUID, + state: "FOLLOWED" | "MUTED" | null + ): Promise { + return this.withDatabase(async () => { + await this.db + .update(participantTable) + .set({ userState: state }) + .where( + and( + eq(participantTable.roomId, roomId), + eq(participantTable.userId, userId) + ) + ); + }, "setParticipantUserState"); + } + + async createRelationship(params: { + userA: UUID; + userB: UUID; + }): Promise { + // Input validation + if (!params.userA || !params.userB) { + throw new Error("userA and userB are required"); + } + + return this.withDatabase(async () => { + try { + const relationshipId = v4(); + await this.db.insert(relationshipTable).values({ + id: relationshipId, + userA: params.userA, + userB: params.userB, + userId: params.userA, + }); + + elizaLogger.debug("Relationship created successfully:", { + relationshipId, + userA: params.userA, + userB: params.userB, + }); + + return true; + } catch (error) { + // Check for unique constraint violation or other specific errors + if ((error as { code?: string }).code === "23505") { + // Unique violation + elizaLogger.warn("Relationship already exists:", { + userA: params.userA, + userB: params.userB, + error: + error instanceof Error + ? error.message + : String(error), + }); + } else { + elizaLogger.error("Failed to create relationship:", { + userA: params.userA, + userB: params.userB, + error: + error instanceof Error + ? error.message + : String(error), + }); + } + return false; + } + }, "createRelationship"); + } + + async getRelationship(params: { + userA: UUID; + userB: UUID; + }): Promise { + if (!params.userA || !params.userB) { + throw new Error("userA and userB are required"); + } + + return this.withDatabase(async () => { + try { + const result = await this.db + .select() + .from(relationshipTable) + .where( + or( + and( + eq(relationshipTable.userA, params.userA), + eq(relationshipTable.userB, params.userB) + ), + and( + eq(relationshipTable.userA, params.userB), + eq(relationshipTable.userB, params.userA) + ) + ) + ) + .limit(1); + + if (result.length > 0) { + return result[0] as unknown as Relationship; + } + + elizaLogger.debug("No relationship found between users:", { + userA: params.userA, + userB: params.userB, + }); + return null; + } catch (error) { + elizaLogger.error("Error fetching relationship:", { + userA: params.userA, + userB: params.userB, + error: + error instanceof Error ? error.message : String(error), + }); + throw error; + } + }, "getRelationship"); + } + + async getRelationships(params: { userId: UUID }): Promise { + if (!params.userId) { + throw new Error("userId is required"); + } + return this.withDatabase(async () => { + try { + const result = await this.db + .select() + .from(relationshipTable) + .where( + or( + eq(relationshipTable.userA, params.userId), + eq(relationshipTable.userB, params.userId) + ) + ) + .orderBy(desc(relationshipTable.createdAt)); + + elizaLogger.debug("Retrieved relationships:", { + userId: params.userId, + count: result.length, + }); + + return result as unknown as Relationship[]; + } catch (error) { + elizaLogger.error("Failed to fetch relationships:", { + userId: params.userId, + error: + error instanceof Error ? error.message : String(error), + }); + throw error; + } + }, "getRelationships"); + } + + private async createKnowledgeChunk( + params: { + id: UUID; + originalId: UUID; + agentId: UUID | null; + content: any; + embedding: Float32Array | undefined | null; + chunkIndex: number; + isShared: boolean; + createdAt: number; + }, + tx: NodePgDatabase + ): Promise { + const embedding = params.embedding + ? Array.from(params.embedding) + : null; + + const patternId = `${params.originalId}-chunk-${params.chunkIndex}`; + const contentWithPatternId = { + ...params.content, + metadata: { + ...params.content.metadata, + patternId, + }, + }; + + await tx.insert(knowledgeTable).values({ + id: params.id, + agentId: params.agentId, + content: sql`${contentWithPatternId}::jsonb`, + embedding: embedding, + isMain: false, + originalId: params.originalId, + chunkIndex: params.chunkIndex, + isShared: params.isShared, + createdAt: params.createdAt, + }); + } + + async removeKnowledge(id: UUID): Promise { + return this.withDatabase(async () => { + try { + await this.db + .delete(knowledgeTable) + .where(eq(knowledgeTable.id, id)); + } catch (error) { + elizaLogger.error("Failed to remove knowledge:", { + error: + error instanceof Error ? error.message : String(error), + id, + }); + throw error; + } + }, "removeKnowledge"); + } + + async clearKnowledge(agentId: UUID, shared?: boolean): Promise { + return this.withDatabase(async () => { + if (shared) { + await this.db + .delete(knowledgeTable) + .where( + or( + eq(knowledgeTable.agentId, agentId), + eq(knowledgeTable.isShared, true) + ) + ); + } else { + await this.db + .delete(knowledgeTable) + .where(eq(knowledgeTable.agentId, agentId)); + } + }, "clearKnowledge"); + } + + async getCache(params: { + agentId: UUID; + key: string; + }): Promise { + return this.withDatabase(async () => { + try { + const result = await this.db + .select() + .from(cacheTable) + .where( + and( + eq(cacheTable.agentId, params.agentId), + eq(cacheTable.key, params.key) + ) + ); + + return result[0]?.value || undefined; + } catch (error) { + elizaLogger.error("Error fetching cache", { + error: + error instanceof Error ? error.message : String(error), + key: params.key, + agentId: params.agentId, + }); + return undefined; + } + }, "getCache"); + } + + async setCache(params: { + agentId: UUID; + key: string; + value: string; + }): Promise { + return this.withDatabase(async () => { + try { + await this.db + .insert(cacheTable) + .values({ + key: params.key, + agentId: params.agentId, + value: sql`${params.value}::jsonb`, + }) + .onConflictDoUpdate({ + target: [cacheTable.key, cacheTable.agentId], + set: { + value: params.value, + }, + }); + return true; + } catch (error) { + elizaLogger.error("Error setting cache", { + error: + error instanceof Error ? error.message : String(error), + key: params.key, + agentId: params.agentId, + }); + return false; + } + }, "setCache"); + } + + async deleteCache(params: { + agentId: UUID; + key: string; + }): Promise { + return this.withDatabase(async () => { + try { + await this.db + .delete(cacheTable) + .where( + and( + eq(cacheTable.agentId, params.agentId), + eq(cacheTable.key, params.key) + ) + ); + return true; + } catch (error) { + elizaLogger.error("Error deleting cache", { + error: + error instanceof Error ? error.message : String(error), + key: params.key, + agentId: params.agentId, + }); + return false; + } + }, "deleteCache"); + } +} diff --git a/packages/plugin-drizzle/src/migrations.ts b/packages/plugin-drizzle/src/migrations.ts new file mode 100644 index 00000000000..6478227582c --- /dev/null +++ b/packages/plugin-drizzle/src/migrations.ts @@ -0,0 +1,22 @@ +import { migrate } from "drizzle-orm/node-postgres/migrator"; +import { fileURLToPath } from 'url'; +import path from "path"; +import { drizzle } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; +import { elizaLogger } from "@elizaos/core"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +export async function runMigrations(pgPool: Pool): Promise { + try { + const db = drizzle(pgPool); + await migrate(db, { + migrationsFolder: path.resolve(__dirname, "../drizzle/migrations"), + }); + elizaLogger.info("Migrations completed successfully!"); + } catch (error) { + elizaLogger.error("Failed to run database migrations:", error); + throw error; + } +} diff --git a/packages/plugin-drizzle/src/schema.ts b/packages/plugin-drizzle/src/schema.ts new file mode 100644 index 00000000000..b1ade7c213d --- /dev/null +++ b/packages/plugin-drizzle/src/schema.ts @@ -0,0 +1,177 @@ +import { + boolean, + customType, + integer, + jsonb, + pgTable, + text, + uuid, + vector, +} from "drizzle-orm/pg-core"; +import { getEmbeddingConfig } from "@elizaos/core"; +import { sql } from "drizzle-orm"; + +// Const. +const DIMENSIONS = getEmbeddingConfig().dimensions; + +// Custom types. +const stringJsonb = customType<{ data: string; driverData: string }>({ + dataType() { + return "jsonb"; + }, + toDriver(value: string): string { + return JSON.stringify(value); + }, + fromDriver(value: string): string { + return JSON.stringify(value); + }, +}); + +const numberTimestamp = customType<{ data: number; driverData: string }>({ + dataType() { + return "timestamptz"; + }, + toDriver(value: number): string { + return new Date(value).toISOString(); + }, + fromDriver(value: string): number { + return new Date(value).getTime(); + }, +}); + +// Tables. +export const accountTable = pgTable("accounts", { + id: uuid("id").primaryKey().notNull(), + createdAt: numberTimestamp("createdAt") + .default(sql`now()`) + .notNull(), + name: text("name"), + username: text("username"), + email: text("email").notNull(), + avatarUrl: text("avatarUrl"), + details: jsonb("details").default(""), +}); + +export const memoryTable = pgTable("memories", { + id: uuid("id").primaryKey().notNull(), + type: text("type").notNull(), + createdAt: numberTimestamp("createdAt") + .default(sql`now()`) + .notNull(), + content: jsonb("content").default(""), + embedding: vector("embedding", { + dimensions: DIMENSIONS, + }), + userId: uuid("userId") + .references(() => accountTable.id) + .references(() => accountTable.id), + agentId: uuid("agentId") + .references(() => accountTable.id) + .references(() => accountTable.id), + roomId: uuid("roomId") + .references(() => roomTable.id) + .references(() => roomTable.id), + unique: boolean("unique").default(true).notNull(), +}); + +export const roomTable = pgTable("rooms", { + id: uuid("id").primaryKey().notNull(), + createdAt: numberTimestamp("createdAt") + .default(sql`now()`) + .notNull(), +}); + +export const goalTable = pgTable("goals", { + id: uuid("id").primaryKey().notNull(), + createdAt: numberTimestamp("createdAt") + .default(sql`now()`) + .notNull(), + userId: uuid("userId") + .references(() => accountTable.id) + .references(() => accountTable.id), + name: text("name"), + status: text("status"), + description: text("description"), + roomId: uuid("roomId") + .references(() => roomTable.id) + .references(() => roomTable.id), + objectives: jsonb("objectives").default(""), +}); + +export const logTable = pgTable("logs", { + id: uuid("id").defaultRandom().primaryKey().notNull(), + createdAt: numberTimestamp("createdAt") + .default(sql`now()`) + .notNull(), + userId: uuid("userId") + .notNull() + .references(() => accountTable.id) + .references(() => accountTable.id), + body: jsonb("body").default(""), + type: text("type").notNull(), + roomId: uuid("roomId") + .notNull() + .references(() => roomTable.id) + .references(() => roomTable.id), +}); + +export const participantTable = pgTable("participants", { + id: uuid("id").primaryKey().notNull(), + createdAt: numberTimestamp("createdAt") + .default(sql`now()`) + .notNull(), + userId: uuid("userId") + .references(() => accountTable.id) + .references(() => accountTable.id), + roomId: uuid("roomId") + .references(() => roomTable.id) + .references(() => roomTable.id), + userState: text("userState"), + lastMessageRead: text("last_message_read"), +}); + +export const relationshipTable = pgTable("relationships", { + id: uuid("id").primaryKey().notNull(), + createdAt: numberTimestamp("createdAt") + .default(sql`now()`) + .notNull(), + userA: uuid("userA") + .notNull() + .references(() => accountTable.id) + .references(() => accountTable.id), + userB: uuid("userB") + .notNull() + .references(() => accountTable.id) + .references(() => accountTable.id), + status: text("status"), + userId: uuid("userId") + .notNull() + .references(() => accountTable.id) + .references(() => accountTable.id), +}); + +export const knowledgeTable = pgTable("knowledge", { + id: uuid("id").primaryKey().notNull(), + agentId: uuid("agentId").references(() => accountTable.id), + content: jsonb("content").default(""), + embedding: vector("embedding", { + dimensions: DIMENSIONS, + }), + createdAt: numberTimestamp("createdAt") + .default(sql`now()`) + .notNull(), + isMain: boolean("isMain").default(false), + originalId: uuid("originalId"), + chunkIndex: integer("chunkIndex"), + isShared: boolean("isShared").default(false), +}); + +export const cacheTable = pgTable("cache", { + key: text("key").notNull(), + agentId: text("agentId").notNull(), + value: stringJsonb("value").default(""), + createdAt: numberTimestamp("createdAt") + .default(sql`now()`) + .notNull(), + expiresAt: numberTimestamp("expiresAt"), +}); diff --git a/packages/plugin-postgres/tsconfig.json b/packages/plugin-drizzle/tsconfig.json similarity index 73% rename from packages/plugin-postgres/tsconfig.json rename to packages/plugin-drizzle/tsconfig.json index ea4e73360bf..ad27e288d02 100644 --- a/packages/plugin-postgres/tsconfig.json +++ b/packages/plugin-drizzle/tsconfig.json @@ -5,6 +5,6 @@ "rootDir": "src", "strict": true }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "src/migrations.ts", "src/schema.ts"], "exclude": ["node_modules", "dist"] } diff --git a/packages/plugin-postgres/tsup.config.ts b/packages/plugin-drizzle/tsup.config.ts similarity index 100% rename from packages/plugin-postgres/tsup.config.ts rename to packages/plugin-drizzle/tsup.config.ts diff --git a/packages/plugin-drizzle/vitest.config.ts b/packages/plugin-drizzle/vitest.config.ts new file mode 100644 index 00000000000..9f496d6c2a2 --- /dev/null +++ b/packages/plugin-drizzle/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + // Increase timeout for all tests + testTimeout: 30000, // 30 seconds + // Increase hook timeout specifically + hookTimeout: 40000, // 40 seconds + }, +}); diff --git a/packages/plugin-dummy-inventory/.npmignore b/packages/plugin-dummy-inventory/.npmignore new file mode 100644 index 00000000000..078562eceab --- /dev/null +++ b/packages/plugin-dummy-inventory/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/plugin-dummy-inventory/README.MD b/packages/plugin-dummy-inventory/README.MD new file mode 100644 index 00000000000..ed4f95d1983 --- /dev/null +++ b/packages/plugin-dummy-inventory/README.MD @@ -0,0 +1,320 @@ +# @elizaos/plugin-solana + +Core Solana blockchain plugin for Eliza OS that provides essential services and actions for token operations, trading, and DeFi integrations. + +## Overview + +The Solana plugin serves as a foundational component of Eliza OS, bridging Solana blockchain capabilities with the Eliza ecosystem. It provides crucial services for token operations, trading, portfolio management, and DeFi integrations, enabling both automated and user-directed interactions with the Solana blockchain. + +## Features + +### Token Operations +- **Token Creation**: Deploy new tokens with customizable metadata +- **Token Transfers**: Send and receive tokens securely +- **Balance Management**: Track and manage token balances +- **Portfolio Analytics**: Real-time portfolio valuation and tracking + +### Trading Operations +- **Token Swaps**: Execute trades between tokens using Jupiter aggregator +- **Order Management**: Place and track token orders +- **Price Monitoring**: Real-time price feeds and historical data +- **Automated Trading**: Configurable trading strategies and automation + +### DeFi Integration +- **Liquidity Analysis**: Monitor and analyze pool liquidity +- **Market Making**: Automated market making capabilities +- **Yield Optimization**: Smart routing for optimal yields +- **Risk Management**: Advanced risk scoring and monitoring + +### Trust & Security +- **Trust Scoring**: Dynamic trust score calculation for tokens +- **Risk Assessment**: Real-time risk evaluation for trades +- **Performance Tracking**: Historical performance monitoring +- **Simulation Mode**: Test strategies without real transactions + +## Security Features + +### Access Control +- **Wallet Management**: Secure wallet key derivation and storage +- **Permission Scoping**: Granular control over trading permissions +- **TEE Integration**: Trusted Execution Environment support +- **Key Protection**: Secure private key handling + +### Risk Management +- **Trade Limits**: Configurable transaction limits +- **Slippage Protection**: Automatic slippage controls +- **Validation Checks**: Multi-level transaction validation +- **Simulation Support**: Pre-execution transaction simulation +## Installation + +```bash +npm install @elizaos/plugin-solana +``` + +## Configuration + +Configure the plugin by setting the following environment variables: + +```typescript +const solanaEnvSchema = { + WALLET_SECRET_SALT: string (optional), + WALLET_SECRET_KEY: string, + WALLET_PUBLIC_KEY: string, + SOL_ADDRESS: string, + SLIPPAGE: string, + RPC_URL: string, + HELIUS_API_KEY: string, + BIRDEYE_API_KEY: string +} +``` + +## Usage + +### Basic Setup +```typescript +import { solanaPlugin } from "@elizaos/plugin-solana"; + +// Initialize the plugin +const runtime = await initializeRuntime({ + plugins: [solanaPlugin] +}); +``` + +### Services + +#### TokenProvider +Manages token operations and information retrieval. +```typescript +const tokenProvider = new TokenProvider(tokenAddress, walletProvider, cacheManager); +await tokenProvider.getTokensInWallet(runtime); +``` + +#### WalletProvider +Handles wallet operations and portfolio management. +```typescript +const walletProvider = new WalletProvider(connection, publicKey); +await walletProvider.getFormattedPortfolio(runtime); +``` + +#### TrustScoreProvider +Evaluates and manages trust scores for tokens and trading activities. +```typescript +const trustScore = await runtime.getProvider("trustScore"); +``` +## Actions + +### executeSwap +Executes a token swap using Jupiter aggregator. + +```typescript +// Example usage +const result = await runtime.executeAction("EXECUTE_SWAP", { + inputTokenSymbol: "SOL", + outputTokenSymbol: "USDC", + amount: 0.1 +}); +``` + +### transferToken +Transfers tokens between wallets. + +```typescript +// Example usage +const result = await runtime.executeAction("TRANSFER_TOKEN", { + tokenAddress: "TokenAddressHere", + recipient: "RecipientAddressHere", + amount: "1000" +}); +``` + +### takeOrder +Places a buy order based on conviction level. + +```typescript +// Example usage +const result = await runtime.executeAction("TAKE_ORDER", { + ticker: "SOL", + contractAddress: "ContractAddressHere" +}); +``` + +### pumpfun +Creates and buys tokens on pump.fun. + +```typescript +// Example usage +const result = await runtime.executeAction("CREATE_AND_BUY_TOKEN", { + tokenMetadata: { + name: "TokenName", + symbol: "SYMBOL", + description: "Token description", + image_description: "Image description" + }, + buyAmountSol: 0.1 +}); +``` + +### fomo +Creates and buys tokens on fomo.fund. + +```typescript +// Example usage +const result = await runtime.executeAction("CREATE_AND_BUY_TOKEN", { + tokenMetadata: { + name: "TokenName", + symbol: "SYMBOL", + description: "Token description", + image_description: "Image description" + }, + buyAmountSol: 0.1, + requiredLiquidity: 1000 +}); +``` +### executeSwapForDAO +Executes token swaps for DAO operations. + +```typescript +// Example usage +const result = await runtime.executeAction("EXECUTE_SWAP_DAO", { + inputTokenSymbol: "SOL", + outputTokenSymbol: "USDC", + amount: 0.1 +}); +``` + +## Performance Optimization + +1. **Cache Management** + - Implement token data caching + - Configure cache TTL settings + - Monitor cache hit rates + +2. **RPC Optimization** + - Use connection pooling + - Implement request batching + - Monitor RPC usage + +3. **Transaction Management** + - Optimize transaction bundling + - Implement retry strategies + - Monitor transaction success rates + + +## System Requirements + +- Node.js 16.x or higher +- Solana CLI tools (optional) +- Minimum 4GB RAM recommended +- Stable internet connection +- Access to Solana RPC endpoint + +## Troubleshooting + +### Common Issues + +1. **Wallet Connection Failures** +```bash +Error: Failed to connect to wallet +``` +- Verify RPC endpoint is accessible +- Check wallet configuration settings +- Ensure proper network selection + +2. **Transaction Errors** +```bash +Error: Transaction simulation failed +``` +- Check account balances +- Verify transaction parameters +- Ensure proper fee configuration + +3. **Price Feed Issues** +```bash +Error: Unable to fetch price data +``` +- Verify API key configuration +- Check network connectivity +- Ensure price feed service status + +## Safety & Security +### Best Practices +1. **Environment Variables** + - Store sensitive keys in environment variables + - Use .env.example for non-sensitive defaults + - Never commit real credentials to version control + +2. **Transaction Limits** + - Set maximum transaction amounts + - Implement daily trading limits + - Configure per-token restrictions + +3. **Monitoring** + - Track failed transaction attempts + - Monitor unusual trading patterns + - Log security-relevant events + +4. **Recovery** + - Implement transaction rollback mechanisms + - Maintain backup RPC endpoints + - Document recovery procedures + + +## Performance Optimization + +1. **Cache Management** + - Implement token data caching + - Configure cache TTL settings + - Monitor cache hit rates + +2. **RPC Optimization** + - Use connection pooling + - Implement request batching + - Monitor RPC usage + +3. **Transaction Management** + - Optimize transaction bundling + - Implement retry strategies + - Monitor transaction success rates + +## Support + +For issues and feature requests, please: +1. Check the troubleshooting guide above +2. Review existing GitHub issues +3. Submit a new issue with: + - System information + - Error logs + - Steps to reproduce + - Transaction IDs (if applicable) + +## Contributing + +Contributions are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) file for more information. + +## Credits + +This plugin integrates with and builds upon several key technologies: + +- [Solana](https://solana.com/) - The core blockchain platform +- [Solana Web3.js](https://github.com/solana-labs/solana-web3.js) - Core Solana interactions +- [SPL Token](https://spl.solana.com/) - Token program interactions +- [Jupiter](https://jup.ag/) - Token swap aggregation +- [Birdeye](https://birdeye.so/) - Price feeds and analytics +- [Helius](https://helius.xyz/) - Enhanced RPC services +- [Anchor](https://project-serum.github.io/anchor/) - Smart contract framework +- [FOMO](https://fomo.fund/) - Token creation and trading +- [Pump.fun](https://pump.fun/) - Token creation and trading + +Special thanks to: +- The Solana ecosystem and all the open-source contributors who make these integrations possible. +- The Eliza community for their contributions and feedback. + +For more information about Solana blockchain capabilities: +- [Solana Documentation](https://docs.solana.com/) +- [Solana Developer Portal](https://solana.com/developers) +- [Solana Network Dashboard](https://solscan.io/) +- [Solana GitHub Repository](https://github.com/solana-labs/solana) + +## License + +This plugin is part of the Eliza project. See the main project repository for license information. diff --git a/packages/plugin-dummy-inventory/eslint.config.mjs b/packages/plugin-dummy-inventory/eslint.config.mjs new file mode 100644 index 00000000000..92fe5bbebef --- /dev/null +++ b/packages/plugin-dummy-inventory/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/plugin-dummy-inventory/package.json b/packages/plugin-dummy-inventory/package.json new file mode 100644 index 00000000000..cbd8aadb88b --- /dev/null +++ b/packages/plugin-dummy-inventory/package.json @@ -0,0 +1,31 @@ +{ + "name": "@elizaos/plugin-dummy-inventory", + "version": "0.2.0-alpha.1", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "@elizaos/source": "./src/index.ts", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist" + ], + "dependencies": { + "@elizaos/core": "workspace:*", + "tsup": "8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint --fix --cache .", + "test": "vitest run" + } +} diff --git a/packages/plugin-dummy-inventory/src/index.ts b/packages/plugin-dummy-inventory/src/index.ts new file mode 100644 index 00000000000..fb11e4296df --- /dev/null +++ b/packages/plugin-dummy-inventory/src/index.ts @@ -0,0 +1,12 @@ +export * from "./inventory/wallet.ts"; + +import { Plugin } from "@elizaos/core"; +import { inventoryProvider } from "./inventory/wallet.ts"; + +export const dummyInventoryPlugin: Plugin = { + name: "dummy-inventory", + description: "Dummy Inventory Plugin for Eliza", + inventoryProviders: [inventoryProvider] +}; + +export default dummyInventoryPlugin; diff --git a/packages/plugin-dummy-inventory/src/inventory/actions/swap.ts b/packages/plugin-dummy-inventory/src/inventory/actions/swap.ts new file mode 100644 index 00000000000..d2ad80488d7 --- /dev/null +++ b/packages/plugin-dummy-inventory/src/inventory/actions/swap.ts @@ -0,0 +1,16 @@ +import { InventoryAction, IAgentRuntime } from "@elizaos/core"; +import { z } from "zod"; + +export const swapAction: InventoryAction = { + name: 'swap', + description: 'Swap one inventory item for another', + parameters: z.object({ + fromContractAddress: z.string(), + toContractAddress: z.string(), + quantity: z.number(), + }), + handler: async (_runtime: IAgentRuntime, params: any, _callback: any | undefined) => { + console.log("Swapping", params); + return JSON.stringify(params.item); + }, + }; \ No newline at end of file diff --git a/packages/plugin-dummy-inventory/src/inventory/actions/transfer.ts b/packages/plugin-dummy-inventory/src/inventory/actions/transfer.ts new file mode 100644 index 00000000000..98302e76e52 --- /dev/null +++ b/packages/plugin-dummy-inventory/src/inventory/actions/transfer.ts @@ -0,0 +1,16 @@ +import { InventoryAction, IAgentRuntime } from "@elizaos/core"; +import { z } from "zod"; + +export const transferAction: InventoryAction = { + name: 'transfer', + description: 'Call some inventory action', + parameters: z.object({ + assetContractAddress: z.string(), + transferToAddress: z.string(), + quantity: z.number(), + }), + handler: async (_runtime: IAgentRuntime, params: any, _callback: any | undefined) => { + console.log("Transferring", params); + + }, + }; \ No newline at end of file diff --git a/packages/plugin-dummy-inventory/src/inventory/wallet.ts b/packages/plugin-dummy-inventory/src/inventory/wallet.ts new file mode 100644 index 00000000000..ad224466731 --- /dev/null +++ b/packages/plugin-dummy-inventory/src/inventory/wallet.ts @@ -0,0 +1,20 @@ +import { IAgentRuntime, InventoryItem, InventoryProvider } from "@elizaos/core"; +import { swapAction } from "./actions/swap"; +import { transferAction } from "./actions/transfer"; + +// example inventory item +const inventoryItem: InventoryItem = { + name: 'ai16z', + ticker: 'ai16z', + address: 'AM84n1iLdxgVTAyENBcLdjXoyvjentTbu5Q6EpKV1PeG', + description: + 'ai16z is the first AI VC fund, fully managed by Marc AIndreessen with recommendations from members of the DAO.', + quantity: 100, + }; + + export const inventoryProvider: InventoryProvider = { + name: 'Example Inventory Provider', + description: 'Inventory of items', + providers: (runtime: IAgentRuntime, params: any) => Promise.resolve([inventoryItem]), + actions: [swapAction, transferAction], + }; \ No newline at end of file diff --git a/packages/plugin-dummy-inventory/tsconfig.json b/packages/plugin-dummy-inventory/tsconfig.json new file mode 100644 index 00000000000..73993deaaf7 --- /dev/null +++ b/packages/plugin-dummy-inventory/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-dummy-inventory/tsup.config.ts b/packages/plugin-dummy-inventory/tsup.config.ts new file mode 100644 index 00000000000..dd25475bb63 --- /dev/null +++ b/packages/plugin-dummy-inventory/tsup.config.ts @@ -0,0 +1,29 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + "safe-buffer", + "base-x", + "bs58", + "borsh", + "@solana/buffer-layout", + "stream", + "buffer", + "querystring", + "amqplib", + // Add other modules you want to externalize + ], +}); diff --git a/packages/plugin-evm b/packages/plugin-evm new file mode 160000 index 00000000000..ade452e9a5f --- /dev/null +++ b/packages/plugin-evm @@ -0,0 +1 @@ +Subproject commit ade452e9a5fb2d7d5751b0f8902a119e5e43972b diff --git a/packages/plugin-local-ai/src/index.ts b/packages/plugin-local-ai/src/index.ts index 7aea4cd0388..c6951889a8b 100644 --- a/packages/plugin-local-ai/src/index.ts +++ b/packages/plugin-local-ai/src/index.ts @@ -1,10 +1,9 @@ -import { z } from "zod"; -import type { Plugin } from "@elizaos/core"; -import { ModelClass, logger } from "@elizaos/core"; +import { ModelClass, Plugin, logger } from "@elizaos/core"; import { AutoTokenizer } from "@huggingface/transformers"; -import { FlagEmbedding, EmbeddingModel } from "fastembed"; +import { EmbeddingModel, FlagEmbedding } from "fastembed"; import path from "node:path"; import { fileURLToPath } from "url"; +import { z } from "zod"; // Configuration schema for the local AI plugin const configSchema = z.object({ @@ -135,7 +134,7 @@ export const localAIPlugin: Plugin = { } }, - handlers: { + models: { // Text generation for small tasks [ModelClass.TEXT_SMALL]: async ({ context, @@ -209,7 +208,7 @@ export const localAIPlugin: Plugin = { }, // Image description using local Florence model - [ModelClass.IMAGE_DESCRIPTION]: async ({ imageUrl, runtime }) => { + [ModelClass.IMAGE_DESCRIPTION]: async (imageUrlw) => { try { // TODO: Add florence diff --git a/packages/plugin-node/package.json b/packages/plugin-node/package.json index e8e0cf25c2d..eb1137912bb 100644 --- a/packages/plugin-node/package.json +++ b/packages/plugin-node/package.json @@ -75,7 +75,7 @@ "wav-encoder": "1.3.0", "wavefile": "11.0.0", "yargs": "17.7.2", - "youtube-dl-exec": "3.0.10", + "youtube-dl-exec": "3.0.15", "cookie": "0.7.0" }, "devDependencies": { diff --git a/packages/plugin-node/src/environment.ts b/packages/plugin-node/src/environment.ts deleted file mode 100644 index cb11bccbd42..00000000000 --- a/packages/plugin-node/src/environment.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { IAgentRuntime } from "@elizaos/core"; -import { z } from "zod"; - -export const nodeEnvSchema = z.object({ - OPENAI_API_KEY: z.string().min(1, "OpenAI API key is required"), - - // Core settings - ELEVENLABS_XI_API_KEY: z.string().optional(), - - // All other settings optional with defaults - ELEVENLABS_MODEL_ID: z.string().optional(), - ELEVENLABS_VOICE_ID: z.string().optional(), - ELEVENLABS_VOICE_STABILITY: z.string().optional(), - ELEVENLABS_VOICE_SIMILARITY_BOOST: z.string().optional(), - ELEVENLABS_VOICE_STYLE: z.string().optional(), - ELEVENLABS_VOICE_USE_SPEAKER_BOOST: z.string().optional(), - ELEVENLABS_OPTIMIZE_STREAMING_LATENCY: z.string().optional(), - ELEVENLABS_OUTPUT_FORMAT: z.string().optional(), - VITS_VOICE: z.string().optional(), - VITS_MODEL: z.string().optional(), -}); - -export type NodeConfig = z.infer; - -export async function validateNodeConfig( - runtime: IAgentRuntime -): Promise { - try { - const voiceSettings = runtime.character.settings?.voice; - const elevenlabs = voiceSettings?.elevenlabs; - - // Only include what's absolutely required - const config = { - OPENAI_API_KEY: - runtime.getSetting("OPENAI_API_KEY") || - process.env.OPENAI_API_KEY, - ELEVENLABS_XI_API_KEY: - runtime.getSetting("ELEVENLABS_XI_API_KEY") || - process.env.ELEVENLABS_XI_API_KEY, - - // Use character card settings first, fall back to env vars, then defaults - ...(runtime.getSetting("ELEVENLABS_XI_API_KEY") && { - ELEVENLABS_MODEL_ID: - elevenlabs?.model || - process.env.ELEVENLABS_MODEL_ID || - "eleven_monolingual_v1", - ELEVENLABS_VOICE_ID: - elevenlabs?.voiceId || process.env.ELEVENLABS_VOICE_ID, - ELEVENLABS_VOICE_STABILITY: - elevenlabs?.stability || - process.env.ELEVENLABS_VOICE_STABILITY || - "0.5", - ELEVENLABS_VOICE_SIMILARITY_BOOST: - elevenlabs?.similarityBoost || - process.env.ELEVENLABS_VOICE_SIMILARITY_BOOST || - "0.75", - ELEVENLABS_VOICE_STYLE: - elevenlabs?.style || - process.env.ELEVENLABS_VOICE_STYLE || - "0", - ELEVENLABS_VOICE_USE_SPEAKER_BOOST: - elevenlabs?.useSpeakerBoost || - process.env.ELEVENLABS_VOICE_USE_SPEAKER_BOOST || - "true", - ELEVENLABS_OPTIMIZE_STREAMING_LATENCY: - process.env.ELEVENLABS_OPTIMIZE_STREAMING_LATENCY || "0", - ELEVENLABS_OUTPUT_FORMAT: - process.env.ELEVENLABS_OUTPUT_FORMAT || "pcm_16000", - }), - - // VITS settings - VITS_VOICE: voiceSettings?.model || process.env.VITS_VOICE, - VITS_MODEL: process.env.VITS_MODEL, - - // AWS settings (only include if present) - ...(runtime.getSetting("AWS_ACCESS_KEY_ID") && { - AWS_ACCESS_KEY_ID: runtime.getSetting("AWS_ACCESS_KEY_ID"), - AWS_SECRET_ACCESS_KEY: runtime.getSetting( - "AWS_SECRET_ACCESS_KEY" - ), - AWS_REGION: runtime.getSetting("AWS_REGION"), - AWS_S3_BUCKET: runtime.getSetting("AWS_S3_BUCKET"), - AWS_S3_UPLOAD_PATH: runtime.getSetting("AWS_S3_UPLOAD_PATH"), - AWS_S3_ENDPOINT: runtime.getSetting("AWS_S3_ENDPOINT"), - AWS_S3_SSL_ENABLED: runtime.getSetting("AWS_S3_SSL_ENABLED"), - AWS_S3_FORCE_PATH_STYLE: runtime.getSetting( - "AWS_S3_FORCE_PATH_STYLE" - ), - }), - }; - - return nodeEnvSchema.parse(config); - } catch (error) { - if (error instanceof z.ZodError) { - const errorMessages = error.errors - .map((err) => `${err.path.join(".")}: ${err.message}`) - .join("\n"); - throw new Error( - `Node configuration validation failed:\n${errorMessages}` - ); - } - throw error; - } -} diff --git a/packages/plugin-node/src/index.ts b/packages/plugin-node/src/index.ts index a181ea73568..da82093505d 100644 --- a/packages/plugin-node/src/index.ts +++ b/packages/plugin-node/src/index.ts @@ -9,18 +9,14 @@ import { VideoService, } from "./services/index.ts"; -export type NodePlugin = ReturnType; - -export function createNodePlugin() { - return { - name: "default", - description: "Default plugin, with basic actions and evaluators", - services: [ - new BrowserService(), - new PdfService(), - new VideoService(), - new AwsS3Service(), - ], - actions: [], - } as const satisfies Plugin; -} +export const nodePlugin: Plugin = { + name: "default", + description: "Default plugin, with basic actions and evaluators", + services: [ + new BrowserService(), + new PdfService(), + new VideoService(), + new AwsS3Service(), + ], + actions: [], +} \ No newline at end of file diff --git a/packages/plugin-node/src/services/browser.ts b/packages/plugin-node/src/services/browser.ts index 407585fe98a..7ca0363c2a4 100644 --- a/packages/plugin-node/src/services/browser.ts +++ b/packages/plugin-node/src/services/browser.ts @@ -38,7 +38,7 @@ async function generateSummary( const response = await generateText({ runtime, context: prompt, - modelClass: ModelClass.SMALL, + modelClass: ModelClass.TEXT_SMALL, }); const parsedResponse = parseJSONObjectFromText(response); diff --git a/packages/plugin-node/src/services/video.ts b/packages/plugin-node/src/services/video.ts index c4879b9ed1c..52105f18f5c 100644 --- a/packages/plugin-node/src/services/video.ts +++ b/packages/plugin-node/src/services/video.ts @@ -12,7 +12,23 @@ import ffmpeg from "fluent-ffmpeg"; import fs from "fs"; import { tmpdir } from "os"; import path from "path"; -import youtubeDl from "youtube-dl-exec"; +import ytdl, {create} from "youtube-dl-exec"; + +function getYoutubeDL() { + // first check if /usr/local/bin/yt-dlp exists + if (fs.existsSync('/usr/local/bin/yt-dlp')) { + return create('/usr/local/bin/yt-dlp'); + } + + // if not, check if /usr/bin/yt-dlp exists + if (fs.existsSync('/usr/bin/yt-dlp')) { + return create('/usr/bin/yt-dlp'); + } + + // use default otherwise + return ytdl; +} + export class VideoService extends Service implements IVideoService { static serviceType: ServiceType = ServiceType.VIDEO; @@ -57,7 +73,7 @@ export class VideoService extends Service implements IVideoService { } try { - await youtubeDl(url, { + await getYoutubeDL()(url, { verbose: true, output: outputFile, writeInfoJson: true, @@ -79,7 +95,7 @@ export class VideoService extends Service implements IVideoService { } try { - await youtubeDl(videoInfo.webpage_url, { + await getYoutubeDL()(videoInfo.webpage_url, { verbose: true, output: outputFile, format: "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best", @@ -178,6 +194,7 @@ export class VideoService extends Service implements IVideoService { } async fetchVideoInfo(url: string): Promise { + console.log("url", url) if (url.endsWith(".mp4") || url.includes(".mp4?")) { try { const response = await fetch(url); @@ -196,7 +213,7 @@ export class VideoService extends Service implements IVideoService { } try { - const result = await youtubeDl(url, { + const result = await getYoutubeDL()(url, { dumpJson: true, verbose: true, callHome: false, @@ -312,18 +329,40 @@ export class VideoService extends Service implements IVideoService { runtime: IAgentRuntime ): Promise { logger.log("Preparing audio for transcription..."); + + // Check if ffmpeg exists in PATH + try { + await new Promise((resolve, reject) => { + ffmpeg.getAvailableCodecs((err, _codecs) => { + if (err) reject(err); + resolve(null); + }); + }); + } catch (error) { + logger.log("FFmpeg not found:", error); + return null; + } + const mp4FilePath = path.join( this.dataDir, `${this.getVideoId(url)}.mp4` ); + const webmFilePath = path.join( + this.dataDir, + `${this.getVideoId(url)}.webm` + ); + const mp3FilePath = path.join( this.dataDir, `${this.getVideoId(url)}.mp3` ); if (!fs.existsSync(mp3FilePath)) { - if (fs.existsSync(mp4FilePath)) { + if(fs.existsSync(webmFilePath)) { + logger.log("WEBM file found. Converting to MP3..."); + await this.convertWebmToMp3(webmFilePath, mp3FilePath); + } else if (fs.existsSync(mp4FilePath)) { logger.log("MP4 file found. Converting to MP3..."); await this.convertMp4ToMp3(mp4FilePath, mp3FilePath); } else { @@ -339,7 +378,7 @@ export class VideoService extends Service implements IVideoService { logger.log("Starting transcription..."); const startTime = Date.now(); - const transcript = await runtime.call(ModelClass.TRANSCRIPTION, audioBuffer); + const transcript = await runtime.useModel(ModelClass.TRANSCRIPTION, audioBuffer); const endTime = Date.now(); logger.log( @@ -371,6 +410,27 @@ export class VideoService extends Service implements IVideoService { }); } + private async convertWebmToMp3( + inputPath: string, + outputPath: string + ): Promise { + return new Promise((resolve, reject) => { + ffmpeg(inputPath) + .output(outputPath) + .noVideo() + .audioCodec("libmp3lame") + .on("end", () => { + logger.log("Conversion to MP3 complete"); + resolve(); + }) + .on("error", (err) => { + logger.log("Error converting to MP3:", err); + reject(err); + }) + .run(); + }); + } + private async downloadAudio( url: string, outputFile: string @@ -412,7 +472,7 @@ export class VideoService extends Service implements IVideoService { logger.log( "YouTube video detected, downloading audio with youtube-dl" ); - await youtubeDl(url, { + await getYoutubeDL()(url, { verbose: true, extractAudio: true, audioFormat: "mp3", diff --git a/packages/plugin-node/src/templates.ts b/packages/plugin-node/src/templates.ts deleted file mode 100644 index de1261d296b..00000000000 --- a/packages/plugin-node/src/templates.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const getFileLocationTemplate = ` -{{recentMessages}} - -extract the file location from the users message or the attachment in the message history that they are referring to. -your job is to infer the correct attachment based on the recent messages, the users most recent message, and the attachments in the message -image attachments are the result of the users uploads, or images you have created. -only respond with the file location, no other text. -typically the file location is in the form of a URL or a file path. - -\`\`\`json -{ - "fileLocation": "file location text goes here" -} -\`\`\` -`; diff --git a/packages/plugin-openai/src/index.ts b/packages/plugin-openai/src/index.ts index 817c74d6ca3..4fb8cde3135 100644 --- a/packages/plugin-openai/src/index.ts +++ b/packages/plugin-openai/src/index.ts @@ -77,7 +77,7 @@ export const openaiPlugin: Plugin = { throw error; } }, - handlers: { + models: { [ModelClass.TEXT_EMBEDDING]: async (text: string | null) => { if (!text) { // Return zero vector of appropriate length for model @@ -108,15 +108,15 @@ export const openaiPlugin: Plugin = { }, [ModelClass.TEXT_TOKENIZER_ENCODE]: async ({ context, - modelClass, + modelClass = ModelClass.TEXT_LARGE, }: TokenizeTextParams) => { - return tokenizeText(modelClass ?? ModelClass.TEXT_LARGE, context); + return await tokenizeText(modelClass ?? ModelClass.TEXT_LARGE, context); }, [ModelClass.TEXT_TOKENIZER_DECODE]: async ({ tokens, - modelClass, + modelClass = ModelClass.TEXT_LARGE, }: DetokenizeTextParams) => { - return detokenizeText(modelClass ?? ModelClass.TEXT_LARGE, tokens); + return await detokenizeText(modelClass ?? ModelClass.TEXT_LARGE, tokens); }, [ModelClass.TEXT_SMALL]: async ({ runtime, @@ -218,39 +218,59 @@ export const openaiPlugin: Plugin = { const typedData = data as { data: { url: string }[] }; return typedData.data; }, - [ModelClass.IMAGE_DESCRIPTION]: async (params: { imageUrl: string }) => { + [ModelClass.IMAGE_DESCRIPTION]: async (imageUrl: string) => { + console.log("IMAGE_DESCRIPTION") const baseURL = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"; + console.log("baseURL", baseURL) const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY, baseURL, }); - const prompt = `Provide a detailed description of the image found at this URL: ${params.imageUrl}`; - const { text: description } = await aiGenerateText({ + + const { text } = await aiGenerateText({ model: openai.languageModel( process.env.OPENAI_SMALL_MODEL ?? "gpt-4o-mini" ), - prompt, + messages: [ + { + role: "system", + content: "Provide a title and brief description of the image. Structure this as XML with the following syntax:\n{{title}}\n{{description}}\nReplacing the handlerbars with the actual text" + }, + { + role: "user", + content: [{ + type: "image" as "image", + image: imageUrl + }] + } + ], temperature: 0.7, - maxTokens: 256, + maxTokens: 1024, frequencyPenalty: 0, presencePenalty: 0, stopSequences: [], }); - return description; + + const titleMatch = text.match(/(.*?)<\/title>/); + const descriptionMatch = text.match(/<description>(.*?)<\/description>/); + + if (!titleMatch || !descriptionMatch) { + throw new Error("Could not parse title or description from response"); + } + + return { + title: titleMatch[1], + description: descriptionMatch[1] + }; }, - [ModelClass.TRANSCRIPTION]: async (params: { - audioFile: any; - language?: string; - }) => { + [ModelClass.TRANSCRIPTION]: async (audioBuffer: Buffer) => { + console.log("audioBuffer", audioBuffer) const baseURL = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1"; const formData = new FormData(); - formData.append("file", params.audioFile); + formData.append("file", new Blob([audioBuffer], { type: "audio/mp3" })); formData.append("model", "whisper-1"); - if (params.language) { - formData.append("language", params.language); - } const response = await fetch(`${baseURL}/audio/transcriptions`, { method: "POST", headers: { diff --git a/packages/plugin-postgres/migrations/20240318103238_remote_schema.sql b/packages/plugin-postgres/migrations/20240318103238_remote_schema.sql deleted file mode 100644 index e903cf1285d..00000000000 --- a/packages/plugin-postgres/migrations/20240318103238_remote_schema.sql +++ /dev/null @@ -1,818 +0,0 @@ - -SET statement_timeout = 0; -SET lock_timeout = 0; -SET idle_in_transaction_session_timeout = 0; -SET client_encoding = 'UTF8'; -SET standard_conforming_strings = on; -SELECT pg_catalog.set_config('search_path', '', false); -SET check_function_bodies = false; -SET xmloption = content; -SET client_min_messages = warning; -SET row_security = off; - -CREATE SCHEMA IF NOT EXISTS "public"; - -ALTER SCHEMA "public" OWNER TO "pg_database_owner"; - -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM pg_extension - WHERE extname = 'vector' - ) THEN - CREATE EXTENSION vector IF NOT EXISTS - SCHEMA extensions; - END IF; -END $$; - -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM pg_extension - WHERE extname = 'fuzzystrmatch' - ) THEN - CREATE EXTENSION fuzzystrmatch IF NOT EXISTS - SCHEMA extensions; - END IF; -END $$; - -CREATE TABLE IF NOT EXISTS "public"."secrets" ( - "key" "text" PRIMARY KEY, - "value" "text" NOT NULL -); - -ALTER TABLE "public"."secrets" OWNER TO "postgres"; - -CREATE TABLE "public"."user_data" ( - owner_id INT, - target_id INT, - data JSONB, - PRIMARY KEY (owner_id, target_id), - FOREIGN KEY (owner_id) REFERENCES accounts(id), - FOREIGN KEY (target_id) REFERENCES accounts(id) -); - -ALTER TABLE "public"."user_data" OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."after_account_created"() RETURNS "trigger" - LANGUAGE "plpgsql" SECURITY DEFINER - SET "search_path" TO 'extensions', 'public', 'pg_temp' - AS $$ -DECLARE - response RECORD; -- Define response with the expected return type - newuser_url TEXT; - token TEXT; -BEGIN - -- Retrieve the newuser URL and token from the secrets table - SELECT value INTO newuser_url FROM secrets WHERE key = 'newuser_url'; - SELECT value INTO token FROM secrets WHERE key = 'token'; - - -- Ensure newuser_url and token are both defined and not empty - IF newuser_url IS NOT NULL AND newuser_url <> '' AND token IS NOT NULL AND token <> '' THEN - -- Make the HTTP POST request to the endpoint - SELECT * INTO response FROM http_post( - newuser_url, - jsonb_build_object( - 'token', token, - 'userId', NEW.id::text - ) - ); - END IF; - - RETURN NEW; -END; -$$; - -ALTER FUNCTION "public"."after_account_created"() OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."check_similarity_and_insert"("query_table_name" "text", "query_userId" "uuid", "query_content" "jsonb", "query_roomId" "uuid", "query_embedding" "extensions"."vector", "similarity_threshold" double precision, "query_createdAt" "timestamp with time zone") -RETURNS "void" -LANGUAGE "plpgsql" -AS $$ -DECLARE - similar_found BOOLEAN := FALSE; - select_query TEXT; - insert_query TEXT; -BEGIN - -- Only perform the similarity check if query_embedding is not NULL - IF query_embedding IS NOT NULL THEN - -- Build a dynamic query to check for existing similar embeddings using cosine distance - select_query := format( - 'SELECT EXISTS (' || - 'SELECT 1 ' || - 'FROM memories ' || - 'WHERE userId = %L ' || - 'AND roomId = %L ' || - 'AND type = %L ' || -- Filter by the 'type' field using query_table_name - 'AND embedding <=> %L < %L ' || - 'LIMIT 1' || - ')', - query_userId, - query_roomId, - query_table_name, -- Use query_table_name to filter by 'type' - query_embedding, - similarity_threshold - ); - - -- Execute the query to check for similarity - EXECUTE select_query INTO similar_found; - END IF; - - -- Prepare the insert query with 'unique' field set based on the presence of similar records or NULL query_embedding - insert_query := format( - 'INSERT INTO memories (userId, content, roomId, type, embedding, "unique", createdAt) ' || -- Insert into the 'memories' table - 'VALUES (%L, %L, %L, %L, %L, %L, %L)', - query_userId, - query_content, - query_roomId, - query_table_name, -- Use query_table_name as the 'type' value - query_embedding, - NOT similar_found OR query_embedding IS NULL -- Set 'unique' to true if no similar record is found or query_embedding is NULL - ); - - -- Execute the insert query - EXECUTE insert_query; -END; -$$; - -ALTER FUNCTION "public"."check_similarity_and_insert"("query_table_name" "text", "query_userId" "uuid", "query_content" "jsonb", "query_roomId" "uuid", "query_embedding" "extensions"."vector", "similarity_threshold" double precision) OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."count_memories"("query_table_name" "text", "query_roomId" "uuid", "query_unique" boolean DEFAULT false) RETURNS bigint - LANGUAGE "plpgsql" - AS $$ -DECLARE - query TEXT; - total BIGINT; -BEGIN - -- Initialize the base query - query := format('SELECT COUNT(*) FROM memories WHERE type = %L', query_table_name); - - -- Add condition for roomId if not null, ensuring proper spacing - IF query_roomId IS NOT NULL THEN - query := query || format(' AND roomId = %L', query_roomId); - END IF; - - -- Add condition for unique if TRUE, ensuring proper spacing - IF query_unique THEN - query := query || ' AND "unique" = TRUE'; -- Use double quotes if "unique" is a reserved keyword or potentially problematic - END IF; - - -- Debug: Output the constructed query - RAISE NOTICE 'Executing query: %', query; - - -- Execute the constructed query - EXECUTE query INTO total; - RETURN total; -END; -$$; - - -ALTER FUNCTION "public"."count_memories"("query_table_name" "text", "query_roomId" "uuid", "query_unique" boolean) OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."create_room"(roomId uuid) - RETURNS TABLE(id uuid) - LANGUAGE plpgsql -AS $function$ -BEGIN - -- Check if the room already exists - IF EXISTS (SELECT 1 FROM rooms WHERE rooms.id = roomId) THEN - RETURN QUERY SELECT rooms.id FROM rooms WHERE rooms.id = roomId; - ELSE - -- Create a new room with the provided roomId - RETURN QUERY INSERT INTO rooms (id) VALUES (roomId) RETURNING rooms.id; - END IF; -END; -$function$ - -ALTER FUNCTION "public"."create_room"() OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."create_friendship_with_host_agent"() RETURNS "trigger" - LANGUAGE "plpgsql" - AS $$ -DECLARE - host_agent_id UUID := '00000000-0000-0000-0000-000000000000'; - new_roomId UUID; -BEGIN - -- Create a new room for the direct message between the new user and the host agent - INSERT INTO rooms DEFAULT VALUES - RETURNING id INTO new_roomId; - - -- Create a new friendship between the new user and the host agent - INSERT INTO relationships (userA, userB, userId, status) - VALUES (NEW.id, host_agent_id, host_agent_id, 'FRIENDS'); - - -- Add both users as participants of the new room - INSERT INTO participants (userId, roomId) - VALUES (NEW.id, new_roomId), (host_agent_id, new_roomId); - - RETURN NEW; -END; -$$; - -ALTER FUNCTION "public"."create_friendship_with_host_agent"() OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."fn_notify_agents"() RETURNS "trigger" - LANGUAGE "plpgsql" - AS $$ -DECLARE - participant RECORD; - agent_flag BOOLEAN; - response RECORD; - payload TEXT; - message_url TEXT; - token TEXT; -BEGIN - -- Retrieve the message URL and token from the secrets table - SELECT value INTO message_url FROM secrets WHERE key = 'message_url'; - SELECT value INTO token FROM secrets WHERE key = 'token'; - - -- Iterate over the participants of the room - FOR participant IN ( - SELECT p.userId - FROM participants p - WHERE p.roomId = NEW.roomId - ) - LOOP - -- Check if the participant is an agent - SELECT is_agent INTO agent_flag FROM accounts WHERE id = participant.userId; - - -- Add a condition to ensure the sender is not the agent - IF agent_flag AND NEW.userId <> participant.userId THEN - -- Construct the payload JSON object and explicitly cast to TEXT - payload := jsonb_build_object( - 'token', token, - 'senderId', NEW.userId::text, - 'content', NEW.content, - 'roomId', NEW.roomId::text - )::text; - - -- Make the HTTP POST request to the Cloudflare worker endpoint - SELECT * INTO response FROM http_post( - message_url, - payload, - 'application/json' - ); - END IF; - END LOOP; - - RETURN NEW; -END; -$$; - - - -ALTER FUNCTION "public"."fn_notify_agents"() OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."get_embedding_list"("query_table_name" "text", "query_threshold" integer, "query_input" "text", "query_field_name" "text", "query_field_sub_name" "text", "query_match_count" integer) -RETURNS TABLE("embedding" "extensions"."vector", "levenshtein_score" integer) -LANGUAGE "plpgsql" -AS $$ -DECLARE - QUERY TEXT; -BEGIN - -- Check the length of query_input - IF LENGTH(query_input) > 255 THEN - -- For inputs longer than 255 characters, use exact match only - QUERY := format(' - SELECT - embedding - FROM - memories - WHERE - type = $1 AND - (content->>''%s'')::TEXT = $2 - LIMIT - $3 - ', query_field_name); - -- Execute the query with adjusted parameters for exact match - RETURN QUERY EXECUTE QUERY USING query_table_name, query_input, query_match_count; - ELSE - -- For inputs of 255 characters or less, use Levenshtein distance - QUERY := format(' - SELECT - embedding, - levenshtein($2, (content->>''%s'')::TEXT) AS levenshtein_score - FROM - memories - WHERE - type = $1 AND - levenshtein($2, (content->>''%s'')::TEXT) <= $3 - ORDER BY - levenshtein_score - LIMIT - $4 - ', query_field_name, query_field_name); - -- Execute the query with original parameters for Levenshtein distance - RETURN QUERY EXECUTE QUERY USING query_table_name, query_input, query_threshold, query_match_count; - END IF; -END; -$$; - -ALTER FUNCTION "public"."get_embedding_list"("query_table_name" "text", "query_threshold" integer, "query_input" "text", "query_field_name" "text", "query_field_sub_name" "text", "query_match_count" integer) OWNER TO "postgres"; - -SET default_tablespace = ''; - -SET default_table_access_method = "heap"; - -CREATE TABLE IF NOT EXISTS "public"."goals" ( - "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, - "createdAt" timestamp with time zone DEFAULT "now"() NOT NULL, - "userId" "uuid", - "roomId" "uuid", - "status" "text", - "name" "text", - "objectives" "jsonb"[] DEFAULT '{}'::"jsonb"[] NOT NULL -); - -ALTER TABLE "public"."goals" OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."get_goals"("query_roomId" "uuid", "query_userId" "uuid" DEFAULT NULL::"uuid", "only_in_progress" boolean DEFAULT true, "row_count" integer DEFAULT 5) RETURNS SETOF "public"."goals" - LANGUAGE "plpgsql" - AS $$ -BEGIN - RETURN QUERY - SELECT * FROM goals - WHERE - (query_userId IS NULL OR userId = query_userId) - AND (roomId = query_roomId) - AND (NOT only_in_progress OR status = 'IN_PROGRESS') - LIMIT row_count; -END; -$$; - -ALTER FUNCTION "public"."get_goals"("query_roomId" "uuid", "query_userId" "uuid", "only_in_progress" boolean, "row_count" integer) OWNER TO "postgres"; - -CREATE TABLE IF NOT EXISTS "public"."relationships" ( - "createdAt" timestamp with time zone DEFAULT ("now"() AT TIME ZONE 'utc'::"text") NOT NULL, - "userA" "uuid", - "userB" "uuid", - "status" "text", - "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, - "userId" "uuid" NOT NULL -); - -ALTER TABLE "public"."relationships" OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."get_relationship"("usera" "uuid", "userb" "uuid") RETURNS SETOF "public"."relationships" - LANGUAGE "plpgsql" STABLE - AS $$ -BEGIN - RETURN QUERY - SELECT * - FROM relationships - WHERE (userA = usera AND userB = userb) - OR (userA = userb AND userB = usera); -END; -$$; - -ALTER FUNCTION "public"."get_relationship"("usera" "uuid", "userb" "uuid") OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."remove_memories"("query_table_name" "text", "query_roomId" "uuid") RETURNS "void" - LANGUAGE "plpgsql" - AS $_$DECLARE - dynamic_query TEXT; -BEGIN - dynamic_query := format('DELETE FROM memories WHERE roomId = $1 AND type = $2'); - EXECUTE dynamic_query USING query_roomId, query_table_name; -END; -$_$; - - -ALTER FUNCTION "public"."remove_memories"("query_table_name" "text", "query_roomId" "uuid") OWNER TO "postgres"; - -CREATE OR REPLACE FUNCTION "public"."search_memories"("query_table_name" "text", "query_roomId" "uuid", "query_embedding" "extensions"."vector", "query_match_threshold" double precision, "query_match_count" integer, "query_unique" boolean) -RETURNS TABLE("id" "uuid", "userId" "uuid", "content" "jsonb", "createdAt" timestamp with time zone, "similarity" double precision, "roomId" "uuid", "embedding" "extensions"."vector") -LANGUAGE "plpgsql" -AS $$ -DECLARE - query TEXT; -BEGIN - query := format($fmt$ - SELECT - id, - userId, - content, - createdAt, - 1 - (embedding <=> %L) AS similarity, -- Use '<=>' for cosine distance - roomId, - embedding - FROM memories - WHERE (1 - (embedding <=> %L) > %L) - AND type = %L - %s -- Additional condition for 'unique' column - %s -- Additional condition for 'roomId' - ORDER BY similarity DESC - LIMIT %L - $fmt$, - query_embedding, - query_embedding, - query_match_threshold, - query_table_name, - CASE WHEN query_unique THEN ' AND "unique" IS TRUE' ELSE '' END, - CASE WHEN query_roomId IS NOT NULL THEN format(' AND roomId = %L', query_roomId) ELSE '' END, - query_match_count - ); - - RETURN QUERY EXECUTE query; -END; -$$; - - - -ALTER FUNCTION "public"."search_memories"("query_table_name" "text", "query_roomId" "uuid", "query_embedding" "extensions"."vector", "query_match_threshold" double precision, "query_match_count" integer, "query_unique" boolean) OWNER TO "postgres"; - -CREATE TABLE IF NOT EXISTS "public"."accounts" ( - "id" "uuid" DEFAULT "auth"."uid"() NOT NULL, - "createdAt" timestamp with time zone DEFAULT ("now"() AT TIME ZONE 'utc'::"text") NOT NULL, - "name" "text", - "username" "text", - "email" "text" NOT NULL, - "avatarUrl" "text", - "is_agent" boolean DEFAULT false NOT NULL, - "location" "text", - "profile_line" "text", - "signed_tos" boolean DEFAULT false NOT NULL -); - -ALTER TABLE "public"."accounts" OWNER TO "postgres"; - -CREATE TABLE IF NOT EXISTS "public"."logs" ( - "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, - "createdAt" timestamp with time zone DEFAULT "now"() NOT NULL, - "userId" "uuid" NOT NULL, - "body" "jsonb" NOT NULL, - "type" "text" NOT NULL, - "roomId" "uuid" -); - -ALTER TABLE "public"."logs" OWNER TO "postgres"; - -CREATE TABLE IF NOT EXISTS "public"."memories" ( - "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, - "createdAt" timestamp with time zone DEFAULT "now"() NOT NULL, - "content" "jsonb" NOT NULL, - "embedding" "extensions"."vector" NOT NULL, - "userId" "uuid", - "roomId" "uuid", - "unique" boolean DEFAULT true NOT NULL, - "type" "text" NOT NULL -); - -ALTER TABLE "public"."memories" OWNER TO "postgres"; - -CREATE TABLE IF NOT EXISTS "public"."participants" ( - "createdAt" timestamp with time zone DEFAULT ("now"() AT TIME ZONE 'utc'::"text") NOT NULL, - "userId" "uuid", - "roomId" "uuid", - "userState" "text" DEFAULT NULL, -- Add userState field to track MUTED, NULL, or FOLLOWED - "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, - "last_message_read" "uuid", - FOREIGN KEY ("userId") REFERENCES "accounts"("id"), - FOREIGN KEY ("roomId") REFERENCES "rooms"("id") -); - - -ALTER TABLE "public"."participants" OWNER TO "postgres"; - - -CREATE OR REPLACE FUNCTION "public"."get_participant_userState"("roomId" "uuid", "userId" "uuid") -RETURNS "text" -LANGUAGE "plpgsql" -AS $$ -BEGIN - RETURN ( - SELECT userState - FROM participants - WHERE roomId = $1 AND userId = $2 - ); -END; -$$; - -CREATE OR REPLACE FUNCTION "public"."set_participant_userState"("roomId" "uuid", "userId" "uuid", "state" "text") -RETURNS "void" -LANGUAGE "plpgsql" -AS $$ -BEGIN - UPDATE participants - SET userState = $3 - WHERE roomId = $1 AND userId = $2; -END; -$$; - -CREATE TABLE IF NOT EXISTS "public"."rooms" ( - "id" "uuid" DEFAULT "gen_random_uuid"() NOT NULL, - "createdAt" timestamp with time zone DEFAULT ("now"() AT TIME ZONE 'utc'::"text") NOT NULL -); - -CREATE OR REPLACE FUNCTION "public"."search_knowledge"( - "query_embedding" "extensions"."vector", - "query_agent_id" "uuid", - "match_threshold" double precision, - "match_count" integer, - "search_text" text -) RETURNS TABLE ( - "id" "uuid", - "agentId" "uuid", - "content" "jsonb", - "embedding" "extensions"."vector", - "createdAt" timestamp with time zone, - "similarity" double precision -) LANGUAGE "plpgsql" AS $$ -BEGIN - RETURN QUERY - WITH vector_matches AS ( - SELECT id, - 1 - (embedding <=> query_embedding) as vector_score - FROM knowledge - WHERE (agentId IS NULL AND isShared = true) OR agentId = query_agent_id - AND embedding IS NOT NULL - ), - keyword_matches AS ( - SELECT id, - CASE - WHEN content->>'text' ILIKE '%' || search_text || '%' THEN 3.0 - ELSE 1.0 - END * - CASE - WHEN content->'metadata'->>'isChunk' = 'true' THEN 1.5 - WHEN content->'metadata'->>'isMain' = 'true' THEN 1.2 - ELSE 1.0 - END as keyword_score - FROM knowledge - WHERE (agentId IS NULL AND isShared = true) OR agentId = query_agent_id - ) - SELECT - k.id, - k."agentId", - k.content, - k.embedding, - k."createdAt", - (v.vector_score * kw.keyword_score) as similarity - FROM knowledge k - JOIN vector_matches v ON k.id = v.id - LEFT JOIN keyword_matches kw ON k.id = kw.id - WHERE (k.agentId IS NULL AND k.isShared = true) OR k.agentId = query_agent_id - AND ( - v.vector_score >= match_threshold - OR (kw.keyword_score > 1.0 AND v.vector_score >= 0.3) - ) - ORDER BY similarity DESC - LIMIT match_count; -END; -$$; - -ALTER TABLE "public"."rooms" OWNER TO "postgres"; - -ALTER TABLE ONLY "public"."relationships" - ADD CONSTRAINT "friendships_id_key" UNIQUE ("id"); - -ALTER TABLE ONLY "public"."relationships" - ADD CONSTRAINT "friendships_pkey" PRIMARY KEY ("id"); - -ALTER TABLE ONLY "public"."goals" - ADD CONSTRAINT "goals_pkey" PRIMARY KEY ("id"); - -ALTER TABLE ONLY "public"."logs" - ADD CONSTRAINT "logs_pkey" PRIMARY KEY ("id"); - -ALTER TABLE ONLY "public"."participants" - ADD CONSTRAINT "participants_id_key" UNIQUE ("id"); - -ALTER TABLE ONLY "public"."participants" - ADD CONSTRAINT "participants_pkey" PRIMARY KEY ("id"); - -ALTER TABLE ONLY "public"."memories" - ADD CONSTRAINT "memories_pkey" PRIMARY KEY ("id"); - -ALTER TABLE ONLY "public"."rooms" - ADD CONSTRAINT "rooms_pkey" PRIMARY KEY ("id"); - -ALTER TABLE ONLY "public"."accounts" - ADD CONSTRAINT "users_email_key" UNIQUE ("email"); - -ALTER TABLE ONLY "public"."accounts" - ADD CONSTRAINT "users_pkey" PRIMARY KEY ("id"); - -CREATE OR REPLACE TRIGGER "trigger_after_account_created" AFTER INSERT ON "public"."accounts" FOR EACH ROW EXECUTE FUNCTION "public"."after_account_created"(); - -CREATE OR REPLACE TRIGGER "trigger_create_friendship_with_host_agent" AFTER INSERT ON "public"."accounts" FOR EACH ROW EXECUTE FUNCTION "public"."create_friendship_with_host_agent"(); - -ALTER TABLE ONLY "public"."participants" - ADD CONSTRAINT "participants_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "public"."rooms"("id"); - -ALTER TABLE ONLY "public"."participants" - ADD CONSTRAINT "participants_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."accounts"("id"); - -ALTER TABLE ONLY "public"."memories" - ADD CONSTRAINT "memories_roomId_fkey" FOREIGN KEY ("roomId") REFERENCES "public"."rooms"("id"); - -ALTER TABLE ONLY "public"."memories" - ADD CONSTRAINT "memories_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."accounts"("id"); - -ALTER TABLE ONLY "public"."relationships" - ADD CONSTRAINT "relationships_userA_fkey" FOREIGN KEY ("userA") REFERENCES "public"."accounts"("id"); - -ALTER TABLE ONLY "public"."relationships" - ADD CONSTRAINT "relationships_userB_fkey" FOREIGN KEY ("userB") REFERENCES "public"."accounts"("id"); - -ALTER TABLE ONLY "public"."relationships" - ADD CONSTRAINT "relationships_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."accounts"("id"); - -ALTER TABLE ONLY "public"."knowledge" - ADD CONSTRAINT "knowledge_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "public"."accounts"("id") ON DELETE CASCADE; - -CREATE POLICY "Can select and update all data" ON "public"."accounts" USING (("auth"."uid"() = "id")) WITH CHECK (("auth"."uid"() = "id")); - -CREATE POLICY "Enable delete for users based on userId" ON "public"."goals" FOR DELETE TO "authenticated" USING (("auth"."uid"() = "userId")); - -CREATE POLICY "Enable insert for authenticated users only" ON "public"."accounts" FOR INSERT TO "authenticated", "anon", "service_role", "supabase_replication_admin", "supabase_read_only_user" WITH CHECK (true); - -CREATE POLICY "Enable insert for authenticated users only" ON "public"."goals" FOR INSERT TO "authenticated" WITH CHECK (true); - -CREATE POLICY "Enable insert for authenticated users only" ON "public"."logs" FOR INSERT TO "authenticated", "anon" WITH CHECK (true); - -CREATE POLICY "Enable insert for authenticated users only" ON "public"."participants" FOR INSERT TO "authenticated" WITH CHECK (true); - -CREATE POLICY "Enable insert for authenticated users only" ON "public"."relationships" FOR INSERT TO "authenticated" WITH CHECK ((("auth"."uid"() = "userA") OR ("auth"."uid"() = "userB"))); - -CREATE POLICY "Enable insert for authenticated users only" ON "public"."rooms" FOR INSERT WITH CHECK (true); - -CREATE POLICY "Enable insert for self id" ON "public"."participants" USING (("auth"."uid"() = "userId")) WITH CHECK (("auth"."uid"() = "userId")); - -CREATE POLICY "Enable read access for all users" ON "public"."accounts" FOR SELECT USING (true); - -CREATE POLICY "Enable read access for all users" ON "public"."goals" FOR SELECT USING (true); - -CREATE POLICY "Enable read access for all users" ON "public"."relationships" FOR SELECT TO "authenticated" USING (true); - -CREATE POLICY "Enable read access for all users" ON "public"."rooms" FOR SELECT TO "authenticated" USING (true); - -CREATE POLICY "Enable read access for own rooms" ON "public"."participants" FOR SELECT TO "authenticated" USING (("auth"."uid"() = "userId")); - -CREATE POLICY "Enable read access for user to their own relationships" ON "public"."relationships" FOR SELECT TO "authenticated" USING ((("auth"."uid"() = "userA") OR ("auth"."uid"() = "userB"))); - -CREATE POLICY "Enable update for users based on email" ON "public"."goals" FOR UPDATE TO "authenticated" USING (true) WITH CHECK (true); - -CREATE POLICY "Enable update for users of own id" ON "public"."rooms" FOR UPDATE USING (true) WITH CHECK (true); - -CREATE POLICY "Enable users to delete their own relationships/friendships" ON "public"."relationships" FOR DELETE TO "authenticated" USING ((("auth"."uid"() = "userA") OR ("auth"."uid"() = "userB"))); - -CREATE POLICY "Enable read access for all users" ON "public"."knowledge" - FOR SELECT USING (true); - -CREATE POLICY "Enable insert for authenticated users only" ON "public"."knowledge" - FOR INSERT TO "authenticated" WITH CHECK (true); - -CREATE POLICY "Enable update for authenticated users" ON "public"."knowledge" - FOR UPDATE TO "authenticated" USING (true) WITH CHECK (true); - -CREATE POLICY "Enable delete for users based on agentId" ON "public"."knowledge" - FOR DELETE TO "authenticated" USING (("auth"."uid"() = "agentId")); - -ALTER TABLE "public"."accounts" ENABLE ROW LEVEL SECURITY; - -ALTER TABLE "public"."goals" ENABLE ROW LEVEL SECURITY; - -ALTER TABLE "public"."logs" ENABLE ROW LEVEL SECURITY; - -ALTER TABLE "public"."memories" ENABLE ROW LEVEL SECURITY; - -ALTER TABLE "public"."participants" ENABLE ROW LEVEL SECURITY; - -ALTER TABLE "public"."relationships" ENABLE ROW LEVEL SECURITY; - -ALTER TABLE "public"."rooms" ENABLE ROW LEVEL SECURITY; - -ALTER TABLE "public"."knowledge" ENABLE ROW LEVEL SECURITY; - -CREATE POLICY "select_own_account" ON "public"."accounts" FOR SELECT USING (("auth"."uid"() = "id")); - -GRANT USAGE ON SCHEMA "public" TO "postgres"; -GRANT USAGE ON SCHEMA "public" TO "authenticated"; -GRANT USAGE ON SCHEMA "public" TO "service_role"; -GRANT USAGE ON SCHEMA "public" TO "supabase_admin"; -GRANT USAGE ON SCHEMA "public" TO "supabase_auth_admin"; - -GRANT ALL ON FUNCTION "public"."after_account_created"() TO "authenticated"; -GRANT ALL ON FUNCTION "public"."after_account_created"() TO "service_role"; -GRANT ALL ON FUNCTION "public"."after_account_created"() TO "supabase_admin"; -GRANT ALL ON FUNCTION "public"."after_account_created"() TO "supabase_auth_admin"; - -GRANT ALL ON FUNCTION "public"."count_memories"("query_table_name" "text", "query_roomId" "uuid", "query_unique" boolean) TO "authenticated"; -GRANT ALL ON FUNCTION "public"."count_memories"("query_table_name" "text", "query_roomId" "uuid", "query_unique" boolean) TO "service_role"; -GRANT ALL ON FUNCTION "public"."count_memories"("query_table_name" "text", "query_roomId" "uuid", "query_unique" boolean) TO "supabase_admin"; -GRANT ALL ON FUNCTION "public"."count_memories"("query_table_name" "text", "query_roomId" "uuid", "query_unique" boolean) TO "supabase_auth_admin"; - -GRANT ALL ON FUNCTION "public"."create_friendship_with_host_agent"() TO "authenticated"; -GRANT ALL ON FUNCTION "public"."create_friendship_with_host_agent"() TO "service_role"; -GRANT ALL ON FUNCTION "public"."create_friendship_with_host_agent"() TO "supabase_admin"; -GRANT ALL ON FUNCTION "public"."create_friendship_with_host_agent"() TO "supabase_auth_admin"; - -GRANT ALL ON FUNCTION "public"."fn_notify_agents"() TO "authenticated"; -GRANT ALL ON FUNCTION "public"."fn_notify_agents"() TO "service_role"; -GRANT ALL ON FUNCTION "public"."fn_notify_agents"() TO "supabase_admin"; -GRANT ALL ON FUNCTION "public"."fn_notify_agents"() TO "supabase_auth_admin"; - -GRANT ALL ON FUNCTION "public"."get_embedding_list"("query_table_name" "text", "query_threshold" integer, "query_input" "text", "query_field_name" "text", "query_field_sub_name" "text", "query_match_count" integer) TO "authenticated"; -GRANT ALL ON FUNCTION "public"."get_embedding_list"("query_table_name" "text", "query_threshold" integer, "query_input" "text", "query_field_name" "text", "query_field_sub_name" "text", "query_match_count" integer) TO "service_role"; -GRANT ALL ON FUNCTION "public"."get_embedding_list"("query_table_name" "text", "query_threshold" integer, "query_input" "text", "query_field_name" "text", "query_field_sub_name" "text", "query_match_count" integer) TO "supabase_admin"; -GRANT ALL ON FUNCTION "public"."get_embedding_list"("query_table_name" "text", "query_threshold" integer, "query_input" "text", "query_field_name" "text", "query_field_sub_name" "text", "query_match_count" integer) TO "supabase_auth_admin"; - -GRANT ALL ON TABLE "public"."goals" TO "authenticated"; -GRANT ALL ON TABLE "public"."goals" TO "service_role"; -GRANT ALL ON TABLE "public"."goals" TO "supabase_admin"; -GRANT ALL ON TABLE "public"."goals" TO "supabase_auth_admin"; - -GRANT ALL ON FUNCTION "public"."get_goals"("query_roomId" "uuid", "query_userId" "uuid", "only_in_progress" boolean, "row_count" integer) TO "authenticated"; -GRANT ALL ON FUNCTION "public"."get_goals"("query_roomId" "uuid", "query_userId" "uuid", "only_in_progress" boolean, "row_count" integer) TO "service_role"; -GRANT ALL ON FUNCTION "public"."get_goals"("query_roomId" "uuid", "query_userId" "uuid", "only_in_progress" boolean, "row_count" integer) TO "supabase_admin"; -GRANT ALL ON FUNCTION "public"."get_goals"("query_roomId" "uuid", "query_userId" "uuid", "only_in_progress" boolean, "row_count" integer) TO "supabase_auth_admin"; - -GRANT ALL ON TABLE "public"."relationships" TO "authenticated"; -GRANT ALL ON TABLE "public"."relationships" TO "service_role"; -GRANT ALL ON TABLE "public"."relationships" TO "supabase_admin"; -GRANT ALL ON TABLE "public"."relationships" TO "supabase_auth_admin"; - -GRANT ALL ON FUNCTION "public"."get_relationship"("usera" "uuid", "userb" "uuid") TO "authenticated"; -GRANT ALL ON FUNCTION "public"."get_relationship"("usera" "uuid", "userb" "uuid") TO "service_role"; -GRANT ALL ON FUNCTION "public"."get_relationship"("usera" "uuid", "userb" "uuid") TO "supabase_admin"; -GRANT ALL ON FUNCTION "public"."get_relationship"("usera" "uuid", "userb" "uuid") TO "supabase_auth_admin"; - -GRANT ALL ON FUNCTION "public"."remove_memories"("query_table_name" "text", "query_roomId" "uuid") TO "authenticated"; -GRANT ALL ON FUNCTION "public"."remove_memories"("query_table_name" "text", "query_roomId" "uuid") TO "service_role"; -GRANT ALL ON FUNCTION "public"."remove_memories"("query_table_name" "text", "query_roomId" "uuid") TO "supabase_admin"; -GRANT ALL ON FUNCTION "public"."remove_memories"("query_table_name" "text", "query_roomId" "uuid") TO "supabase_auth_admin"; - -GRANT ALL ON TABLE "public"."accounts" TO "authenticated"; -GRANT ALL ON TABLE "public"."accounts" TO "service_role"; -GRANT SELECT,INSERT ON TABLE "public"."accounts" TO "authenticator"; -GRANT ALL ON TABLE "public"."accounts" TO "supabase_admin"; -GRANT ALL ON TABLE "public"."accounts" TO "supabase_auth_admin"; - -GRANT ALL ON TABLE "public"."logs" TO "authenticated"; -GRANT ALL ON TABLE "public"."logs" TO "service_role"; -GRANT ALL ON TABLE "public"."logs" TO "supabase_admin"; -GRANT ALL ON TABLE "public"."logs" TO "supabase_auth_admin"; - -GRANT ALL ON TABLE "public"."memories" TO "authenticated"; -GRANT ALL ON TABLE "public"."memories" TO "service_role"; -GRANT ALL ON TABLE "public"."memories" TO "supabase_admin"; -GRANT ALL ON TABLE "public"."memories" TO "supabase_auth_admin"; - -GRANT ALL ON TABLE "public"."participants" TO "authenticated"; -GRANT ALL ON TABLE "public"."participants" TO "service_role"; -GRANT ALL ON TABLE "public"."participants" TO "supabase_admin"; -GRANT ALL ON TABLE "public"."participants" TO "supabase_auth_admin"; - -GRANT ALL ON TABLE "public"."rooms" TO "authenticated"; -GRANT ALL ON TABLE "public"."rooms" TO "service_role"; -GRANT ALL ON TABLE "public"."rooms" TO "supabase_admin"; -GRANT ALL ON TABLE "public"."rooms" TO "supabase_auth_admin"; - -GRANT ALL ON TABLE "public"."secrets" TO "authenticated"; -GRANT ALL ON TABLE "public"."secrets" TO "service_role"; -GRANT ALL ON TABLE "public"."secrets" TO "supabase_admin"; -GRANT ALL ON TABLE "public"."secrets" TO "supabase_auth_admin"; - -GRANT ALL ON TABLE "public"."knowledge" TO "authenticated"; -GRANT ALL ON TABLE "public"."knowledge" TO "service_role"; -GRANT ALL ON TABLE "public"."knowledge" TO "supabase_admin"; -GRANT ALL ON TABLE "public"."knowledge" TO "supabase_auth_admin"; - -GRANT ALL ON FUNCTION "public"."get_participant_userState"("roomId" "uuid", "userId" "uuid") TO "authenticated"; -GRANT ALL ON FUNCTION "public"."get_participant_userState"("roomId" "uuid", "userId" "uuid") TO "service_role"; -GRANT ALL ON FUNCTION "public"."get_participant_userState"("roomId" "uuid", "userId" "uuid") TO "supabase_admin"; -GRANT ALL ON FUNCTION "public"."get_participant_userState"("roomId" "uuid", "userId" "uuid") TO "supabase_auth_admin"; - -GRANT ALL ON FUNCTION "public"."set_participant_userState"("roomId" "uuid", "userId" "uuid", "state" "text") TO "authenticated"; -GRANT ALL ON FUNCTION "public"."set_participant_userState"("roomId" "uuid", "userId" "uuid", "state" "text") TO "service_role"; -GRANT ALL ON FUNCTION "public"."set_participant_userState"("roomId" "uuid", "userId" "uuid", "state" "text") TO "supabase_admin"; -GRANT ALL ON FUNCTION "public"."set_participant_userState"("roomId" "uuid", "userId" "uuid", "state" "text") TO "supabase_auth_admin"; - - -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "postgres"; -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "authenticated"; -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "service_role"; -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "supabase_admin"; -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON SEQUENCES TO "supabase_auth_admin"; - -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "postgres"; -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "authenticated"; -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "service_role"; -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "supabase_admin"; -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON FUNCTIONS TO "supabase_auth_admin"; - -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "postgres"; -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "authenticated"; -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "service_role"; -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "supabase_admin"; -ALTER DEFAULT PRIVILEGES FOR ROLE "postgres" IN SCHEMA "public" GRANT ALL ON TABLES TO "supabase_auth_admin"; - -GRANT ALL ON FUNCTION "public"."search_knowledge"("query_embedding" "extensions"."vector", "query_agent_id" "uuid", "match_threshold" double precision, "match_count" integer, "search_text" text) TO "authenticated"; -GRANT ALL ON FUNCTION "public"."search_knowledge"("query_embedding" "extensions"."vector", "query_agent_id" "uuid", "match_threshold" double precision, "match_count" integer, "search_text" text) TO "service_role"; -GRANT ALL ON FUNCTION "public"."search_knowledge"("query_embedding" "extensions"."vector", "query_agent_id" "uuid", "match_threshold" double precision, "match_count" integer, "search_text" text) TO "supabase_admin"; -GRANT ALL ON FUNCTION "public"."search_knowledge"("query_embedding" "extensions"."vector", "query_agent_id" "uuid", "match_threshold" double precision, "match_count" integer, "search_text" text) TO "supabase_auth_admin"; - -RESET ALL; \ No newline at end of file diff --git a/packages/plugin-postgres/package.json b/packages/plugin-postgres/package.json deleted file mode 100644 index 28dcb925372..00000000000 --- a/packages/plugin-postgres/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@elizaos-plugins/postgres", - "version": "0.1.9", - "type": "module", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "exports": { - "./package.json": "./package.json", - ".": { - "import": { - "@elizaos/source": "./src/index.ts", - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - } - }, - "files": [ - "dist", - "schema.sql", - "seed.sql" - ], - "dependencies": { - "@elizaos/core": "workspace:*", - "@types/pg": "8.11.10", - "pg": "8.13.1" - }, - "devDependencies": { - "tsup": "8.3.5" - }, - "scripts": { - "build": "tsup --format esm --dts", - "dev": "tsup --format esm --dts --watch" - } -} diff --git a/packages/plugin-postgres/seed.sql b/packages/plugin-postgres/seed.sql deleted file mode 100644 index 7901408d1de..00000000000 --- a/packages/plugin-postgres/seed.sql +++ /dev/null @@ -1,9 +0,0 @@ - -INSERT INTO public.accounts (id, name, email, "avatarUrl") -VALUES ('00000000-0000-0000-0000-000000000000', 'Default Agent', 'default@agent.com', ''); - -INSERT INTO public.rooms (id) -VALUES ('00000000-0000-0000-0000-000000000000'); - -INSERT INTO public.participants (id, "userId", "roomId") -VALUES ('00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000000', '00000000-0000-0000-0000-000000000000'); diff --git a/packages/plugin-postgres/src/__tests__/README.md b/packages/plugin-postgres/src/__tests__/README.md deleted file mode 100644 index 98896ff4f2b..00000000000 --- a/packages/plugin-postgres/src/__tests__/README.md +++ /dev/null @@ -1,71 +0,0 @@ -# PostgreSQL Adapter Tests - -This directory contains tests for the PostgreSQL adapter with vector extension support. - -## Prerequisites - -- Docker installed and running -- Node.js and pnpm installed -- Bash shell (for Unix/Mac) or Git Bash (for Windows) - -## Test Environment - -The tests run against a PostgreSQL instance with the `pgvector` extension enabled. We use Docker to ensure a consistent test environment: - -- PostgreSQL 16 with pgvector extension -- Test database: `eliza_test` -- Port: 5433 (to avoid conflicts with local PostgreSQL) -- Vector dimensions: 1536 (OpenAI compatible) - -## Running Tests - -The easiest way to run tests is using the provided script: - -```bash -./run_tests.sh -``` - -This script will: -1. Start the PostgreSQL container with vector extension -2. Wait for the database to be ready -3. Run the test suite - -## Manual Setup - -If you prefer to run tests manually: - -1. Start the test database: - ```bash - docker compose -f docker-compose.test.yml up -d - ``` - -2. Wait for the database to be ready (about 30 seconds) - -3. Run tests: - ```bash - pnpm vitest vector-extension.test.ts - ``` - -## Test Structure - -- `vector-extension.test.ts`: Main test suite for vector operations -- `docker-compose.test.yml`: Docker configuration for test database -- `run_tests.sh`: Helper script to run tests - -## Troubleshooting - -1. If tests fail with connection errors: - - Check if Docker is running - - Verify port 5433 is available - - Wait a bit longer for database initialization - -2. If vector operations fail: - - Check if pgvector extension is properly loaded - - Verify schema initialization - - Check vector dimensions match (1536 for OpenAI) - -## Notes - -- Tests automatically clean up after themselves -- Each test run starts with a fresh database -- Vector extension is initialized as part of the schema setup \ No newline at end of file diff --git a/packages/plugin-postgres/src/__tests__/docker-compose.test.yml b/packages/plugin-postgres/src/__tests__/docker-compose.test.yml deleted file mode 100644 index 7a589ec1926..00000000000 --- a/packages/plugin-postgres/src/__tests__/docker-compose.test.yml +++ /dev/null @@ -1,16 +0,0 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json -version: '3.8' -services: - postgres-test: - image: pgvector/pgvector:pg16 - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: eliza_test - ports: - - "5433:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 5s - timeout: 5s - retries: 5 \ No newline at end of file diff --git a/packages/plugin-postgres/src/__tests__/run_tests.sh b/packages/plugin-postgres/src/__tests__/run_tests.sh deleted file mode 100755 index 8abe9af4a0c..00000000000 --- a/packages/plugin-postgres/src/__tests__/run_tests.sh +++ /dev/null @@ -1,78 +0,0 @@ -#!/bin/bash - -# Color output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Get script directory -SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -SCHEMA_PATH="$SCRIPT_DIR/../../schema.sql" - -echo -e "${YELLOW}Starting PostgreSQL test environment...${NC}" - -# Determine Docker Compose command -if [[ "$OSTYPE" == "darwin"* ]]; then - DOCKER_COMPOSE_CMD="docker compose" -else - DOCKER_COMPOSE_CMD="docker-compose" -fi - -# Stop any existing containers -echo -e "${YELLOW}Cleaning up existing containers...${NC}" -$DOCKER_COMPOSE_CMD -f docker-compose.test.yml down - -# Start fresh container -echo -e "${YELLOW}Starting PostgreSQL container...${NC}" -$DOCKER_COMPOSE_CMD -f docker-compose.test.yml up -d - -# Function to check if PostgreSQL is ready -check_postgres() { - $DOCKER_COMPOSE_CMD -f docker-compose.test.yml exec -T postgres-test pg_isready -U postgres -} - -# Wait for PostgreSQL to be ready -echo -e "${YELLOW}Waiting for PostgreSQL to be ready...${NC}" -RETRIES=30 -until check_postgres || [ $RETRIES -eq 0 ]; do - echo -e "${YELLOW}Waiting for PostgreSQL to be ready... ($RETRIES attempts left)${NC}" - RETRIES=$((RETRIES-1)) - sleep 1 -done - -if [ $RETRIES -eq 0 ]; then - echo -e "${RED}Failed to connect to PostgreSQL${NC}" - $DOCKER_COMPOSE_CMD -f docker-compose.test.yml logs - exit 1 -fi - -echo -e "${GREEN}PostgreSQL is ready!${NC}" - -# Load schema -echo -e "${YELLOW}Loading database schema...${NC}" -if [ ! -f "$SCHEMA_PATH" ]; then - echo -e "${RED}Schema file not found at: $SCHEMA_PATH${NC}" - exit 1 -fi - -# Fix: Check exit code directly instead of using $? -if ! $DOCKER_COMPOSE_CMD -f docker-compose.test.yml exec -T postgres-test psql -U postgres -d eliza_test -f - < "$SCHEMA_PATH"; then - echo -e "${RED}Failed to load schema${NC}" - exit 1 -fi -echo -e "${GREEN}Schema loaded successfully!${NC}" - -# Run the tests -echo -e "${YELLOW}Running tests...${NC}" -if ! bun run vitest vector-extension.test.ts; then - echo -e "${RED}Tests failed!${NC}" - $DOCKER_COMPOSE_CMD -f docker-compose.test.yml down - exit 1 -fi - -echo -e "${GREEN}Tests completed successfully!${NC}" - -# Clean up -echo -e "${YELLOW}Cleaning up test environment...${NC}" -$DOCKER_COMPOSE_CMD -f docker-compose.test.yml down \ No newline at end of file diff --git a/packages/plugin-postgres/src/__tests__/vector-extension.test.ts b/packages/plugin-postgres/src/__tests__/vector-extension.test.ts deleted file mode 100644 index a3b912935f6..00000000000 --- a/packages/plugin-postgres/src/__tests__/vector-extension.test.ts +++ /dev/null @@ -1,416 +0,0 @@ -import { PostgresDatabaseAdapter } from '../index'; -import pg from 'pg'; -import fs from 'fs'; -import path from 'path'; -import { describe, test, expect, beforeEach, afterEach, vi, beforeAll } from 'vitest'; -import { logger, type Memory, type Content } from '@elizaos/core'; - -// Increase test timeout -vi.setConfig({ testTimeout: 15000 }); - -// Mock the @elizaos/core module -vi.mock('@elizaos/core', () => ({ - logger: { - error: vi.fn().mockImplementation(console.error), - info: vi.fn().mockImplementation(console.log), - success: vi.fn().mockImplementation(console.log), - debug: vi.fn().mockImplementation(console.log), - warn: vi.fn().mockImplementation(console.warn), - }, - DatabaseAdapter: class { - protected circuitBreaker = { - execute: async <T>(operation: () => Promise<T>) => operation() - }; - protected async withCircuitBreaker<T>(operation: () => Promise<T>) { - return this.circuitBreaker.execute(operation); - } - }, - EmbeddingProvider: { - OpenAI: 'OpenAI', - Ollama: 'Ollama', - BGE: 'BGE' - } -})); - -// Helper function to parse vector string from PostgreSQL -const parseVectorString = (vectorStr: string): number[] => { - if (!vectorStr) return []; - // Remove brackets and split by comma - return vectorStr.replace(/[[\]]/g, '').split(',').map(Number); -}; - -describe('PostgresDatabaseAdapter - Vector Extension Validation', () => { - let adapter: PostgresDatabaseAdapter; - let testClient: pg.PoolClient; - let testPool: pg.Pool; - - const initializeDatabase = async (client: pg.PoolClient) => { - logger.info('Initializing database with schema...'); - try { - // Read and execute schema file - const schemaPath = path.resolve(__dirname, '../../schema.sql'); - const schema = fs.readFileSync(schemaPath, 'utf8'); - await client.query(schema); - - // Verify schema setup - const { rows: vectorExt } = await client.query(` - SELECT * FROM pg_extension WHERE extname = 'vector' - `); - logger.info('Vector extension status:', { isInstalled: vectorExt.length > 0 }); - - const { rows: dimension } = await client.query('SELECT get_embedding_dimension()'); - logger.info('Vector dimension:', { dimension: dimension[0].get_embedding_dimension }); - - // Verify search path - const { rows: searchPath } = await client.query('SHOW search_path'); - logger.info('Search path:', { searchPath: searchPath[0].search_path }); - - } catch (error) { - logger.error(`Database initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - throw error; - } - }; - - const cleanDatabase = async (client: pg.PoolClient) => { - logger.info('Starting database cleanup...'); - try { - await client.query('DROP TABLE IF EXISTS relationships CASCADE'); - await client.query('DROP TABLE IF EXISTS participants CASCADE'); - await client.query('DROP TABLE IF EXISTS logs CASCADE'); - await client.query('DROP TABLE IF EXISTS goals CASCADE'); - await client.query('DROP TABLE IF EXISTS memories CASCADE'); - await client.query('DROP TABLE IF EXISTS rooms CASCADE'); - await client.query('DROP TABLE IF EXISTS accounts CASCADE'); - await client.query('DROP TABLE IF EXISTS cache CASCADE'); - await client.query('DROP EXTENSION IF EXISTS vector CASCADE'); - await client.query('DROP SCHEMA IF EXISTS extensions CASCADE'); - logger.success('Database cleanup completed successfully'); - } catch (error) { - logger.error(`Database cleanup failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - throw error; - } - }; - - beforeAll(async () => { - logger.info('Setting up test database...'); - const setupPool = new pg.Pool({ - host: 'localhost', - port: 5433, - database: 'eliza_test', - user: 'postgres', - password: 'postgres' - }); - - const setupClient = await setupPool.connect(); - try { - await cleanDatabase(setupClient); - await initializeDatabase(setupClient); - } finally { - await setupClient.release(); - await setupPool.end(); - } - }); - - beforeEach(async () => { - logger.info('Setting up test environment...'); - try { - // Setup test database connection - testPool = new pg.Pool({ - host: 'localhost', - port: 5433, - database: 'eliza_test', - user: 'postgres', - password: 'postgres' - }); - - testClient = await testPool.connect(); - logger.debug('Database connection established'); - - await cleanDatabase(testClient); - logger.debug('Database cleaned'); - - adapter = new PostgresDatabaseAdapter({ - host: 'localhost', - port: 5433, - database: 'eliza_test', - user: 'postgres', - password: 'postgres' - }); - logger.success('Test environment setup completed'); - } catch (error) { - logger.error(`Test environment setup failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - throw error; - } - }); - - afterEach(async () => { - logger.info('Cleaning up test environment...'); - try { - await cleanDatabase(testClient); - await testClient?.release(); - await testPool?.end(); - await adapter?.close(); - logger.success('Test environment cleanup completed'); - } catch (error) { - logger.error(`Test environment cleanup failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - throw error; - } - }); - - describe('Schema and Extension Management', () => { - test('should initialize with vector extension', async () => { - logger.info('Testing vector extension initialization...'); - try { - // Act - logger.debug('Initializing adapter...'); - await adapter.init(); - logger.success('Adapter initialized successfully'); - - // Assert - logger.debug('Verifying vector extension existence...'); - const { rows } = await testClient.query(` - SELECT 1 FROM pg_extension WHERE extname = 'vector' - `); - expect(rows.length).toBe(1); - logger.success('Vector extension verified successfully'); - } catch (error) { - logger.error(`Vector extension test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - throw error; - } - }); - - test('should handle missing rooms table', async () => { - logger.info('Testing rooms table creation...'); - try { - // Act - logger.debug('Initializing adapter...'); - await adapter.init(); - logger.success('Adapter initialized successfully'); - - // Assert - logger.debug('Verifying rooms table existence...'); - const { rows } = await testClient.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_name = 'rooms' - ); - `); - expect(rows[0].exists).toBe(true); - logger.success('Rooms table verified successfully'); - } catch (error) { - logger.error(`Rooms table test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - throw error; - } - }); - - test('should not reapply schema when everything exists', async () => { - logger.info('Testing schema reapplication prevention...'); - try { - // Arrange - logger.debug('Setting up initial schema...'); - await adapter.init(); - logger.success('Initial schema setup completed'); - - const spy = vi.spyOn(fs, 'readFileSync'); - logger.debug('File read spy installed'); - - // Act - logger.debug('Attempting schema reapplication...'); - await adapter.init(); - logger.success('Second initialization completed'); - - // Assert - expect(spy).not.toHaveBeenCalled(); - logger.success('Verified schema was not reapplied'); - spy.mockRestore(); - } catch (error) { - logger.error(`Schema reapplication test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - throw error; - } - }); - - test('should handle transaction rollback on error', async () => { - logger.info('Testing transaction rollback...'); - try { - // Arrange - logger.debug('Setting up file read error simulation...'); - const spy = vi.spyOn(fs, 'readFileSync').mockImplementation(() => { - logger.warn('Simulating schema read error'); - throw new Error('Schema read error'); - }); - - // Act & Assert - logger.debug('Attempting initialization with error...'); - await expect(adapter.init()).rejects.toThrow('Schema read error'); - logger.success('Error thrown as expected'); - - // Verify no tables were created - logger.debug('Verifying rollback...'); - const { rows } = await testClient.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_name = 'rooms' - ); - `); - expect(rows[0].exists).toBe(false); - logger.success('Rollback verified successfully'); - spy.mockRestore(); - } catch (error) { - logger.error(`Transaction rollback test failed: ${error instanceof Error ? error.message : 'Unknown error'}`); - throw error; - } - }); - }); - - // Memory Operations tests will be updated in the next iteration - describe('Memory Operations with Vector', () => { - const TEST_UUID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; - const TEST_TABLE = 'test_memories'; - - beforeEach(async () => { - logger.info('Setting up memory operations test...'); - try { - // Ensure clean state and proper initialization - await adapter.init(); - - // Create necessary account and room first - await testClient.query('BEGIN'); - try { - await testClient.query(` - INSERT INTO accounts (id, email) - VALUES ($1, 'test@test.com') - ON CONFLICT (id) DO NOTHING - `, [TEST_UUID]); - - await testClient.query(` - INSERT INTO rooms (id) - VALUES ($1) - ON CONFLICT (id) DO NOTHING - `, [TEST_UUID]); - - await testClient.query('COMMIT'); - } catch (error) { - await testClient.query('ROLLBACK'); - throw error; - } - - } catch (error) { - logger.error('Memory operations setup failed:', { - error: error instanceof Error ? error.message : String(error) - }); - throw error; - } - }); - - test('should create and retrieve memory with vector embedding', async () => { - // Arrange - const content: Content = { - text: 'test content' - }; - - const memory: Memory = { - id: TEST_UUID, - content, - embedding: new Array(1536).fill(0.1), - unique: true, - userId: TEST_UUID, - agentId: TEST_UUID, - roomId: TEST_UUID, - createdAt: Date.now() - }; - - // Act - await testClient.query('BEGIN'); - try { - await adapter.createMemory(memory, TEST_TABLE); - await testClient.query('COMMIT'); - } catch (error) { - await testClient.query('ROLLBACK'); - throw error; - } - - // Verify the embedding dimension - const { rows: [{ get_embedding_dimension }] } = await testClient.query('SELECT get_embedding_dimension()'); - expect(get_embedding_dimension).toBe(1536); - - // Retrieve and verify - const retrieved = await adapter.getMemoryById(TEST_UUID); - expect(retrieved).toBeDefined(); - const parsedEmbedding = typeof retrieved?.embedding === 'string' ? parseVectorString(retrieved.embedding) : retrieved?.embedding; - expect(Array.isArray(parsedEmbedding)).toBe(true); - expect(parsedEmbedding).toHaveLength(1536); - expect(retrieved?.content).toEqual(content); - }); - - test('should search memories by embedding', async () => { - // Arrange - const content: Content = { text: 'test content' }; - const embedding = new Array(1536).fill(0.1); - const memory: Memory = { - id: TEST_UUID, - content, - embedding, - unique: true, - userId: TEST_UUID, - agentId: TEST_UUID, - roomId: TEST_UUID, - createdAt: Date.now() - }; - - // Create memory within transaction - await testClient.query('BEGIN'); - try { - await adapter.createMemory(memory, TEST_TABLE); - await testClient.query('COMMIT'); - } catch (error) { - await testClient.query('ROLLBACK'); - throw error; - } - - // Act - const results = await adapter.searchMemoriesByEmbedding(embedding, { - tableName: TEST_TABLE, - roomId: TEST_UUID, - match_threshold: 0.8, - count: 1 - }); - - // Assert - expect(results).toBeDefined(); - expect(Array.isArray(results)).toBe(true); - expect(results.length).toBeGreaterThan(0); - const parsedEmbedding = typeof results[0].embedding === 'string' ? parseVectorString(results[0].embedding) : results[0].embedding; - expect(parsedEmbedding).toHaveLength(1536); - }); - - test('should handle invalid embedding dimensions', async () => { - // Arrange - const content: Content = { - text: 'test content' - }; - - const memory: Memory = { - id: TEST_UUID, - content, - embedding: new Array(100).fill(0.1), // Wrong dimension - unique: true, - userId: TEST_UUID, - agentId: TEST_UUID, - roomId: TEST_UUID, - createdAt: Date.now() - }; - - // Act & Assert - await testClient.query('BEGIN'); - try { - await expect(adapter.createMemory(memory, TEST_TABLE)) - .rejects - .toThrow('Invalid embedding dimension: expected 1536, got 100'); - await testClient.query('ROLLBACK'); - } catch (error) { - await testClient.query('ROLLBACK'); - throw error; - } - }, { timeout: 30000 }); // Increased timeout for retry attempts - }); -}); \ No newline at end of file diff --git a/packages/plugin-postgres/src/index.ts b/packages/plugin-postgres/src/index.ts deleted file mode 100644 index 6009f8b5b6d..00000000000 --- a/packages/plugin-postgres/src/index.ts +++ /dev/null @@ -1,1437 +0,0 @@ -import { v4 } from "uuid"; - -// Import the entire module as default -import pg from "pg"; -type Pool = pg.Pool; - -import { - type Account, - type Actor, - DatabaseAdapter, - type GoalStatus, - type Participant, - logger, - type Goal, - type IDatabaseCacheAdapter, - type Memory, - type Relationship, - type UUID, -} from "@elizaos/core"; -import fs from "fs"; -import path from "path"; -import type { - QueryConfig, - QueryConfigValues, - QueryResult, - QueryResultRow, -} from "pg"; -import { fileURLToPath } from "url"; - -const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file -const __dirname = path.dirname(__filename); // get the name of the directory - -export class PostgresDatabaseAdapter - extends DatabaseAdapter<Pool> - implements IDatabaseCacheAdapter -{ - private pool: Pool; - private readonly maxRetries: number = 3; - private readonly baseDelay: number = 1000; // 1 second - private readonly maxDelay: number = 10000; // 10 seconds - private readonly jitterMax: number = 1000; // 1 second - private readonly connectionTimeout: number = 5000; // 5 seconds - - constructor(connectionConfig: any) { - super(); - - const defaultConfig = { - max: 20, - idleTimeoutMillis: 30000, - connectionTimeoutMillis: this.connectionTimeout, - }; - - this.pool = new pg.Pool({ - ...defaultConfig, - ...connectionConfig, // Allow overriding defaults - }); - - this.pool.on("error", (err) => { - logger.error("Unexpected pool error", err); - this.handlePoolError(err); - }); - - this.setupPoolErrorHandling(); - this.testConnection(); - } - - private setupPoolErrorHandling() { - process.on("SIGINT", async () => { - await this.cleanup(); - process.exit(0); - }); - - process.on("SIGTERM", async () => { - await this.cleanup(); - process.exit(0); - }); - - process.on("beforeExit", async () => { - await this.cleanup(); - }); - } - - private async withDatabase<T>( - operation: () => Promise<T>, - context: string - ): Promise<T> { - return this.withRetry(operation); - } - - private async withRetry<T>(operation: () => Promise<T>): Promise<T> { - let lastError: Error = new Error("Unknown error"); // Initialize with default - - for (let attempt = 1; attempt <= this.maxRetries; attempt++) { - try { - return await operation(); - } catch (error) { - lastError = error as Error; - - if (attempt < this.maxRetries) { - // Calculate delay with exponential backoff - const backoffDelay = Math.min( - this.baseDelay * Math.pow(2, attempt - 1), - this.maxDelay - ); - - // Add jitter to prevent thundering herd - const jitter = Math.random() * this.jitterMax; - const delay = backoffDelay + jitter; - - logger.warn( - `Database operation failed (attempt ${attempt}/${this.maxRetries}):`, - { - error: - error instanceof Error - ? error.message - : String(error), - nextRetryIn: `${(delay / 1000).toFixed(1)}s`, - } - ); - - await new Promise((resolve) => setTimeout(resolve, delay)); - } else { - logger.error("Max retry attempts reached:", { - error: - error instanceof Error - ? error.message - : String(error), - totalAttempts: attempt, - }); - throw error instanceof Error - ? error - : new Error(String(error)); - } - } - } - - throw lastError; - } - - private async handlePoolError(error: Error) { - logger.error("Pool error occurred, attempting to reconnect", { - error: error.message, - }); - - try { - // Close existing pool - await this.pool.end(); - - // Create new pool - this.pool = new pg.Pool({ - ...this.pool.options, - connectionTimeoutMillis: this.connectionTimeout, - }); - - await this.testConnection(); - logger.success("Pool reconnection successful"); - } catch (reconnectError) { - logger.error("Failed to reconnect pool", { - error: - reconnectError instanceof Error - ? reconnectError.message - : String(reconnectError), - }); - throw reconnectError; - } - } - - async query<R extends QueryResultRow = any, I = any[]>( - queryTextOrConfig: string | QueryConfig<I>, - values?: QueryConfigValues<I> - ): Promise<QueryResult<R>> { - return this.withDatabase(async () => { - return await this.pool.query(queryTextOrConfig, values); - }, "query"); - } - - private async validateVectorSetup(): Promise<boolean> { - try { - const vectorExt = await this.query(` - SELECT 1 FROM pg_extension WHERE extname = 'vector' - `); - const hasVector = vectorExt.rows.length > 0; - - if (!hasVector) { - logger.error("Vector extension not found in database"); - return false; - } - - return true; - } catch (error) { - logger.error("Failed to validate vector extension:", { - error: error instanceof Error ? error.message : String(error), - }); - return false; - } - } - - async init() { - await this.testConnection(); - - const client = await this.pool.connect(); - try { - await client.query("BEGIN"); - - // Check if schema already exists (check for a core table) - const { rows } = await client.query(` - SELECT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_name = 'rooms' - ); - `); - - if (!rows[0].exists || !(await this.validateVectorSetup())) { - logger.info( - "Applying database schema - tables or vector extension missing" - ); - const schema = fs.readFileSync( - path.resolve(__dirname, "../schema.sql"), - "utf8" - ); - await client.query(schema); - } - - await client.query("COMMIT"); - } catch (error) { - await client.query("ROLLBACK"); - throw error; - } finally { - client.release(); - } - } - - async close() { - await this.pool.end(); - } - - async testConnection(): Promise<boolean> { - let client; - try { - client = await this.pool.connect(); - const result = await client.query("SELECT NOW()"); - logger.success( - "Database connection test successful:", - result.rows[0] - ); - return true; - } catch (error) { - logger.error("Database connection test failed:", error); - throw new Error( - `Failed to connect to database: ${(error as Error).message}` - ); - } finally { - if (client) client.release(); - } - } - - async cleanup(): Promise<void> { - try { - await this.pool.end(); - logger.info("Database pool closed"); - } catch (error) { - logger.error("Error closing database pool:", error); - } - } - - async getRoom(roomId: UUID): Promise<UUID | null> { - return this.withDatabase(async () => { - const { rows } = await this.pool.query( - "SELECT id FROM rooms WHERE id = $1", - [roomId] - ); - return rows.length > 0 ? (rows[0].id as UUID) : null; - }, "getRoom"); - } - - async getParticipantsForAccount(userId: UUID): Promise<Participant[]> { - return this.withDatabase(async () => { - const { rows } = await this.pool.query( - `SELECT id, "userId", "roomId", "last_message_read" - FROM participants - WHERE "userId" = $1`, - [userId] - ); - return rows as Participant[]; - }, "getParticipantsForAccount"); - } - - async getParticipantUserState( - roomId: UUID, - userId: UUID - ): Promise<"FOLLOWED" | "MUTED" | null> { - return this.withDatabase(async () => { - const { rows } = await this.pool.query( - `SELECT "userState" FROM participants WHERE "roomId" = $1 AND "userId" = $2`, - [roomId, userId] - ); - return rows.length > 0 ? rows[0].userState : null; - }, "getParticipantUserState"); - } - - async getMemoriesByRoomIds(params: { - roomIds: UUID[]; - agentId?: UUID; - tableName: string; - limit?: number; - }): Promise<Memory[]> { - return this.withDatabase(async () => { - if (params.roomIds.length === 0) return []; - const placeholders = params.roomIds - .map((_, i) => `$${i + 2}`) - .join(", "); - - let query = `SELECT * FROM memories WHERE type = $1 AND "roomId" IN (${placeholders})`; - let queryParams = [params.tableName, ...params.roomIds]; - - if (params.agentId) { - query += ` AND "agentId" = $${params.roomIds.length + 2}`; - queryParams = [...queryParams, params.agentId]; - } - - // Add sorting, and conditionally add LIMIT if provided - query += ` ORDER BY "createdAt" DESC`; - if (params.limit) { - query += ` LIMIT $${queryParams.length + 1}`; - queryParams.push(params.limit.toString()); - } - - const { rows } = await this.pool.query(query, queryParams); - return rows.map((row) => ({ - ...row, - content: - typeof row.content === "string" - ? JSON.parse(row.content) - : row.content, - })); - }, "getMemoriesByRoomIds"); - } - - async setParticipantUserState( - roomId: UUID, - userId: UUID, - state: "FOLLOWED" | "MUTED" | null - ): Promise<void> { - return this.withDatabase(async () => { - await this.pool.query( - `UPDATE participants SET "userState" = $1 WHERE "roomId" = $2 AND "userId" = $3`, - [state, roomId, userId] - ); - }, "setParticipantUserState"); - } - - async getParticipantsForRoom(roomId: UUID): Promise<UUID[]> { - return this.withDatabase(async () => { - const { rows } = await this.pool.query( - 'SELECT "userId" FROM participants WHERE "roomId" = $1', - [roomId] - ); - return rows.map((row) => row.userId); - }, "getParticipantsForRoom"); - } - - async getAccountById(userId: UUID): Promise<Account | null> { - return this.withDatabase(async () => { - const { rows } = await this.pool.query( - "SELECT * FROM accounts WHERE id = $1", - [userId] - ); - if (rows.length === 0) { - logger.debug("Account not found:", { userId }); - return null; - } - - return rows[0]; - }, "getAccountById"); - } - - async createAccount(account: Account): Promise<boolean> { - return this.withDatabase(async () => { - try { - const accountId = account.id ?? v4(); - await this.pool.query( - `INSERT INTO accounts (id, name, username, email, "avatarUrl") - VALUES ($1, $2, $3, $4, $5)`, - [ - accountId, - account.name, - account.username || "", - account.email || "", - account.avatarUrl || "" - ] - ); - logger.debug("Account created successfully:", { - accountId, - }); - return true; - } catch (error) { - logger.error("Error creating account:", { - error: - error instanceof Error ? error.message : String(error), - accountId: account.id, - name: account.name, // Only log non-sensitive fields - }); - return false; // Return false instead of throwing to maintain existing behavior - } - }, "createAccount"); - } - - async getActorById(params: { roomId: UUID }): Promise<Actor[]> { - return this.withDatabase(async () => { - const { rows } = await this.pool.query( - `SELECT a.id, a.name, a.username - FROM participants p - LEFT JOIN accounts a ON p."userId" = a.id - WHERE p."roomId" = $1`, - [params.roomId] - ); - - logger.debug("Retrieved actors:", { - roomId: params.roomId, - actorCount: rows.length, - }); - - return rows; - }, "getActorById").catch((error) => { - logger.error("Failed to get actors:", { - roomId: params.roomId, - error: error.message, - }); - throw error; // Re-throw to let caller handle database errors - }); - } - - async getMemoryById(id: UUID): Promise<Memory | null> { - return this.withDatabase(async () => { - const { rows } = await this.pool.query( - "SELECT * FROM memories WHERE id = $1", - [id] - ); - if (rows.length === 0) return null; - - return { - ...rows[0], - content: - typeof rows[0].content === "string" - ? JSON.parse(rows[0].content) - : rows[0].content, - }; - }, "getMemoryById"); - } - - async getMemoriesByIds( - memoryIds: UUID[], - tableName?: string - ): Promise<Memory[]> { - return this.withDatabase(async () => { - if (memoryIds.length === 0) return []; - const placeholders = memoryIds.map((_, i) => `$${i + 1}`).join(","); - let sql = `SELECT * FROM memories WHERE id IN (${placeholders})`; - const queryParams: any[] = [...memoryIds]; - - if (tableName) { - sql += ` AND type = $${memoryIds.length + 1}`; - queryParams.push(tableName); - } - - const { rows } = await this.pool.query(sql, queryParams); - - return rows.map((row) => ({ - ...row, - content: - typeof row.content === "string" - ? JSON.parse(row.content) - : row.content, - })); - }, "getMemoriesByIds"); - } - - async createMemory(memory: Memory, tableName: string): Promise<void> { - return this.withDatabase(async () => { - logger.debug("PostgresAdapter createMemory:", { - memoryId: memory.id, - embeddingLength: memory.embedding?.length, - contentLength: memory.content?.text?.length, - }); - - let isUnique = true; - if (memory.embedding) { - const similarMemories = await this.searchMemoriesByEmbedding( - memory.embedding, - { - tableName, - roomId: memory.roomId, - match_threshold: 0.95, - count: 1, - } - ); - isUnique = similarMemories.length === 0; - } - - await this.pool.query( - `INSERT INTO memories ( - id, type, content, embedding, "userId", "roomId", "agentId", "unique", "createdAt" - ) VALUES ($1, $2, $3, $4, $5::uuid, $6::uuid, $7::uuid, $8, to_timestamp($9/1000.0))`, - [ - memory.id ?? v4(), - tableName, - JSON.stringify(memory.content), - memory.embedding ? `[${memory.embedding.join(",")}]` : null, - memory.userId, - memory.roomId, - memory.agentId, - memory.unique ?? isUnique, - Date.now(), - ] - ); - }, "createMemory"); - } - - async searchMemories(params: { - tableName: string; - agentId: UUID; - roomId: UUID; - embedding: number[]; - match_threshold: number; - count: number; - unique: boolean; - }): Promise<Memory[]> { - return await this.searchMemoriesByEmbedding(params.embedding, { - match_threshold: params.match_threshold, - count: params.count, - agentId: params.agentId, - roomId: params.roomId, - unique: params.unique, - tableName: params.tableName, - }); - } - - async getMemories(params: { - roomId: UUID; - count?: number; - unique?: boolean; - tableName: string; - agentId?: UUID; - start?: number; - end?: number; - }): Promise<Memory[]> { - // Parameter validation - if (!params.tableName) throw new Error("tableName is required"); - if (!params.roomId) throw new Error("roomId is required"); - - return this.withDatabase(async () => { - // Build query - let sql = `SELECT * FROM memories WHERE type = $1 AND "roomId" = $2`; - const values: any[] = [params.tableName, params.roomId]; - let paramCount = 2; - - // Add time range filters - if (params.start) { - paramCount++; - sql += ` AND "createdAt" >= to_timestamp($${paramCount})`; - values.push(params.start / 1000); - } - - if (params.end) { - paramCount++; - sql += ` AND "createdAt" <= to_timestamp($${paramCount})`; - values.push(params.end / 1000); - } - - // Add other filters - if (params.unique) { - sql += ` AND "unique" = true`; - } - - if (params.agentId) { - paramCount++; - sql += ` AND "agentId" = $${paramCount}`; - values.push(params.agentId); - } - - // Add ordering and limit - sql += ' ORDER BY "createdAt" DESC'; - - if (params.count) { - paramCount++; - sql += ` LIMIT $${paramCount}`; - values.push(params.count); - } - - logger.debug("Fetching memories:", { - roomId: params.roomId, - tableName: params.tableName, - unique: params.unique, - agentId: params.agentId, - timeRange: - params.start || params.end - ? { - start: params.start - ? new Date(params.start).toISOString() - : undefined, - end: params.end - ? new Date(params.end).toISOString() - : undefined, - } - : undefined, - limit: params.count, - }); - - const { rows } = await this.pool.query(sql, values); - return rows.map((row) => ({ - ...row, - content: - typeof row.content === "string" - ? JSON.parse(row.content) - : row.content, - })); - }, "getMemories"); - } - - async getGoals(params: { - roomId: UUID; - userId?: UUID | null; - onlyInProgress?: boolean; - count?: number; - }): Promise<Goal[]> { - return this.withDatabase(async () => { - let sql = `SELECT * FROM goals WHERE "roomId" = $1`; - const values: any[] = [params.roomId]; - let paramCount = 1; - - if (params.userId) { - paramCount++; - sql += ` AND "userId" = $${paramCount}`; - values.push(params.userId); - } - - if (params.onlyInProgress) { - sql += " AND status = 'IN_PROGRESS'"; - } - - if (params.count) { - paramCount++; - sql += ` LIMIT $${paramCount}`; - values.push(params.count); - } - - const { rows } = await this.pool.query(sql, values); - return rows.map((row) => ({ - ...row, - objectives: - typeof row.objectives === "string" - ? JSON.parse(row.objectives) - : row.objectives, - })); - }, "getGoals"); - } - - async updateGoal(goal: Goal): Promise<void> { - return this.withDatabase(async () => { - try { - await this.pool.query( - `UPDATE goals SET name = $1, status = $2, objectives = $3 WHERE id = $4`, - [ - goal.name, - goal.status, - JSON.stringify(goal.objectives), - goal.id, - ] - ); - } catch (error) { - logger.error("Failed to update goal:", { - goalId: goal.id, - error: - error instanceof Error ? error.message : String(error), - status: goal.status, - }); - throw error; - } - }, "updateGoal"); - } - - async createGoal(goal: Goal): Promise<void> { - return this.withDatabase(async () => { - await this.pool.query( - `INSERT INTO goals (id, "roomId", "userId", name, status, objectives) - VALUES ($1, $2, $3, $4, $5, $6)`, - [ - goal.id ?? v4(), - goal.roomId, - goal.userId, - goal.name, - goal.status, - JSON.stringify(goal.objectives), - ] - ); - }, "createGoal"); - } - - async removeGoal(goalId: UUID): Promise<void> { - if (!goalId) throw new Error("Goal ID is required"); - - return this.withDatabase(async () => { - try { - const result = await this.pool.query( - "DELETE FROM goals WHERE id = $1 RETURNING id", - [goalId] - ); - - logger.debug("Goal removal attempt:", { - goalId, - removed: result?.rowCount ?? 0 > 0, - }); - } catch (error) { - logger.error("Failed to remove goal:", { - goalId, - error: - error instanceof Error ? error.message : String(error), - }); - throw error; - } - }, "removeGoal"); - } - - async createRoom(roomId?: UUID): Promise<UUID> { - return this.withDatabase(async () => { - const newRoomId = roomId || v4(); - await this.pool.query("INSERT INTO rooms (id) VALUES ($1)", [ - newRoomId, - ]); - return newRoomId as UUID; - }, "createRoom"); - } - - async removeRoom(roomId: UUID): Promise<void> { - if (!roomId) throw new Error("Room ID is required"); - - return this.withDatabase(async () => { - const client = await this.pool.connect(); - try { - await client.query("BEGIN"); - - // First check if room exists - const checkResult = await client.query( - "SELECT id FROM rooms WHERE id = $1", - [roomId] - ); - - if (checkResult.rowCount === 0) { - logger.warn("No room found to remove:", { roomId }); - throw new Error(`Room not found: ${roomId}`); - } - - // Remove related data first (if not using CASCADE) - await client.query('DELETE FROM memories WHERE "roomId" = $1', [ - roomId, - ]); - await client.query( - 'DELETE FROM participants WHERE "roomId" = $1', - [roomId] - ); - await client.query('DELETE FROM goals WHERE "roomId" = $1', [ - roomId, - ]); - - // Finally remove the room - const result = await client.query( - "DELETE FROM rooms WHERE id = $1 RETURNING id", - [roomId] - ); - - await client.query("COMMIT"); - - logger.debug( - "Room and related data removed successfully:", - { - roomId, - removed: result?.rowCount ?? 0 > 0, - } - ); - } catch (error) { - await client.query("ROLLBACK"); - logger.error("Failed to remove room:", { - roomId, - error: - error instanceof Error ? error.message : String(error), - }); - throw error; - } finally { - if (client) client.release(); - } - }, "removeRoom"); - } - - async createRelationship(params: { - userA: UUID; - userB: UUID; - }): Promise<boolean> { - // Input validation - if (!params.userA || !params.userB) { - throw new Error("userA and userB are required"); - } - - return this.withDatabase(async () => { - try { - const relationshipId = v4(); - await this.pool.query( - `INSERT INTO relationships (id, "userA", "userB", "userId") - VALUES ($1, $2, $3, $4) - RETURNING id`, - [relationshipId, params.userA, params.userB, params.userA] - ); - - logger.debug("Relationship created successfully:", { - relationshipId, - userA: params.userA, - userB: params.userB, - }); - - return true; - } catch (error) { - // Check for unique constraint violation or other specific errors - if ((error as { code?: string }).code === "23505") { - // Unique violation - logger.warn("Relationship already exists:", { - userA: params.userA, - userB: params.userB, - error: - error instanceof Error - ? error.message - : String(error), - }); - } else { - logger.error("Failed to create relationship:", { - userA: params.userA, - userB: params.userB, - error: - error instanceof Error - ? error.message - : String(error), - }); - } - return false; - } - }, "createRelationship"); - } - - async getRelationship(params: { - userA: UUID; - userB: UUID; - }): Promise<Relationship | null> { - if (!params.userA || !params.userB) { - throw new Error("userA and userB are required"); - } - - return this.withDatabase(async () => { - try { - const { rows } = await this.pool.query( - `SELECT * FROM relationships - WHERE ("userA" = $1 AND "userB" = $2) - OR ("userA" = $2 AND "userB" = $1)`, - [params.userA, params.userB] - ); - - if (rows.length > 0) { - logger.debug("Relationship found:", { - relationshipId: rows[0].id, - userA: params.userA, - userB: params.userB, - }); - return rows[0]; - } - - logger.debug("No relationship found between users:", { - userA: params.userA, - userB: params.userB, - }); - return null; - } catch (error) { - logger.error("Error fetching relationship:", { - userA: params.userA, - userB: params.userB, - error: - error instanceof Error ? error.message : String(error), - }); - throw error; - } - }, "getRelationship"); - } - - async getRelationships(params: { userId: UUID }): Promise<Relationship[]> { - if (!params.userId) { - throw new Error("userId is required"); - } - - return this.withDatabase(async () => { - try { - const { rows } = await this.pool.query( - `SELECT * FROM relationships - WHERE "userA" = $1 OR "userB" = $1 - ORDER BY "createdAt" DESC`, // Add ordering if you have this field - [params.userId] - ); - - logger.debug("Retrieved relationships:", { - userId: params.userId, - count: rows.length, - }); - - return rows; - } catch (error) { - logger.error("Failed to fetch relationships:", { - userId: params.userId, - error: - error instanceof Error ? error.message : String(error), - }); - throw error; - } - }, "getRelationships"); - } - - async getCachedEmbeddings(opts: { - query_table_name: string; - query_threshold: number; - query_input: string; - query_field_name: string; - query_field_sub_name: string; - query_match_count: number; - }): Promise<{ embedding: number[]; levenshtein_score: number }[]> { - // Input validation - if (!opts.query_table_name) - throw new Error("query_table_name is required"); - if (!opts.query_input) throw new Error("query_input is required"); - if (!opts.query_field_name) - throw new Error("query_field_name is required"); - if (!opts.query_field_sub_name) - throw new Error("query_field_sub_name is required"); - if (opts.query_match_count <= 0) - throw new Error("query_match_count must be positive"); - - return this.withDatabase(async () => { - try { - logger.debug("Fetching cached embeddings:", { - tableName: opts.query_table_name, - fieldName: opts.query_field_name, - subFieldName: opts.query_field_sub_name, - matchCount: opts.query_match_count, - inputLength: opts.query_input.length, - }); - - const sql = ` - WITH content_text AS ( - SELECT - embedding, - COALESCE( - content->$2->>$3, - '' - ) as content_text - FROM memories - WHERE type = $4 - AND content->$2->>$3 IS NOT NULL - ) - SELECT - embedding, - levenshtein( - $1, - content_text - ) as levenshtein_score - FROM content_text - WHERE levenshtein( - $1, - content_text - ) <= $6 -- Add threshold check - ORDER BY levenshtein_score - LIMIT $5 - `; - - const { rows } = await this.pool.query(sql, [ - opts.query_input, - opts.query_field_name, - opts.query_field_sub_name, - opts.query_table_name, - opts.query_match_count, - opts.query_threshold, - ]); - - logger.debug("Retrieved cached embeddings:", { - count: rows.length, - tableName: opts.query_table_name, - matchCount: opts.query_match_count, - }); - - return rows - .map( - ( - row - ): { - embedding: number[]; - levenshtein_score: number; - } | null => { - if (!Array.isArray(row.embedding)) return null; - return { - embedding: row.embedding, - levenshtein_score: Number( - row.levenshtein_score - ), - }; - } - ) - .filter( - ( - row - ): row is { - embedding: number[]; - levenshtein_score: number; - } => row !== null - ); - } catch (error) { - logger.error("Error in getCachedEmbeddings:", { - error: - error instanceof Error ? error.message : String(error), - tableName: opts.query_table_name, - fieldName: opts.query_field_name, - }); - throw error; - } - }, "getCachedEmbeddings"); - } - - async log(params: { - body: { [key: string]: unknown }; - userId: UUID; - roomId: UUID; - type: string; - }): Promise<void> { - // Input validation - if (!params.userId) throw new Error("userId is required"); - if (!params.roomId) throw new Error("roomId is required"); - if (!params.type) throw new Error("type is required"); - if (!params.body || typeof params.body !== "object") { - throw new Error("body must be a valid object"); - } - - return this.withDatabase(async () => { - try { - const logId = v4(); // Generate ID for tracking - await this.pool.query( - `INSERT INTO logs ( - id, - body, - "userId", - "roomId", - type, - "createdAt" - ) VALUES ($1, $2, $3, $4, $5, NOW()) - RETURNING id`, - [ - logId, - JSON.stringify(params.body), // Ensure body is stringified - params.userId, - params.roomId, - params.type, - ] - ); - - logger.debug("Log entry created:", { - logId, - type: params.type, - roomId: params.roomId, - userId: params.userId, - bodyKeys: Object.keys(params.body), - }); - } catch (error) { - logger.error("Failed to create log entry:", { - error: - error instanceof Error ? error.message : String(error), - type: params.type, - roomId: params.roomId, - userId: params.userId, - }); - throw error; - } - }, "log"); - } - - async searchMemoriesByEmbedding( - embedding: number[], - params: { - match_threshold?: number; - count?: number; - agentId?: UUID; - roomId?: UUID; - unique?: boolean; - tableName: string; - } - ): Promise<Memory[]> { - return this.withDatabase(async () => { - logger.debug("Incoming vector:", { - length: embedding.length, - sample: embedding.slice(0, 5), - isArray: Array.isArray(embedding), - allNumbers: embedding.every((n) => typeof n === "number"), - }); - - // Ensure vector is properly formatted - const cleanVector = embedding.map((n) => { - if (!Number.isFinite(n)) return 0; - // Limit precision to avoid floating point issues - return Number(n.toFixed(6)); - }); - - // Format for Postgres pgvector - const vectorStr = `[${cleanVector.join(",")}]`; - - logger.debug("Vector debug:", { - originalLength: embedding.length, - cleanLength: cleanVector.length, - sampleStr: vectorStr.slice(0, 100), - }); - - let sql = ` - SELECT *, - 1 - (embedding <-> $1::vector(${embedding.length})) as similarity - FROM memories - WHERE type = $2 - `; - - const values: any[] = [vectorStr, params.tableName]; - - // Log the query for debugging - logger.debug("Query debug:", { - sql: sql.slice(0, 200), - paramTypes: values.map((v) => typeof v), - vectorStrLength: vectorStr.length, - }); - - let paramCount = 2; - - if (params.unique) { - sql += ` AND "unique" = true`; - } - - if (params.agentId) { - paramCount++; - sql += ` AND "agentId" = $${paramCount}`; - values.push(params.agentId); - } - - if (params.roomId) { - paramCount++; - sql += ` AND "roomId" = $${paramCount}::uuid`; - values.push(params.roomId); - } - - if (params.match_threshold) { - paramCount++; - sql += ` AND 1 - (embedding <-> $1::vector) >= $${paramCount}`; - values.push(params.match_threshold); - } - - sql += ` ORDER BY embedding <-> $1::vector`; - - if (params.count) { - paramCount++; - sql += ` LIMIT $${paramCount}`; - values.push(params.count); - } - - const { rows } = await this.pool.query(sql, values); - return rows.map((row) => ({ - ...row, - content: - typeof row.content === "string" - ? JSON.parse(row.content) - : row.content, - similarity: row.similarity, - })); - }, "searchMemoriesByEmbedding"); - } - - async addParticipant(userId: UUID, roomId: UUID): Promise<boolean> { - return this.withDatabase(async () => { - try { - await this.pool.query( - `INSERT INTO participants (id, "userId", "roomId") - VALUES ($1, $2, $3)`, - [v4(), userId, roomId] - ); - return true; - } catch (error) { - console.log("Error adding participant", error); - return false; - } - }, "addParticpant"); - } - - async removeParticipant(userId: UUID, roomId: UUID): Promise<boolean> { - return this.withDatabase(async () => { - try { - await this.pool.query( - `DELETE FROM participants WHERE "userId" = $1 AND "roomId" = $2`, - [userId, roomId] - ); - return true; - } catch (error) { - console.log("Error removing participant", error); - return false; - } - }, "removeParticipant"); - } - - async updateGoalStatus(params: { - goalId: UUID; - status: GoalStatus; - }): Promise<void> { - return this.withDatabase(async () => { - await this.pool.query( - "UPDATE goals SET status = $1 WHERE id = $2", - [params.status, params.goalId] - ); - }, "updateGoalStatus"); - } - - async removeMemory(memoryId: UUID, tableName: string): Promise<void> { - return this.withDatabase(async () => { - await this.pool.query( - "DELETE FROM memories WHERE type = $1 AND id = $2", - [tableName, memoryId] - ); - }, "removeMemory"); - } - - async removeAllMemories(roomId: UUID, tableName: string): Promise<void> { - return this.withDatabase(async () => { - await this.pool.query( - `DELETE FROM memories WHERE type = $1 AND "roomId" = $2`, - [tableName, roomId] - ); - }, "removeAllMemories"); - } - - async countMemories( - roomId: UUID, - unique = true, - tableName = "" - ): Promise<number> { - if (!tableName) throw new Error("tableName is required"); - - return this.withDatabase(async () => { - let sql = `SELECT COUNT(*) as count FROM memories WHERE type = $1 AND "roomId" = $2`; - if (unique) { - sql += ` AND "unique" = true`; - } - - const { rows } = await this.pool.query(sql, [tableName, roomId]); - return Number.parseInt(rows[0].count); - }, "countMemories"); - } - - async removeAllGoals(roomId: UUID): Promise<void> { - return this.withDatabase(async () => { - await this.pool.query(`DELETE FROM goals WHERE "roomId" = $1`, [ - roomId, - ]); - }, "removeAllGoals"); - } - - async getRoomsForParticipant(userId: UUID): Promise<UUID[]> { - return this.withDatabase(async () => { - const { rows } = await this.pool.query( - `SELECT "roomId" FROM participants WHERE "userId" = $1`, - [userId] - ); - return rows.map((row) => row.roomId); - }, "getRoomsForParticipant"); - } - - async getRoomsForParticipants(userIds: UUID[]): Promise<UUID[]> { - return this.withDatabase(async () => { - const placeholders = userIds.map((_, i) => `$${i + 1}`).join(", "); - const { rows } = await this.pool.query( - `SELECT DISTINCT "roomId" FROM participants WHERE "userId" IN (${placeholders})`, - userIds - ); - return rows.map((row) => row.roomId); - }, "getRoomsForParticipants"); - } - - async getActorDetails(params: { roomId: string }): Promise<Actor[]> { - if (!params.roomId) { - throw new Error("roomId is required"); - } - - return this.withDatabase(async () => { - try { - const sql = ` - SELECT - a.id, - a.name, - a.username, - a."avatarUrl" - FROM participants p - LEFT JOIN accounts a ON p."userId" = a.id - WHERE p."roomId" = $1 - ORDER BY a.name - `; - - const result = await this.pool.query<Actor>(sql, [ - params.roomId, - ]); - - logger.debug("Retrieved actor details:", { - roomId: params.roomId, - actorCount: result.rows.length, - }); - - return result.rows; - } catch (error) { - logger.error("Failed to fetch actor details:", { - roomId: params.roomId, - error: - error instanceof Error ? error.message : String(error), - }); - throw new Error( - `Failed to fetch actor details: ${error instanceof Error ? error.message : String(error)}` - ); - } - }, "getActorDetails"); - } - - async getCache(params: { - key: string; - agentId: UUID; - }): Promise<string | undefined> { - return this.withDatabase(async () => { - try { - const sql = `SELECT "value"::TEXT FROM cache WHERE "key" = $1 AND "agentId" = $2`; - const { rows } = await this.query<{ value: string }>(sql, [ - params.key, - params.agentId, - ]); - return rows[0]?.value ?? undefined; - } catch (error) { - logger.error("Error fetching cache", { - error: - error instanceof Error ? error.message : String(error), - key: params.key, - agentId: params.agentId, - }); - return undefined; - } - }, "getCache"); - } - - async setCache(params: { - key: string; - agentId: UUID; - value: string; - }): Promise<boolean> { - return this.withDatabase(async () => { - try { - const client = await this.pool.connect(); - try { - await client.query("BEGIN"); - await client.query( - `INSERT INTO cache ("key", "agentId", "value", "createdAt") - VALUES ($1, $2, $3, CURRENT_TIMESTAMP) - ON CONFLICT ("key", "agentId") - DO UPDATE SET "value" = EXCLUDED.value, "createdAt" = CURRENT_TIMESTAMP`, - [params.key, params.agentId, params.value] - ); - await client.query("COMMIT"); - return true; - } catch (error) { - await client.query("ROLLBACK"); - logger.error("Error setting cache", { - error: - error instanceof Error - ? error.message - : String(error), - key: params.key, - agentId: params.agentId, - }); - return false; - } finally { - if (client) client.release(); - } - } catch (error) { - logger.error( - "Database connection error in setCache", - error - ); - return false; - } - }, "setCache"); - } - - async deleteCache(params: { - key: string; - agentId: UUID; - }): Promise<boolean> { - return this.withDatabase(async () => { - try { - const client = await this.pool.connect(); - try { - await client.query("BEGIN"); - await client.query( - `DELETE FROM cache WHERE "key" = $1 AND "agentId" = $2`, - [params.key, params.agentId] - ); - await client.query("COMMIT"); - return true; - } catch (error) { - await client.query("ROLLBACK"); - logger.error("Error deleting cache", { - error: - error instanceof Error - ? error.message - : String(error), - key: params.key, - agentId: params.agentId, - }); - return false; - } finally { - client.release(); - } - } catch (error) { - logger.error( - "Database connection error in deleteCache", - error - ); - return false; - } - }, "deleteCache"); - } -} - -export default PostgresDatabaseAdapter; diff --git a/packages/plugin-solana b/packages/plugin-solana new file mode 160000 index 00000000000..cfe8c8351f2 --- /dev/null +++ b/packages/plugin-solana @@ -0,0 +1 @@ +Subproject commit cfe8c8351f23d87a70f5a4987e8185c293aea6b0 diff --git a/packages/plugin-supabase b/packages/plugin-supabase deleted file mode 160000 index 64e9e181b8b..00000000000 --- a/packages/plugin-supabase +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 64e9e181b8be1e43b3c01830b6415ddf4e6382da diff --git a/packages/plugin-telegram/src/messageManager.ts b/packages/plugin-telegram/src/messageManager.ts index 25ba71fd060..cd8a09cbea1 100644 --- a/packages/plugin-telegram/src/messageManager.ts +++ b/packages/plugin-telegram/src/messageManager.ts @@ -1,15 +1,14 @@ import { + ModelClass, composeContext, - composeRandomUser, type Content, - logger, generateMessageResponse, generateShouldRespond, type HandlerCallback, type IAgentRuntime, + logger, type Media, type Memory, - ModelClass, type State, stringToUuid, type UUID, @@ -17,10 +16,8 @@ import { import type { Message } from "@telegraf/types"; import type { Context, Telegraf } from "telegraf"; import { - telegramAutoPostTemplate, telegramMessageHandlerTemplate, - telegramPinnedMessageTemplate, - telegramShouldRespondTemplate, + telegramShouldRespondTemplate } from "./templates"; import { escapeMarkdown } from "./utils"; @@ -41,16 +38,6 @@ interface MessageContext { timestamp: number; } -interface AutoPostConfig { - enabled: boolean; - monitorTime: number; - inactivityThreshold: number; // milliseconds - mainChannelId: string; - pinnedMessagesGroups: string[]; // Instead of announcementChannelIds - lastAutoPost?: number; - minTimeBetweenPosts?: number; -} - export type InterestChats = { [key: string]: { currentHandler: string | undefined; @@ -67,7 +54,6 @@ export class MessageManager { private interestChats: InterestChats = {}; private teamMemberUsernames: Map<string, string> = new Map(); - private autoPostConfig: AutoPostConfig; private lastChannelActivity: { [channelId: string]: number } = {}; private autoPostInterval: NodeJS.Timeout; @@ -81,31 +67,6 @@ export class MessageManager { error ) ); - - this.autoPostConfig = { - enabled: - this.runtime.character.clientConfig?.telegram?.autoPost - ?.enabled || false, - monitorTime: - this.runtime.character.clientConfig?.telegram?.autoPost - ?.monitorTime || 300000, - inactivityThreshold: - this.runtime.character.clientConfig?.telegram?.autoPost - ?.inactivityThreshold || 3600000, - mainChannelId: - this.runtime.character.clientConfig?.telegram?.autoPost - ?.mainChannelId, - pinnedMessagesGroups: - this.runtime.character.clientConfig?.telegram?.autoPost - ?.pinnedMessagesGroups || [], - minTimeBetweenPosts: - this.runtime.character.clientConfig?.telegram?.autoPost - ?.minTimeBetweenPosts || 7200000, - }; - - if (this.autoPostConfig.enabled) { - this._startAutoPostMonitoring(); - } } private async _initializeTeamMemberUsernames(): Promise<void> { @@ -133,302 +94,6 @@ export class MessageManager { } } - private _startAutoPostMonitoring(): void { - // Wait for bot to be ready - if (this.bot.botInfo) { - logger.info( - "[AutoPost Telegram] Bot ready, starting monitoring" - ); - this._initializeAutoPost(); - } else { - logger.info( - "[AutoPost Telegram] Bot not ready, waiting for ready event" - ); - this.bot.telegram.getMe().then(() => { - logger.info( - "[AutoPost Telegram] Bot ready, starting monitoring" - ); - this._initializeAutoPost(); - }); - } - } - - private _initializeAutoPost(): void { - // Give the bot a moment to fully initialize - setTimeout(() => { - // Monitor with random intervals between 2-6 hours - // Monitor with random intervals between 2-6 hours - this.autoPostInterval = setInterval(() => { - this._checkChannelActivity(); - }, Math.floor(Math.random() * (4 * 60 * 60 * 1000) + 2 * 60 * 60 * 1000)); - }, 5000); - } - - private async _checkChannelActivity(): Promise<void> { - if (!this.autoPostConfig.enabled || !this.autoPostConfig.mainChannelId) - return; - - try { - // Get last message time - const now = Date.now(); - const lastActivityTime = - this.lastChannelActivity[this.autoPostConfig.mainChannelId] || - 0; - const timeSinceLastMessage = now - lastActivityTime; - const timeSinceLastAutoPost = - now - (this.autoPostConfig.lastAutoPost || 0); - - // Add some randomness to the inactivity threshold (±30 minutes) - const randomThreshold = - this.autoPostConfig.inactivityThreshold + - (Math.random() * 1800000 - 900000); - - // Check if we should post - if ( - timeSinceLastMessage > randomThreshold && - timeSinceLastAutoPost > - (this.autoPostConfig.minTimeBetweenPosts || 0) - ) { - try { - const roomId = stringToUuid( - this.autoPostConfig.mainChannelId + - "-" + - this.runtime.agentId - ); - const memory = { - id: stringToUuid(`autopost-${Date.now()}`), - userId: this.runtime.agentId, - agentId: this.runtime.agentId, - roomId, - content: { - text: "AUTO_POST_ENGAGEMENT", - source: "telegram", - }, - createdAt: Date.now(), - }; - - let state = await this.runtime.composeState(memory, { - telegramBot: this.bot, - agentName: this.runtime.character.name, - }); - - const context = composeContext({ - state, - template: - this.runtime.character.templates - ?.telegramAutoPostTemplate || - telegramAutoPostTemplate, - }); - - const responseContent = await this._generateResponse( - memory, - state, - context - ); - if (!responseContent?.text) return; - - console.log( - `[Auto Post Telegram] Recent Messages: ${responseContent}` - ); - - // Send message directly using telegram bot - const messages = await Promise.all( - this.splitMessage(responseContent.text.trim()).map( - (chunk) => - this.bot.telegram.sendMessage( - this.autoPostConfig.mainChannelId, - chunk - ) - ) - ); - - // Create and store memories - const memories = messages.map((m) => ({ - id: stringToUuid( - roomId + "-" + m.message_id.toString() - ), - userId: this.runtime.agentId, - agentId: this.runtime.agentId, - content: { - ...responseContent, - text: m.text, - }, - roomId, - createdAt: m.date * 1000, - })); - - for (const m of memories) { - await this.runtime.messageManager.createMemory(m); - } - - this.autoPostConfig.lastAutoPost = Date.now(); - state = await this.runtime.updateRecentMessageState(state); - await this.runtime.evaluate(memory, state, true); - } catch (error) { - logger.warn("[AutoPost Telegram] Error:", error); - } - } else { - logger.warn( - "[AutoPost Telegram] Activity within threshold. Not posting." - ); - } - } catch (error) { - logger.warn( - "[AutoPost Telegram] Error checking channel activity:", - error - ); - } - } - - private async _monitorPinnedMessages(ctx: Context): Promise<void> { - if (!this.autoPostConfig.pinnedMessagesGroups.length) { - logger.warn( - "[AutoPost Telegram] Auto post config no pinned message groups" - ); - return; - } - - if (!ctx.message || !("pinned_message" in ctx.message)) { - return; - } - - const pinnedMessage = ctx.message.pinned_message; - if (!pinnedMessage) return; - - if ( - !this.autoPostConfig.pinnedMessagesGroups.includes( - ctx.chat.id.toString() - ) - ) - return; - - const mainChannel = this.autoPostConfig.mainChannelId; - if (!mainChannel) return; - - try { - logger.info( - `[AutoPost Telegram] Processing pinned message in group ${ctx.chat.id}` - ); - - // Explicitly type and handle message content - const messageContent: string = - "text" in pinnedMessage && - typeof pinnedMessage.text === "string" - ? pinnedMessage.text - : "caption" in pinnedMessage && - typeof pinnedMessage.caption === "string" - ? pinnedMessage.caption - : "New pinned message"; - - const roomId = stringToUuid( - mainChannel + "-" + this.runtime.agentId - ); - const memory = { - id: stringToUuid(`pinned-${Date.now()}`), - userId: this.runtime.agentId, - agentId: this.runtime.agentId, - roomId, - content: { - text: messageContent, - source: "telegram", - metadata: { - messageId: pinnedMessage.message_id, - pinnedMessageData: pinnedMessage, - }, - }, - createdAt: Date.now(), - }; - - let state = await this.runtime.composeState(memory, { - telegramBot: this.bot, - pinnedMessageContent: messageContent, - pinnedGroupId: ctx.chat.id.toString(), - agentName: this.runtime.character.name, - }); - - const context = composeContext({ - state, - template: - this.runtime.character.templates - ?.telegramPinnedMessageTemplate || - telegramPinnedMessageTemplate, - }); - - const responseContent = await this._generateResponse( - memory, - state, - context - ); - if (!responseContent?.text) return; - - // Send message using telegram bot - const messages = await Promise.all( - this.splitMessage(responseContent.text.trim()).map((chunk) => - this.bot.telegram.sendMessage(mainChannel, chunk) - ) - ); - - const memories = messages.map((m) => ({ - id: stringToUuid(roomId + "-" + m.message_id.toString()), - userId: this.runtime.agentId, - agentId: this.runtime.agentId, - content: { - ...responseContent, - text: m.text, - }, - roomId, - createdAt: m.date * 1000, - })); - - for (const m of memories) { - await this.runtime.messageManager.createMemory(m); - } - - state = await this.runtime.updateRecentMessageState(state); - await this.runtime.evaluate(memory, state, true); - } catch (error) { - logger.warn( - `[AutoPost Telegram] Error processing pinned message:`, - error - ); - } - } - - private _isMessageForMe(message: Message): boolean { - const botUsername = this.bot.botInfo?.username; - if (!botUsername) return false; - - const messageText = - "text" in message - ? message.text - : "caption" in message - ? message.caption - : ""; - if (!messageText) return false; - - const isReplyToBot = - (message as any).reply_to_message?.from?.is_bot === true && - (message as any).reply_to_message?.from?.username === botUsername; - const isMentioned = messageText.includes(`@${botUsername}`); - const hasUsername = messageText - .toLowerCase() - .includes(botUsername.toLowerCase()); - - return ( - isReplyToBot || - isMentioned || - (!this.runtime.character.clientConfig?.telegram - ?.shouldRespondOnlyToMentions && - hasUsername) - ); - } - - private _checkInterest(chatId: string): boolean { - const chatState = this.interestChats[chatId]; - if (!chatState) return false; - return true; - } - // Process image messages and generate descriptions private async processImage( message: Message @@ -456,9 +121,7 @@ export class MessageManager { if (imageUrl) { const { title, description } = - await this.runtime.call(ModelClass.IMAGE_DESCRIPTION, { - imageUrl - }) + await this.runtime.useModel(ModelClass.IMAGE_DESCRIPTION, imageUrl) return { description: `[Image: ${title}\n${description}]` }; } } catch (error) { @@ -473,13 +136,6 @@ export class MessageManager { message: Message, state: State ): Promise<boolean> { - if ( - this.runtime.character.clientConfig?.telegram - ?.shouldRespondOnlyToMentions - ) { - return this._isMessageForMe(message); - } - // Respond if bot is mentioned if ( "text" in message && @@ -511,13 +167,13 @@ export class MessageManager { this.runtime.character.templates ?.telegramShouldRespondTemplate || this.runtime.character?.templates?.shouldRespondTemplate || - composeRandomUser(telegramShouldRespondTemplate, 2), + telegramShouldRespondTemplate, }); const response = await generateShouldRespond({ runtime: this.runtime, context: shouldRespondContext, - modelClass: ModelClass.SMALL, + modelClass: ModelClass.TEXT_SMALL, }); return response === "RESPOND"; @@ -681,7 +337,7 @@ export class MessageManager { const response = await generateMessageResponse({ runtime: this.runtime, context, - modelClass: ModelClass.LARGE, + modelClass: ModelClass.TEXT_LARGE, }); if (!response) { @@ -707,17 +363,6 @@ export class MessageManager { this.lastChannelActivity[ctx.chat.id.toString()] = Date.now(); - // Check for pinned message and route to monitor function - if ( - this.autoPostConfig.enabled && - ctx.message && - "pinned_message" in ctx.message - ) { - // We know this is a message update context now - await this._monitorPinnedMessages(ctx); - return; - } - if ( this.runtime.character.clientConfig?.telegram ?.shouldIgnoreBotMessages && diff --git a/packages/plugin-twitter/package.json b/packages/plugin-twitter/package.json index 740a56e45c0..4852369a161 100644 --- a/packages/plugin-twitter/package.json +++ b/packages/plugin-twitter/package.json @@ -19,7 +19,6 @@ "dist" ], "dependencies": { - "discord.js": "14.16.3", "glob": "11.0.0", "@roamhq/wrtc": "^0.8.0", "@sinclair/typebox": "^0.32.20", diff --git a/packages/plugin-twitter/src/SttTtsSpacesPlugin.ts b/packages/plugin-twitter/src/SttTtsSpacesPlugin.ts index 8e26359bb42..2d8924b3ab6 100644 --- a/packages/plugin-twitter/src/SttTtsSpacesPlugin.ts +++ b/packages/plugin-twitter/src/SttTtsSpacesPlugin.ts @@ -7,7 +7,6 @@ import { type Plugin, type State, composeContext, - composeRandomUser, logger, generateMessageResponse, generateShouldRespond, @@ -288,7 +287,7 @@ export class SttTtsPlugin implements Plugin { const wavBuffer = await this.convertPcmToWavInMemory(merged, 48000); // Whisper STT - const sttText = await this.runtime.call(ModelClass.TRANSCRIPTION, wavBuffer); + const sttText = await this.runtime.useModel(ModelClass.TRANSCRIPTION, wavBuffer); logger.log( `[SttTtsPlugin] Transcription result: "${sttText}"`, @@ -487,7 +486,7 @@ export class SttTtsPlugin implements Plugin { const response = await generateMessageResponse({ runtime: this.runtime, context, - modelClass: ModelClass.SMALL, + modelClass: ModelClass.TEXT_SMALL, }); response.source = "discord"; @@ -579,13 +578,13 @@ export class SttTtsPlugin implements Plugin { this.runtime.character.templates ?.twitterShouldRespondTemplate || this.runtime.character.templates?.shouldRespondTemplate || - composeRandomUser(twitterShouldRespondTemplate, 2), + twitterShouldRespondTemplate, }); const response = await generateShouldRespond({ runtime: this.runtime, context: shouldRespondContext, - modelClass: ModelClass.SMALL, + modelClass: ModelClass.TEXT_SMALL, }); if (response === "RESPOND") { diff --git a/packages/plugin-twitter/src/base.ts b/packages/plugin-twitter/src/base.ts index 14df856ff28..f64fe3c5bf2 100644 --- a/packages/plugin-twitter/src/base.ts +++ b/packages/plugin-twitter/src/base.ts @@ -134,7 +134,7 @@ export class ClientBase extends EventEmitter { /** * Parse the raw tweet data into a standardized Tweet object. */ - private parseTweet(raw: any, depth = 0, maxDepth = 3): Tweet { + parseTweet(raw: any, depth = 0, maxDepth = 3): Tweet { // If we've reached maxDepth, don't parse nested quotes/retweets further const canRecurse = depth < maxDepth; diff --git a/packages/plugin-twitter/src/interactions.ts b/packages/plugin-twitter/src/interactions.ts index 2c287a1e339..c44be8b74fb 100644 --- a/packages/plugin-twitter/src/interactions.ts +++ b/packages/plugin-twitter/src/interactions.ts @@ -348,7 +348,7 @@ export class TwitterInteractionClient { const imageDescriptionsArray = []; try{ for (const photo of tweet.photos) { - const description = await this.runtime.call(ModelClass.IMAGE_DESCRIPTION, photo.url) + const description = await this.runtime.useModel(ModelClass.IMAGE_DESCRIPTION, photo.url) imageDescriptionsArray.push(description); } } catch (error) { @@ -453,7 +453,7 @@ export class TwitterInteractionClient { const response = await generateMessageResponse({ runtime: this.runtime, context, - modelClass: ModelClass.LARGE, + modelClass: ModelClass.TEXT_LARGE, }); const removeQuotes = (str: string) => diff --git a/packages/plugin-twitter/src/post.ts b/packages/plugin-twitter/src/post.ts index c6ed24a5420..ae92380b698 100644 --- a/packages/plugin-twitter/src/post.ts +++ b/packages/plugin-twitter/src/post.ts @@ -14,13 +14,6 @@ import { truncateToCompleteSentence, type UUID, } from "@elizaos/core"; -import { - Client, - Events, - GatewayIntentBits, - Partials, - TextChannel, -} from "discord.js"; import type { ClientBase } from "./base.ts"; import type { Tweet } from "./client"; import { DEFAULT_MAX_TWEET_LENGTH } from "./environment.ts"; @@ -98,10 +91,6 @@ export class TwitterPostClient { private lastProcessTime = 0; private stopProcessingActions = false; private isDryRun: boolean; - private discordClientForApproval: Client; - private approvalRequired = false; - private discordApprovalChannelId: string; - private approvalCheckInterval: number; constructor(client: ClientBase, runtime: IAgentRuntime) { this.client = client; @@ -158,74 +147,6 @@ export class TwitterPostClient { "Twitter client initialized in dry run mode - no actual tweets should be posted" ); } - - // Initialize Discord webhook - const approvalRequired: boolean = - this.runtime - .getSetting("TWITTER_APPROVAL_ENABLED") - ?.toLocaleLowerCase() === "true"; - if (approvalRequired) { - const discordToken = this.runtime.getSetting( - "TWITTER_APPROVAL_DISCORD_BOT_TOKEN" - ); - const approvalChannelId = this.runtime.getSetting( - "TWITTER_APPROVAL_DISCORD_CHANNEL_ID" - ); - - const APPROVAL_CHECK_INTERVAL = - Number.parseInt( - this.runtime.getSetting("TWITTER_APPROVAL_CHECK_INTERVAL") - ) || 5 * 60 * 1000; // 5 minutes - - this.approvalCheckInterval = APPROVAL_CHECK_INTERVAL; - - if (!discordToken || !approvalChannelId) { - throw new Error( - "TWITTER_APPROVAL_DISCORD_BOT_TOKEN and TWITTER_APPROVAL_DISCORD_CHANNEL_ID are required for approval workflow" - ); - } - - this.approvalRequired = true; - this.discordApprovalChannelId = approvalChannelId; - - // Set up Discord client event handlers - this.setupDiscordClient(); - } - } - - private setupDiscordClient() { - this.discordClientForApproval = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - GatewayIntentBits.GuildMessageReactions, - ], - partials: [Partials.Channel, Partials.Message, Partials.Reaction], - }); - this.discordClientForApproval.once( - Events.ClientReady, - (readyClient) => { - logger.log( - `Discord bot is ready as ${readyClient.user.tag}!` - ); - - // Generate invite link with required permissions - const invite = `https://discord.com/api/oauth2/authorize?client_id=${readyClient.user.id}&permissions=274877991936&scope=bot`; - // 274877991936 includes permissions for: - // - Send Messages - // - Read Messages/View Channels - // - Read Message History - - logger.log( - `Use this link to properly invite the Twitter Post Approval Discord bot: ${invite}` - ); - } - ); - // Login to Discord - this.discordClientForApproval.login( - this.runtime.getSetting("TWITTER_APPROVAL_DISCORD_BOT_TOKEN") - ); } async start() { @@ -302,15 +223,6 @@ export class TwitterPostClient { ); }); } - - // Start the pending tweet check loop if enabled - if (this.approvalRequired) this.runPendingTweetCheckLoop(); - } - - private runPendingTweetCheckLoop() { - setInterval(async () => { - await this.handlePendingTweet(); - }, this.approvalCheckInterval); } createTweetObject( @@ -537,7 +449,7 @@ export class TwitterPostClient { const response = await generateText({ runtime: this.runtime, context, - modelClass: ModelClass.SMALL, + modelClass: ModelClass.TEXT_SMALL, }); const rawTweetContent = cleanJsonResponse(response); @@ -606,31 +518,18 @@ export class TwitterPostClient { } try { - if (this.approvalRequired) { - // Send for approval instead of posting directly - logger.log( - `Sending Tweet For Approval:\n ${tweetTextForPosting}` - ); - await this.sendForApproval( - tweetTextForPosting, - roomId, - rawTweetContent - ); - logger.log("Tweet sent for approval"); - } else { - logger.log( - `Posting new tweet:\n ${tweetTextForPosting}` - ); - this.postTweet( - this.runtime, - this.client, - tweetTextForPosting, - roomId, - rawTweetContent, - this.twitterUsername, - mediaData - ); - } + logger.log( + `Posting new tweet:\n ${tweetTextForPosting}` + ); + this.postTweet( + this.runtime, + this.client, + tweetTextForPosting, + roomId, + rawTweetContent, + this.twitterUsername, + mediaData + ); } catch (error) { logger.error("Error sending tweet:", error); } @@ -657,7 +556,7 @@ export class TwitterPostClient { const response = await generateText({ runtime: this.runtime, context: options?.context || context, - modelClass: ModelClass.SMALL, + modelClass: ModelClass.TEXT_SMALL, }); logger.log("generate tweet content response:\n" + response); @@ -732,7 +631,7 @@ export class TwitterPostClient { "twitter" ); - const timeline = await this.client.fetchFollowingTimeline(MAX_TIMELINES_TO_FETCH, []).map((tweet) => this.client.parseTweet(tweet)) + const timeline = (await this.client.twitterClient.fetchFollowingTimeline(MAX_TIMELINES_TO_FETCH, [])).map((tweet) => this.client.parseTweet(tweet)) const maxActionsProcessing = this.client.twitterConfig.MAX_ACTIONS_PROCESSING; @@ -780,7 +679,7 @@ export class TwitterPostClient { const actionResponse = await generateTweetActions({ runtime: this.runtime, context: actionContext, - modelClass: ModelClass.SMALL, + modelClass: ModelClass.TEXT_SMALL, }); if (!actionResponse) { @@ -933,7 +832,7 @@ export class TwitterPostClient { "Processing images in tweet for context" ); for (const photo of tweet.photos) { - const description = await this.runtime.call(ModelClass.IMAGE_DESCRIPTION, photo.url); + const description = await this.runtime.useModel(ModelClass.IMAGE_DESCRIPTION, photo.url); imageDescriptions.push(description); } } @@ -1141,7 +1040,7 @@ export class TwitterPostClient { if (tweet.photos?.length > 0) { logger.log("Processing images in tweet for context"); for (const photo of tweet.photos) { - const description = await this.runtime.call(ModelClass.IMAGE_DESCRIPTION, photo.url) + const description = await this.runtime.useModel(ModelClass.IMAGE_DESCRIPTION, photo.url) imageDescriptions.push(description); } } @@ -1246,129 +1145,6 @@ export class TwitterPostClient { this.stopProcessingActions = true; } - private async sendForApproval( - tweetTextForPosting: string, - roomId: UUID, - rawTweetContent: string - ): Promise<string | null> { - try { - const embed = { - title: "New Tweet Pending Approval", - description: tweetTextForPosting, - fields: [ - { - name: "Character", - value: this.client.profile.username, - inline: true, - }, - { - name: "Length", - value: tweetTextForPosting.length.toString(), - inline: true, - }, - ], - footer: { - text: "Reply with '👍' to post or '❌' to discard, This will automatically expire and remove after 24 hours if no response received", - }, - timestamp: new Date().toISOString(), - }; - - const channel = await this.discordClientForApproval.channels.fetch( - this.discordApprovalChannelId - ); - - if (!channel || !(channel instanceof TextChannel)) { - throw new Error("Invalid approval channel"); - } - - const message = await channel.send({ embeds: [embed] }); - - // Store the pending tweet - const pendingTweetsKey = `twitter/${this.client.profile.username}/pendingTweet`; - const currentPendingTweets = - (await this.runtime.cacheManager.get<PendingTweet[]>( - pendingTweetsKey - )) || []; - // Add new pending tweet - currentPendingTweets.push({ - tweetTextForPosting, - roomId, - rawTweetContent, - discordMessageId: message.id, - channelId: this.discordApprovalChannelId, - timestamp: Date.now(), - }); - - // Store updated array - await this.runtime.cacheManager.set( - pendingTweetsKey, - currentPendingTweets - ); - - return message.id; - } catch (error) { - logger.error( - "Error Sending Twitter Post Approval Request:", - error - ); - return null; - } - } - - private async checkApprovalStatus( - discordMessageId: string - ): Promise<PendingTweetApprovalStatus> { - try { - // Fetch message and its replies from Discord - const channel = await this.discordClientForApproval.channels.fetch( - this.discordApprovalChannelId - ); - - logger.log(`channel ${JSON.stringify(channel)}`); - - if (!(channel instanceof TextChannel)) { - logger.error("Invalid approval channel"); - return "PENDING"; - } - - // Fetch the original message and its replies - const message = await channel.messages.fetch(discordMessageId); - - // Look for thumbs up reaction ('👍') - const thumbsUpReaction = message.reactions.cache.find( - (reaction) => reaction.emoji.name === "👍" - ); - - // Look for reject reaction ('❌') - const rejectReaction = message.reactions.cache.find( - (reaction) => reaction.emoji.name === "❌" - ); - - // Check if the reaction exists and has reactions - if (rejectReaction) { - const count = rejectReaction.count; - if (count > 0) { - return "REJECTED"; - } - } - - // Check if the reaction exists and has reactions - if (thumbsUpReaction) { - // You might want to check for specific users who can approve - // For now, we'll return true if anyone used thumbs up - const count = thumbsUpReaction.count; - if (count > 0) { - return "APPROVED"; - } - } - - return "PENDING"; - } catch (error) { - logger.error("Error checking approval status:", error); - return "PENDING"; - } - } - private async cleanupPendingTweet(discordMessageId: string) { const pendingTweetsKey = `twitter/${this.client.profile.username}/pendingTweet`; const currentPendingTweets = @@ -1390,110 +1166,4 @@ export class TwitterPostClient { ); } } - - private async handlePendingTweet() { - logger.log("Checking Pending Tweets..."); - const pendingTweetsKey = `twitter/${this.client.profile.username}/pendingTweet`; - const pendingTweets = - (await this.runtime.cacheManager.get<PendingTweet[]>( - pendingTweetsKey - )) || []; - - for (const pendingTweet of pendingTweets) { - // Check if tweet is older than 24 hours - const isExpired = - Date.now() - pendingTweet.timestamp > 24 * 60 * 60 * 1000; - - if (isExpired) { - logger.log("Pending tweet expired, cleaning up"); - - // Notify on Discord about expiration - try { - const channel = - await this.discordClientForApproval.channels.fetch( - pendingTweet.channelId - ); - if (channel instanceof TextChannel) { - const originalMessage = await channel.messages.fetch( - pendingTweet.discordMessageId - ); - await originalMessage.reply( - "This tweet approval request has expired (24h timeout)." - ); - } - } catch (error) { - logger.error( - "Error sending expiration notification:", - error - ); - } - - await this.cleanupPendingTweet(pendingTweet.discordMessageId); - return; - } - - // Check approval status - logger.log("Checking approval status..."); - const approvalStatus: PendingTweetApprovalStatus = - await this.checkApprovalStatus(pendingTweet.discordMessageId); - - if (approvalStatus === "APPROVED") { - logger.log("Tweet Approved, Posting"); - await this.postTweet( - this.runtime, - this.client, - pendingTweet.tweetTextForPosting, - pendingTweet.roomId, - pendingTweet.rawTweetContent, - this.twitterUsername - ); - - // Notify on Discord about posting - try { - const channel = - await this.discordClientForApproval.channels.fetch( - pendingTweet.channelId - ); - if (channel instanceof TextChannel) { - const originalMessage = await channel.messages.fetch( - pendingTweet.discordMessageId - ); - await originalMessage.reply( - "Tweet has been posted successfully! ✅" - ); - } - } catch (error) { - logger.error( - "Error sending post notification:", - error - ); - } - - await this.cleanupPendingTweet(pendingTweet.discordMessageId); - } else if (approvalStatus === "REJECTED") { - logger.log("Tweet Rejected, Cleaning Up"); - await this.cleanupPendingTweet(pendingTweet.discordMessageId); - // Notify about Rejection of Tweet - try { - const channel = - await this.discordClientForApproval.channels.fetch( - pendingTweet.channelId - ); - if (channel instanceof TextChannel) { - const originalMessage = await channel.messages.fetch( - pendingTweet.discordMessageId - ); - await originalMessage.reply( - "Tweet has been rejected! ❌" - ); - } - } catch (error) { - logger.error( - "Error sending rejection notification:", - error - ); - } - } - } - } } diff --git a/packages/plugin-twitter/src/search.ts b/packages/plugin-twitter/src/search.ts index 5cf170830ab..9f20cc5887f 100644 --- a/packages/plugin-twitter/src/search.ts +++ b/packages/plugin-twitter/src/search.ts @@ -135,7 +135,7 @@ export class TwitterSearchClient { const mostInterestingTweetResponse = await generateText({ runtime: this.runtime, context: prompt, - modelClass: ModelClass.SMALL, + modelClass: ModelClass.TEXT_SMALL, }); const tweetId = mostInterestingTweetResponse.trim(); @@ -219,7 +219,7 @@ export class TwitterSearchClient { // Generate image descriptions using GPT-4 vision API const imageDescriptions = []; for (const photo of selectedTweet.photos) { - const description = await this.runtime.call(ModelClass.IMAGE_DESCRIPTION, photo.url) + const description = await this.runtime.useModel(ModelClass.IMAGE_DESCRIPTION, photo.url) imageDescriptions.push(description); } @@ -249,7 +249,7 @@ export class TwitterSearchClient { const responseContent = await generateMessageResponse({ runtime: this.runtime, context, - modelClass: ModelClass.LARGE, + modelClass: ModelClass.TEXT_LARGE, }); responseContent.inReplyTo = message.id; diff --git a/packages/plugin-twitter/src/spaces.ts b/packages/plugin-twitter/src/spaces.ts index dc3a6a41765..56c6aae574f 100644 --- a/packages/plugin-twitter/src/spaces.ts +++ b/packages/plugin-twitter/src/spaces.ts @@ -47,7 +47,7 @@ Only return the text, no additional formatting. const output = await generateText({ runtime, context, - modelClass: ModelClass.SMALL, + modelClass: ModelClass.TEXT_SMALL, }); return output.trim(); } catch (err) { @@ -99,7 +99,7 @@ Example: const response = await generateText({ runtime, context, - modelClass: ModelClass.SMALL, + modelClass: ModelClass.TEXT_SMALL, }); const topics = response .split(",")