diff --git a/.env.example b/.env.example index 0d808f655b..fd5cdaa408 100644 --- a/.env.example +++ b/.env.example @@ -271,6 +271,12 @@ AWS_REGION= AWS_S3_BUCKET= AWS_S3_UPLOAD_PATH= +# WordPress Configuration +WORDPRESS_DRY_RUN=false +WORDPRESS_USERNAME= +WORDPRESS_PASSWORD=# Application password +WORDPRESS_URL= + # Deepgram DEEPGRAM_API_KEY= diff --git a/agent/package.json b/agent/package.json index e27d4aa5ee..95f01d4349 100644 --- a/agent/package.json +++ b/agent/package.json @@ -24,6 +24,7 @@ "@ai16z/client-discord": "workspace:*", "@ai16z/client-farcaster": "workspace:*", "@ai16z/client-telegram": "workspace:*", + "@ai16z/client-wordpress": "workspace:*", "@ai16z/client-twitter": "workspace:*", "@ai16z/eliza": "workspace:*", "@ai16z/plugin-0g": "workspace:*", diff --git a/agent/src/index.ts b/agent/src/index.ts index cd92b6005d..953cb949b0 100644 --- a/agent/src/index.ts +++ b/agent/src/index.ts @@ -5,6 +5,7 @@ import { DirectClientInterface } from "@ai16z/client-direct"; import { DiscordClientInterface } from "@ai16z/client-discord"; import { TelegramClientInterface } from "@ai16z/client-telegram"; import { TwitterClientInterface } from "@ai16z/client-twitter"; +import { WordpressClientInterface } from "@ai16z/client-wordpress"; import { FarcasterAgentClient } from "@ai16z/client-farcaster"; import { AgentRuntime, @@ -356,6 +357,11 @@ export async function initializeClients( if (twitterClient) clients.twitter = twitterClient; } + if (clientTypes.includes("wordpress")) { + const wordpressClient = await WordpressClientInterface.start(runtime); + clients.push(wordpressClient); + } + if (clientTypes.includes("farcaster")) { // why is this one different :( const farcasterClient = new FarcasterAgentClient(runtime); diff --git a/packages/client-wordpress/.npmignore b/packages/client-wordpress/.npmignore new file mode 100644 index 0000000000..078562ecea --- /dev/null +++ b/packages/client-wordpress/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/client-wordpress/eslint.config.mjs b/packages/client-wordpress/eslint.config.mjs new file mode 100644 index 0000000000..92fe5bbebe --- /dev/null +++ b/packages/client-wordpress/eslint.config.mjs @@ -0,0 +1,3 @@ +import eslintGlobalConfig from "../../eslint.config.mjs"; + +export default [...eslintGlobalConfig]; diff --git a/packages/client-wordpress/package.json b/packages/client-wordpress/package.json new file mode 100644 index 0000000000..675f4d2e01 --- /dev/null +++ b/packages/client-wordpress/package.json @@ -0,0 +1,22 @@ +{ + "name": "@ai16z/client-wordpress", + "version": "0.1.5-alpha.5", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@ai16z/eliza": "workspace:*", + "axios": "1.7.8" + }, + "devDependencies": { + "tsup": "8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "lint": "eslint . --fix" + }, + "peerDependencies": { + "whatwg-url": "7.1.0" + } +} diff --git a/packages/client-wordpress/src/blog.ts b/packages/client-wordpress/src/blog.ts new file mode 100644 index 0000000000..94aef9161e --- /dev/null +++ b/packages/client-wordpress/src/blog.ts @@ -0,0 +1,142 @@ +import { WordpressClient } from "./client"; +import { + elizaLogger, + stringToUuid, + composeContext, + IAgentRuntime, + generateText, + ModelClass +} from "@ai16z/eliza"; + +const wordpressPostTemplate = `{{timeline}} + +# Knowledge +{{knowledge}} + +About {{agentName}}: +{{bio}} + +{{summary}} +{{postDirections}} + +{{providers}} + +# Task: Generate a blog post in the voice and style of {{agentName}} +Write a post that is {{adjective}} about {{topic}}, from the perspective of {{agentName}}. +`; + +export class WordpressBlogClient { + + runtime: IAgentRuntime; + client: WordpressClient; + + async start() { + await this.client.init(); + + const generateNewPostLoop = async () => { + await this.generateNewBlogPost(); + setTimeout(generateNewPostLoop, 1000 * 60 * 60 * 24); // 24 hours + }; + + generateNewPostLoop(); + + }; + + constructor(client: WordpressClient, runtime: IAgentRuntime) { + this.runtime = runtime; + this.client = client; + } + + private async generateNewBlogPost() { + elizaLogger.log("Generating new blog post"); + + try { + // get the last 5 posts + const posts = await this.client.getPosts(); + const last5Posts = posts.slice(-5); + const formattedPosts = last5Posts.map(post => + `Title: ${post.title.rendered}\nContent: ${post.content.rendered}`).join("\n\n"); + const topics = this.runtime.character.topics.join(', '); + + const state = await this.runtime.composeState( + { + userId: this.runtime.agentId, + roomId: stringToUuid('wordpress_generate_room'), + agentId: this.runtime.agentId, + content: { + text: topics, + action: '' + } + }, + { + timeline: formattedPosts, + } + ); + const context = composeContext({ + state, + template: this.runtime.character.templates?.wordpressPostTemplate || wordpressPostTemplate + }); + + elizaLogger.debug('Generate post prompt:\n' + context); + + const newBlogContent = await generateText({ + runtime: this.runtime, + context, + modelClass: ModelClass.SMALL + }); + + // Generate a title for the post + const title = await generateText({ + runtime: this.runtime, + context: `Generate a title for the post, only return the title, no other text: ${newBlogContent}`, + modelClass: ModelClass.SMALL + }); + + if (this.runtime.getSetting('WORDPRESS_DRY_RUN') === 'true') { + elizaLogger.info(`Dry run: would have posted:\nTitle: ${title}\nContent: ${newBlogContent}`); + return; + } + try { + elizaLogger.log(`Posting new WordPress blog post:\n${newBlogContent}`); + + const result = await this.client.addToRequestQueue( + async () => await this.client.createPost({ + title: title, + content: newBlogContent, + status: 'draft' + }) + ); + + await this.runtime.cacheManager.set( + `wordpress/${this.client.getPublicConfig().username}/lastPost`, + { + id: result.id, + timestamp: Date.now() + } + ); + + const roomId = stringToUuid(`wordpress-post-${result.id}`); + await this.runtime.messageManager.createMemory({ + id: stringToUuid(`${result.id}-${this.runtime.agentId}`), + userId: this.runtime.agentId, + content: { + text: newBlogContent, + url: result.url, + source: 'wordpress' + }, + agentId: this.runtime.agentId, + roomId, + createdAt: Date.now() + }); + + elizaLogger.log(`WordPress post created: ${result.url}`); + } catch (error) { + elizaLogger.error('Error creating WordPress post:', error); + } + + } catch (error) { + elizaLogger.error("Error generating new blog post", error); + } + } + +} \ No newline at end of file diff --git a/packages/client-wordpress/src/client.ts b/packages/client-wordpress/src/client.ts new file mode 100644 index 0000000000..f8637f2f11 --- /dev/null +++ b/packages/client-wordpress/src/client.ts @@ -0,0 +1,98 @@ +import axios, { AxiosInstance } from "axios"; +import { WpConfig, WpPost } from "./types"; +import { IAgentRuntime } from "@ai16z/eliza"; + +class RequestQueue { + private queue: (() => Promise)[] = []; + private processing = false; + + async add(request: () => Promise): Promise { + return new Promise((resolve, reject) => { + this.queue.push(async () => { + try { + const result = await request(); + resolve(result); + } catch (error) { + reject(error); + } + }); + this.processQueue(); + }); + } + + private async processQueue() { + if (this.processing || this.queue.length === 0) { + return; + } + + this.processing = true; + while (this.queue.length > 0) { + const request = this.queue.shift(); + if (!request) continue; + try { + await request(); + } catch (error) { + console.error("Error processing request:", error); + this.queue.unshift(request); + await this.exponentialBackoff(this.queue.length); + } + await this.randomDelay(); + } + this.processing = false; + } + + private async exponentialBackoff(retryCount: number) { + const delay = Math.pow(2, retryCount) * 1000; + await new Promise((resolve) => setTimeout(resolve, delay)); + } + + private async randomDelay() { + const delay = Math.floor(Math.random() * 2000) + 1500; + await new Promise((resolve) => setTimeout(resolve, delay)); + } +} + +export class WordpressClient { + private client: AxiosInstance; + private config: WpConfig; + protected requestQueue: RequestQueue = new RequestQueue(); + + constructor(config: WpConfig) { + this.config = config; + this.client = axios.create({ + baseURL: `${config.url}/wp-json/wp/v2`, + headers: { + Authorization: `Basic ${Buffer.from( + `${config.username}:${config.password}` + ).toString("base64")}`, + "Content-Type": "application/json", + }, + }); + } + + async init() { + // check if the client is initialized + if (this.client) { + return; + } + } + + public getPublicConfig() { + const { username } = this.config; + return { username }; + } + + async createPost(post: WpPost): Promise { + const response = await this.client.post("/posts", post); + return response.data; + } + + async getPosts(): Promise { + const response = await this.client.get("/posts"); + return response.data; + } + + public async addToRequestQueue(task: () => Promise) { + return this.requestQueue.add(task); + } +} diff --git a/packages/client-wordpress/src/environment.ts b/packages/client-wordpress/src/environment.ts new file mode 100644 index 0000000000..7162270d1e --- /dev/null +++ b/packages/client-wordpress/src/environment.ts @@ -0,0 +1,40 @@ +import { IAgentRuntime } from "@ai16z/eliza"; +import { z } from "zod"; + +export const wordpressEnvSchema = z.object({ + WORDPRESS_URL: z.string().min(1, "Wordpress url is required"), + WORDPRESS_USERNAME: z.string().min(1, "Wordpress username is required"), + WORDPRESS_PASSWORD: z.string().min(1, "Wordpress password is required"), +}); + +export type WordpressConfig = z.infer; + +export async function validateWordpressConfig( + runtime: IAgentRuntime +): Promise { + try { + const config = { + WORDPRESS_URL: + runtime.getSetting("WORDPRESS_URL") || + process.env.WORDPRESS_URL, + WORDPRESS_USERNAME: + runtime.getSetting("WORDPRESS_USERNAME") || + process.env.WORDPRESS_USERNAME, + WORDPRESS_PASSWORD: + runtime.getSetting("WORDPRESS_PASSWORD") || + process.env.WORDPRESS_PASSWORD, + }; + + return wordpressEnvSchema.parse(config); + } catch (error) { + if (error instanceof z.ZodError) { + const errorMessages = error.errors + .map((err) => `${err.path.join(".")}: ${err.message}`) + .join("\n"); + throw new Error( + `Wordpress configuration validation failed:\n${errorMessages}` + ); + } + throw error; + } +} diff --git a/packages/client-wordpress/src/index.ts b/packages/client-wordpress/src/index.ts new file mode 100644 index 0000000000..3c99f81f9e --- /dev/null +++ b/packages/client-wordpress/src/index.ts @@ -0,0 +1,33 @@ +import { Client, IAgentRuntime, elizaLogger } from "@ai16z/eliza"; +import { WordpressClient } from "./client"; +import { validateWordpressConfig } from "./environment"; +import { WordpressBlogClient } from "./blog"; + +export class WordpressManager { + client: WordpressClient; + blog: WordpressBlogClient; + constructor(runtime: IAgentRuntime) { + this.client = new WordpressClient({ + url: runtime.getSetting("WORDPRESS_URL"), + username: runtime.getSetting("WORDPRESS_USERNAME"), + password: runtime.getSetting("WORDPRESS_PASSWORD"), + }); + this.blog = new WordpressBlogClient(this.client, runtime); + } +} + +export const WordpressClientInterface: Client = { + async start(runtime: IAgentRuntime) { + await validateWordpressConfig(runtime); + const wp = new WordpressManager(runtime); + + wp.blog.start(); + + return wp; + }, + async stop(_runtime: IAgentRuntime) { + elizaLogger.warn("Wordpress client does not support stopping yet"); + }, +}; + +export default WordpressClientInterface; diff --git a/packages/client-wordpress/src/types.ts b/packages/client-wordpress/src/types.ts new file mode 100644 index 0000000000..f34b55fb41 --- /dev/null +++ b/packages/client-wordpress/src/types.ts @@ -0,0 +1,11 @@ +export interface WpConfig { + url: string; + username: string; + password: string; +} + +export interface WpPost { + title: string; + content: string; + status: string; +} \ No newline at end of file diff --git a/packages/client-wordpress/tsconfig.json b/packages/client-wordpress/tsconfig.json new file mode 100644 index 0000000000..73993deaaf --- /dev/null +++ b/packages/client-wordpress/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/client-wordpress/tsup.config.ts b/packages/client-wordpress/tsup.config.ts new file mode 100644 index 0000000000..e42bf4efea --- /dev/null +++ b/packages/client-wordpress/tsup.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], // Ensure you're targeting CommonJS + external: [ + "dotenv", // Externalize dotenv to prevent bundling + "fs", // Externalize fs to use Node.js built-in module + "path", // Externalize other built-ins if necessary + "@reflink/reflink", + "@node-llama-cpp", + "https", + "http", + "agentkeepalive", + // Add other modules you want to externalize + ], +}); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 860e68a6d0..73e288f303 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -611,6 +611,7 @@ export enum Clients { TWITTER = "twitter", TELEGRAM = "telegram", FARCASTER = "farcaster", + WORDPRESS = "wordpress", } /** * Configuration for an agent character @@ -657,6 +658,7 @@ export type Character = { discordVoiceHandlerTemplate?: string; discordShouldRespondTemplate?: string; discordMessageHandlerTemplate?: string; + wordpressPostTemplate?: string; }; /** Character biography */