diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 00000000000..06cd0272520
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+v23.1.0
\ No newline at end of file
diff --git a/README.md b/README.md
index a9abbd61e9a..e2127ddcd70 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,8 @@
## 🌍 README Translations
-[中文说明](./README_CN.md) | [日本語の説明](./README_JA.md) | [한국어 설명](./README_KOR.md) | [Français](./README_FR.md) | [Português](./README_PTBR.md) | [Türkçe](./README_TR.md) | [Русский](./README_RU.md) | [Español](./README_ES.md)
+
+[中文说明](./README_CN.md) | [日本語の説明](./README_JA.md) | [한국어 설명](./README_KOR.md) | [Français](./README_FR.md) | [Português](./README_PTBR.md) | [Türkçe](./README_TR.md) | [Русский](./README_RU.md) | [Español](./README_ES.md) | [Italiano](./README_IT.md)
## ✨ Features
@@ -40,7 +41,7 @@
- [Node.js 22+](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
- [pnpm](https://pnpm.io/installation)
-> **Note for Windows Users:** WSL is required
+> **Note for Windows Users:** [WSL 2](https://learn.microsoft.com/en-us/windows/wsl/install-manual) is required.
### Edit the .env file
diff --git a/README_IT.md b/README_IT.md
new file mode 100644
index 00000000000..ab74ca0ec41
--- /dev/null
+++ b/README_IT.md
@@ -0,0 +1,92 @@
+# Eliza 🤖
+
+
+

+
+
+## ✨ Caratteristiche
+
+- 🛠️ Connettori completi per Discord, Twitter e Telegram
+- 🔗 Supporto per tutti i modelli (Llama, Grok, OpenAI, Anthropic, ecc.)
+- 👥 Supporto multi-agente e per stanze
+- 📚 Acquisisci ed interagisci facilmente con i tuoi documenti
+- 💾 Memoria recuperabile e archivio documenti
+- 🚀 Altamente estensibile - crea le tue azioni e clients personalizzati
+- ☁️ Supporto di numerosi modelli (Llama locale, OpenAI, Anthropic, Groq, ecc.)
+- 📦 Funziona e basta!
+
+## 🎯 Casi d'Uso
+
+- 🤖 Chatbot
+- 🕵️ Agenti Autonomi
+- 📈 Gestione Processi Aziendali
+- 🎮 NPC per Videogiochi
+- 🧠 Trading
+
+## 🚀 Avvio Rapido
+
+### Prerequisiti
+
+- [Python 2.7+](https://www.python.org/downloads/)
+- [Node.js 22+](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)
+- [pnpm](https://pnpm.io/installation)
+
+> **Nota per gli utenti Windows:** È richiesto WSL
+
+### Modifica il file .env
+
+Copia .env.example in .env e inserisci i valori appropriati
+
+```
+cp .env.example .env
+```
+
+### Avvia Eliza Automaticamente
+
+Questo script eseguirà tutti i comandi necessari per configurare il progetto e avviare il bot con il personaggio predefinito.
+
+```bash
+sh scripts/start.sh
+```
+
+### Modifica il file del personaggio
+
+1. Apri `packages/agent/src/character.ts` per modificare il personaggio predefinito. Decommentare e modificare.
+
+2. Per caricare personaggi personalizzati:
+ - Usa `pnpm start --characters="percorso/del/tuo/personaggio.json"`
+ - È possibile caricare più file di personaggi contemporaneamente
+
+### Avvia Eliza Manualmente
+
+```bash
+pnpm i
+pnpm build
+pnpm start
+
+# Il progetto evolve rapidamente; a volte è necessario pulire il progetto se si ritorna sul progetto dopo un po' di tempo
+pnpm clean
+```
+
+#### Requisiti Aggiuntivi
+
+Potrebbe essere necessario installare Sharp. Se vedi un errore all'avvio, prova a installarlo con il seguente comando:
+
+```
+pnpm install --include=optional sharp
+```
+
+### Community e contatti
+
+- [GitHub Issues](https://github.com/ai16z/eliza/issues). Ideale per: bug riscontrati utilizzando Eliza e proposte di funzionalità.
+- [Discord](https://discord.gg/ai16z). Ideale per: condividere le tue applicazioni e interagire con la community.
+
+## Contributori
+
+
+
+
+
+## Cronologia Stelle
+
+[](https://star-history.com/#ai16z/eliza&Date)
\ No newline at end of file
diff --git a/docs/docs/packages/adapters.md b/docs/docs/packages/adapters.md
index 1ad639f23bc..374dc1d17bc 100644
--- a/docs/docs/packages/adapters.md
+++ b/docs/docs/packages/adapters.md
@@ -425,9 +425,42 @@ async searchMemories(params: {
### PostgreSQL Schema
```sql
--- migrations/20240318103238_remote_schema.sql
CREATE EXTENSION IF NOT EXISTS vector;
+CREATE TABLE IF NOT EXISTS accounts (
+ id UUID PRIMARY KEY,
+ "createdAt" DEFAULT CURRENT_TIMESTAMP,
+ "name" TEXT,
+ "username" TEXT,
+ "email" TEXT NOT NULL,
+ "avatarUrl" TEXT,
+ "details" JSONB DEFAULT '{}'::"jsonb",
+ "is_agent" BOOLEAN DEFAULT false NOT NULL,
+ "location" TEXT,
+ "profile_line" TEXT,
+ "signed_tos" BOOLEAN DEFAULT false NOT NULL
+);
+
+ALTER TABLE ONLY accounts ADD CONSTRAINT users_email_key UNIQUE (email);
+
+CREATE TABLE IF NOT EXISTS participants (
+ "id" UUID PRIMARY KEY,
+ "createdAt" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ "userId" UUID REFERENCES accounts(id),
+ "roomId" UUID REFERENCES rooms(id),
+ "userState" TEXT, -- For MUTED, NULL, or FOLLOWED states
+ "last_message_read" UUID
+);
+
+ALTER TABLE ONLY participants ADD CONSTRAINT participants_id_key UNIQUE (id);
+ALTER TABLE ONLY participants ADD CONSTRAINT participants_roomId_fkey FOREIGN KEY ("roomId") REFERENCES rooms(id);
+ALTER TABLE ONLY participants ADD CONSTRAINT participants_userId_fkey FOREIGN KEY ("userId") REFERENCES accounts(id);
+
+CREATE TABLE rooms (
+ id UUID PRIMARY KEY,
+ "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
CREATE TABLE memories (
id UUID PRIMARY KEY,
type TEXT NOT NULL,
@@ -440,6 +473,9 @@ CREATE TABLE memories (
"createdAt" TIMESTAMP NOT NULL
);
+ALTER TABLE ONLY memories ADD CONSTRAINT memories_roomId_fkey FOREIGN KEY ("roomId") REFERENCES rooms(id);
+ALTER TABLE ONLY memories ADD CONSTRAINT memories_userId_fkey FOREIGN KEY ("userId") REFERENCES accounts(id);
+
CREATE INDEX memory_embedding_idx ON
memories USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
@@ -452,6 +488,11 @@ CREATE TABLE relationships (
"createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
+ALTER TABLE ONLY relationships ADD CONSTRAINT friendships_id_key UNIQUE (id);
+ALTER TABLE ONLY relationships ADD CONSTRAINT relationships_userA_fkey FOREIGN KEY ("userA") REFERENCES accounts(id);
+ALTER TABLE ONLY relationships ADD CONSTRAINT relationships_userB_fkey FOREIGN KEY ("userB") REFERENCES accounts(id);
+ALTER TABLE ONLY relationships ADD CONSTRAINT relationships_userId_fkey FOREIGN KEY ("userId") REFERENCES accounts(id);
+
CREATE TABLE goals (
id UUID PRIMARY KEY,
"roomId" UUID NOT NULL,
diff --git a/package.json b/package.json
index 938819873fd..048aaef8f27 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,8 @@
"docker:run": "bash ./scripts/docker.sh run",
"docker:bash": "bash ./scripts/docker.sh bash",
"docker:start": "bash ./scripts/docker.sh start",
- "docker": "pnpm docker:build && pnpm docker:run && pnpm docker:bash"
+ "docker": "pnpm docker:build && pnpm docker:run && pnpm docker:bash",
+ "test": "pnpm --dir packages/core test"
},
"devDependencies": {
"concurrently": "^9.1.0",
@@ -27,7 +28,9 @@
"only-allow": "^1.2.1",
"prettier": "^3.3.3",
"typedoc": "^0.26.11",
- "typescript": "5.6.3"
+ "typescript": "5.6.3",
+ "vite": "^5.4.11",
+ "vitest": "^2.1.5"
},
"pnpm": {
"overrides": {
diff --git a/packages/agent/src/index.ts b/packages/agent/src/index.ts
index 725a65cfca8..9d53a965f3f 100644
--- a/packages/agent/src/index.ts
+++ b/packages/agent/src/index.ts
@@ -5,14 +5,14 @@ import { DiscordClientInterface } from "@ai16z/client-discord";
import { AutoClientInterface } from "@ai16z/client-auto";
import { TelegramClientInterface } from "@ai16z/client-telegram";
import { TwitterClientInterface } from "@ai16z/client-twitter";
-import { defaultCharacter } from "@ai16z/eliza";
-import { AgentRuntime } from "@ai16z/eliza";
-import { settings } from "@ai16z/eliza";
import {
+ defaultCharacter,
+ AgentRuntime,
+ settings,
Character,
IAgentRuntime,
- IDatabaseAdapter,
ModelProviderName,
+ elizaLogger,
} from "@ai16z/eliza";
import { bootstrapPlugin } from "@ai16z/plugin-bootstrap";
import { solanaPlugin } from "@ai16z/plugin-solana";
@@ -219,7 +219,11 @@ export async function createAgent(
db: any,
token: string
) {
- console.log("Creating runtime for character", character.name);
+ elizaLogger.success(
+ elizaLogger.successesTitle,
+ "Creating runtime for character",
+ character.name
+ );
return new AgentRuntime({
databaseAdapter: db,
token,
@@ -279,7 +283,7 @@ const startAgents = async () => {
await startAgent(character, directClient);
}
} catch (error) {
- console.error("Error starting agents:", error);
+ elizaLogger.error("Error starting agents:", error);
}
function chat() {
@@ -292,12 +296,12 @@ const startAgents = async () => {
});
}
- console.log("Chat started. Type 'exit' to quit.");
+ elizaLogger.log("Chat started. Type 'exit' to quit.");
chat();
};
startAgents().catch((error) => {
- console.error("Unhandled error in startAgents:", error);
+ elizaLogger.error("Unhandled error in startAgents:", error);
process.exit(1); // Exit the process after logging
});
diff --git a/packages/client-direct/src/index.ts b/packages/client-direct/src/index.ts
index 1ca8a97658e..123600bf555 100644
--- a/packages/client-direct/src/index.ts
+++ b/packages/client-direct/src/index.ts
@@ -61,7 +61,7 @@ export class DirectClient {
private agents: Map;
constructor() {
- console.log("DirectClient constructor");
+ elizaLogger.log("DirectClient constructor");
this.app = express();
this.app.use(cors());
this.agents = new Map();
diff --git a/packages/client-discord/src/actions/download_media.ts b/packages/client-discord/src/actions/download_media.ts
index 0b535139cda..8c68ea44676 100644
--- a/packages/client-discord/src/actions/download_media.ts
+++ b/packages/client-discord/src/actions/download_media.ts
@@ -86,8 +86,8 @@ export default {
callback: HandlerCallback
) => {
const videoService = runtime
- .getService(ServiceType.VIDEO)
- .getInstance();
+ .getService(ServiceType.VIDEO)
+ .getInstance();
if (!state) {
state = (await runtime.composeState(message)) as State;
}
diff --git a/packages/client-discord/src/attachments.ts b/packages/client-discord/src/attachments.ts
index ffe67bea150..7746beda4e3 100644
--- a/packages/client-discord/src/attachments.ts
+++ b/packages/client-discord/src/attachments.ts
@@ -104,8 +104,7 @@ export class AttachmentManager {
} else if (
attachment.contentType?.startsWith("video/") ||
this.runtime
- .getService(ServiceType.VIDEO)
- .getInstance()
+ .getService(ServiceType.VIDEO)
.isVideoUrl(attachment.url)
) {
media = await this.processVideoAttachment(attachment);
@@ -137,10 +136,16 @@ export class AttachmentManager {
throw new Error("Unsupported audio/video format");
}
- const transcription = await this.runtime
- .getService(ServiceType.TRANSCRIPTION)
- .getInstance()
- .transcribeAttachment(audioBuffer);
+ const transcriptionService =
+ this.runtime.getService(
+ ServiceType.TRANSCRIPTION
+ );
+ if (!transcriptionService) {
+ throw new Error("Transcription service not found");
+ }
+
+ const transcription =
+ await transcriptionService.transcribeAttachment(audioBuffer);
const { title, description } = await generateSummary(
this.runtime,
transcription
@@ -220,8 +225,7 @@ export class AttachmentManager {
const response = await fetch(attachment.url);
const pdfBuffer = await response.arrayBuffer();
const text = await this.runtime
- .getService(ServiceType.PDF)
- .getInstance()
+ .getService(ServiceType.PDF)
.convertPdfToText(Buffer.from(pdfBuffer));
const { title, description } = await generateSummary(
this.runtime,
@@ -289,8 +293,9 @@ export class AttachmentManager {
): Promise {
try {
const { description, title } = await this.runtime
- .getService(ServiceType.IMAGE_DESCRIPTION)
- .getInstance()
+ .getService(
+ ServiceType.IMAGE_DESCRIPTION
+ )
.describeImage(attachment.url);
return {
id: attachment.id,
@@ -322,16 +327,16 @@ export class AttachmentManager {
private async processVideoAttachment(
attachment: Attachment
): Promise {
- if (
- this.runtime
- .getService(ServiceType.VIDEO)
- .getInstance()
- .isVideoUrl(attachment.url)
- ) {
- const videoInfo = await this.runtime
- .getService(ServiceType.VIDEO)
- .getInstance()
- .processVideo(attachment.url);
+ const videoService = this.runtime.getService(
+ ServiceType.VIDEO
+ );
+
+ if (!videoService) {
+ throw new Error("Video service not found");
+ }
+
+ if (videoService.isVideoUrl(attachment.url)) {
+ const videoInfo = await videoService.processVideo(attachment.url);
return {
id: attachment.id,
url: attachment.url,
diff --git a/packages/client-discord/src/index.ts b/packages/client-discord/src/index.ts
index 992d4e9255f..0b27015b65a 100644
--- a/packages/client-discord/src/index.ts
+++ b/packages/client-discord/src/index.ts
@@ -25,8 +25,8 @@ import { VoiceManager } from "./voice.ts";
export class DiscordClient extends EventEmitter {
apiToken: string;
- private client: Client;
- private runtime: IAgentRuntime;
+ client: Client;
+ runtime: IAgentRuntime;
character: Character;
private messageManager: MessageManager;
private voiceManager: VoiceManager;
@@ -193,7 +193,7 @@ export class DiscordClient extends EventEmitter {
}
async handleReactionRemove(reaction: MessageReaction, user: User) {
- console.log("Reaction removed");
+ elizaLogger.log("Reaction removed");
// if (user.bot) return;
let emoji = reaction.emoji.name;
diff --git a/packages/client-discord/src/messages.ts b/packages/client-discord/src/messages.ts
index 4d1631664b2..25227e3e946 100644
--- a/packages/client-discord/src/messages.ts
+++ b/packages/client-discord/src/messages.ts
@@ -515,10 +515,23 @@ export class MessageManager {
}
if (message.channel.type === ChannelType.GuildVoice) {
// For voice channels, use text-to-speech
- const audioStream = await this.runtime
- .getService(ServiceType.SPEECH_GENERATION)
- .getInstance()
- .generate(this.runtime, content.text);
+
+ const speechService =
+ this.runtime.getService(
+ ServiceType.SPEECH_GENERATION
+ );
+
+ if (!speechService) {
+ throw new Error(
+ "Speech generation service not found"
+ );
+ }
+
+ const audioStream = await speechService.generate(
+ this.runtime,
+ content.text
+ );
+
await this.voiceManager.playAudioStream(
userId,
audioStream
@@ -603,10 +616,18 @@ export class MessageManager {
if (message.channel.type === ChannelType.GuildVoice) {
// 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
- .getService(ServiceType.SPEECH_GENERATION)
- .getInstance()
- .generate(this.runtime, errorMessage);
+
+ const speechService = this.runtime.getService(
+ ServiceType.SPEECH_GENERATION
+ );
+ if (!speechService) {
+ throw new Error("Speech generation service not found");
+ }
+
+ const audioStream = await speechService.generate(
+ this.runtime,
+ errorMessage
+ );
await this.voiceManager.playAudioStream(userId, audioStream);
} else {
// For text channels, send the error message
@@ -670,14 +691,17 @@ export class MessageManager {
for (const url of urls) {
if (
this.runtime
- .getService(ServiceType.VIDEO)
- .getInstance()
+ .getService(ServiceType.VIDEO)
.isVideoUrl(url)
) {
- const videoInfo = await this.runtime
- .getService(ServiceType.VIDEO)
- .getInstance()
- .processVideo(url);
+ const videoService = this.runtime.getService(
+ ServiceType.VIDEO
+ );
+ if (!videoService) {
+ throw new Error("Video service not found");
+ }
+ const videoInfo = await videoService.processVideo(url);
+
attachments.push({
id: `youtube-${Date.now()}`,
url: url,
@@ -687,10 +711,16 @@ export class MessageManager {
text: videoInfo.text,
});
} else {
- const { title, bodyContent } = await this.runtime
- .getService(ServiceType.BROWSER)
- .getInstance()
- .getPageContent(url, this.runtime);
+ const browserService = this.runtime.getService(
+ ServiceType.BROWSER
+ );
+ if (!browserService) {
+ throw new Error("Browser service not found");
+ }
+
+ const { title, bodyContent } =
+ await browserService.getPageContent(url, this.runtime);
+
const { title: newTitle, description } = await generateSummary(
this.runtime,
title + "\n" + bodyContent
diff --git a/packages/client-discord/src/voice.ts b/packages/client-discord/src/voice.ts
index e2abc0927ea..db628447562 100644
--- a/packages/client-discord/src/voice.ts
+++ b/packages/client-discord/src/voice.ts
@@ -20,7 +20,7 @@ import {
import EventEmitter from "events";
import prism from "prism-media";
import { Readable, pipeline } from "stream";
-import { composeContext } from "@ai16z/eliza";
+import { composeContext, elizaLogger } from "@ai16z/eliza";
import { generateMessageResponse } from "@ai16z/eliza";
import { embeddingZeroVector } from "@ai16z/eliza";
import {
@@ -64,6 +64,7 @@ export function getWavHeader(
}
import { messageCompletionFooter } from "@ai16z/eliza/src/parsing.ts";
+import { DiscordClient } from ".";
const discordVoiceHandlerTemplate =
`# Task: Generate conversational voice dialog for {{agentName}}.
@@ -120,7 +121,7 @@ export class AudioMonitor {
}
});
this.readable.on("end", () => {
- console.log("AudioMonitor ended");
+ elizaLogger.log("AudioMonitor ended");
this.ended = true;
if (this.lastFlagged < 0) return;
callback(this.getBufferFromStart());
@@ -128,13 +129,13 @@ export class AudioMonitor {
});
this.readable.on("speakingStopped", () => {
if (this.ended) return;
- console.log("Speaking stopped");
+ elizaLogger.log("Speaking stopped");
if (this.lastFlagged < 0) return;
callback(this.getBufferFromStart());
});
this.readable.on("speakingStarted", () => {
if (this.ended) return;
- console.log("Speaking started");
+ elizaLogger.log("Speaking started");
this.reset();
});
}
@@ -183,7 +184,7 @@ export class VoiceManager extends EventEmitter {
{ channel: BaseGuildVoiceChannel; monitor: AudioMonitor }
> = new Map();
- constructor(client: any) {
+ constructor(client: DiscordClient) {
super();
this.client = client.client;
this.runtime = client.runtime;
@@ -260,10 +261,10 @@ export class VoiceManager extends EventEmitter {
member: GuildMember,
channel: BaseGuildVoiceChannel
) {
- const userId = member.id;
- const userName = member.user.username;
- const name = member.user.displayName;
- const connection = getVoiceConnection(member.guild.id);
+ const userId = member?.id;
+ const userName = member?.user?.username;
+ const name = member?.user?.displayName;
+ const connection = getVoiceConnection(member?.guild?.id);
const receiveStream = connection?.receiver.subscribe(userId, {
autoDestroy: true,
emitClose: true,
@@ -368,13 +369,11 @@ export class VoiceManager extends EventEmitter {
let lastChunkTime = Date.now();
let transcriptionStarted = false;
let transcriptionText = "";
- console.log("new audio monitor for: ", userId);
const monitor = new AudioMonitor(
audioStream,
10000000,
async (buffer) => {
- console.log("buffer: ", buffer);
const currentTime = Date.now();
const silenceDuration = currentTime - lastChunkTime;
if (!buffer) {
@@ -397,12 +396,20 @@ export class VoiceManager extends EventEmitter {
const wavBuffer =
await this.convertOpusToWav(inputBuffer);
- console.log("starting transcription");
- const text = await this.runtime
- .getService(ServiceType.TRANSCRIPTION)
- .getInstance()
- .transcribe(wavBuffer);
- console.log("transcribed text: ", text);
+ const transcriptionService =
+ this.runtime.getService(
+ ServiceType.TRANSCRIPTION
+ );
+
+ if (!transcriptionService) {
+ throw new Error(
+ "Transcription generation service not found"
+ );
+ }
+
+ const text =
+ await transcriptionService.transcribe(wavBuffer);
+
transcriptionText += text;
} catch (error) {
console.error("Error processing audio stream:", error);
@@ -539,10 +546,22 @@ export class VoiceManager extends EventEmitter {
await this.runtime.updateRecentMessageState(
state
);
- const responseStream = await this.runtime
- .getService(ServiceType.SPEECH_GENERATION)
- .getInstance()
- .generate(this.runtime, content.text);
+
+ const speechService =
+ this.runtime.getService(
+ ServiceType.SPEECH_GENERATION
+ );
+ if (!speechService) {
+ throw new Error(
+ "Speech generation service not found"
+ );
+ }
+
+ const responseStream =
+ await speechService.generate(
+ this.runtime,
+ content.text
+ );
if (responseStream) {
await this.playAudioStream(
diff --git a/packages/client-telegram/src/messageManager.ts b/packages/client-telegram/src/messageManager.ts
index ca6ceb04944..3b3f53d3bf8 100644
--- a/packages/client-telegram/src/messageManager.ts
+++ b/packages/client-telegram/src/messageManager.ts
@@ -178,9 +178,8 @@ export class MessageManager {
}
if (imageUrl) {
- const { title, description } = await this.imageService
- .getInstance()
- .describeImage(imageUrl);
+ const { title, description } =
+ await this.imageService.describeImage(imageUrl);
const fullDescription = `[Image: ${title}\n${description}]`;
return { description: fullDescription };
}
diff --git a/packages/client-telegram/src/telegramClient.ts b/packages/client-telegram/src/telegramClient.ts
index 462517c2a23..dd769c25f10 100644
--- a/packages/client-telegram/src/telegramClient.ts
+++ b/packages/client-telegram/src/telegramClient.ts
@@ -32,7 +32,7 @@ export class TelegramClient {
this.bot.botInfo = botInfo;
});
- console.log(`Bot username: @${this.bot.botInfo?.username}`);
+ elizaLogger.success(`Bot username: @${this.bot.botInfo?.username}`);
this.messageManager.bot = this.bot;
diff --git a/packages/client-twitter/src/base.ts b/packages/client-twitter/src/base.ts
index 1a910c37691..96f3f29a171 100644
--- a/packages/client-twitter/src/base.ts
+++ b/packages/client-twitter/src/base.ts
@@ -220,7 +220,7 @@ export class ClientBase extends EventEmitter {
this.runtime.getSetting("TWITTER_EMAIL"),
this.runtime.getSetting("TWITTER_2FA_SECRET")
);
- console.log("Logged in to Twitter");
+ elizaLogger.log("Logged in to Twitter");
const cookies = await this.twitterClient.getCookies();
fs.writeFileSync(
cookiesFilePath,
@@ -270,7 +270,7 @@ export class ClientBase extends EventEmitter {
console.error("Failed to get user ID");
return;
}
- console.log("Twitter user ID:", userId);
+ elizaLogger.log("Twitter user ID:", userId);
this.twitterUserId = userId;
// Initialize Twitter profile
diff --git a/packages/client-twitter/src/index.ts b/packages/client-twitter/src/index.ts
index 6a3097c524a..742b5ac34dc 100644
--- a/packages/client-twitter/src/index.ts
+++ b/packages/client-twitter/src/index.ts
@@ -1,7 +1,7 @@
import { TwitterPostClient } from "./post.ts";
import { TwitterSearchClient } from "./search.ts";
import { TwitterInteractionClient } from "./interactions.ts";
-import { IAgentRuntime, Client } from "@ai16z/eliza";
+import { IAgentRuntime, Client, elizaLogger } from "@ai16z/eliza";
class TwitterAllClient {
post: TwitterPostClient;
@@ -19,11 +19,11 @@ class TwitterAllClient {
export const TwitterClientInterface: Client = {
async start(runtime: IAgentRuntime) {
- console.log("Twitter client started");
+ elizaLogger.log("Twitter client started");
return new TwitterAllClient(runtime);
},
async stop(runtime: IAgentRuntime) {
- console.warn("Twitter client does not support stopping yet");
+ elizaLogger.warn("Twitter client does not support stopping yet");
},
};
diff --git a/packages/client-twitter/src/interactions.ts b/packages/client-twitter/src/interactions.ts
index 75c4f8c2c88..f0a97aa73e2 100644
--- a/packages/client-twitter/src/interactions.ts
+++ b/packages/client-twitter/src/interactions.ts
@@ -238,7 +238,7 @@ export class TwitterInteractionClient extends ClientBase {
);
}
- console.log("Thread: ", thread);
+ elizaLogger.debug("Thread: ", thread);
const formattedConversation = thread
.map(
(tweet) => `@${tweet.username} (${new Date(
@@ -253,7 +253,7 @@ export class TwitterInteractionClient extends ClientBase {
)
.join("\n\n");
- console.log("formattedConversation: ", formattedConversation);
+ elizaLogger.debug("formattedConversation: ", formattedConversation);
const formattedHomeTimeline =
`# ${this.runtime.character.name}'s Home Timeline\n\n` +
diff --git a/packages/client-twitter/src/post.ts b/packages/client-twitter/src/post.ts
index d74d34ef2e1..02778dae2da 100644
--- a/packages/client-twitter/src/post.ts
+++ b/packages/client-twitter/src/post.ts
@@ -1,6 +1,6 @@
import { Tweet } from "agent-twitter-client";
import fs from "fs";
-import { composeContext } from "@ai16z/eliza";
+import { composeContext, elizaLogger } from "@ai16z/eliza";
import { generateText } from "@ai16z/eliza";
import { embeddingZeroVector } from "@ai16z/eliza";
import { IAgentRuntime, ModelClass } from "@ai16z/eliza";
@@ -76,7 +76,7 @@ export class TwitterPostClient extends ClientBase {
generateNewTweetLoop(); // Set up next iteration
}, delay);
- console.log(`Next tweet scheduled in ${randomMinutes} minutes`);
+ elizaLogger.log(`Next tweet scheduled in ${randomMinutes} minutes`);
};
if (postImmediately) {
@@ -92,7 +92,7 @@ export class TwitterPostClient extends ClientBase {
}
private async generateNewTweet() {
- console.log("Generating new tweet");
+ elizaLogger.log("Generating new tweet");
try {
await this.runtime.ensureUserExists(
this.runtime.agentId,
diff --git a/packages/client-twitter/src/search.ts b/packages/client-twitter/src/search.ts
index 3ece65fa639..38d8e2d2ecb 100644
--- a/packages/client-twitter/src/search.ts
+++ b/packages/client-twitter/src/search.ts
@@ -234,8 +234,10 @@ export class TwitterSearchClient extends ClientBase {
const imageDescriptions = [];
for (const photo of selectedTweet.photos) {
const description = await this.runtime
- .getService(ServiceType.IMAGE_DESCRIPTION)
- .getInstance()
+ .getService(
+ ServiceType.IMAGE_DESCRIPTION
+ )
+ .getInstance()
.describeImage(photo.url);
imageDescriptions.push(description);
}
diff --git a/packages/core/.env.test b/packages/core/.env.test
index 3f383adc69f..d295bdfb3b4 100644
--- a/packages/core/.env.test
+++ b/packages/core/.env.test
@@ -1,2 +1,6 @@
TEST_DATABASE_CLIENT=sqlite
NODE_ENV=test
+MAIN_WALLET_ADDRESS=TEST_MAIN_WALLET_ADDRESS_VALUE
+OPENAI_API_KEY=TEST_OPENAI_API_KEY_VALUE
+RPC_URL=https://api.mainnet-beta.solana.com
+WALLET_PUBLIC_KEY=2weMjPLLybRMMva1fM3U31goWWrCpF59CHWNhnCJ9Vyh
\ No newline at end of file
diff --git a/packages/core/.gitignore b/packages/core/.gitignore
index e1a400663fa..d365b5f35b6 100644
--- a/packages/core/.gitignore
+++ b/packages/core/.gitignore
@@ -1,4 +1,5 @@
node_modules
dist
elizaConfig.yaml
-custom_actions/
\ No newline at end of file
+custom_actions/
+cache/
\ No newline at end of file
diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js
deleted file mode 100644
index 35d5f9f0b3c..00000000000
--- a/packages/core/jest.config.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/** @type {import('ts-jest').JestConfigWithTsJest} */
-export default {
- preset: "ts-jest",
- testEnvironment: "node",
- rootDir: "./src",
- testMatch: ["**/*.test.ts"],
- testTimeout: 120000,
- globals: {
- __DEV__: true,
- __TEST__: true,
- __VERSION__: "0.0.1",
- },
- transform: {
- "^.+\\.tsx?$": [
- "ts-jest",
- {
- useESM: true,
- },
- ],
- },
- moduleNameMapper: {
- "^(\\.{1,2}/.*)\\.js$": "$1",
- },
- extensionsToTreatAsEsm: [".ts"],
-};
diff --git a/packages/core/package.json b/packages/core/package.json
index e550d9ea8da..8f637f0879d 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -11,8 +11,8 @@
"watch": "tsc --watch",
"dev": "tsup --format esm --dts --watch",
"build:docs": "cd docs && pnpm run build",
- "test": "jest --runInBand",
- "test:watch": "jest --runInBand --watch"
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"author": "",
"license": "MIT",
@@ -48,7 +48,8 @@
"ts-node": "10.9.2",
"tslib": "2.8.0",
"tsup": "^8.3.5",
- "typescript": "5.6.3"
+ "typescript": "5.6.3",
+ "@solana/web3.js": "1.95.4"
},
"dependencies": {
"@ai-sdk/anthropic": "^0.0.53",
diff --git a/packages/core/src/embedding.ts b/packages/core/src/embedding.ts
index 4c775d39465..27401c2593a 100644
--- a/packages/core/src/embedding.ts
+++ b/packages/core/src/embedding.ts
@@ -93,7 +93,6 @@ export async function embed(runtime: IAgentRuntime, input: string) {
if (
isNode &&
runtime.character.modelProvider !== ModelProviderName.OPENAI &&
- runtime.character.modelProvider !== ModelProviderName.OLLAMA &&
!settings.USE_OPENAI_EMBEDDING
) {
return await getLocalEmbedding(input);
diff --git a/packages/core/src/generation.ts b/packages/core/src/generation.ts
index 1b82fae9b9f..90e687ff83c 100644
--- a/packages/core/src/generation.ts
+++ b/packages/core/src/generation.ts
@@ -244,17 +244,24 @@ export async function generateText({
elizaLogger.debug(
"Using local Llama model for text completion."
);
- response = await runtime
- .getService(ServiceType.TEXT_GENERATION)
- .getInstance()
- .queueTextCompletion(
- context,
- temperature,
- _stop,
- frequency_penalty,
- presence_penalty,
- max_response_length
- );
+ const textGenerationService = runtime
+ .getService(
+ ServiceType.TEXT_GENERATION
+ )
+ .getInstance();
+
+ if (!textGenerationService) {
+ throw new Error("Text generation service not found");
+ }
+
+ response = await textGenerationService.queueTextCompletion(
+ context,
+ temperature,
+ _stop,
+ frequency_penalty,
+ presence_penalty,
+ max_response_length
+ );
elizaLogger.debug("Received response from local Llama model.");
break;
}
@@ -852,16 +859,20 @@ export const generateCaption = async (
description: string;
}> => {
const { imageUrl } = data;
- const resp = await runtime
- .getService(ServiceType.IMAGE_DESCRIPTION)
- .getInstance()
- .describeImage(imageUrl);
+ const imageDescriptionService = runtime
+ .getService(ServiceType.IMAGE_DESCRIPTION)
+ .getInstance();
+
+ if (!imageDescriptionService) {
+ throw new Error("Image description service not found");
+ }
+
+ const resp = await imageDescriptionService.describeImage(imageUrl);
return {
title: resp.title.trim(),
description: resp.description.trim(),
};
};
-
/**
* Configuration options for generating objects with a model.
*/
diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts
index dee40e7cfdc..6813573a897 100644
--- a/packages/core/src/runtime.ts
+++ b/packages/core/src/runtime.ts
@@ -42,6 +42,7 @@ import {
type Memory,
} from "./types.ts";
import { stringToUuid } from "./uuid.ts";
+import { v4 as uuidv4 } from 'uuid';
/**
* Represents the runtime environment for an agent, handling message processing,
@@ -150,17 +151,19 @@ export class AgentRuntime implements IAgentRuntime {
return this.memoryManagers.get(tableName) || null;
}
- getService(service: ServiceType): typeof Service | null {
+ getService(service: ServiceType): T | null {
const serviceInstance = this.services.get(service);
if (!serviceInstance) {
elizaLogger.error(`Service ${service} not found`);
return null;
}
- return serviceInstance as typeof Service;
+ return serviceInstance as T;
}
- registerService(service: Service): void {
- const serviceType = (service as typeof Service).serviceType;
+
+ async registerService(service: Service): Promise {
+ const serviceType = service.serviceType;
elizaLogger.log("Registering service:", serviceType);
+
if (this.services.has(serviceType)) {
elizaLogger.warn(
`Service ${serviceType} is already registered. Skipping registration.`
@@ -168,7 +171,19 @@ export class AgentRuntime implements IAgentRuntime {
return;
}
- this.services.set((service as typeof Service).serviceType, service);
+ try {
+ await service.initialize(this);
+ this.services.set(serviceType, service);
+ elizaLogger.success(
+ `Service ${serviceType} initialized successfully`
+ );
+ } catch (error) {
+ elizaLogger.error(
+ `Failed to initialize service ${serviceType}:`,
+ error
+ );
+ throw error;
+ }
}
/**
@@ -213,9 +228,9 @@ export class AgentRuntime implements IAgentRuntime {
this.databaseAdapter = opts.databaseAdapter;
// use the character id if it exists, otherwise use the agentId if it is passed in, otherwise use the character name
this.agentId =
- opts.character.id ??
- opts.agentId ??
- stringToUuid(opts.character.name);
+ opts.character?.id ??
+ opts?.agentId ??
+ stringToUuid(opts.character?.name ?? uuidv4());
elizaLogger.success("Agent ID", this.agentId);
@@ -269,7 +284,7 @@ export class AgentRuntime implements IAgentRuntime {
this.token = opts.token;
- [...(opts.character.plugins || []), ...(opts.plugins || [])].forEach(
+ [...(opts.character?.plugins || []), ...(opts.plugins || [])].forEach(
(plugin) => {
plugin.actions?.forEach((action) => {
this.registerAction(action);
diff --git a/packages/core/src/test_resources/constants.ts b/packages/core/src/test_resources/constants.ts
new file mode 100644
index 00000000000..f60b632a03f
--- /dev/null
+++ b/packages/core/src/test_resources/constants.ts
@@ -0,0 +1,12 @@
+import { type UUID } from "@ai16z/eliza/src/types.ts";
+
+export const SERVER_URL = "http://localhost:7998";
+export const SUPABASE_URL = "https://pronvzrzfwsptkojvudd.supabase.co";
+export const SUPABASE_ANON_KEY =
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InByb252enJ6ZndzcHRrb2p2dWRkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDY4NTYwNDcsImV4cCI6MjAyMjQzMjA0N30.I6_-XrqssUb2SWYg5DjsUqSodNS3_RPoET3-aPdqywM";
+export const TEST_EMAIL = "testuser123@gmail.com";
+export const TEST_PASSWORD = "testuser123@gmail.com";
+export const TEST_EMAIL_2 = "testuser234@gmail.com";
+export const TEST_PASSWORD_2 = "testuser234@gmail.com";
+
+export const zeroUuid = "00000000-0000-0000-0000-000000000000" as UUID;
diff --git a/packages/core/src/test_resources/createRuntime.ts b/packages/core/src/test_resources/createRuntime.ts
new file mode 100644
index 00000000000..d1ad826a6ba
--- /dev/null
+++ b/packages/core/src/test_resources/createRuntime.ts
@@ -0,0 +1,145 @@
+import { SqliteDatabaseAdapter, loadVecExtensions } from "@ai16z/adapter-sqlite";
+import { SqlJsDatabaseAdapter } from "@ai16z/adapter-sqljs";
+import { SupabaseDatabaseAdapter } from "@ai16z/adapter-supabase";
+import { DatabaseAdapter } from "../database.ts";
+import { AgentRuntime } from "../runtime.ts";
+import {
+ Action,
+ Evaluator,
+ ModelProviderName,
+ Provider,
+} from "../types.ts";
+import {
+ SUPABASE_ANON_KEY,
+ SUPABASE_URL,
+ TEST_EMAIL,
+ TEST_PASSWORD,
+ zeroUuid,
+} from "./constants.ts";
+import { User } from "./types.ts";
+import { getEndpoint } from "../models.ts";
+
+export async function createRuntime({
+ env,
+ conversationLength,
+ evaluators = [],
+ actions = [],
+ providers = [],
+}: {
+ env?: Record | NodeJS.ProcessEnv;
+ conversationLength?: number;
+ evaluators?: Evaluator[];
+ actions?: Action[];
+ providers?: Provider[];
+}) {
+ let adapter: DatabaseAdapter;
+ let user: User;
+ let session: {
+ user: User;
+ };
+
+ switch (env?.TEST_DATABASE_CLIENT as string) {
+ case "sqljs":
+ {
+ const module = await import("sql.js");
+
+ const initSqlJs = module.default;
+
+ // SQLite adapter
+ const SQL = await initSqlJs({});
+ const db = new SQL.Database();
+
+ adapter = new SqlJsDatabaseAdapter(db);
+
+ // Load sqlite-vss
+ loadVecExtensions((adapter as SqlJsDatabaseAdapter).db);
+ // Create a test user and session
+ session = {
+ user: {
+ id: zeroUuid,
+ email: "test@example.com",
+ },
+ };
+ }
+ break;
+ case "supabase": {
+ const module = await import("@supabase/supabase-js");
+
+ const { createClient } = module;
+
+ const supabase = createClient(
+ env?.SUPABASE_URL ?? SUPABASE_URL,
+ env?.SUPABASE_SERVICE_API_KEY ?? SUPABASE_ANON_KEY
+ );
+
+ const { data } = await supabase.auth.signInWithPassword({
+ email: TEST_EMAIL!,
+ password: TEST_PASSWORD!,
+ });
+
+ user = data.user as User;
+ session = data.session as unknown as { user: User };
+
+ if (!session) {
+ const response = await supabase.auth.signUp({
+ email: TEST_EMAIL!,
+ password: TEST_PASSWORD!,
+ });
+
+ // Change the name of the user
+ const { error } = await supabase
+ .from("accounts")
+ .update({ name: "Test User" })
+ .eq("id", response.data.user?.id);
+
+ if (error) {
+ throw new Error(
+ "Create runtime error: " + JSON.stringify(error)
+ );
+ }
+
+ user = response.data.user as User;
+ session = response.data.session as unknown as { user: User };
+ }
+
+ adapter = new SupabaseDatabaseAdapter(
+ env?.SUPABASE_URL ?? SUPABASE_URL,
+ env?.SUPABASE_SERVICE_API_KEY ?? SUPABASE_ANON_KEY
+ );
+ }
+ case "sqlite":
+ default:
+ {
+ const module = await import("better-sqlite3");
+
+ const Database = module.default;
+
+ // SQLite adapter
+ adapter = new SqliteDatabaseAdapter(new Database(":memory:"));
+
+ // Load sqlite-vss
+ await loadVecExtensions((adapter as SqliteDatabaseAdapter).db);
+ // Create a test user and session
+ session = {
+ user: {
+ id: zeroUuid,
+ email: "test@example.com",
+ },
+ };
+ }
+ break;
+ }
+
+ const runtime = new AgentRuntime({
+ serverUrl: getEndpoint(ModelProviderName.OPENAI),
+ conversationLength,
+ token: env!.OPENAI_API_KEY!,
+ modelProvider: ModelProviderName.OPENAI,
+ actions: actions ?? [],
+ evaluators: evaluators ?? [],
+ providers: providers ?? [],
+ databaseAdapter: adapter,
+ });
+
+ return { user, session, runtime };
+}
diff --git a/packages/core/src/test_resources/testSetup.ts b/packages/core/src/test_resources/testSetup.ts
new file mode 100644
index 00000000000..badfd3cc6a7
--- /dev/null
+++ b/packages/core/src/test_resources/testSetup.ts
@@ -0,0 +1,11 @@
+import dotenv from "dotenv";
+import path from "path";
+
+// Load test environment variables
+const envPath = path.resolve(__dirname, "../../.env.test");
+console.log('Current directory:', __dirname);
+console.log('Trying to load env from:', envPath);
+const result = dotenv.config({ path: envPath });
+if (result.error) {
+ console.error('Error loading .env.test:', result.error);
+}
diff --git a/packages/core/src/test_resources/types.ts b/packages/core/src/test_resources/types.ts
new file mode 100644
index 00000000000..634e266cbe4
--- /dev/null
+++ b/packages/core/src/test_resources/types.ts
@@ -0,0 +1,6 @@
+export interface User {
+ id: string;
+ email?: string;
+ phone?: string;
+ role?: string;
+}
diff --git a/packages/core/src/tests/env.test.ts b/packages/core/src/tests/env.test.ts
new file mode 100644
index 00000000000..369884bdf3e
--- /dev/null
+++ b/packages/core/src/tests/env.test.ts
@@ -0,0 +1,26 @@
+import { describe, it, expect } from 'vitest';
+import fs from 'fs';
+import path from 'path';
+
+describe('Environment Setup', () => {
+ it('should verify .env.test file exists', () => {
+ const possiblePaths = [
+ path.join(process.cwd(), '.env.test'),
+ path.join(process.cwd(), 'packages/core/.env.test'),
+ path.join(__dirname, '../../.env.test'),
+ path.join(__dirname, '../.env.test'),
+ path.join(__dirname, '.env.test'),
+ ];
+
+ console.log('Current working directory:', process.cwd());
+ console.log('__dirname:', __dirname);
+
+ const existingPaths = possiblePaths.filter(p => {
+ const exists = fs.existsSync(p);
+ console.log(`Path ${p} exists: ${exists}`);
+ return exists;
+ });
+
+ expect(existingPaths.length).toBeGreaterThan(0);
+ });
+});
\ No newline at end of file
diff --git a/packages/core/src/tests/goals.test.ts b/packages/core/src/tests/goals.test.ts
index cd845d12209..26d3fee77ac 100644
--- a/packages/core/src/tests/goals.test.ts
+++ b/packages/core/src/tests/goals.test.ts
@@ -15,18 +15,21 @@ import {
Memory,
ModelProviderName,
Service,
+ ServiceType,
State,
} from "../types";
+import { describe, test, expect, beforeEach, vi } from 'vitest';
+
// Mock the database adapter
-const mockDatabaseAdapter = {
- getGoals: jest.fn(),
- updateGoal: jest.fn(),
- createGoal: jest.fn(),
+export const mockDatabaseAdapter = {
+ getGoals: vi.fn(),
+ updateGoal: vi.fn(),
+ createGoal: vi.fn(),
};
-
+const services = new Map();
// Mock the runtime
-const mockRuntime: IAgentRuntime = {
+export const mockRuntime: IAgentRuntime = {
databaseAdapter: mockDatabaseAdapter as any,
agentId: "qweqew-qweqwe-qweqwe-qweqwe-qweeqw",
serverUrl: "",
@@ -87,8 +90,8 @@ const mockRuntime: IAgentRuntime = {
getMemoryManager: function (_name: string): IMemoryManager | null {
throw new Error("Function not implemented.");
},
- registerService: function (_service: Service): void {
- throw new Error("Function not implemented.");
+ registerService: function (service: Service): void {
+ services.set(service.serviceType, service);
},
getSetting: function (_key: string): string | null {
throw new Error("Function not implemented.");
@@ -155,8 +158,10 @@ const mockRuntime: IAgentRuntime = {
updateRecentMessageState: function (_state: State): Promise {
throw new Error("Function not implemented.");
},
- getService: function (_service: string): typeof Service | null {
- throw new Error("Function not implemented.");
+ getService: function (
+ serviceType: ServiceType
+ ): T | null {
+ return (services.get(serviceType) as T) || null;
},
};
@@ -175,7 +180,7 @@ const sampleGoal: Goal = {
describe("getGoals", () => {
it("retrieves goals successfully", async () => {
- (mockDatabaseAdapter.getGoals as jest.Mock).mockResolvedValue([
+ (mockDatabaseAdapter.getGoals).mockResolvedValue([
sampleGoal,
]);
@@ -194,7 +199,7 @@ describe("getGoals", () => {
});
it("handles failure to retrieve goals", async () => {
- (mockDatabaseAdapter.getGoals as jest.Mock).mockRejectedValue(
+ (mockDatabaseAdapter.getGoals).mockRejectedValue(
new Error("Failed to retrieve goals")
);
@@ -220,7 +225,7 @@ describe("formatGoalsAsString", () => {
describe("updateGoal", () => {
it("updates a goal successfully", async () => {
- (mockDatabaseAdapter.updateGoal as jest.Mock).mockResolvedValue(
+ (mockDatabaseAdapter.updateGoal).mockResolvedValue(
undefined
);
@@ -231,7 +236,7 @@ describe("updateGoal", () => {
});
it("handles failure to update a goal", async () => {
- (mockDatabaseAdapter.updateGoal as jest.Mock).mockRejectedValue(
+ (mockDatabaseAdapter.updateGoal).mockRejectedValue(
new Error("Failed to update goal")
);
@@ -243,7 +248,7 @@ describe("updateGoal", () => {
describe("createGoal", () => {
it("creates a goal successfully", async () => {
- (mockDatabaseAdapter.createGoal as jest.Mock).mockResolvedValue(
+ (mockDatabaseAdapter.createGoal).mockResolvedValue(
undefined
);
@@ -254,7 +259,7 @@ describe("createGoal", () => {
});
it("handles failure to create a goal", async () => {
- (mockDatabaseAdapter.createGoal as jest.Mock).mockRejectedValue(
+ (mockDatabaseAdapter.createGoal).mockRejectedValue(
new Error("Failed to create goal")
);
diff --git a/packages/core/src/tests/messages.test.ts b/packages/core/src/tests/messages.test.ts
index 4fef72a4ff8..e82c4690bdd 100644
--- a/packages/core/src/tests/messages.test.ts
+++ b/packages/core/src/tests/messages.test.ts
@@ -5,6 +5,7 @@ import {
formatTimestamp,
} from "../messages.ts";
import { IAgentRuntime, Actor, Content, Memory, UUID } from "../types.ts";
+import { describe, test, expect, vi, beforeAll } from 'vitest';
describe("Messages Library", () => {
let runtime: IAgentRuntime;
@@ -15,9 +16,9 @@ describe("Messages Library", () => {
// Mock runtime with necessary methods
runtime = {
databaseAdapter: {
- // Casting to a Jest mock function
- getParticipantsForRoom: jest.fn(),
- getAccountById: jest.fn(),
+ // Using vi.fn() instead of jest.fn()
+ getParticipantsForRoom: vi.fn(),
+ getAccountById: vi.fn(),
},
} as unknown as IAgentRuntime;
@@ -38,29 +39,22 @@ describe("Messages Library", () => {
});
test("getActorDetails should return actors based on roomId", async () => {
- // Mocking the database adapter methods
const roomId: UUID = "room1234-1234-1234-1234-123456789abc" as UUID;
- // Properly mocking the resolved values of the mocked methods
- (
- runtime.databaseAdapter.getParticipantsForRoom as jest.Mock
- ).mockResolvedValue([userId]);
- (runtime.databaseAdapter.getAccountById as jest.Mock).mockResolvedValue(
- {
- id: userId,
- name: "Test User",
- username: "testuser",
- details: {
- tagline: "A test user",
- summary: "This is a test user for the system.",
- },
- }
- );
+ // Using vi.mocked() type assertion instead of jest.Mock casting
+ vi.mocked(runtime.databaseAdapter.getParticipantsForRoom).mockResolvedValue([userId]);
+ vi.mocked(runtime.databaseAdapter.getAccountById).mockResolvedValue({
+ id: userId,
+ name: "Test User",
+ username: "testuser",
+ details: {
+ tagline: "A test user",
+ summary: "This is a test user for the system.",
+ },
+ });
- // Calling the function under test
const result = await getActorDetails({ runtime, roomId });
- // Assertions
expect(result.length).toBeGreaterThan(0);
expect(result[0].name).toBe("Test User");
expect(result[0].details?.tagline).toBe("A test user");
@@ -69,7 +63,6 @@ describe("Messages Library", () => {
test("formatActors should format actors into a readable string", () => {
const formattedActors = formatActors({ actors });
- // Assertions
expect(formattedActors).toContain("Test User");
expect(formattedActors).toContain("A test user");
expect(formattedActors).toContain(
diff --git a/packages/core/src/tests/models.test.ts b/packages/core/src/tests/models.test.ts
index fd602abe060..6cc554cbe37 100644
--- a/packages/core/src/tests/models.test.ts
+++ b/packages/core/src/tests/models.test.ts
@@ -1,9 +1,19 @@
import { getModel, getEndpoint } from "../models.ts";
import { ModelProviderName, ModelClass } from "../types.ts";
+import { describe, test, expect, vi } from 'vitest';
-jest.mock("../settings", () => ({
- loadEnv: jest.fn(), // Mock the loadEnv function
-}));
+vi.mock("../settings", () => {
+ return {
+ default: {
+ SMALL_OPENROUTER_MODEL: "mock-small-model",
+ OPENROUTER_MODEL: "mock-default-model",
+ OPENAI_API_KEY: "mock-openai-key",
+ ANTHROPIC_API_KEY: "mock-anthropic-key",
+ OPENROUTER_API_KEY: "mock-openrouter-key",
+ },
+ loadEnv: vi.fn(),
+ }
+});
describe("Model Provider Tests", () => {
test("should retrieve the correct model for OpenAI SMALL", () => {
@@ -21,6 +31,11 @@ describe("Model Provider Tests", () => {
expect(model).toBe("llama-3.2-90b-text-preview");
});
+ test("should retrieve the correct model for OpenRouter SMALL", () => {
+ const model = getModel(ModelProviderName.OPENROUTER, ModelClass.SMALL);
+ expect(model).toBe("mock-small-model");
+ });
+
test("should retrieve the correct endpoint for OpenAI", () => {
const endpoint = getEndpoint(ModelProviderName.OPENAI);
expect(endpoint).toBe("https://api.openai.com/v1");
@@ -31,7 +46,7 @@ describe("Model Provider Tests", () => {
expect(endpoint).toBe("https://api.anthropic.com/v1");
});
- test("should handle invalid model provider", () => {
+ test("should handle invalid model provider for getModel", () => {
expect(() =>
getModel("INVALID_PROVIDER" as any, ModelClass.SMALL)
).toThrow();
diff --git a/packages/core/src/tests/relationships.test.ts b/packages/core/src/tests/relationships.test.ts
index cb42f0ac1c3..5b52dc0c9ba 100644
--- a/packages/core/src/tests/relationships.test.ts
+++ b/packages/core/src/tests/relationships.test.ts
@@ -5,12 +5,13 @@ import {
formatRelationships,
} from "../relationships";
import { IAgentRuntime, type Relationship, type UUID } from "../types";
+import { describe, expect, vi } from 'vitest';
// Mock runtime and databaseAdapter
const mockDatabaseAdapter = {
- createRelationship: jest.fn(),
- getRelationship: jest.fn(),
- getRelationships: jest.fn(),
+ createRelationship: vi.fn(),
+ getRelationship: vi.fn(),
+ getRelationships: vi.fn(),
};
const mockRuntime: IAgentRuntime = {
databaseAdapter: mockDatabaseAdapter,
@@ -26,7 +27,7 @@ describe("Relationships Module", () => {
const mockUserId: UUID = generateRandomUUID();
afterEach(() => {
- jest.clearAllMocks();
+ vi.clearAllMocks();
});
describe("createRelationship", () => {
diff --git a/packages/core/src/tests/token.test.ts b/packages/core/src/tests/token.test.ts
new file mode 100644
index 00000000000..b464043f2fa
--- /dev/null
+++ b/packages/core/src/tests/token.test.ts
@@ -0,0 +1,76 @@
+// Now import other modules
+import { createRuntime } from "../test_resources/createRuntime";
+import { TokenProvider, WalletProvider } from "@ai16z/plugin-solana";
+import { Connection, PublicKey } from "@solana/web3.js";
+import { describe, test, expect, beforeEach, vi } from 'vitest';
+import NodeCache from 'node-cache';
+
+describe("TokenProvider Tests", async () => {
+ let tokenProvider: TokenProvider;
+
+ beforeEach(async () => {
+ // Clear all mocks before each test
+ vi.clearAllMocks();
+
+ const { runtime } = await createRuntime({
+ env: process.env,
+ conversationLength: 10,
+ });
+
+ const walletProvider = new WalletProvider(
+ new Connection(runtime.getSetting("RPC_URL")),
+ new PublicKey(runtime.getSetting("WALLET_PUBLIC_KEY"))
+ );
+ // Create new instance of TokenProvider
+ tokenProvider = new TokenProvider(
+ "2weMjPLLybRMMva1fM3U31goWWrCpF59CHWNhnCJ9Vyh",
+ walletProvider
+ );
+
+ // Clear the cache and ensure it's empty
+ (tokenProvider as any).cache.flushAll();
+ (tokenProvider as any).cache.close();
+ (tokenProvider as any).cache = new NodeCache();
+
+ // Mock the getCachedData method instead
+ vi.spyOn(tokenProvider as any, 'getCachedData').mockReturnValue(null);
+ });
+
+ test("should fetch token security data", async () => {
+
+ // Mock the response for the fetchTokenSecurity call
+ const mockFetchResponse = {
+ success: true,
+ data: {
+ ownerBalance: "100",
+ creatorBalance: "50",
+ ownerPercentage: 10,
+ creatorPercentage: 5,
+ top10HolderBalance: "200",
+ top10HolderPercent: 20,
+ },
+ };
+
+ // Mock fetchWithRetry function
+ const fetchSpy = vi
+ .spyOn(tokenProvider as any, "fetchWithRetry")
+ .mockResolvedValue(mockFetchResponse);
+
+ // Run the fetchTokenSecurity method
+ const securityData = await tokenProvider.fetchTokenSecurity();
+ // Check if the data returned is correct
+ expect(securityData).toEqual({
+ ownerBalance: "100",
+ creatorBalance: "50",
+ ownerPercentage: 10,
+ creatorPercentage: 5,
+ top10HolderBalance: "200",
+ top10HolderPercent: 20,
+ });
+
+ // Ensure the mock was called with correct URL
+ expect(fetchSpy).toHaveBeenCalledWith(
+ expect.stringContaining("https://public-api.birdeye.so/defi/token_security?address=2weMjPLLybRMMva1fM3U31goWWrCpF59CHWNhnCJ9Vyh"),
+ );
+ });
+});
diff --git a/packages/core/src/tests/videoGeneration.test.ts b/packages/core/src/tests/videoGeneration.test.ts
new file mode 100644
index 00000000000..e331e8168ee
--- /dev/null
+++ b/packages/core/src/tests/videoGeneration.test.ts
@@ -0,0 +1,126 @@
+import { IAgentRuntime, Memory, State } from "@ai16z/eliza";
+import { videoGenerationPlugin } from "../index";
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+
+// Mock the fetch function
+global.fetch = vi.fn();
+
+// Mock the fs module
+vi.mock('fs', () => ({
+ writeFileSync: vi.fn(),
+ existsSync: vi.fn(),
+ mkdirSync: vi.fn(),
+}));
+
+describe('Video Generation Plugin', () => {
+ let mockRuntime: IAgentRuntime;
+ let mockCallback: ReturnType;
+
+ beforeEach(() => {
+ // Reset mocks
+ vi.clearAllMocks();
+
+ // Setup mock runtime
+ mockRuntime = {
+ getSetting: vi.fn().mockReturnValue('mock-api-key'),
+ agentId: 'mock-agent-id',
+ composeState: vi.fn().mockResolvedValue({}),
+ } as unknown as IAgentRuntime;
+
+ mockCallback = vi.fn();
+
+ // Setup fetch mock for successful response
+ (global.fetch as ReturnType).mockImplementation(() =>
+ Promise.resolve({
+ ok: true,
+ json: () => Promise.resolve({
+ id: 'mock-generation-id',
+ status: 'completed',
+ assets: {
+ video: 'https://example.com/video.mp4'
+ }
+ }),
+ text: () => Promise.resolve(''),
+ })
+ );
+ });
+
+ it('should validate when API key is present', async () => {
+ const mockMessage = {} as Memory;
+ const result = await videoGenerationPlugin.actions[0].validate(mockRuntime, mockMessage);
+ expect(result).toBe(true);
+ expect(mockRuntime.getSetting).toHaveBeenCalledWith('LUMA_API_KEY');
+ });
+
+ it('should handle video generation request', async () => {
+ const mockMessage = {
+ content: {
+ text: 'Generate a video of a sunset'
+ }
+ } as Memory;
+ const mockState = {} as State;
+
+ await videoGenerationPlugin.actions[0].handler(
+ mockRuntime,
+ mockMessage,
+ mockState,
+ {},
+ mockCallback
+ );
+
+ // Check initial callback
+ expect(mockCallback).toHaveBeenCalledWith(
+ expect.objectContaining({
+ text: expect.stringContaining('I\'ll generate a video based on your prompt')
+ })
+ );
+
+ // Check final callback with video
+ expect(mockCallback).toHaveBeenCalledWith(
+ expect.objectContaining({
+ text: 'Here\'s your generated video!',
+ attachments: expect.arrayContaining([
+ expect.objectContaining({
+ source: 'videoGeneration'
+ })
+ ])
+ }),
+ expect.arrayContaining([expect.stringMatching(/generated_video_.*\.mp4/)])
+ );
+ });
+
+ it('should handle API errors gracefully', async () => {
+ // Mock API error
+ (global.fetch as ReturnType).mockImplementationOnce(() =>
+ Promise.resolve({
+ ok: false,
+ status: 500,
+ statusText: 'Internal Server Error',
+ text: () => Promise.resolve('API Error'),
+ })
+ );
+
+ const mockMessage = {
+ content: {
+ text: 'Generate a video of a sunset'
+ }
+ } as Memory;
+ const mockState = {} as State;
+
+ await videoGenerationPlugin.actions[0].handler(
+ mockRuntime,
+ mockMessage,
+ mockState,
+ {},
+ mockCallback
+ );
+
+ // Check error callback
+ expect(mockCallback).toHaveBeenCalledWith(
+ expect.objectContaining({
+ text: expect.stringContaining('Video generation failed'),
+ error: true
+ })
+ );
+ });
+});
\ No newline at end of file
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index b4fefaa1145..1c89a76c283 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -524,15 +524,24 @@ export interface IMemoryManager {
export abstract class Service {
private static instance: Service | null = null;
- static serviceType: ServiceType;
+
+ static get serviceType(): ServiceType {
+ throw new Error("Service must implement static serviceType getter");
+ }
public static getInstance(): T {
if (!Service.instance) {
- // Use this.prototype.constructor to instantiate the concrete class
Service.instance = new (this as any)();
}
return Service.instance as T;
}
+
+ get serviceType(): ServiceType {
+ return (this.constructor as typeof Service).serviceType;
+ }
+
+ // Add abstract initialize method that must be implemented by derived classes
+ abstract initialize(runtime: IAgentRuntime): Promise;
}
export interface IAgentRuntime {
@@ -556,7 +565,7 @@ export interface IAgentRuntime {
getMemoryManager(name: string): IMemoryManager | null;
- getService(service: string): typeof Service | null;
+ getService(service: ServiceType): T | null;
registerService(service: Service): void;
@@ -601,13 +610,13 @@ export interface IAgentRuntime {
export interface IImageDescriptionService extends Service {
getInstance(): IImageDescriptionService;
- initialize(modelId?: string | null, device?: string | null): Promise;
describeImage(
imageUrl: string
): Promise<{ title: string; description: string }>;
}
export interface ITranscriptionService extends Service {
+ getInstance(): ITranscriptionService;
transcribeAttachment(audioBuffer: ArrayBuffer): Promise;
transcribeAttachmentLocally(
audioBuffer: ArrayBuffer
@@ -617,6 +626,7 @@ export interface ITranscriptionService extends Service {
}
export interface IVideoService extends Service {
+ getInstance(): IVideoService;
isVideoUrl(url: string): boolean;
processVideo(url: string): Promise;
fetchVideoInfo(url: string): Promise;
@@ -646,7 +656,7 @@ export interface ITextGenerationService extends Service {
}
export interface IBrowserService extends Service {
- initialize(): Promise;
+ getInstance(): IBrowserService;
closeBrowser(): Promise;
getPageContent(
url: string,
@@ -655,10 +665,12 @@ export interface IBrowserService extends Service {
}
export interface ISpeechService extends Service {
+ getInstance(): ISpeechService;
generate(runtime: IAgentRuntime, text: string): Promise;
}
export interface IPdfService extends Service {
+ getInstance(): IPdfService;
convertPdfToText(pdfBuffer: Buffer): Promise;
}
diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts
new file mode 100644
index 00000000000..2ce1d5d7f32
--- /dev/null
+++ b/packages/core/vitest.config.ts
@@ -0,0 +1,16 @@
+import { defineConfig } from 'vitest/config';
+import path from 'path';
+
+export default defineConfig({
+ test: {
+ setupFiles: ['./src/test_resources/testSetup.ts'],
+ environment: 'node',
+ globals: true,
+ testTimeout: 120000,
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+});
\ No newline at end of file
diff --git a/packages/plugin-node/src/index.ts b/packages/plugin-node/src/index.ts
index 678db6460e2..6969a05592d 100644
--- a/packages/plugin-node/src/index.ts
+++ b/packages/plugin-node/src/index.ts
@@ -1,31 +1,28 @@
-export * from "./services/browser.ts";
-export * from "./services/image.ts";
-export * from "./services/llama.ts";
-export * from "./services/pdf.ts";
-export * from "./services/speech.ts";
-export * from "./services/transcription.ts";
-export * from "./services/video.ts";
+export * from "./services/index.ts";
import { Plugin } from "@ai16z/eliza";
-import { BrowserService } from "./services/browser.ts";
-import { ImageDescriptionService } from "./services/image.ts";
-import { LlamaService } from "./services/llama.ts";
-import { PdfService } from "./services/pdf.ts";
-import { SpeechService } from "./services/speech.ts";
-import { TranscriptionService } from "./services/transcription.ts";
-import { VideoService } from "./services/video.ts";
+
+import {
+ BrowserService,
+ ImageDescriptionService,
+ LlamaService,
+ PdfService,
+ SpeechService,
+ TranscriptionService,
+ VideoService,
+} from "./services/index.ts";
export const nodePlugin: Plugin = {
name: "default",
description: "Default plugin, with basic actions and evaluators",
services: [
- BrowserService,
- ImageDescriptionService,
- LlamaService,
- PdfService,
- SpeechService,
- TranscriptionService,
- VideoService,
+ new BrowserService(),
+ new ImageDescriptionService(),
+ new LlamaService(),
+ new PdfService(),
+ new SpeechService(),
+ new TranscriptionService(),
+ new VideoService(),
],
};
diff --git a/packages/plugin-node/src/services/image.ts b/packages/plugin-node/src/services/image.ts
index e4430c651ac..ac4333abe32 100644
--- a/packages/plugin-node/src/services/image.ts
+++ b/packages/plugin-node/src/services/image.ts
@@ -1,5 +1,4 @@
-// Current image recognition service -- local recognition working, no openai recognition
-import { models } from "@ai16z/eliza";
+import { elizaLogger, models } from "@ai16z/eliza";
import { Service } from "@ai16z/eliza";
import { IAgentRuntime, ModelProviderName, ServiceType } from "@ai16z/eliza";
import {
@@ -19,64 +18,24 @@ import os from "os";
import path from "path";
export class ImageDescriptionService extends Service {
+ static serviceType: ServiceType = ServiceType.IMAGE_DESCRIPTION;
+
private modelId: string = "onnx-community/Florence-2-base-ft";
private device: string = "gpu";
private model: PreTrainedModel | null = null;
private processor: Florence2Processor | null = null;
private tokenizer: PreTrainedTokenizer | null = null;
private initialized: boolean = false;
-
- static serviceType: ServiceType = ServiceType.IMAGE_DESCRIPTION;
-
+ private runtime: IAgentRuntime | null = null;
private queue: string[] = [];
private processing: boolean = false;
- constructor() {
- super();
- }
-
- async initialize(
- device: string | null = null,
- runtime: IAgentRuntime
- ): Promise {
- if (this.initialized) {
- return;
- }
-
+ async initialize(runtime: IAgentRuntime): Promise {
+ this.runtime = runtime;
const model = models[runtime?.character?.modelProvider];
if (model === models[ModelProviderName.LLAMALOCAL]) {
- this.modelId = "onnx-community/Florence-2-base-ft";
-
- env.allowLocalModels = false;
- env.allowRemoteModels = true;
- env.backends.onnx.logLevel = "fatal";
- env.backends.onnx.wasm.proxy = false;
- env.backends.onnx.wasm.numThreads = 1;
-
- console.log("Downloading model...");
-
- this.model =
- await Florence2ForConditionalGeneration.from_pretrained(
- this.modelId,
- {
- device: "gpu",
- progress_callback: (progress) => {
- if (progress.status === "downloading") {
- console.log(
- `Model download progress: ${JSON.stringify(progress)}`
- );
- }
- },
- }
- );
-
- console.log("Model downloaded successfully.");
-
- this.processor = (await AutoProcessor.from_pretrained(
- this.modelId
- )) as Florence2Processor;
- this.tokenizer = await AutoTokenizer.from_pretrained(this.modelId);
+ await this.initializeLocalModel();
} else {
this.modelId = "gpt-4o-mini";
this.device = "cloud";
@@ -85,43 +44,77 @@ export class ImageDescriptionService extends Service {
this.initialized = true;
}
+ private async initializeLocalModel(): Promise {
+ env.allowLocalModels = false;
+ env.allowRemoteModels = true;
+ env.backends.onnx.logLevel = "fatal";
+ env.backends.onnx.wasm.proxy = false;
+ env.backends.onnx.wasm.numThreads = 1;
+
+ elizaLogger.log("Downloading Florence model...");
+
+ this.model = await Florence2ForConditionalGeneration.from_pretrained(
+ this.modelId,
+ {
+ device: "gpu",
+ progress_callback: (progress) => {
+ if (progress.status === "downloading") {
+ elizaLogger.log(
+ `Model download progress: ${JSON.stringify(progress)}`
+ );
+ }
+ },
+ }
+ );
+
+ elizaLogger.success("Florence model downloaded successfully");
+
+ this.processor = (await AutoProcessor.from_pretrained(
+ this.modelId
+ )) as Florence2Processor;
+ this.tokenizer = await AutoTokenizer.from_pretrained(this.modelId);
+ }
+
async describeImage(
- imageUrl: string,
- device?: string,
- runtime?: IAgentRuntime
+ imageUrl: string
): Promise<{ title: string; description: string }> {
- this.initialize(device, runtime);
+ if (!this.initialized) {
+ throw new Error("ImageDescriptionService not initialized");
+ }
if (this.device === "cloud") {
- return this.recognizeWithOpenAI(imageUrl, runtime);
- } else {
- this.queue.push(imageUrl);
- this.processQueue();
-
- return new Promise((resolve, reject) => {
- const checkQueue = () => {
- const index = this.queue.indexOf(imageUrl);
- if (index !== -1) {
- setTimeout(checkQueue, 100);
- } else {
- resolve(this.processImage(imageUrl));
- }
- };
- checkQueue();
- });
+ if (!this.runtime) {
+ throw new Error(
+ "Runtime is required for OpenAI image recognition"
+ );
+ }
+ return this.recognizeWithOpenAI(imageUrl);
}
+
+ this.queue.push(imageUrl);
+ this.processQueue();
+
+ return new Promise((resolve, reject) => {
+ const checkQueue = () => {
+ const index = this.queue.indexOf(imageUrl);
+ if (index !== -1) {
+ setTimeout(checkQueue, 100);
+ } else {
+ resolve(this.processImage(imageUrl));
+ }
+ };
+ checkQueue();
+ });
}
private async recognizeWithOpenAI(
- imageUrl: string,
- runtime
+ imageUrl: string
): Promise<{ title: string; description: string }> {
const isGif = imageUrl.toLowerCase().endsWith(".gif");
let imageData: Buffer | null = null;
try {
if (isGif) {
- console.log("Processing GIF: extracting first frame");
const { filePath } =
await this.extractFirstFrameFromGif(imageUrl);
imageData = fs.readFileSync(filePath);
@@ -141,19 +134,20 @@ export class ImageDescriptionService extends Service {
const prompt =
"Describe this image and give it a title. The first line should be the title, and then a line break, then a detailed description of the image. Respond with the format 'title\ndescription'";
-
const text = await this.requestOpenAI(
imageUrl,
imageData,
prompt,
- isGif,
- runtime
+ isGif
);
- const title = text.split("\n")[0];
- const description = text.split("\n").slice(1).join("\n");
- return { title, description };
+
+ const [title, ...descriptionParts] = text.split("\n");
+ return {
+ title,
+ description: descriptionParts.join("\n"),
+ };
} catch (error) {
- console.error("Error in recognizeWithOpenAI:", error);
+ elizaLogger.error("Error in recognizeWithOpenAI:", error);
throw error;
}
}
@@ -162,50 +156,21 @@ export class ImageDescriptionService extends Service {
imageUrl: string,
imageData: Buffer,
prompt: string,
- isGif: boolean,
- runtime: IAgentRuntime
+ isGif: boolean
): Promise {
- for (let retryAttempts = 0; retryAttempts < 3; retryAttempts++) {
+ for (let attempt = 0; attempt < 3; attempt++) {
try {
- let body;
- if (isGif) {
- const base64Image = imageData.toString("base64");
- body = JSON.stringify({
- model: "gpt-4o-mini",
- messages: [
- {
- role: "user",
- content: [
- { type: "text", text: prompt },
- {
- type: "image_url",
- image_url: {
- url: `data:image/png;base64,${base64Image}`,
- },
- },
- ],
- },
- ],
- max_tokens: 500,
- });
- } else {
- body = JSON.stringify({
- model: "gpt-4o-mini",
- messages: [
- {
- role: "user",
- content: [
- { type: "text", text: prompt },
- {
- type: "image_url",
- image_url: { url: imageUrl },
- },
- ],
- },
- ],
- max_tokens: 300,
- });
- }
+ const content = [
+ { type: "text", text: prompt },
+ {
+ type: "image_url",
+ image_url: {
+ url: isGif
+ ? `data:image/png;base64,${imageData.toString("base64")}`
+ : imageUrl,
+ },
+ },
+ ];
const response = await fetch(
"https://api.openai.com/v1/chat/completions",
@@ -213,9 +178,13 @@ export class ImageDescriptionService extends Service {
method: "POST",
headers: {
"Content-Type": "application/json",
- Authorization: `Bearer ${runtime.getSetting("OPENAI_API_KEY")}`,
+ Authorization: `Bearer ${this.runtime.getSetting("OPENAI_API_KEY")}`,
},
- body: body,
+ body: JSON.stringify({
+ model: "gpt-4o-mini",
+ messages: [{ role: "user", content }],
+ max_tokens: isGif ? 500 : 300,
+ }),
}
);
@@ -226,13 +195,11 @@ export class ImageDescriptionService extends Service {
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
- console.log(
- `Error during OpenAI request (attempt ${retryAttempts + 1}):`,
+ elizaLogger.error(
+ `OpenAI request failed (attempt ${attempt + 1}):`,
error
);
- if (retryAttempts === 2) {
- throw error;
- }
+ if (attempt === 2) throw error;
}
}
throw new Error(
@@ -241,30 +208,30 @@ export class ImageDescriptionService extends Service {
}
private async processQueue(): Promise {
- if (this.processing || this.queue.length === 0) {
- return;
- }
+ if (this.processing || this.queue.length === 0) return;
this.processing = true;
-
while (this.queue.length > 0) {
const imageUrl = this.queue.shift();
await this.processImage(imageUrl);
}
-
this.processing = false;
}
private async processImage(
imageUrl: string
): Promise<{ title: string; description: string }> {
- console.log("***** PROCESSING IMAGE", imageUrl);
+ if (!this.model || !this.processor || !this.tokenizer) {
+ throw new Error("Model components not initialized");
+ }
+
+ elizaLogger.log("Processing image:", imageUrl);
const isGif = imageUrl.toLowerCase().endsWith(".gif");
let imageToProcess = imageUrl;
try {
if (isGif) {
- console.log("Processing GIF: extracting first frame");
+ elizaLogger.log("Extracting first frame from GIF");
const { filePath } =
await this.extractFirstFrameFromGif(imageUrl);
imageToProcess = filePath;
@@ -272,44 +239,32 @@ export class ImageDescriptionService extends Service {
const image = await RawImage.fromURL(imageToProcess);
const visionInputs = await this.processor(image);
-
const prompts =
this.processor.construct_prompts("");
const textInputs = this.tokenizer(prompts);
- console.log("***** GENERATING");
-
+ elizaLogger.log("Generating image description");
const generatedIds = (await this.model.generate({
...textInputs,
...visionInputs,
max_new_tokens: 256,
})) as Tensor;
- console.log("***** GENERATED IDS", generatedIds);
-
const generatedText = this.tokenizer.batch_decode(generatedIds, {
skip_special_tokens: false,
})[0];
- console.log("***** GENERATED TEXT");
- console.log(generatedText);
-
const result = this.processor.post_process_generation(
generatedText,
"",
image.size
);
- console.log("***** RESULT");
- console.log(result);
-
const detailedCaption = result[""] as string;
-
- // TODO: handle this better
-
return { title: detailedCaption, description: detailedCaption };
} catch (error) {
- console.error("Error in processImage:", error);
+ elizaLogger.error("Error processing image:", error);
+ throw error;
} finally {
if (isGif && imageToProcess !== imageUrl) {
fs.unlinkSync(imageToProcess);
@@ -325,19 +280,16 @@ export class ImageDescriptionService extends Service {
frames: 1,
outputType: "png",
});
- const firstFrame = frameData[0];
- const tempDir = os.tmpdir();
- const tempFilePath = path.join(tempDir, `gif_frame_${Date.now()}.png`);
+ const tempFilePath = path.join(
+ os.tmpdir(),
+ `gif_frame_${Date.now()}.png`
+ );
return new Promise((resolve, reject) => {
const writeStream = fs.createWriteStream(tempFilePath);
- firstFrame.getImage().pipe(writeStream);
-
- writeStream.on("finish", () => {
- resolve({ filePath: tempFilePath });
- });
-
+ frameData[0].getImage().pipe(writeStream);
+ writeStream.on("finish", () => resolve({ filePath: tempFilePath }));
writeStream.on("error", reject);
});
}
diff --git a/packages/plugin-node/src/services/index.ts b/packages/plugin-node/src/services/index.ts
new file mode 100644
index 00000000000..95ed3e04ae9
--- /dev/null
+++ b/packages/plugin-node/src/services/index.ts
@@ -0,0 +1,17 @@
+import { BrowserService } from "./browser.ts";
+import { ImageDescriptionService } from "./image.ts";
+import { LlamaService } from "./llama.ts";
+import { PdfService } from "./pdf.ts";
+import { SpeechService } from "./speech.ts";
+import { TranscriptionService } from "./transcription.ts";
+import { VideoService } from "./video.ts";
+
+export {
+ BrowserService,
+ ImageDescriptionService,
+ LlamaService,
+ PdfService,
+ SpeechService,
+ TranscriptionService,
+ VideoService,
+};
diff --git a/packages/plugin-node/src/services/llama.ts b/packages/plugin-node/src/services/llama.ts
index f8bc8be4147..720972278f3 100644
--- a/packages/plugin-node/src/services/llama.ts
+++ b/packages/plugin-node/src/services/llama.ts
@@ -1,4 +1,4 @@
-import { elizaLogger, ServiceType } from "@ai16z/eliza";
+import { elizaLogger, IAgentRuntime, ServiceType } from "@ai16z/eliza";
import { Service } from "@ai16z/eliza";
import fs from "fs";
import https from "https";
@@ -180,6 +180,9 @@ export class LlamaService extends Service {
const modelName = "model.gguf";
this.modelPath = path.join(__dirname, modelName);
}
+
+ async initialize(runtime: IAgentRuntime): Promise {}
+
private async ensureInitialized() {
if (!this.modelInitialized) {
await this.initializeModel();
diff --git a/packages/plugin-node/src/services/pdf.ts b/packages/plugin-node/src/services/pdf.ts
index ad899672fc1..b1138814486 100644
--- a/packages/plugin-node/src/services/pdf.ts
+++ b/packages/plugin-node/src/services/pdf.ts
@@ -1,4 +1,4 @@
-import { Service, ServiceType } from "@ai16z/eliza";
+import { IAgentRuntime, Service, ServiceType } from "@ai16z/eliza";
import { getDocument, PDFDocumentProxy } from "pdfjs-dist";
import { TextItem, TextMarkedContent } from "pdfjs-dist/types/src/display/api";
@@ -9,6 +9,8 @@ export class PdfService extends Service {
super();
}
+ async initialize(runtime: IAgentRuntime): Promise {}
+
async convertPdfToText(pdfBuffer: Buffer): Promise {
// Convert Buffer to Uint8Array
const uint8Array = new Uint8Array(pdfBuffer);
diff --git a/packages/plugin-node/src/services/speech.ts b/packages/plugin-node/src/services/speech.ts
index 66e45a81edf..e5a0beed45e 100644
--- a/packages/plugin-node/src/services/speech.ts
+++ b/packages/plugin-node/src/services/speech.ts
@@ -3,6 +3,7 @@ import { IAgentRuntime, ISpeechService, ServiceType } from "@ai16z/eliza";
import { getWavHeader } from "./audioUtils.ts";
import { synthesize } from "../vendor/vits.ts";
import { Service } from "@ai16z/eliza";
+
function prependWavHeader(
readable: Readable,
audioLength: number,
@@ -107,8 +108,11 @@ async function textToSpeech(runtime: IAgentRuntime, text: string) {
}
}
-export class SpeechService extends Service implements ISpeechService {
+export class SpeechService extends Service {
static serviceType: ServiceType = ServiceType.SPEECH_GENERATION;
+
+ async initialize(runtime: IAgentRuntime): Promise {}
+
async generate(runtime: IAgentRuntime, text: string): Promise {
// check for elevenlabs API key
if (runtime.getSetting("ELEVENLABS_XI_API_KEY")) {
diff --git a/packages/plugin-node/src/services/transcription.ts b/packages/plugin-node/src/services/transcription.ts
index 0360dcec918..dd2da549494 100644
--- a/packages/plugin-node/src/services/transcription.ts
+++ b/packages/plugin-node/src/services/transcription.ts
@@ -1,4 +1,4 @@
-import { settings } from "@ai16z/eliza";
+import { IAgentRuntime, settings } from "@ai16z/eliza";
import { Service, ServiceType } from "@ai16z/eliza";
import { exec } from "child_process";
import { File } from "formdata-node";
@@ -27,6 +27,8 @@ export class TranscriptionService extends Service {
private queue: { audioBuffer: ArrayBuffer; resolve: Function }[] = [];
private processing: boolean = false;
+ async initialize(runtime: IAgentRuntime): Promise {}
+
constructor() {
super();
const rootDir = path.resolve(__dirname, "../../");
diff --git a/packages/plugin-node/src/services/video.ts b/packages/plugin-node/src/services/video.ts
index a8bee25ac7e..bbc16b09735 100644
--- a/packages/plugin-node/src/services/video.ts
+++ b/packages/plugin-node/src/services/video.ts
@@ -22,6 +22,8 @@ export class VideoService extends Service {
this.ensureCacheDirectoryExists();
}
+ async initialize(runtime: IAgentRuntime): Promise {}
+
private ensureCacheDirectoryExists() {
if (!fs.existsSync(this.CONTENT_CACHE_DIR)) {
fs.mkdirSync(this.CONTENT_CACHE_DIR);
@@ -327,10 +329,15 @@ export class VideoService extends Service {
console.log("Starting transcription...");
const startTime = Date.now();
- const transcript = await runtime
- .getService(ServiceType.TRANSCRIPTION)
- .getInstance()
- .transcribe(audioBuffer);
+ const transcriptionService = runtime
+ .getService(ServiceType.TRANSCRIPTION)
+ .getInstance();
+ if (!transcriptionService) {
+ throw new Error("Transcription service not found");
+ }
+
+ const transcript = await transcriptionService.transcribe(audioBuffer);
+
const endTime = Date.now();
console.log(
`Transcription completed in ${(endTime - startTime) / 1000} seconds`
diff --git a/packages/plugin-solana/src/index.ts b/packages/plugin-solana/src/index.ts
index 78d992be442..6fda33841df 100644
--- a/packages/plugin-solana/src/index.ts
+++ b/packages/plugin-solana/src/index.ts
@@ -12,6 +12,10 @@ import { Plugin } from "@ai16z/eliza";
import { walletProvider } from "./providers/wallet.ts";
import { trustScoreProvider } from "./providers/trustScoreProvider.ts";
import { trustEvaluator } from "./evaluators/trust.ts";
+import { TokenProvider } from "./providers/token.ts";
+import { WalletProvider } from "./providers/wallet.ts";
+
+export { TokenProvider, WalletProvider };
export const solanaPlugin: Plugin = {
name: "solana",
diff --git a/packages/plugin-starknet/src/actions/unruggable.ts b/packages/plugin-starknet/src/actions/unruggable.ts
index d558c27587d..7318ceb8e41 100644
--- a/packages/plugin-starknet/src/actions/unruggable.ts
+++ b/packages/plugin-starknet/src/actions/unruggable.ts
@@ -1,5 +1,3 @@
-// TODO: add unruggable
-
import {
ActionExample,
elizaLogger,
@@ -12,13 +10,13 @@ import {
} from "@ai16z/eliza";
import { composeContext } from "@ai16z/eliza";
import { generateObject } from "@ai16z/eliza";
-import {
- executeSwap as executeAvnuSwap,
- fetchQuotes,
- QuoteRequest,
-} from "@avnu/avnu-sdk";
-import { getStarknetAccount, validateSettings } from "../utils/index.ts";
+import {
+ getStarknetAccount,
+ getStarknetProvider,
+ validateSettings,
+} from "../utils/index.ts";
+import { DeployData, Factory } from "@unruggable_starknet/core";
interface SwapContent {
sellTokenAddress: string;
@@ -26,66 +24,64 @@ interface SwapContent {
sellAmount: string;
}
-export function isSwapContent(content: SwapContent): content is SwapContent {
+export function isDeployTokenContent(
+ content: DeployData
+): content is DeployData {
// Validate types
const validTypes =
- typeof content.sellTokenAddress === "string" &&
- typeof content.buyTokenAddress === "string" &&
- typeof content.sellAmount === "string";
+ typeof content.name === "string" &&
+ typeof content.symbol === "string" &&
+ typeof content.owner === "string" &&
+ typeof content.initialSupply === "string";
if (!validTypes) {
return false;
}
// Validate addresses (must be 32-bytes long with 0x prefix)
const validAddresses =
- content.sellTokenAddress.startsWith("0x") &&
- content.sellTokenAddress.length === 66 &&
- content.buyTokenAddress.startsWith("0x") &&
- content.buyTokenAddress.length === 66;
+ content.name.length > 2 &&
+ content.symbol.length > 2 &&
+ parseInt(content.initialSupply) > 0 &&
+ content.owner.startsWith("0x") &&
+ content.owner.length === 66;
return validAddresses;
}
-const swapTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.
-
-These are known addresses you will get asked to swap, use these addresses for sellTokenAddress and buyTokenAddress:
-- BROTHER/brother/$brother: 0x03b405a98c9e795d427fe82cdeeeed803f221b52471e3a757574a2b4180793ee
-- BTC/btc: 0x03fe2b97c1fd336e750087d68b9b867997fd64a2661ff3ca5a7c771641e8e7ac
-- ETH/eth: 0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
-- STRK/strk: 0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d
-- LORDS/lords: 0x0124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49
+const deployTemplate = `Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.
Example response:
\`\`\`json
{
- "sellTokenAddress": "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7",
- "buyTokenAddress": "0x124aeb495b947201f5fac96fd1138e326ad86195b98df6dec9009158a533b49",
- "sellAmount": "1000000000000000000"
+ "name": "Brother",
+ "symbol": "BROTHER",
+ "owner": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "initialSupply": "1000000000000000000"
}
\`\`\`
{{recentMessages}}
-Extract the following information about the requested token swap:
-- Sell token address
-- Buy token address
-- Amount to sell (in wei)
+Extract the following information about the requested token deployment:
+- Token Name
+- Token Symbol
+- Token Owner
+- Token initial supply
Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined.`;
export const deployToken: Action = {
- name: "EXECUTE_STARKNET_SWAP",
+ name: "DEPLOY_STARKNET_UNRUGGABLE_MEME_TOKEN",
similes: [
- "STARKNET_SWAP_TOKENS",
- "STARKNET_TOKEN_SWAP",
- "STARKNET_TRADE_TOKENS",
- "STARKNET_EXCHANGE_TOKENS",
+ "DEPLOY_STARKNET_UNRUGGABLE_TOKEN",
+ "STARKNET_DEPLOY_MEMECOIN",
+ "STARKNET_CREATE_MEMECOIN",
],
validate: async (runtime: IAgentRuntime, message: Memory) => {
return validateSettings(runtime);
},
description:
- "Perform a token swap on starknet. Use this action when a user asks you to swap tokens anything.",
+ "Deploy an Unruggable Memecoin on Starknet. Use this action when a user asks you to deploy a new token on Starknet.",
handler: async (
runtime: IAgentRuntime,
message: Memory,
@@ -93,62 +89,74 @@ export const deployToken: Action = {
_options: { [key: string]: unknown },
callback?: HandlerCallback
): Promise => {
- elizaLogger.log("Starting EXECUTE_STARKNET_SWAP handler...");
+ elizaLogger.log(
+ "Starting DEPLOY_STARKNET_UNRUGGABLE_MEME_TOKEN handler..."
+ );
if (!state) {
state = (await runtime.composeState(message)) as State;
} else {
state = await runtime.updateRecentMessageState(state);
}
- const swapContext = composeContext({
+ const deployContext = composeContext({
state,
- template: swapTemplate,
+ template: deployTemplate,
});
const response = await generateObject({
runtime,
- context: swapContext,
+ context: deployContext,
modelClass: ModelClass.MEDIUM,
});
+ elizaLogger.log("init supply." + response.initialSupply);
+ elizaLogger.log(response);
- if (!isSwapContent(response)) {
- callback?.({ text: "Invalid swap content, please try again." });
+ if (!isDeployTokenContent(response)) {
+ callback?.({
+ text: "Invalid deployment content, please try again.",
+ });
return false;
}
try {
- // Get quote
- const quoteParams: QuoteRequest = {
- sellTokenAddress: response.sellTokenAddress,
- buyTokenAddress: response.buyTokenAddress,
- sellAmount: BigInt(response.sellAmount),
- };
-
- const quote = await fetchQuotes(quoteParams);
-
- // Execute swap
- const swapResult = await executeAvnuSwap(
- getStarknetAccount(runtime),
- quote[0],
- {
- slippage: 0.05, // 5% slippage
- executeApprove: true,
- }
- );
+ const provider = getStarknetProvider(runtime);
+ const account = getStarknetAccount(runtime);
+
+ const factory = new Factory({
+ provider,
+ chainId: await provider.getChainId(),
+ });
+
+ const { tokenAddress, calls } = factory.getDeployCalldata({
+ name: response.name,
+ symbol: response.symbol,
+ owner: response.owner,
+ initialSupply: response.initialSupply,
+ });
elizaLogger.log(
- "Swap completed successfully!" + swapResult.transactionHash
+ "Deployment has been initiated for coin: " +
+ response.name +
+ " at address: " +
+ tokenAddress
);
+ const tx = await account.execute(calls);
+
callback?.({
text:
- "Swap completed successfully! tx: " +
- swapResult.transactionHash,
+ "Token Deployment completed successfully!" +
+ response.symbol +
+ " deployed in tx: " +
+ tx.transaction_hash,
});
return true;
} catch (error) {
- elizaLogger.error("Error during token swap:", error);
- callback?.({ text: `Error during swap:` });
+ elizaLogger.error("Error during token deployment:", error);
+ callback?.({
+ text: `Error during deployment: ${error.message}`,
+ content: { error: error.message },
+ });
return false;
}
},
@@ -157,13 +165,13 @@ export const deployToken: Action = {
{
user: "{{user1}}",
content: {
- text: "Swap 10 ETH for LORDS",
+ text: "Deploy a new token called Lords with the symbol LORDS, owned by 0x024BA6a4023fB90962bDfc2314F3B94372aa382D155291635fc3E6b777657A5B and initial supply of 1000000000000000000 on Starknet",
},
},
{
user: "{{agent}}",
content: {
- text: "Ok, I'll swap 10 ETH for LORDS",
+ text: "Ok, I'll deploy the Lords token to Starknet",
},
},
],
@@ -171,13 +179,13 @@ export const deployToken: Action = {
{
user: "{{user1}}",
content: {
- text: "Swap 100 $lords on starknet",
+ text: "Deploy the SLINK coin to Starknet",
},
},
{
user: "{{agent}}",
content: {
- text: "Ok, I'll swap 100 $lords on starknet",
+ text: "Ok, I'll deploy your coin on Starknet",
},
},
],
@@ -185,13 +193,13 @@ export const deployToken: Action = {
{
user: "{{user1}}",
content: {
- text: "Swap 0.5 BTC for LORDS",
+ text: "Create a new coin on Starknet",
},
},
{
user: "{{agent}}",
content: {
- text: "Ok, I'll swap 0.5 BTC for LORDS",
+ text: "Ok, I'll create a new coin for you on Starknet",
},
},
],
diff --git a/packages/plugin-starknet/src/index.ts b/packages/plugin-starknet/src/index.ts
index 5ed9e48db70..30211f77d7e 100644
--- a/packages/plugin-starknet/src/index.ts
+++ b/packages/plugin-starknet/src/index.ts
@@ -1,7 +1,7 @@
import { Plugin } from "@ai16z/eliza";
import { executeSwap } from "./actions/swap";
import transfer from "./actions/transfer";
-
+import { deployToken } from "./actions/unruggable";
export const PROVIDER_CONFIG = {
AVNU_API: "https://starknet.impulse.avnu.fi/v1",
MAX_RETRIES: 3,
@@ -20,7 +20,7 @@ export const PROVIDER_CONFIG = {
export const starknetPlugin: Plugin = {
name: "starknet",
description: "Starknet Plugin for Eliza",
- actions: [transfer, executeSwap],
+ actions: [transfer, executeSwap, deployToken],
evaluators: [],
providers: [],
};
diff --git a/packages/plugin-video-generation/package.json b/packages/plugin-video-generation/package.json
new file mode 100644
index 00000000000..c311e78d2e2
--- /dev/null
+++ b/packages/plugin-video-generation/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "@ai16z/plugin-video-generation",
+ "version": "0.0.1",
+ "main": "dist/index.js",
+ "type": "module",
+ "types": "dist/index.d.ts",
+ "dependencies": {
+ "@ai16z/eliza": "workspace:*",
+ "tsup": "^8.3.5"
+ },
+ "scripts": {
+ "build": "tsup --format esm --dts",
+ "dev": "tsup --watch"
+ },
+ "peerDependencies": {
+ "whatwg-url": "7.1.0"
+ }
+}
\ No newline at end of file
diff --git a/packages/plugin-video-generation/src/constants.ts b/packages/plugin-video-generation/src/constants.ts
new file mode 100644
index 00000000000..4f7428d8f76
--- /dev/null
+++ b/packages/plugin-video-generation/src/constants.ts
@@ -0,0 +1,4 @@
+export const LUMA_CONSTANTS = {
+ API_URL: 'https://api.lumalabs.ai/dream-machine/v1/generations',
+ API_KEY_SETTING: "LUMA_API_KEY" // The setting name to fetch from runtime
+};
\ No newline at end of file
diff --git a/packages/plugin-video-generation/src/index.ts b/packages/plugin-video-generation/src/index.ts
new file mode 100644
index 00000000000..0723485263b
--- /dev/null
+++ b/packages/plugin-video-generation/src/index.ts
@@ -0,0 +1,221 @@
+import { elizaLogger } from "@ai16z/eliza/src/logger.ts";
+import {
+ Action,
+ HandlerCallback,
+ IAgentRuntime,
+ Memory,
+ Plugin,
+ State,
+} from "@ai16z/eliza/src/types.ts";
+import fs from "fs";
+import { LUMA_CONSTANTS } from './constants';
+
+const generateVideo = async (prompt: string, runtime: IAgentRuntime) => {
+ const API_KEY = runtime.getSetting(LUMA_CONSTANTS.API_KEY_SETTING);
+
+ try {
+ elizaLogger.log("Starting video generation with prompt:", prompt);
+
+ const response = await fetch(LUMA_CONSTANTS.API_URL, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${API_KEY}`,
+ 'accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({ prompt })
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ elizaLogger.error("Luma API error:", {
+ status: response.status,
+ statusText: response.statusText,
+ error: errorText
+ });
+ throw new Error(`Luma API error: ${response.statusText} - ${errorText}`);
+ }
+
+ const data = await response.json();
+ elizaLogger.log("Generation request successful, received response:", data);
+
+ // Poll for completion
+ let status = data.status;
+ let videoUrl = null;
+ const generationId = data.id;
+
+ while (status !== 'completed' && status !== 'failed') {
+ await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
+
+ const statusResponse = await fetch(`${LUMA_CONSTANTS.API_URL}/${generationId}`, {
+ method: 'GET',
+ headers: {
+ 'Authorization': `Bearer ${API_KEY}`,
+ 'accept': 'application/json'
+ }
+ });
+
+ if (!statusResponse.ok) {
+ const errorText = await statusResponse.text();
+ elizaLogger.error("Status check error:", {
+ status: statusResponse.status,
+ statusText: statusResponse.statusText,
+ error: errorText
+ });
+ throw new Error('Failed to check generation status: ' + errorText);
+ }
+
+ const statusData = await statusResponse.json();
+ elizaLogger.log("Status check response:", statusData);
+
+ status = statusData.state;
+ if (status === 'completed') {
+ videoUrl = statusData.assets?.video;
+ }
+ }
+
+ if (status === 'failed') {
+ throw new Error('Video generation failed');
+ }
+
+ if (!videoUrl) {
+ throw new Error('No video URL in completed response');
+ }
+
+ return {
+ success: true,
+ data: videoUrl
+ };
+ } catch (error) {
+ elizaLogger.error("Video generation error:", error);
+ return {
+ success: false,
+ error: error.message || 'Unknown error occurred'
+ };
+ }
+}
+
+const videoGeneration: Action = {
+ name: "GENERATE_VIDEO",
+ similes: [
+ "VIDEO_GENERATION",
+ "VIDEO_GEN",
+ "CREATE_VIDEO",
+ "MAKE_VIDEO",
+ "RENDER_VIDEO",
+ "ANIMATE",
+ "CREATE_ANIMATION",
+ "VIDEO_CREATE",
+ "VIDEO_MAKE"
+ ],
+ description: "Generate a video based on a text prompt",
+ validate: async (runtime: IAgentRuntime, message: Memory) => {
+ elizaLogger.log("Validating video generation action");
+ const lumaApiKey = runtime.getSetting("LUMA_API_KEY");
+ elizaLogger.log("LUMA_API_KEY present:", !!lumaApiKey);
+ return !!lumaApiKey;
+ },
+ handler: async (
+ runtime: IAgentRuntime,
+ message: Memory,
+ state: State,
+ options: any,
+ callback: HandlerCallback
+ ) => {
+ elizaLogger.log("Video generation request:", message);
+
+ // Clean up the prompt by removing mentions and commands
+ let videoPrompt = message.content.text
+ .replace(/<@\d+>/g, '') // Remove mentions
+ .replace(/generate video|create video|make video|render video/gi, '') // Remove commands
+ .trim();
+
+ if (!videoPrompt || videoPrompt.length < 5) {
+ callback({
+ text: "Could you please provide more details about what kind of video you'd like me to generate? For example: 'Generate a video of a sunset on a beach' or 'Create a video of a futuristic city'",
+ });
+ return;
+ }
+
+ elizaLogger.log("Video prompt:", videoPrompt);
+
+ callback({
+ text: `I'll generate a video based on your prompt: "${videoPrompt}". This might take a few minutes...`,
+ });
+
+ try {
+ const result = await generateVideo(videoPrompt, runtime);
+
+ if (result.success && result.data) {
+ // Download the video file
+ const response = await fetch(result.data);
+ const arrayBuffer = await response.arrayBuffer();
+ const videoFileName = `content_cache/generated_video_${Date.now()}.mp4`;
+
+ // Save video file
+ fs.writeFileSync(videoFileName, Buffer.from(arrayBuffer));
+
+ callback({
+ text: "Here's your generated video!",
+ attachments: [
+ {
+ id: crypto.randomUUID(),
+ url: result.data,
+ title: "Generated Video",
+ source: "videoGeneration",
+ description: videoPrompt,
+ text: videoPrompt,
+ },
+ ],
+ }, [videoFileName]); // Add the video file to the attachments
+ } else {
+ callback({
+ text: `Video generation failed: ${result.error}`,
+ error: true
+ });
+ }
+ } catch (error) {
+ elizaLogger.error(`Failed to generate video. Error: ${error}`);
+ callback({
+ text: `Video generation failed: ${error.message}`,
+ error: true
+ });
+ }
+ },
+ examples: [
+ [
+ {
+ user: "{{user1}}",
+ content: { text: "Generate a video of a cat playing piano" },
+ },
+ {
+ user: "{{agentName}}",
+ content: {
+ text: "I'll create a video of a cat playing piano for you",
+ action: "GENERATE_VIDEO"
+ },
+ }
+ ],
+ [
+ {
+ user: "{{user1}}",
+ content: { text: "Can you make a video of a sunset at the beach?" },
+ },
+ {
+ user: "{{agentName}}",
+ content: {
+ text: "I'll generate a beautiful beach sunset video for you",
+ action: "GENERATE_VIDEO"
+ },
+ }
+ ]
+ ]
+} as Action;
+
+export const videoGenerationPlugin: Plugin = {
+ name: "videoGeneration",
+ description: "Generate videos using Luma AI",
+ actions: [videoGeneration],
+ evaluators: [],
+ providers: [],
+};
\ No newline at end of file
diff --git a/packages/plugin-video-generation/tsconfig.json b/packages/plugin-video-generation/tsconfig.json
new file mode 100644
index 00000000000..c065d9145a4
--- /dev/null
+++ b/packages/plugin-video-generation/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "dist",
+ "rootDir": ".",
+ "module": "ESNext",
+ "moduleResolution": "Bundler",
+ "types": [
+ "node"
+ ]
+ },
+ "include": [
+ "src"
+ ]
+}
\ No newline at end of file
diff --git a/packages/plugin-video-generation/tsup.config.ts b/packages/plugin-video-generation/tsup.config.ts
new file mode 100644
index 00000000000..4b66bbcbbde
--- /dev/null
+++ b/packages/plugin-video-generation/tsup.config.ts
@@ -0,0 +1,19 @@
+import { defineConfig } from "tsup";
+
+export default defineConfig({
+ entry: ["src/index.ts"],
+ outDir: "dist",
+ sourcemap: true,
+ clean: true,
+ format: ["esm"],
+ external: [
+ "dotenv",
+ "fs",
+ "path",
+ "@reflink/reflink",
+ "@node-llama-cpp",
+ "https",
+ "http",
+ "agentkeepalive"
+ ],
+});
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a97848a2ed5..692f2dd4493 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3050,8 +3050,8 @@ packages:
cpu: [x64]
os: [win32]
- '@octokit/app@15.1.0':
- resolution: {integrity: sha512-TkBr7QgOmE6ORxvIAhDbZsqPkF7RSqTY4pLTtUQCvr6dTXqvi2fFo46q3h1lxlk/sGMQjqyZ0kEahkD/NyzOHg==}
+ '@octokit/app@15.1.1':
+ resolution: {integrity: sha512-fk8xrCSPTJGpyBdBNI+DcZ224dm0aApv4vi6X7/zTmANXlegKV2Td+dJ+fd7APPaPN7R+xttUsj2Fm+AFDSfMQ==}
engines: {node: '>= 18'}
'@octokit/auth-app@7.1.3':
@@ -8380,8 +8380,8 @@ packages:
resolution: {integrity: sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==}
engines: {node: '>= 12.13.0'}
- local-pkg@0.5.0:
- resolution: {integrity: sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==}
+ local-pkg@0.5.1:
+ resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==}
engines: {node: '>=14'}
locate-character@3.0.0:
@@ -15032,7 +15032,7 @@ snapshots:
'@iconify/types': 2.0.0
debug: 4.3.7(supports-color@5.5.0)
kolorist: 1.8.0
- local-pkg: 0.5.0
+ local-pkg: 0.5.1
mlly: 1.7.3
transitivePeerDependencies:
- supports-color
@@ -15720,7 +15720,7 @@ snapshots:
'@nx/nx-win32-x64-msvc@20.1.2':
optional: true
- '@octokit/app@15.1.0':
+ '@octokit/app@15.1.1':
dependencies:
'@octokit/auth-app': 7.1.3
'@octokit/auth-unauthenticated': 6.1.0
@@ -22118,7 +22118,7 @@ snapshots:
loader-utils@3.3.1: {}
- local-pkg@0.5.0:
+ local-pkg@0.5.1:
dependencies:
mlly: 1.7.3
pkg-types: 1.2.1
@@ -22169,7 +22169,7 @@ snapshots:
log-symbols@4.1.0:
dependencies:
- chalk: 4.1.0
+ chalk: 4.1.2
is-unicode-supported: 0.1.0
log-symbols@6.0.0:
@@ -23440,7 +23440,7 @@ snapshots:
octokit@4.0.2:
dependencies:
- '@octokit/app': 15.1.0
+ '@octokit/app': 15.1.1
'@octokit/core': 6.1.2
'@octokit/oauth-app': 7.1.3
'@octokit/plugin-paginate-graphql': 5.2.4(@octokit/core@6.1.2)