From 24be11b1db68358e16073383f772319fb293e763 Mon Sep 17 00:00:00 2001 From: Preston Brown Date: Mon, 23 Dec 2024 22:37:02 -0600 Subject: [PATCH 1/2] twitter convo action --- agent/package.json | 121 ++++++------ packages/plugin-twitter/.npmignore | 6 + packages/plugin-twitter/package.json | 17 ++ packages/plugin-twitter/src/actions/post.ts | 200 ++++++++++++++++++++ packages/plugin-twitter/src/index.ts | 12 ++ packages/plugin-twitter/tsconfig.json | 13 ++ packages/plugin-twitter/tsup.config.ts | 10 + 7 files changed, 319 insertions(+), 60 deletions(-) create mode 100644 packages/plugin-twitter/.npmignore create mode 100644 packages/plugin-twitter/package.json create mode 100644 packages/plugin-twitter/src/actions/post.ts create mode 100644 packages/plugin-twitter/src/index.ts create mode 100644 packages/plugin-twitter/tsconfig.json create mode 100644 packages/plugin-twitter/tsup.config.ts diff --git a/agent/package.json b/agent/package.json index 91a900d600..217a6ed6c7 100644 --- a/agent/package.json +++ b/agent/package.json @@ -1,62 +1,63 @@ { - "name": "@elizaos/agent", - "version": "0.1.7-alpha.1", - "main": "src/index.ts", - "type": "module", - "scripts": { - "start": "node --loader ts-node/esm src/index.ts", - "dev": "node --loader ts-node/esm src/index.ts", - "check-types": "tsc --noEmit" - }, - "nodemonConfig": { - "watch": [ - "src", - "../core/dist" - ], - "ext": "ts,json", - "exec": "node --enable-source-maps --loader ts-node/esm src/index.ts" - }, - "dependencies": { - "@elizaos/adapter-postgres": "workspace:*", - "@elizaos/adapter-redis": "workspace:*", - "@elizaos/adapter-sqlite": "workspace:*", - "@elizaos/client-auto": "workspace:*", - "@elizaos/client-direct": "workspace:*", - "@elizaos/client-discord": "workspace:*", - "@elizaos/client-farcaster": "workspace:*", - "@elizaos/client-lens": "workspace:*", - "@elizaos/client-telegram": "workspace:*", - "@elizaos/client-twitter": "workspace:*", - "@elizaos/client-slack": "workspace:*", - "@elizaos/core": "workspace:*", - "@elizaos/plugin-0g": "workspace:*", - "@elizaos/plugin-aptos": "workspace:*", - "@elizaos/plugin-bootstrap": "workspace:*", - "@elizaos/plugin-intiface": "workspace:*", - "@elizaos/plugin-coinbase": "workspace:*", - "@elizaos/plugin-conflux": "workspace:*", - "@elizaos/plugin-evm": "workspace:*", - "@elizaos/plugin-flow": "workspace:*", - "@elizaos/plugin-story": "workspace:*", - "@elizaos/plugin-goat": "workspace:*", - "@elizaos/plugin-icp": "workspace:*", - "@elizaos/plugin-image-generation": "workspace:*", - "@elizaos/plugin-nft-generation": "workspace:*", - "@elizaos/plugin-node": "workspace:*", - "@elizaos/plugin-solana": "workspace:*", - "@elizaos/plugin-starknet": "workspace:*", - "@elizaos/plugin-ton": "workspace:*", - "@elizaos/plugin-sui": "workspace:*", - "@elizaos/plugin-tee": "workspace:*", - "@elizaos/plugin-multiversx": "workspace:*", - "@elizaos/plugin-near": "workspace:*", - "@elizaos/plugin-zksync-era": "workspace:*", - "readline": "1.3.0", - "ws": "8.18.0", - "yargs": "17.7.2" - }, - "devDependencies": { - "ts-node": "10.9.2", - "tsup": "8.3.5" - } + "name": "@elizaos/agent", + "version": "0.1.7-alpha.1", + "main": "src/index.ts", + "type": "module", + "scripts": { + "start": "node --loader ts-node/esm src/index.ts", + "dev": "node --loader ts-node/esm src/index.ts", + "check-types": "tsc --noEmit" + }, + "nodemonConfig": { + "watch": [ + "src", + "../core/dist" + ], + "ext": "ts,json", + "exec": "node --enable-source-maps --loader ts-node/esm src/index.ts" + }, + "dependencies": { + "@elizaos/adapter-postgres": "workspace:*", + "@elizaos/adapter-redis": "workspace:*", + "@elizaos/adapter-sqlite": "workspace:*", + "@elizaos/client-auto": "workspace:*", + "@elizaos/client-direct": "workspace:*", + "@elizaos/client-discord": "workspace:*", + "@elizaos/client-farcaster": "workspace:*", + "@elizaos/client-lens": "workspace:*", + "@elizaos/client-telegram": "workspace:*", + "@elizaos/client-twitter": "workspace:*", + "@elizaos/client-slack": "workspace:*", + "@elizaos/core": "workspace:*", + "@elizaos/plugin-0g": "workspace:*", + "@elizaos/plugin-aptos": "workspace:*", + "@elizaos/plugin-bootstrap": "workspace:*", + "@elizaos/plugin-intiface": "workspace:*", + "@elizaos/plugin-coinbase": "workspace:*", + "@elizaos/plugin-conflux": "workspace:*", + "@elizaos/plugin-evm": "workspace:*", + "@elizaos/plugin-flow": "workspace:*", + "@elizaos/plugin-story": "workspace:*", + "@elizaos/plugin-goat": "workspace:*", + "@elizaos/plugin-icp": "workspace:*", + "@elizaos/plugin-image-generation": "workspace:*", + "@elizaos/plugin-nft-generation": "workspace:*", + "@elizaos/plugin-node": "workspace:*", + "@elizaos/plugin-solana": "workspace:*", + "@elizaos/plugin-starknet": "workspace:*", + "@elizaos/plugin-ton": "workspace:*", + "@elizaos/plugin-sui": "workspace:*", + "@elizaos/plugin-tee": "workspace:*", + "@elizaos/plugin-multiversx": "workspace:*", + "@elizaos/plugin-near": "workspace:*", + "@elizaos/plugin-zksync-era": "workspace:*", + "@elizaos/plugin-twitter": "workspace:*", + "readline": "1.3.0", + "ws": "8.18.0", + "yargs": "17.7.2" + }, + "devDependencies": { + "ts-node": "10.9.2", + "tsup": "8.3.5" + } } diff --git a/packages/plugin-twitter/.npmignore b/packages/plugin-twitter/.npmignore new file mode 100644 index 0000000000..078562ecea --- /dev/null +++ b/packages/plugin-twitter/.npmignore @@ -0,0 +1,6 @@ +* + +!dist/** +!package.json +!readme.md +!tsup.config.ts \ No newline at end of file diff --git a/packages/plugin-twitter/package.json b/packages/plugin-twitter/package.json new file mode 100644 index 0000000000..520ce565f8 --- /dev/null +++ b/packages/plugin-twitter/package.json @@ -0,0 +1,17 @@ +{ + "name": "@elizaos/plugin-twitter", + "version": "0.1.7-alpha.1", + "main": "dist/index.js", + "type": "module", + "types": "dist/index.d.ts", + "dependencies": { + "@elizaos/core": "workspace:*", + "agent-twitter-client": "^1.0.0", + "tsup": "8.3.5" + }, + "scripts": { + "build": "tsup --format esm --dts", + "dev": "tsup --format esm --dts --watch", + "test": "vitest run" + } +} diff --git a/packages/plugin-twitter/src/actions/post.ts b/packages/plugin-twitter/src/actions/post.ts new file mode 100644 index 0000000000..dd06b5ab09 --- /dev/null +++ b/packages/plugin-twitter/src/actions/post.ts @@ -0,0 +1,200 @@ +import { + Action, + IAgentRuntime, + Memory, + State, + composeContext, + elizaLogger, + generateText, + ModelClass, + formatMessages, +} from "@elizaos/core"; +import { Scraper } from "agent-twitter-client"; + +async function composeTweet( + runtime: IAgentRuntime, + message: Memory, + state?: State +): Promise { + try { + // Get recent conversation history + const recentMessages = await runtime.messageManager.getMemories({ + roomId: message.roomId, + count: 5, + }); + + const formattedHistory = formatMessages({ + messages: recentMessages, + actors: state?.actorsData, + }); + + // Template for generating the tweet + const tweetTemplate = ` +# Context +Recent conversation: +${formattedHistory} + +Character style: +${runtime.character.style.post.join("\n")} + +Topics of expertise: +${runtime.character.topics.join(", ")} + +# Task +Generate a tweet that: +1. Relates to the recent conversation or requested topic +2. Matches the character's style and voice +3. Is concise and engaging +4. Must be UNDER 180 characters (this is a strict requirement) +5. Speaks from the perspective of ${runtime.character.name} + +Generate only the tweet text, no other commentary.`; + + const context = await composeContext({ + state, + template: tweetTemplate, + }); + + const tweetContent = await generateText({ + runtime, + context, + modelClass: ModelClass.SMALL, + stop: ["\n"], + }); + + const trimmedContent = tweetContent.trim(); + + // Enforce character limit + if (trimmedContent.length > 180) { + elizaLogger.warn( + `Tweet too long (${trimmedContent.length} chars), truncating...` + ); + return trimmedContent.substring(0, 177) + "..."; + } + + return trimmedContent; + } catch (error) { + elizaLogger.error("Error composing tweet:", error); + throw error; + } +} + +async function postTweet(content: string): Promise { + try { + const scraper = new Scraper(); + const username = process.env.TWITTER_USERNAME; + const password = process.env.TWITTER_PASSWORD; + const email = process.env.TWITTER_EMAIL; + const twitter2faSecret = process.env.TWITTER_2FA_SECRET; + + if (!username || !password) { + throw new Error( + "Twitter credentials not configured in environment" + ); + } + + // Login with credentials + await scraper.login(username, password, email, twitter2faSecret); + if (!(await scraper.isLoggedIn())) { + throw new Error("Failed to login to Twitter"); + } + + // Send the tweet + elizaLogger.log("Attempting to send tweet:", content); + const result = await scraper.sendTweet(content); + + const body = await result.json(); + elizaLogger.log("Tweet response:", body); + + // Check for Twitter API errors + if (body.errors) { + const error = body.errors[0]; + throw new Error( + `Twitter API error (${error.code}): ${error.message}` + ); + } + + // Check for successful tweet creation + if (!body?.data?.create_tweet?.tweet_results?.result) { + throw new Error( + "Failed to post tweet: No tweet result in response" + ); + } + + return true; + } catch (error) { + // Log the full error details + elizaLogger.error("Error posting tweet:", { + message: error.message, + stack: error.stack, + name: error.name, + cause: error.cause, + }); + return false; + } +} + +export const postAction: Action = { + name: "POST_TWEET", + similes: ["TWEET", "POST", "SEND_TWEET"], + description: "Post a tweet to Twitter", + validate: async ( + runtime: IAgentRuntime, + message: Memory, + state?: State + ) => { + const hasCredentials = + !!process.env.TWITTER_USERNAME && !!process.env.TWITTER_PASSWORD; + elizaLogger.log(`Has credentials: ${hasCredentials}`); + + return hasCredentials; + }, + handler: async ( + runtime: IAgentRuntime, + message: Memory, + state?: State + ): Promise => { + try { + // Generate tweet content using context + const tweetContent = await composeTweet(runtime, message, state); + + if (!tweetContent) { + elizaLogger.error("No content generated for tweet"); + return false; + } + + elizaLogger.log(`Generated tweet content: ${tweetContent}`); + + // Check for dry run mode - explicitly check for string "true" + if ( + process.env.TWITTER_DRY_RUN && + process.env.TWITTER_DRY_RUN.toLowerCase() === "true" + ) { + elizaLogger.info( + `Dry run: would have posted tweet: ${tweetContent}` + ); + return true; + } + + return await postTweet(tweetContent); + } catch (error) { + elizaLogger.error("Error in post action:", error); + return false; + } + }, + examples: [ + [ + { + user: "{{user1}}", + content: { text: "Share your thoughts on AI" }, + }, + { + user: "{{agentName}}", + content: { + text: "The future of AI lies in responsible development and ethical considerations. We must ensure it benefits all of humanity.", + action: "POST_TWEET", + }, + }, + ], + ], +}; diff --git a/packages/plugin-twitter/src/index.ts b/packages/plugin-twitter/src/index.ts new file mode 100644 index 0000000000..ec979236ab --- /dev/null +++ b/packages/plugin-twitter/src/index.ts @@ -0,0 +1,12 @@ +import { Plugin } from "@elizaos/core"; +import { postAction } from "./actions/post"; + +export const twitterPlugin: Plugin = { + name: "twitter", + description: "Twitter integration plugin for posting tweets", + actions: [postAction], + evaluators: [], + providers: [], +}; + +export default twitterPlugin; diff --git a/packages/plugin-twitter/tsconfig.json b/packages/plugin-twitter/tsconfig.json new file mode 100644 index 0000000000..834c4dce26 --- /dev/null +++ b/packages/plugin-twitter/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../core/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": [ + "node" + ] + }, + "include": [ + "src/**/*.ts" + ] +} \ No newline at end of file diff --git a/packages/plugin-twitter/tsup.config.ts b/packages/plugin-twitter/tsup.config.ts new file mode 100644 index 0000000000..430573c247 --- /dev/null +++ b/packages/plugin-twitter/tsup.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + outDir: "dist", + sourcemap: true, + clean: true, + format: ["esm"], + external: ["dotenv", "fs", "path", "https", "http", "agentkeepalive"], +}); From fb7c09cdbb82f0bb5e19e300e99b5f1ab447e0fd Mon Sep 17 00:00:00 2001 From: Preston Brown Date: Tue, 24 Dec 2024 22:49:43 -0600 Subject: [PATCH 2/2] address review comments --- packages/plugin-twitter/src/actions/post.ts | 129 +++++++++++++------- packages/plugin-twitter/src/templates.ts | 22 ++++ packages/plugin-twitter/src/types.ts | 13 ++ 3 files changed, 118 insertions(+), 46 deletions(-) create mode 100644 packages/plugin-twitter/src/templates.ts create mode 100644 packages/plugin-twitter/src/types.ts diff --git a/packages/plugin-twitter/src/actions/post.ts b/packages/plugin-twitter/src/actions/post.ts index dd06b5ab09..6b737921fc 100644 --- a/packages/plugin-twitter/src/actions/post.ts +++ b/packages/plugin-twitter/src/actions/post.ts @@ -5,67 +5,48 @@ import { State, composeContext, elizaLogger, - generateText, ModelClass, formatMessages, + generateObject, } from "@elizaos/core"; import { Scraper } from "agent-twitter-client"; +import { tweetTemplate } from "../templates"; +import { isTweetContent, TweetSchema } from "../types"; async function composeTweet( runtime: IAgentRuntime, - message: Memory, + _message: Memory, state?: State ): Promise { try { - // Get recent conversation history - const recentMessages = await runtime.messageManager.getMemories({ - roomId: message.roomId, - count: 5, - }); - - const formattedHistory = formatMessages({ - messages: recentMessages, - actors: state?.actorsData, - }); - - // Template for generating the tweet - const tweetTemplate = ` -# Context -Recent conversation: -${formattedHistory} - -Character style: -${runtime.character.style.post.join("\n")} - -Topics of expertise: -${runtime.character.topics.join(", ")} - -# Task -Generate a tweet that: -1. Relates to the recent conversation or requested topic -2. Matches the character's style and voice -3. Is concise and engaging -4. Must be UNDER 180 characters (this is a strict requirement) -5. Speaks from the perspective of ${runtime.character.name} - -Generate only the tweet text, no other commentary.`; - - const context = await composeContext({ + const context = composeContext({ state, template: tweetTemplate, }); - const tweetContent = await generateText({ + const tweetContentObject = await generateObject({ runtime, context, modelClass: ModelClass.SMALL, + schema: TweetSchema, stop: ["\n"], }); - const trimmedContent = tweetContent.trim(); + if (!isTweetContent(tweetContentObject.object)) { + elizaLogger.error( + "Invalid tweet content:", + tweetContentObject.object + ); + return; + } + + const trimmedContent = tweetContentObject.object.text.trim(); - // Enforce character limit - if (trimmedContent.length > 180) { + // Skip truncation if TWITTER_PREMIUM is true + if ( + process.env.TWITTER_PREMIUM?.toLowerCase() !== "true" && + trimmedContent.length > 180 + ) { elizaLogger.warn( `Tweet too long (${trimmedContent.length} chars), truncating...` ); @@ -88,15 +69,17 @@ async function postTweet(content: string): Promise { const twitter2faSecret = process.env.TWITTER_2FA_SECRET; if (!username || !password) { - throw new Error( + elizaLogger.error( "Twitter credentials not configured in environment" ); + return false; } // Login with credentials await scraper.login(username, password, email, twitter2faSecret); if (!(await scraper.isLoggedIn())) { - throw new Error("Failed to login to Twitter"); + elizaLogger.error("Failed to login to Twitter"); + return false; } // Send the tweet @@ -109,16 +92,18 @@ async function postTweet(content: string): Promise { // Check for Twitter API errors if (body.errors) { const error = body.errors[0]; - throw new Error( + elizaLogger.error( `Twitter API error (${error.code}): ${error.message}` ); + return false; } // Check for successful tweet creation if (!body?.data?.create_tweet?.tweet_results?.result) { - throw new Error( + elizaLogger.error( "Failed to post tweet: No tweet result in response" ); + return false; } return true; @@ -186,12 +171,64 @@ export const postAction: Action = { [ { user: "{{user1}}", - content: { text: "Share your thoughts on AI" }, + content: { text: "You should tweet that" }, + }, + { + user: "{{agentName}}", + content: { + text: "I'll share this update with my followers right away!", + action: "POST_TWEET", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Post this tweet" }, + }, + { + user: "{{agentName}}", + content: { + text: "I'll post that as a tweet now.", + action: "POST_TWEET", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Share that on Twitter" }, + }, + { + user: "{{agentName}}", + content: { + text: "I'll share this message on Twitter.", + action: "POST_TWEET", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "Post that on X" }, + }, + { + user: "{{agentName}}", + content: { + text: "I'll post this message on X right away.", + action: "POST_TWEET", + }, + }, + ], + [ + { + user: "{{user1}}", + content: { text: "You should put that on X dot com" }, }, { user: "{{agentName}}", content: { - text: "The future of AI lies in responsible development and ethical considerations. We must ensure it benefits all of humanity.", + text: "I'll put this message up on X.com now.", action: "POST_TWEET", }, }, diff --git a/packages/plugin-twitter/src/templates.ts b/packages/plugin-twitter/src/templates.ts new file mode 100644 index 0000000000..4578396bce --- /dev/null +++ b/packages/plugin-twitter/src/templates.ts @@ -0,0 +1,22 @@ +export const tweetTemplate = ` +# Context +{{recentMessages}} + +# Topics +{{topics}} + +# Post Directions +{{postDirections}} + +# Recent interactions between {{agentName}} and other users: +{{recentPostInteractions}} + +# Task +Generate a tweet that: +1. Relates to the recent conversation or requested topic +2. Matches the character's style and voice +3. Is concise and engaging +4. Must be UNDER 180 characters (this is a strict requirement) +5. Speaks from the perspective of {{agentName}} + +Generate only the tweet text, no other commentary.`; diff --git a/packages/plugin-twitter/src/types.ts b/packages/plugin-twitter/src/types.ts new file mode 100644 index 0000000000..1f4537b0ac --- /dev/null +++ b/packages/plugin-twitter/src/types.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; + +export interface TweetContent { + text: string; +} + +export const TweetSchema = z.object({ + text: z.string().describe("The text of the tweet"), +}); + +export const isTweetContent = (obj: any): obj is TweetContent => { + return TweetSchema.safeParse(obj).success; +};