Skip to content

Commit

Permalink
added wordpress client
Browse files Browse the repository at this point in the history
  • Loading branch information
0xNerd committed Dec 16, 2024
1 parent 67f85fb commit e72eac0
Show file tree
Hide file tree
Showing 14 changed files with 400 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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=
1 change: 1 addition & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:*",
Expand Down
6 changes: 6 additions & 0 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 6 additions & 0 deletions packages/client-wordpress/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*

!dist/**
!package.json
!readme.md
!tsup.config.ts
3 changes: 3 additions & 0 deletions packages/client-wordpress/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import eslintGlobalConfig from "../../eslint.config.mjs";

export default [...eslintGlobalConfig];
22 changes: 22 additions & 0 deletions packages/client-wordpress/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
142 changes: 142 additions & 0 deletions packages/client-wordpress/src/blog.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}

}
98 changes: 98 additions & 0 deletions packages/client-wordpress/src/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import axios, { AxiosInstance } from "axios";
import { WpConfig, WpPost } from "./types";
import { IAgentRuntime } from "@ai16z/eliza";

class RequestQueue {
private queue: (() => Promise<any>)[] = [];
private processing = false;

async add<T>(request: () => Promise<T>): Promise<T> {
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<any> {
const response = await this.client.post("/posts", post);
return response.data;
}

async getPosts(): Promise<any> {
const response = await this.client.get("/posts");
return response.data;
}

public async addToRequestQueue(task: () => Promise<any>) {
return this.requestQueue.add(task);
}
}
40 changes: 40 additions & 0 deletions packages/client-wordpress/src/environment.ts
Original file line number Diff line number Diff line change
@@ -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<typeof wordpressEnvSchema>;

export async function validateWordpressConfig(
runtime: IAgentRuntime
): Promise<WordpressConfig> {
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;
}
}
33 changes: 33 additions & 0 deletions packages/client-wordpress/src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit e72eac0

Please sign in to comment.