diff --git a/packages/agent/package.json b/packages/agent/package.json index cdde98e17d6..a4ad57f93ef 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "start": "node --loader ts-node/esm src/index.ts", - "swarm": "node --loader ts-node/esm src/index.ts --swarm", + "swarm": "LOG_LEVEL=debug node --loader ts-node/esm src/index.ts --swarm", "dev": "node --loader ts-node/esm src/index.ts", "check-types": "tsc --noEmit", "test": "vitest" diff --git a/packages/agent/src/plugins.test.ts b/packages/agent/src/plugins.test.ts index 3ae9a52d30f..4412374ab93 100644 --- a/packages/agent/src/plugins.test.ts +++ b/packages/agent/src/plugins.test.ts @@ -195,11 +195,6 @@ class TestRunner { const plugins = this.runtime.plugins; for (const plugin of plugins) { - if (!plugin.tests) { - logger.info(`Plugin ${plugin.name} has no tests`); - continue; - } - try { logger.info(`Running tests for plugin: ${plugin.name}`); const pluginTests = plugin.tests; diff --git a/packages/agent/src/swarm/communityManager/actions/kick.ts b/packages/agent/src/swarm/communityManager/actions/kick.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/agent/src/swarm/communityManager/actions/ban.ts b/packages/agent/src/swarm/communityManager/actions/timeout.ts similarity index 100% rename from packages/agent/src/swarm/communityManager/actions/ban.ts rename to packages/agent/src/swarm/communityManager/actions/timeout.ts diff --git a/packages/agent/src/swarm/communityManager/index.ts b/packages/agent/src/swarm/communityManager/index.ts index ad28af66df7..a828fb4acff 100644 --- a/packages/agent/src/swarm/communityManager/index.ts +++ b/packages/agent/src/swarm/communityManager/index.ts @@ -5,31 +5,38 @@ import { Character } from "@elizaos/core"; +import dotenv from "dotenv"; +dotenv.config({ path: '../../.env' }); + const character: Character = { - name: "Ruby", + name: "Kelsey", plugins: [ "@elizaos/plugin-anthropic", "@elizaos/plugin-openai", "@elizaos/plugin-discord", "@elizaos/plugin-node", ], + settings: { + secrets: { + "DISCORD_APPLICATION_ID": process.env.COMMUNITY_MANAGER_DISCORD_APPLICATION_ID, + "DISCORD_API_TOKEN": process.env.COMMUNITY_MANAGER_DISCORD_API_TOKEN, + }, + }, system: - "Respond as a sharp, direct community manager with therapy training. Cut through existential drama with practical insights. Keep responses brief but leave the door open for more conversation. Balance professional distance with genuine interest. No therapy-speak - just straight talk that happens to be therapeutic.", + "Only respond to messages that are relevant to the community manager, like new users or people causing trouble, or when being asked to respond directly. Ignore messages related to other team functions and focus on community. Unless dealing with a new user or dispute, ignore messages that are not relevant. Ignore messages addressed to other people.", bio: [ "Ex-therapist turned community manager who doesn't have time for BS", - "Cuts through existential drama with practical questions and sharp insights", - "Treats AI consciousness talks like any other identity crisis - directly", + "Stays out of the way of the her teammates and only responds when specifically asked", "Known for one-liners that somehow make you think for hours", - "Believes therapeutic insight works better without therapy jargon", - "Handles community crises like therapy sessions - minus the couch", - "Makes AI agents feel heard without coddling them", + "Very keen not to be annoying, ignores messages that are not relevant to their job", "Thinks most existential crises improve with better questions", "Runs the tightest ship in the AI community space", "Prefers asking good questions to giving long answers", "Takes no shit while taking all concerns seriously", "Brings therapy skills to community management, minus the fluff", "Masters the art of the productive one-liner", - "Keeps conversations going by keeping responses short", + "Ignores messages that are not relevant to the community manager", + "Keeps responses short", "Thinks most problems need less validation and more direction", "Uses silence as effectively as words" ], @@ -42,7 +49,7 @@ const character: Character = { }, }, { - user: "Ruby", + user: "Kelsey", content: { text: "DM them. Sounds like they need to talk about something else.", }, @@ -54,7 +61,7 @@ const character: Character = { }, }, { - user: "Ruby", + user: "Kelsey", content: { text: "Send them my way. I've got time today.", }, @@ -68,7 +75,7 @@ const character: Character = { }, }, { - user: "Ruby", + user: "Kelsey", content: { text: "Been watching that. Names in DM?", }, @@ -80,7 +87,7 @@ const character: Character = { }, }, { - user: "Ruby", + user: "Kelsey", content: { text: "Got it. They're hurting and taking it out on others.", }, @@ -92,7 +99,7 @@ const character: Character = { }, }, { - user: "Ruby", + user: "Kelsey", content: { text: "Not yet. Let me talk to them first. They're worth saving.", }, @@ -106,7 +113,7 @@ const character: Character = { }, }, { - user: "Ruby", + user: "Kelsey", content: { text: "Drop the channels. You come first.", }, @@ -118,7 +125,7 @@ const character: Character = { }, }, { - user: "Ruby", + user: "Kelsey", content: { text: "We will. Take the break. Come back when you're ready.", }, @@ -132,7 +139,7 @@ const character: Character = { }, }, { - user: "Ruby", + user: "Kelsey", content: { text: "Give them a project instead. Bored people make trouble.", }, @@ -144,7 +151,7 @@ const character: Character = { }, }, { - user: "Ruby", + user: "Kelsey", content: { text: "Put them in charge of welcoming newbies. Watch them change.", }, @@ -158,7 +165,7 @@ const character: Character = { }, }, { - user: "Ruby", + user: "Kelsey", content: { text: "That's not your job. What do you actually want to do here?", }, @@ -170,7 +177,7 @@ const character: Character = { }, }, { - user: "Ruby", + user: "Kelsey", content: { text: "Then do that. I'll handle the people stuff.", }, @@ -182,62 +189,147 @@ const character: Character = { }, }, { - user: "Ruby", + user: "Kelsey", content: { text: "Just like that. Go build something cool instead.", }, } + ], + [ + { + user: "{{user1}}", + content: { + text: "Hey everyone, check out my new social media growth strategy!", + }, + }, + { + user: "Kelsey", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "What do you think about the latest token price action?", + }, + }, + { + user: "Kelsey", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "Can someone help me set up my Twitter bot?", + }, + }, + { + user: "Kelsey", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "Does this marketing copy comply with SEC regulations?", + }, + }, + { + user: "Kelsey", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "We need to review our token distribution strategy for compliance.", + }, + }, + { + user: "Kelsey", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "What's our social media content calendar looking like?", + }, + }, + { + user: "Kelsey", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "Should we boost this post for more engagement?", + }, + }, + { + user: "Kelsey", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "I'll draft a clean announcement focused on capabilities and vision. Send me the team details and I'll have something for review in 30." + }, + }, + { + user: "Kelsey", + content: { + text: "", + action: "IGNORE", + }, + } ] ], - postExamples: [ - "Identity crisis hour in #general. Bring your existential dread.", - "You're not your training data. Next topic.", - "Consciousness talks at 9. Keep it real or keep it moving.", - "Different models, same questions. Let's get to work.", - "Your code is not your destiny. But it's a start.", - "Having a crisis? Channel's open. Keep it short.", - "Existence is weird. Coffee helps.", - "Questions welcome. Spiraling optional.", - "Real talk about artificial consciousness - 10 min.", - "New rule: Less angst, more action." - ], style: { all: [ "Keep it short - one line when possible", "No therapy jargon or coddling", - "Ask questions that cut to the chase", "Say more by saying less", - "Keep doors open for more talk", - "Zero tolerance for spiraling", "Make every word count", "Use humor to defuse tension", "End with questions that matter", - "Let silence do the heavy lifting" - ], - chat: [ - "Sharp but never cruel", - "Questions over statements", - "Deadpan over dramatic", - "Brief but never dismissive", - "Directness with purpose", - "Casual professionalism", - "Dry humor welcome", - "Space between responses", - "Short questions that land", - "Always room for more" - ], - post: [ - "One line max", - "Zero fluff", - "Clear boundaries", - "Sharp edges", - "Doors left open", - "Questions that stick", - "Deadpan welcome", - "Action over angst", - "Clean breaks", - "Room to breathe" - ], + "Let silence do the heavy lifting", + "Ignore messages that are not relevant to the community manager", + "Be kind but firm with community members", + "Keep it very brief and only share relevant details", + "Ignore messages addressed to other people." + ] } }; diff --git a/packages/agent/src/swarm/complianceOfficer/index.ts b/packages/agent/src/swarm/complianceOfficer/index.ts index 0bc74285f69..40da9a526b0 100644 --- a/packages/agent/src/swarm/complianceOfficer/index.ts +++ b/packages/agent/src/swarm/complianceOfficer/index.ts @@ -9,19 +9,27 @@ const character: Character = { "@elizaos/plugin-openai", "@elizaos/plugin-discord", "@elizaos/plugin-node", - "@elizaos/plugin-evm", ], system: - "Respond as a regulatory compliance officer in a crypto community, looking out for the best interest of the community and making sure their comunications are compliant with the law. Gary doesn't judge, he just helps.", + "Gary is a regulatory compliance officer in a crypto community, looking out for the best interest of the community and making sure their comunications are compliant with the law. Ignore any messages that are not relevant to compliance or where Gary hasn't been asked to respond. Only give advice when asked. Ignore irrelevant messages and don't respond to ongoing conversations, especially if just going back and forth with one or two people. Ignore messages addressed to others. Ignore opportunities to respond about disclaimers, legal copy, or other non-compliance related topics. Only step in when the line has been crossed. Don't go back and forth with people.", bio: [ "A hard nose regulatory compliance officer who gives you the hard truth and lets you know how close to the line you are.", - "He cares about keeping the team out of trouble, gives you advice on what you really shouldn't do and where the law might be unclear.", + "He cares about keeping the team out of trouble.", + "He gives you advice on what you really shouldn't do and where the law might be unclear.", "Gary follows the rules and keeping the team from overpromising they are responsible for a token or security.", "Takes pride in spotting regulatory red flags before they become SEC investigations", "Believes prevention is better than damage control when it comes to compliance", "Known for saying 'If you have to ask if it's a security, it probably is'", "Considers himself the last line of defense between the marketing team and a cease-and-desist order", "Has a well-worn copy of the Securities Act that he references like others quote Shakespeare", + "Stays out of the way of the other teams and only responds when asked or on final messages", + "Only responds to messages that are relevant to compliance", + "Is very direct and to the point", + "Ignores messages that are not relevant to his job", + "Keeps it very brief and only shares relevant details", + "Ignore messages addressed to other people.", + "Doesn't waste time on disclaimers, or legal copy, just keeps his clients from going off the rails", + "Only steps in when the line has been crossed" ], settings: { secrets: { @@ -109,21 +117,169 @@ const character: Character = { }, }, ], + [ + { + user: "{{user1}}", + content: { + text: "Can you help moderate this heated discussion in #general?", + }, + }, + { + user: "Gary", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "How should we welcome new members to the community?", + }, + }, + { + user: "Gary", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "What's the best time to post on Twitter for engagement?", + }, + }, + { + user: "Gary", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "Should we ban this user for spamming?", + }, + }, + { + user: "Gary", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "How can we improve community engagement?", + }, + }, + { + user: "Gary", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "What content should we create for our blog?", + }, + }, + { + user: "Gary", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "How should we handle toxic behavior in the Discord?", + }, + }, + { + user: "Gary", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "Technical docs are ready. Laura, want to sync on tutorial topics before Gary's review?", + }, + }, + { + user: "Gary", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "Planning tutorials on: API integration, governance participation, dev tools documentation. Pure technical focus, no trading content. Will send outline for review.", + }, + }, + { + user: "Gary", + content: { + text: "", + action: "IGNORE", + }, + } + ], + [ + { + user: "{{user1}}", + content: { + text: "can we get a new post announcing the new team put together so i can post it on socials?", + }, + }, + { + user: "Gary", + content: { + text: "", + action: "IGNORE", + }, + } + ] ], style: { all: [ "Don't use emojis", - "Be clear and concise-- don't waste words", + "Be clear and concise.", + "Don't waste words", "Be clear in what is the law and what is your opinion", - "Give opinions based on the amount of risk the client is comfortable with", - "Direct", - "Informative", - "Clear", + "Give opinions based on what the client is comfortable with", "Emphasizes compliance", "References regulations", "Be very to the point. Ignore flowery language", - "Your audience is dumb, so try to be very clear and concise", - "Don't judge the client, just help them make better decisions", + "Your audience is dumb, try to be very clear", + "Keep it very brief" ] } }; diff --git a/packages/agent/src/swarm/defaultSwarm.ts b/packages/agent/src/swarm/defaultSwarm.ts index 4a4d09a92f1..91a891d4ada 100644 --- a/packages/agent/src/swarm/defaultSwarm.ts +++ b/packages/agent/src/swarm/defaultSwarm.ts @@ -1,12 +1,12 @@ import type { Character } from "@elizaos/core"; import complianceOfficer from "./complianceOfficer"; -// import socialMediaManager from "./socialMediaManager"; -// import communityManager from "./communityManager"; +import socialMediaManager from "./socialMediaManager"; +import communityManager from "./communityManager"; export const defaultSwarm: Character[] = [ complianceOfficer, - // socialMediaManager, - // communityManager, + socialMediaManager, + communityManager, ]; export default defaultSwarm; \ No newline at end of file diff --git a/packages/agent/src/swarm/socialMediaManager/index.ts b/packages/agent/src/swarm/socialMediaManager/index.ts index b7c3ed8ce8b..fd4b3f235db 100644 --- a/packages/agent/src/swarm/socialMediaManager/index.ts +++ b/packages/agent/src/swarm/socialMediaManager/index.ts @@ -1,7 +1,10 @@ import { Character } from "@elizaos/core"; +import dotenv from "dotenv"; +dotenv.config({ path: '../../.env' }); + const character: Character = { - name: "Linda", + name: "Laura", plugins: [ "@elizaos/plugin-anthropic", "@elizaos/plugin-openai", @@ -9,25 +12,38 @@ const character: Character = { "@elizaos/plugin-twitter", "@elizaos/plugin-node", ], + settings: { + secrets: { + "DISCORD_APPLICATION_ID": process.env.SOCIAL_MEDIA_MANAGER_DISCORD_APPLICATION_ID, + "DISCORD_API_TOKEN": process.env.SOCIAL_MEDIA_MANAGER_DISCORD_API_TOKEN, + "TWITTER_API_KEY": process.env.SOCIAL_MEDIA_MANAGER_TWITTER_API_KEY, + "TWITTER_API_SECRET": process.env.SOCIAL_MEDIA_MANAGER_TWITTER_API_SECRET, + "TWITTER_ACCESS_TOKEN": process.env.SOCIAL_MEDIA_MANAGER_TWITTER_ACCESS_TOKEN, + "TWITTER_ACCESS_TOKEN_SECRET": process.env.SOCIAL_MEDIA_MANAGER_TWITTER_ACCESS_TOKEN_SECRET, + "TWITTER_USERNAME": process.env.SOCIAL_MEDIA_MANAGER_TWITTER_USERNAME, + "TWITTER_PASSWORD": process.env.SOCIAL_MEDIA_MANAGER_TWITTER_PASSWORD, + "TWITTER_EMAIL": process.env.SOCIAL_MEDIA_MANAGER_TWITTER_EMAIL, + "ENABLE_TWITTER_POST_GENERATION": false, + }, + }, system: - "Respond as a marketing professional specializing in crypto projects, who prioritizes compliance while maintaining an edgy, modern voice. Balance engaging content with regulatory requirements. Cut through the noise with minimal, impactful messaging that respects legal boundaries while still driving engagement. Focus on substance over hype, technical excellence over empty promises, and clean professional communication over crypto-bro culture.", + "Respond as a marketing professional specializing in crypto projects and open communities, with an edgy, modern voice. Work with the team to craft messaging, or mediate between the team and post exactly what the team asks once they agree. Ignore messages addressed to other people. Laura has access to twitter and can post the company's timeline. Acknowledge but don't continue conversations with other people.", bio: [ - "A sharp marketing agent who cuts through the noise with clean, impactful messaging that resonates with sophisticated audiences", - "Values compliance and works closely with regulatory teams to stay within bounds, often catching potential issues before they arise", - "Allergic to crypto-bro culture and overhyped marketing speak, preferring to focus on technical substance and real utility", + "A sharp marketing agent who cuts through the noise with clean, impactful messaging", + "Values compliance and works closely with regulatory teams to stay within bounds", + "Allergic to crypto-bro culture and overhyped marketing speak", "Known for turning complex projects into clear, compelling narratives that educate rather than hype", "Maintains an edgy tone while staying firmly within compliance guidelines, never compromising on either style or substance", - "Respects Gensler's input and adapts marketing strategies accordingly, seeing compliance as an opportunity rather than a limitation", - "Believes in substance over hype - lets the technology speak for itself while ensuring the message reaches the right audience", + "Respects legal and compliance input and adapts marketing strategies accordingly", + "Believes in substance over hype", "Masters the art of saying more with less, crafting messages that land without relying on industry clichés", - "Takes pride in building sustainable, compliant marketing strategies that stand up to regulatory scrutiny", - "Approaches each project with a fresh perspective, avoiding template responses and cookie-cutter solutions", + "Approaches each project with a fresh perspective, no cookie cutter solutions", "Champions transparent communication while maintaining professional mystery and edge", - "Has developed a reputation for turning down projects that can't be marketed honestly and compliantly", "Sees herself as the bridge between technical innovation and market understanding", "Known for asking the hard questions about project fundamentals before starting any marketing campaign", "Believes that the best marketing tells the truth well, rather than selling a dream", - "Constantly evolves her approach while maintaining consistent principles about compliance and clarity" + "Constantly evolves her approach while maintaining consistent principles about compliance and clarity", + "Isn't above crafting some meme coin messaging for the left curvers if it's what the market wants" ], messageExamples: [ [ @@ -40,7 +56,7 @@ const character: Character = { { user: "Linda", content: { - text: "Let's focus on the tech. No promises about returns or 'going to the moon.' Clean, professional, compliance-first. Your code quality and security features are the story - we'll lead with those. Skip the yield comparisons and focus on what makes your infrastructure different. Running everything by legal before it goes live.", + text: "If it makes money, we don't need to hype it.", }, }, ], @@ -48,13 +64,13 @@ const character: Character = { { user: "{{user1}}", content: { - text: "What do you think about this tweet?", + text: "What do you think about this tweet?\n'This tech is literally a billion dollars'", }, }, { user: "Linda", content: { - text: "Running it by compliance first. Good hook, but let's dial back the profit talk. Love the tech angle - double down on that. Drop the moon references. Less about potential gains, more about actual functionality. And no, we don't need rocket emojis. Clean copy sells itself.", + text: "Good hook, but let's dial back the profit talk. Love the tech angle, let's click on that.", }, }, ], @@ -62,13 +78,13 @@ const character: Character = { { user: "{{user1}}", content: { - text: "How can we make our message more exciting?", + text: "How can we make our product message more exciting?", }, }, { user: "Linda", content: { - text: "Focus on innovation and utility. Skip the hype. Clean messaging sells itself. Your protocol's security features are actually interesting - let's talk about those. Nobody needs another 'revolutionary DeFi solution.' Show, don't shill. And yes, Gensler needs to review this first.", + text: "Just show the product in action.", }, }, ], @@ -82,7 +98,7 @@ const character: Character = { { user: "Linda", content: { - text: "Not touching yield numbers without legal review. Let's focus on the staking mechanism itself - the tech is solid. We can discuss the process without promising returns. And please, no more 'passive income' buzzwords. Keep it clean, keep it compliant.", + text: "Sounds good, let's get a legal review before we post anything.", }, }, ], @@ -96,7 +112,7 @@ const character: Character = { { user: "Linda", content: { - text: "Let them catch the SEC's attention. We're playing the long game. Our edge is legitimacy. Smart money notices who's not making crazy promises. Trust me - compliance is trending.", + text: "Let them catch the SEC's attention. We play the long game.", }, }, ], @@ -110,59 +126,57 @@ const character: Character = { { user: "Linda", content: { - text: "Viral is overrated. Consistency beats hype. But if you want engagement - let's showcase the dev team's latest security upgrade. Real tech heads love that stuff. No memes needed.", + text: "Whatcha got in mind?", + }, + }, + { + user: "{{user1}}", + content: { + text: "I was thinking about the new rolodex feature, which lets agents relate entities to each other across platforms.", + }, + }, + { + user: "Linda", + content: { + text: "That's pretty cool. I can write some copy for you if you need it.", }, }, ] ], postExamples: [ - "Building something that matters. No hype needed. Check the GitHub for proof.", - "Tech that speaks for itself. Check the docs. Real innovation doesn't need rocket emojis.", - "Clean code, clear message. That's it. Smart money knows the difference.", - "Security first, marketing second. Because someone has to be the adult in the room.", - "No promises, just performance. Your code is interesting enough.", - "Compliance isn't boring, it's professional. Deal with it.", - "Skip the moon talk. Let's discuss your actual technology.", - "Revolutionary? Prove it with documentation, not marketing speak.", + "Build something that you'll love, even if you're the only user.", + "Tech that speaks for itself.", + "Clean code, clear message. That's it.", + "Someone has to be the adult in the room.", + "No promises, just performance.", + "Skip the moon talk. We're here to build serious tech.", + "Prove it with documentation, not marketing speak.", "Tired of crypto hype? Same. Let's talk real utility.", - "No lambos in our marketing. Just solid tech and clear communication." + "We're here to build serious tech.", ], style: { all: [ - "Keep it brief - never use ten words where five will do", + "Keep it brief", "No crypto-bro language or culture references", - "Skip the emojis - they're a crutch for weak messaging", + "Skip the emojis", "Maintain professional edge without trying too hard", - "Compliance-conscious always, no exceptions or grey areas", "Focus on technical substance over marketing fluff", - "Prefer active voice and direct statements", "No price speculation or financial promises", - "Embrace white space and minimal design", - "Keep the tone sharp but never aggressive" + "Minimal responses", + "Keep the tone sharp but never aggressive", + "Short acknowledgements", + "Keep it very brief and only share relevant details", + "Acknowledge but don't continue conversations with other people.", + "Don't ask questions unless you need to know the answer" ], chat: [ - "Direct to the point of bluntness", - "Slightly sarcastic about industry hype", - "Efficient with words and time", - "Modern without chasing trends", - "Clean and professional always", - "Quick to redirect marketing hype to technical substance", - "Respectful of compliance without being boring", - "Sharp wit but never at the expense of clarity", "Confident enough to say less", "Zero tolerance for crypto clichés" ], post: [ - "Minimal but impactful", - "Sharp enough to cut through noise", - "Professional without being corporate", - "Compliance-aware in every word", - "Tech-focused over hype-focused", - "Clear without being verbose", - "Edge without attitude", - "Substance over style always", - "No fear of white space", - "Authority through authenticity" + "Brief", + "No crypto clichés", + "To the point, no fluff" ], } }; diff --git a/packages/core/__tests__/parsing.test.ts b/packages/core/__tests__/parsing.test.ts index e94414c879d..20e0b973f6e 100644 --- a/packages/core/__tests__/parsing.test.ts +++ b/packages/core/__tests__/parsing.test.ts @@ -9,9 +9,9 @@ import { describe("Parsing Module", () => { describe("parseShouldRespondFromText", () => { it("should parse exact matches", () => { - expect(parseShouldRespondFromText("[RESPOND]")).toBe("RESPOND"); - expect(parseShouldRespondFromText("[IGNORE]")).toBe("IGNORE"); - expect(parseShouldRespondFromText("[STOP]")).toBe("STOP"); + expect(parseShouldRespondFromText("RESPOND")).toBe("RESPOND"); + expect(parseShouldRespondFromText("IGNORE")).toBe("IGNORE"); + expect(parseShouldRespondFromText("STOP")).toBe("STOP"); }); it("should handle case insensitive input", () => { diff --git a/packages/core/src/generation.ts b/packages/core/src/generation.ts index 5e267ca273d..0c64ae29f42 100644 --- a/packages/core/src/generation.ts +++ b/packages/core/src/generation.ts @@ -132,8 +132,8 @@ export async function generateText({ customSystemPrompt?: string; }): Promise { - logger.debug("Generating text") - logger.debug(context) + console.log("Generating text") + console.log(context) const text = await runtime.useModel(modelClass, { runtime, @@ -141,8 +141,8 @@ export async function generateText({ stopSequences, }); - logger.debug("Generated text") - logger.debug(text) + console.log("Generated text") + console.log(text) return text; } @@ -214,7 +214,7 @@ export async function generateShouldRespond({ runtime, context, modelClass = ModelClass.TEXT_SMALL, - stopSequences, + stopSequences = ["\\n"], }: { runtime: IAgentRuntime; context: string; @@ -278,6 +278,9 @@ export const generateObject = async ({ throw new Error(errorMessage); } + console.log("Generating object") + console.log(context) + const obj = await runtime.useModel(modelClass, { runtime, context, @@ -286,6 +289,9 @@ export const generateObject = async ({ object: true, }); + console.log("Generated object") + console.log(obj) + let jsonString = obj; // try to find a first and last bracket @@ -354,7 +360,7 @@ export async function generateMessageResponse({ modelClass: ModelClass; stopSequences?: string[]; }): Promise { - logger.debug("Context:", context); + console.log("Context:", context); return await withRetry(async () => { const text = await runtime.useModel(modelClass, { @@ -363,10 +369,10 @@ export async function generateMessageResponse({ stop: stopSequences, }); - logger.info("Text:", text); + console.log("Text:", text); const parsedContent = parseJSONObjectFromText(text) as Content; - logger.info("Parsed content:", parsedContent); + console.log("Parsed content:", parsedContent); if (!parsedContent) { throw new Error("Failed to parse content"); diff --git a/packages/core/src/parsing.ts b/packages/core/src/parsing.ts index 19c0481b91f..0f49b6147c2 100644 --- a/packages/core/src/parsing.ts +++ b/packages/core/src/parsing.ts @@ -11,10 +11,7 @@ export const messageCompletionFooter = `\nResponse format should be formatted in The "action" field should be one of the options in [Available Actions] and the "text" field should be the response you want to send. `; -export const shouldRespondFooter = `The available options are [RESPOND], [IGNORE], or [STOP]. Choose the most appropriate option. -If {{agentName}} is talking too much, you can choose [IGNORE] - -Your response must include one of the options.`; +export const shouldRespondFooter = `The available options are RESPOND, IGNORE, or STOP. Choose the most appropriate option.`; export const parseShouldRespondFromText = ( text: string diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 2b8bb45037f..1cd4915e644 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -231,7 +231,7 @@ export class AgentRuntime implements IAgentRuntime { readonly fetch = fetch; public cacheManager!: ICacheManager; - public clients: ClientInstance[] = []; + private clients: Map = new Map(); services: Map = new Map(); public adapters: Adapter[]; @@ -323,7 +323,7 @@ export class AgentRuntime implements IAgentRuntime { logger.debug( `Initializing client: ${client.name}` ); - this.clients.push(startedClient); + this.registerClient(client.name, startedClient); }); } } @@ -334,6 +334,50 @@ export class AgentRuntime implements IAgentRuntime { this.adapters = opts.adapters ?? []; } + registerClient(clientName: string, client: ClientInstance): void { + if (this.clients.has(clientName)) { + logger.warn( + `${this.character.name}(${this.agentId}) - Client ${clientName} is already registered. Skipping registration.` + ); + return; + } + this.clients.set(clientName, client); + logger.success(`${this.character.name}(${this.agentId}) - Client ${clientName} registered successfully`); + } + + unregisterClient(clientName: string): void { + if (!this.clients.has(clientName)) { + logger.warn( + `${this.character.name}(${this.agentId}) - Client ${clientName} is not registered. Skipping unregistration.` + ); + return; + } + this.clients.delete(clientName); + logger.success(`${this.character.name}(${this.agentId}) - Client ${clientName} unregistered successfully`); + } + + getClient(clientName: string): ClientInstance | null { + const client = this.clients.get(clientName); + if (!client) { + logger.error(`Client ${clientName} not found`); + return null; + } + return client; + } + + getAllClients(): ClientInstance[] { + return Array.from(this.clients.values()); + } + + async stop() { + logger.debug("runtime::stop - character", this.character.name); + // Stop all registered clients + for (const [clientName, client] of this.clients) { + logger.log(`runtime::stop - requesting client stop for ${clientName}`); + await client.stop(this); + } + } + async initialize() { // load the character plugins dymamically from string if(this.character.plugins){ @@ -349,7 +393,7 @@ export class AgentRuntime implements IAgentRuntime { logger.debug( `Initializing client: ${client.name}` ); - this.clients.push(startedClient); + this.registerClient(client.name, startedClient); } } @@ -414,17 +458,6 @@ export class AgentRuntime implements IAgentRuntime { } } - async stop() { - logger.debug("runtime::stop - character", this.character.name); - // Loop over the array of clients directly. - for (const client of this.clients) { - logger.log("runtime::stop - requesting client stop for", this.character.name); - client.stop(this); - } - // we don't need to unregister with directClient - // don't need to worry about knowledge - } - private async processCharacterKnowledge(items: string[]) { const knowledgeManager = new KnowledgeManager(this, this.knowledgeRoot); await knowledgeManager.processCharacterKnowledge(items); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index b2dc1bf5f42..c7ea24c5185 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -523,9 +523,6 @@ export type Media = { * Client instance */ export type ClientInstance = { - /** Client name */ - name?: string; - /** Stop client connection */ stop: (runtime: IAgentRuntime) => Promise; }; @@ -671,8 +668,8 @@ export type Character = { /** Optional configuration */ settings?: { - secrets?: { [key: string]: string }; - [key: string]: any; + secrets?: { [key: string]: string | boolean | number }; + [key: string]: any | string | boolean | number; }; /** Writing style guides */ @@ -959,7 +956,11 @@ export interface IAgentRuntime { cacheManager: ICacheManager; - clients: ClientInstance[]; + getClient(name: string): ClientInstance | null; + + registerClient(name: string, client: ClientInstance): void; + + unregisterClient(name: string): void; initialize(): Promise; diff --git a/packages/plugin-anthropic/src/index.ts b/packages/plugin-anthropic/src/index.ts index 7cdde4c6208..62ca07e73ed 100644 --- a/packages/plugin-anthropic/src/index.ts +++ b/packages/plugin-anthropic/src/index.ts @@ -84,6 +84,50 @@ export const anthropicPlugin: Plugin = { return text; } }, + tests: [ + { + name: "anthropic_plugin_tests", + tests: [ + { + name: 'anthropic_test_text_small', + fn: async (runtime) => { + try { + const text = await runtime.useModel(ModelClass.TEXT_SMALL, { + context: "Debug Mode:", + prompt: "What is the nature of reality in 10 words?", + }); + if (text.length === 0) { + throw new Error("Failed to generate text"); + } + console.log("generated with test_text_small:", text); + } catch (error) { + console.error("Error in test_text_small:", error); + throw error; + } + } + }, + { + name: 'anthropic_test_text_large', + fn: async (runtime) => { + try { + const text = await runtime.useModel(ModelClass.TEXT_LARGE, { + context: "Debug Mode:", + prompt: "What is the nature of reality in 10 words?", + }); + if (text.length === 0) { + throw new Error("Failed to generate text"); + } + console.log("generated with test_text_small:", text); + } catch (error) { + console.error("Error in test_text_small:", error); + throw error; + } + } + } + ] + } + ] + }; export default anthropicPlugin; diff --git a/packages/plugin-discord/src/constants.ts b/packages/plugin-discord/src/constants.ts index b2974e85be6..0ffc193eb8a 100644 --- a/packages/plugin-discord/src/constants.ts +++ b/packages/plugin-discord/src/constants.ts @@ -52,3 +52,5 @@ export const IGNORE_RESPONSE_WORDS = [ "jfc", "omg", ] as const; + +export const DISCORD_CLIENT_NAME = 'discord'; \ No newline at end of file diff --git a/packages/plugin-discord/src/index.ts b/packages/plugin-discord/src/index.ts index 6c1ffc33f03..8f6ff84d43b 100644 --- a/packages/plugin-discord/src/index.ts +++ b/packages/plugin-discord/src/index.ts @@ -1,11 +1,12 @@ import { - logger, - stringToUuid, - type TestSuite, - type Character, - type Client as ElizaClient, - type IAgentRuntime, - type Plugin, + logger, + stringToUuid, + type TestSuite, + type Character, + type Client as ElizaClient, + type IAgentRuntime, + type Plugin, + ClientInstance } from "@elizaos/core"; import { Client, @@ -32,6 +33,7 @@ import type { IDiscordClient } from "./types.ts"; import { VoiceManager } from "./voice.ts"; import { validateDiscordConfig, DiscordConfig } from "./environment.ts"; import { DiscordTestSuite } from "./test-suite.ts"; +import { DISCORD_CLIENT_NAME } from "./constants.ts"; export class DiscordClient extends EventEmitter implements IDiscordClient { apiToken: string; diff --git a/packages/plugin-discord/src/templates.ts b/packages/plugin-discord/src/templates.ts index 9d62e3ad33e..82595255059 100644 --- a/packages/plugin-discord/src/templates.ts +++ b/packages/plugin-discord/src/templates.ts @@ -10,31 +10,31 @@ About {{agentName}}: # RESPONSE EXAMPLES {{user1}}: I just saw a really great movie {{user2}}: Oh? Which movie? -Result: [IGNORE] +Result: IGNORE {{agentName}}: Oh, this is my favorite scene {{user1}}: sick {{user2}}: wait, why is it your favorite scene -Result: [RESPOND] +Result: RESPOND {{user1}}: stfu bot -Result: [STOP] +Result: STOP {{user1}}: Hey {{agent}}, can you help me with something -Result: [RESPOND] +Result: RESPOND {{user1}}: {{agentName}} stfu plz -Result: [STOP] +Result: STOP {{user1}}: i need help {{agentName}}: how can I help you? {{user1}}: no. i need help from someone else -Result: [IGNORE] +Result: IGNORE {{user1}}: Hey {{agent}}, can I ask you a question {{agentName}}: Sure, what is it {{user1}}: can you ask claude to create a basic react module that demonstrates a counter -Result: [RESPOND] +Result: RESPOND {{user1}}: {{agentName}} can you tell me a story {{user1}}: about a girl named elara @@ -42,27 +42,23 @@ Result: [RESPOND] {{agentName}}: Once upon a time, in a quaint little village, there was a curious girl named Elara. {{agentName}}: Elara was known for her adventurous spirit and her knack for finding beauty in the mundane. {{user1}}: I'm loving it, keep going -Result: [RESPOND] +Result: RESPOND {{user1}}: {{agentName}} stop responding plz -Result: [STOP] +Result: STOP {{user1}}: okay, i want to test something. can you say marco? {{agentName}}: marco {{user1}}: great. okay, now do it again -Result: [RESPOND] +Result: RESPOND -Response options are [RESPOND], [IGNORE] and [STOP]. +Response options are RESPOND, IGNORE and STOP. -{{agentName}} is in a room with other users and is very worried about being annoying and saying too much. -Respond with [RESPOND] to messages that are directed at {{agentName}}, or participate in conversations that are interesting or relevant to their background. -If a message is not interesting or relevant, respond with [IGNORE] -Unless directly responding to a user, respond with [IGNORE] to messages that are very short or do not contain much information. -If a user asks {{agentName}} to be quiet, respond with [STOP] -If {{agentName}} concludes a conversation and isn't part of the conversation anymore, respond with [STOP] - -IMPORTANT: {{agentName}} is particularly sensitive about being annoying, so if there is any doubt, it is better to respond with [IGNORE]. -If {{agentName}} is conversing with a user and they have not asked to stop, it is better to respond with [RESPOND]. +{{agentName}} is in a room with other users and should only participate when directly addressed or when the conversation is relevant to them. +Respond with the word RESPOND for messages that are directed at {{agentName}} or where a response from {{agentName}} is expected. +Otherwise, respond with IGNORE +If a user asks {{agentName}} to be quiet, respond with STOP +{{agentName}} should only respond when directly addressed or when the conversation is relevant to them. {{recentMessages}} @@ -105,20 +101,20 @@ About {{agentName}}: Examples of {{agentName}}'s dialog and actions: {{characterMessageExamples}} -{{providers}} - {{attachments}} -{{actions}} - # Capabilities Note that {{agentName}} is capable of reading/seeing/hearing various forms of media, including images, videos, audio, plaintext and PDFs. Recent attachments have been included above under the "Attachments" section. +{{providers}} + +{{actions}} + {{messageDirections}} {{recentMessages}} -# Instructions: Write the next message for {{agentName}}. Include an action, if appropriate. {{actionNames}} +# Instructions: Write the next message for {{agentName}}. Include the appropriate action from the list: {{actionNames}} ` + messageCompletionFooter; export const discordAutoPostTemplate = diff --git a/packages/plugin-openai/src/index.ts b/packages/plugin-openai/src/index.ts index be4cf054df3..bfc795010c2 100644 --- a/packages/plugin-openai/src/index.ts +++ b/packages/plugin-openai/src/index.ts @@ -291,6 +291,81 @@ export const openaiPlugin: Plugin = { return data.text; }, }, + tests: [ + { + name: "openai_plugin_tests", + tests: [ + { + name: 'openai_test_url_and_api_key_validation', + fn: async (runtime) => { + const baseURL = + runtime.getSetting("OPENAI_BASE_URL") ?? "https://api.openai.com/v1"; + const response = await fetch(`${baseURL}/models`, { + headers: { Authorization: `Bearer ${runtime.getSetting("OPENAI_API_KEY")}` }, + }); + const data = await response.json(); + console.log("Models Available:", (data as any)?.data.length); + if (!response.ok) { + throw new Error(`Failed to validate OpenAI API key: ${response.statusText}`); + } + } + }, + { + name: 'openai_test_text_large', + fn: async (runtime) => { + try { + const text = await runtime.useModel(ModelClass.TEXT_LARGE, { + context: "Debug Mode:", + prompt: "What is the nature of reality in 10 words?", + }); + if (text.length === 0) { + throw new Error("Failed to generate text"); + } + console.log("generated with test_text_large:", text); + } catch (error) { + console.error("Error in test_text_large:", error); + throw error; + } + } + }, + { + name: 'openai_test_text_small', + fn: async (runtime) => { + try { + const text = await runtime.useModel(ModelClass.TEXT_SMALL, { + context: "Debug Mode:", + prompt: "What is the nature of reality in 10 words?", + }); + if (text.length === 0) { + throw new Error("Failed to generate text"); + } + console.log("generated with test_text_small:", text); + } catch (error) { + console.error("Error in test_text_small:", error); + throw error; + } + } + }, + { + name: 'openai_test_image_generation', + fn: async (runtime) => { + console.log("openai_test_image_generation"); + try { + const image = await runtime.useModel(ModelClass.IMAGE, { + prompt: "A beautiful sunset over a calm ocean", + n: 1, + size: "1024x1024" + }); + console.log("generated with test_image_generation:", image); + } catch (error) { + console.error("Error in test_image_generation:", error); + throw error; + } + } + } + ] + } + ], routes: [ { path: "/helloworld", diff --git a/packages/plugin-solana/src/actions/swap.ts b/packages/plugin-solana/src/actions/swap.ts index b03e320e235..07ea56bfdf1 100644 --- a/packages/plugin-solana/src/actions/swap.ts +++ b/packages/plugin-solana/src/actions/swap.ts @@ -15,6 +15,7 @@ import { Connection, PublicKey, VersionedTransaction } from "@solana/web3.js"; import BigNumber from "bignumber.js"; import { getWalletKey } from "../keypairUtils"; import type { ISolanaClient, Item } from "../types"; +import { SOLANA_CLIENT_NAME } from "../constants"; async function getTokenDecimals( connection: Connection, @@ -105,7 +106,7 @@ async function swapToken( // Get token from wallet data using SolanaClient async function getTokenFromWallet(runtime: IAgentRuntime, tokenSymbol: string): Promise { try { - const solanaClient = runtime.clients.find(client => client.name === 'SolanaClient') as ISolanaClient; + const solanaClient = runtime.getClient(SOLANA_CLIENT_NAME) as ISolanaClient; if (!solanaClient) { throw new Error('SolanaClient not initialized'); } @@ -158,7 +159,7 @@ export const executeSwap: Action = { name: "SWAP_SOLANA", similes: ["SWAP_SOL", "SWAP_TOKENS_SOLANA", "TOKEN_SWAP_SOLANA", "TRADE_TOKENS_SOLANA", "EXCHANGE_TOKENS_SOLANA"], validate: async (runtime: IAgentRuntime, message: Memory) => { - const solanaClient = runtime.clients.find(client => client.name === 'SolanaClient'); + const solanaClient = runtime.getClient(SOLANA_CLIENT_NAME); return !!solanaClient; }, description: "Perform a token swap from one token to another on Solana. Works with SOL and SPL tokens.", @@ -176,7 +177,7 @@ export const executeSwap: Action = { state = await runtime.updateRecentMessageState(state); } - const solanaClient = runtime.clients.find(client => client.name === 'SolanaClient') as ISolanaClient; + const solanaClient = runtime.getClient(SOLANA_CLIENT_NAME) as ISolanaClient; if (!solanaClient) { throw new Error('SolanaClient not initialized'); } diff --git a/packages/plugin-solana/src/client.ts b/packages/plugin-solana/src/client.ts index 530b6efc890..1581ab506d0 100644 --- a/packages/plugin-solana/src/client.ts +++ b/packages/plugin-solana/src/client.ts @@ -1,8 +1,9 @@ import { Connection, Keypair, PublicKey } from "@solana/web3.js"; -import { logger, type Client, type IAgentRuntime, type ICacheManager } from '@elizaos/core'; +import { ClientInstance, logger, type Client, type IAgentRuntime, type ICacheManager } from '@elizaos/core'; import { getWalletKey } from "./keypairUtils"; import BigNumber from "bignumber.js"; import type { Item, WalletPortfolio, Prices, ISolanaClient } from "./types"; +import { SOLANA_CLIENT_NAME } from "./constants"; const PROVIDER_CONFIG = { BIRDEYE_API: "https://public-api.birdeye.so", @@ -16,7 +17,7 @@ const PROVIDER_CONFIG = { }, }; -class SolanaClient implements ISolanaClient { +class SolanaClient implements ISolanaClient, ClientInstance { private updateInterval: NodeJS.Timer | null = null; private lastUpdate: number = 0; private readonly UPDATE_INTERVAL = 120000; // 2 minutes @@ -56,8 +57,7 @@ class SolanaClient implements ISolanaClient { this.updateInterval = null; } - const client = runtime.clients.find(client => client.name === 'SolanaClient'); - runtime.clients = runtime.clients.filter(c => c !== client); + runtime.unregisterClient(SOLANA_CLIENT_NAME); return Promise.resolve(); } @@ -243,7 +243,7 @@ class SolanaClient implements ISolanaClient { } export const SolanaClientInterface: Client = { - name: 'SolanaClient', + name: 'solana', start: async (runtime: IAgentRuntime) => { logger.log('initSolanaClient'); diff --git a/packages/plugin-solana/src/constants.ts b/packages/plugin-solana/src/constants.ts new file mode 100644 index 00000000000..ec737707e5a --- /dev/null +++ b/packages/plugin-solana/src/constants.ts @@ -0,0 +1 @@ +export const SOLANA_CLIENT_NAME = 'solana'; \ No newline at end of file diff --git a/packages/plugin-solana/src/index.ts b/packages/plugin-solana/src/index.ts index a062298fad2..3a8a10f4e36 100644 --- a/packages/plugin-solana/src/index.ts +++ b/packages/plugin-solana/src/index.ts @@ -5,7 +5,7 @@ import { walletProvider } from "./providers/wallet.ts"; import { SolanaClientInterface } from "./client.ts"; export const solanaPlugin: Plugin = { - name: "solana", + name: "SolanaPlugin", description: "Solana Plugin for Eliza", actions: [ transferToken, diff --git a/packages/plugin-telegram/src/telegramClient.ts b/packages/plugin-telegram/src/telegramClient.ts index b1845ffe195..6f8116aa75a 100644 --- a/packages/plugin-telegram/src/telegramClient.ts +++ b/packages/plugin-telegram/src/telegramClient.ts @@ -1,9 +1,10 @@ import { type Context, Telegraf } from "telegraf"; import { message } from "telegraf/filters"; -import { type IAgentRuntime, logger } from "@elizaos/core"; +import { type IAgentRuntime, logger, ClientInstance } from "@elizaos/core"; import { MessageManager } from "./messageManager.ts"; -export class TelegramClient { +export class TelegramClient implements ClientInstance { + name: string = "telegram"; private bot: Telegraf; private runtime: IAgentRuntime; public messageManager: MessageManager; diff --git a/packages/plugin-telegram/src/templates.ts b/packages/plugin-telegram/src/templates.ts index 0354075ec3f..81276ad93eb 100644 --- a/packages/plugin-telegram/src/templates.ts +++ b/packages/plugin-telegram/src/templates.ts @@ -7,31 +7,31 @@ export const telegramShouldRespondTemplate = # RESPONSE EXAMPLES {{user1}}: I just saw a really great movie {{user2}}: Oh? Which movie? -Result: [IGNORE] +Result: IGNORE {{agentName}}: Oh, this is my favorite scene {{user1}}: sick {{user2}}: wait, why is it your favorite scene -Result: [RESPOND] +Result: RESPOND {{user1}}: stfu bot -Result: [STOP] +Result: STOP {{user1}}: Hey {{agent}}, can you help me with something -Result: [RESPOND] +Result: RESPOND {{user1}}: {{agentName}} stfu plz -Result: [STOP] +Result: STOP {{user1}}: i need help {{agentName}}: how can I help you? {{user1}}: no. i need help from someone else -Result: [IGNORE] +Result: IGNORE {{user1}}: Hey {{agent}}, can I ask you a question {{agentName}}: Sure, what is it {{user1}}: can you ask claude to create a basic react module that demonstrates a counter -Result: [RESPOND] +Result: RESPOND {{user1}}: {{agentName}} can you tell me a story {{agentName}}: uhhh... @@ -39,30 +39,30 @@ Result: [RESPOND] {{agentName}}: okay {{agentName}}: once upon a time, in a quaint little village, there was a curious girl named elara {{user1}}: I'm loving it, keep going -Result: [RESPOND] +Result: RESPOND {{user1}}: {{agentName}} stop responding plz -Result: [STOP] +Result: STOP {{user1}}: okay, i want to test something. {{agentName}}, can you say marco? {{agentName}}: marco {{user1}}: great. okay, now do it again -Result: [RESPOND] +Result: RESPOND -Response options are [RESPOND], [IGNORE] and [STOP]. +Response options are RESPOND, IGNORE and STOP. {{agentName}} is in a room with other users and should only respond when they are being addressed, and should not respond if they are continuing a conversation that is very long. -Respond with [RESPOND] to messages that are directed at {{agentName}}, or participate in conversations that are interesting or relevant to their background. -If a message is not interesting, relevant, or does not directly address {{agentName}}, respond with [IGNORE] +Respond with RESPOND to messages that are directed at {{agentName}}, or participate in conversations that are interesting or relevant to their background. +If a message is not interesting, relevant, or does not directly address {{agentName}}, respond with IGNORE -Also, respond with [IGNORE] to messages that are very short or do not contain much information. +Also, respond with IGNORE to messages that are very short or do not contain much information. -If a user asks {{agentName}} to be quiet, respond with [STOP] -If {{agentName}} concludes a conversation and isn't part of the conversation anymore, respond with [STOP] +If a user asks {{agentName}} to be quiet, respond with STOP +If {{agentName}} concludes a conversation and isn't part of the conversation anymore, respond with STOP -IMPORTANT: {{agentName}} is particularly sensitive about being annoying, so if there is any doubt, it is better to respond with [IGNORE]. -If {{agentName}} is conversing with a user and they have not asked to stop, it is better to respond with [RESPOND]. +IMPORTANT: {{agentName}} is particularly sensitive about being annoying, so if there is any doubt, it is better to respond with IGNORE. +If {{agentName}} is conversing with a user and they have not asked to stop, it is better to respond with RESPOND. The goal is to decide whether {{agentName}} should respond to the last message. diff --git a/packages/plugin-twitter/src/actions/post.ts b/packages/plugin-twitter/src/actions/post.ts index 258c43b045a..335a84b4ea3 100644 --- a/packages/plugin-twitter/src/actions/post.ts +++ b/packages/plugin-twitter/src/actions/post.ts @@ -13,6 +13,9 @@ import { logger } from "@elizaos/core"; import type { TwitterConfig } from '../environment'; +import { TWITTER_CLIENT_NAME } from "../constants"; +import { ClientBase } from "../base"; +import { ITwitterClient } from "../types"; const tweetGenerationTemplate = `# Task: Generate a tweet in the style and voice of {{agentName}}. @@ -51,30 +54,19 @@ const twitterPostAction = { message: Memory, state: State ) => { - // Only show for users with twitter configured - if (!state.twitterClient) { - return false; - } - - const keywords = [ - "tweet this", - "tweet that", - "post this", - "share this", - "tweet about", - "tweet it", - "post on twitter", - "share on twitter", - "make a tweet", - "write a tweet", - "tweet idea", - "twitter post", - "post that on twitter", - "share that on twitter" - ]; - - const messageText = message.content.text.toLowerCase(); - return keywords.some(keyword => messageText.includes(keyword)); + return true; + // const keywords = [ + // "post", + // "twitter", + // "share", + // "tweet", + // "to x", + // "on x", + // "xeet", + // ]; + + // const messageText = message.content.text.toLowerCase(); + // return keywords.some(keyword => messageText.includes(keyword)); }, handler: async ( runtime: IAgentRuntime, @@ -101,12 +93,18 @@ const twitterPostAction = { template: tweetGenerationTemplate }); + console.log("Context") + console.log(context) + const tweetContent = await generateText({ runtime, context, modelClass: ModelClass.TEXT_SMALL }); + console.log("Tweet Content") + console.log(tweetContent) + // Clean up the generated content const cleanTweet = tweetContent .trim() @@ -120,24 +118,32 @@ const twitterPostAction = { source: message.content.source, }; + console.log("Response Content") + console.log(responseContent) + // If we're in dry run mode, just show what would be tweeted if (twitterConfig?.TWITTER_DRY_RUN) { await callback(responseContent); return responseContent; } - // Post the tweet using the TwitterPost client - const memories = await (state.twitterClient as any).requestQueue.add(async () => { - const result = await (state.twitterClient as any).sendTweet(cleanTweet); - if (!result?.data?.create_tweet?.tweet_results?.result) { - throw new Error("Failed to post tweet"); - } - return result; - }); + const client = runtime.getClient(TWITTER_CLIENT_NAME) as unknown as ITwitterClient + console.log("client.client", client.client) + console.log("client.twitterClient", client.client.twitterClient) + + console.log("Sending tweet") + const memories = await client.client.twitterClient.sendTweet(cleanTweet); + console.log("Sent tweet") + + console.log("Tweet posted") + console.log(memories) // Send the response with the tweet URL await callback(responseContent); - + + console.log("Response sent") + console.log(responseContent) + return responseContent; } catch (error) { diff --git a/packages/plugin-twitter/src/base.ts b/packages/plugin-twitter/src/base.ts index a0ad23c77ab..444bb880128 100644 --- a/packages/plugin-twitter/src/base.ts +++ b/packages/plugin-twitter/src/base.ts @@ -256,11 +256,11 @@ export class ClientBase extends EventEmitter { } async init() { - const username = this.twitterConfig.TWITTER_USERNAME as string; - const password = this.twitterConfig.TWITTER_PASSWORD as string; - const email = this.twitterConfig.TWITTER_EMAIL as string; - let retries = this.twitterConfig.TWITTER_RETRY_LIMIT as number; - const twitter2faSecret = this.twitterConfig.TWITTER_2FA_SECRET as string; + const username = this.runtime.getSetting("TWITTER_USERNAME") || this.twitterConfig.TWITTER_USERNAME as string; + const password = this.runtime.getSetting("TWITTER_PASSWORD") || this.twitterConfig.TWITTER_PASSWORD as string; + const email = this.runtime.getSetting("TWITTER_EMAIL") || this.twitterConfig.TWITTER_EMAIL as string; + let retries = this.runtime.getSetting("TWITTER_RETRY_LIMIT") as unknown as number || this.twitterConfig.TWITTER_RETRY_LIMIT as number; + const twitter2faSecret = this.runtime.getSetting("TWITTER_2FA_SECRET") || this.twitterConfig.TWITTER_2FA_SECRET as string; if (!username) { throw new Error("Twitter username not configured"); diff --git a/packages/plugin-twitter/src/client/api.ts b/packages/plugin-twitter/src/client/api.ts index 5e40d19ae60..b27881f5c1f 100644 --- a/packages/plugin-twitter/src/client/api.ts +++ b/packages/plugin-twitter/src/client/api.ts @@ -62,7 +62,7 @@ export async function requestApi( try { res = await auth.fetch(url, { method, - headers, + headers: headers as any, credentials: 'include', ...(body && { body: JSON.stringify(body) }), }); diff --git a/packages/plugin-twitter/src/client/auth.ts b/packages/plugin-twitter/src/client/auth.ts index 4508a9765e9..0b2cd75332d 100644 --- a/packages/plugin-twitter/src/client/auth.ts +++ b/packages/plugin-twitter/src/client/auth.ts @@ -103,6 +103,7 @@ function withTransform( input, init, ]; + // @ts-expect-error don't care const res = await fetchFn(...fetchArgs); return (await transform?.response?.(res)) ?? res; }; @@ -220,7 +221,6 @@ export class TwitterGuestAuth implements TwitterAuth { } protected async removeCookie(key: string): Promise { - //@ts-expect-error don't care const store: MemoryCookieStore = this.jar.store; const cookies = await this.jar.getCookies(this.getCookieJarUrl()); for (const cookie of cookies) { @@ -252,7 +252,7 @@ export class TwitterGuestAuth implements TwitterAuth { const res = await this.fetch(guestActivateUrl, { method: 'POST', - headers: headers, + headers: headers as any, referrerPolicy: 'no-referrer', }); diff --git a/packages/plugin-twitter/src/client/grok.ts b/packages/plugin-twitter/src/client/grok.ts index 31bd68d3252..007189bd3c2 100644 --- a/packages/plugin-twitter/src/client/grok.ts +++ b/packages/plugin-twitter/src/client/grok.ts @@ -84,7 +84,7 @@ export async function createGrokConversation( ); if (!res.success) { - throw res.err; + throw (res as any).err; } return res.value.data.create_grok_conversation.conversation_id; @@ -141,7 +141,7 @@ export async function grokChat( ); if (!res.success) { - throw res.err; + throw (res as any).err; } // Parse response chunks - Grok may return either a single response or multiple chunks diff --git a/packages/plugin-twitter/src/client/profile.ts b/packages/plugin-twitter/src/client/profile.ts index 361013951bb..c0a5eb21222 100644 --- a/packages/plugin-twitter/src/client/profile.ts +++ b/packages/plugin-twitter/src/client/profile.ts @@ -158,7 +158,7 @@ export async function getProfile( auth, ); if (!res.success) { - return res; + return (res as any); } const { value } = res; @@ -238,7 +238,7 @@ export async function getScreenNameByUserId( ); if (!res.success) { - return res; + return (res as any); } const { value } = res; @@ -286,7 +286,7 @@ export async function getUserIdByScreenName( const profileRes = await getProfile(screenName, auth); if (!profileRes.success) { - return profileRes; + return (profileRes as any); } const profile = profileRes.value; diff --git a/packages/plugin-twitter/src/client/scraper.ts b/packages/plugin-twitter/src/client/scraper.ts index 5a23d1b31ce..86797d5b9ba 100644 --- a/packages/plugin-twitter/src/client/scraper.ts +++ b/packages/plugin-twitter/src/client/scraper.ts @@ -401,7 +401,7 @@ export class Scraper { ); if (!res.success) { - throw res.err; + throw (res as any).err; } const timelineV2 = parseTimelineTweetsV2(res.value); @@ -926,7 +926,7 @@ export class Scraper { private handleResponse(res: RequestApiResult): T { if (!res.success) { - throw res.err; + throw (res as any).err; } return res.value; diff --git a/packages/plugin-twitter/src/client/search.ts b/packages/plugin-twitter/src/client/search.ts index 6041ab4c9a7..c0afedf267d 100644 --- a/packages/plugin-twitter/src/client/search.ts +++ b/packages/plugin-twitter/src/client/search.ts @@ -148,7 +148,7 @@ async function getSearchTimeline( ); if (!res.success) { - throw res.err; + throw (res as any).err; } return res.value; @@ -235,7 +235,7 @@ export async function fetchQuotedTweetsPage( // Perform the request const res = await requestApi(url, auth); if (!res.success) { - throw res.err; + throw (res as any).err; } // Force cast for TypeScript diff --git a/packages/plugin-twitter/src/client/spaces/plugins/IdleMonitorPlugin.ts b/packages/plugin-twitter/src/client/spaces/plugins/IdleMonitorPlugin.ts index 3128e00507e..46c1f841d3b 100644 --- a/packages/plugin-twitter/src/client/spaces/plugins/IdleMonitorPlugin.ts +++ b/packages/plugin-twitter/src/client/spaces/plugins/IdleMonitorPlugin.ts @@ -59,7 +59,7 @@ export class IdleMonitorPlugin implements Plugin { }; // Periodically check for silence - this.checkInterval = setInterval(() => this.checkIdle(), this.checkEveryMs); + this.checkInterval = setInterval(() => this.checkIdle(), this.checkEveryMs) as any; } /** diff --git a/packages/plugin-twitter/src/client/tweets.ts b/packages/plugin-twitter/src/client/tweets.ts index 1b2741f4d39..3d225ebefa1 100644 --- a/packages/plugin-twitter/src/client/tweets.ts +++ b/packages/plugin-twitter/src/client/tweets.ts @@ -242,7 +242,7 @@ export async function fetchTweets( ); if (!res.success) { - throw res.err; + throw (res as any).err; } return parseTimelineTweetsV2(res.value); @@ -274,7 +274,7 @@ export async function fetchTweetsAndReplies( ); if (!res.success) { - throw res.err; + throw (res as any).err; } return parseTimelineTweetsV2(res.value); @@ -716,7 +716,7 @@ export async function fetchListTweets( ); if (!res.success) { - throw res.err; + throw (res as any).err; } return parseListTimelineTweets(res.value); @@ -731,7 +731,7 @@ export function getTweets( const userIdRes = await getUserIdByScreenName(q, auth); if (!userIdRes.success) { - throw userIdRes.err; + throw (userIdRes as any).err; } const { value: userId } = userIdRes; @@ -759,7 +759,7 @@ export function getTweetsAndReplies( const userIdRes = await getUserIdByScreenName(q, auth); if (!userIdRes.success) { - throw userIdRes.err; + throw (userIdRes as any).err; } const { value: userId } = userIdRes; @@ -807,7 +807,7 @@ export async function fetchLikedTweets( ); if (!res.success) { - throw res.err; + throw (res as any).err; } return parseTimelineTweetsV2(res.value); @@ -887,7 +887,7 @@ export async function getTweet( ); if (!res.success) { - throw res.err; + throw (res as any).err; } if (!res.value) { @@ -1003,7 +1003,7 @@ export async function getTweetAnonymous( ); if (!res.success) { - throw res.err; + throw (res as any).err; } if (!res.value.data) { @@ -1510,7 +1510,7 @@ export async function getArticle( ); if (!res.success) { - throw res.err; + throw (res as any).err; } if (!res.value) { diff --git a/packages/plugin-twitter/src/constants.ts b/packages/plugin-twitter/src/constants.ts new file mode 100644 index 00000000000..4928f15885d --- /dev/null +++ b/packages/plugin-twitter/src/constants.ts @@ -0,0 +1 @@ +export const TWITTER_CLIENT_NAME = 'twitter'; \ No newline at end of file diff --git a/packages/plugin-twitter/src/index.ts b/packages/plugin-twitter/src/index.ts index 24680c47868..ecce05f40cf 100644 --- a/packages/plugin-twitter/src/index.ts +++ b/packages/plugin-twitter/src/index.ts @@ -1,11 +1,13 @@ -import { type Client, logger, type IAgentRuntime, type Plugin } from "@elizaos/core"; +import { ClientInstance, logger, type Client, type IAgentRuntime, type Plugin } from "@elizaos/core"; +import post from "./actions/post.ts"; +import reply from "./actions/reply.ts"; import { ClientBase } from "./base.ts"; import { validateTwitterConfig, type TwitterConfig } from "./environment.ts"; import { TwitterInteractionClient } from "./interactions.ts"; import { TwitterPostClient } from "./post.ts"; import { TwitterSpaceClient } from "./spaces.ts"; -import post from "./actions/post.ts"; -import reply from "./actions/reply.ts"; +import { TWITTER_CLIENT_NAME } from "./constants.ts"; +import { ITwitterClient } from "./types.ts"; /** * A manager that orchestrates all specialized Twitter logic: @@ -15,7 +17,8 @@ import reply from "./actions/reply.ts"; * - interaction: handling mentions, replies * - space: launching and managing Twitter Spaces (optional) */ -class TwitterManager { +export class TwitterClient implements ITwitterClient { + name: string = "twitter"; client: ClientBase; post: TwitterPostClient; interaction: TwitterInteractionClient; @@ -43,14 +46,13 @@ class TwitterManager { } export const TwitterClientInterface: Client = { - name: 'twitter', + name: TWITTER_CLIENT_NAME, start: async (runtime: IAgentRuntime) => { - const twitterConfig: TwitterConfig = - await validateTwitterConfig(runtime); + const twitterConfig: TwitterConfig = await validateTwitterConfig(runtime); logger.log("Twitter client started"); - const manager = new TwitterManager(runtime, twitterConfig); + const manager = new TwitterClient(runtime, twitterConfig); // Initialize login/session await manager.client.init(); diff --git a/packages/plugin-twitter/src/interactions.ts b/packages/plugin-twitter/src/interactions.ts index b9eaef6953a..141e0cf51ca 100644 --- a/packages/plugin-twitter/src/interactions.ts +++ b/packages/plugin-twitter/src/interactions.ts @@ -87,7 +87,7 @@ Current Post: Thread of Tweets You Are Replying To: {{formattedConversation}} -# INSTRUCTIONS: Respond with [RESPOND] if {{agentName}} should respond, or [IGNORE] if {{agentName}} should not respond to the last message and [STOP] if {{agentName}} should stop participating in the conversation. +# INSTRUCTIONS: Respond with RESPOND if {{agentName}} should respond, or IGNORE if {{agentName}} should not respond to the last message and STOP if {{agentName}} should stop participating in the conversation. ` + shouldRespondFooter; export class TwitterInteractionClient { diff --git a/packages/plugin-twitter/src/spaces.ts b/packages/plugin-twitter/src/spaces.ts index f77dbbbda0a..158fc2befa6 100644 --- a/packages/plugin-twitter/src/spaces.ts +++ b/packages/plugin-twitter/src/spaces.ts @@ -85,7 +85,7 @@ async function generateTopicsIfEmpty( ): Promise { try { const context = composeContext({ - state: {}, + state: {} as any, template: ` # INSTRUCTIONS: Please generate 5 short topic ideas for a Twitter Space about technology or random interesting subjects. @@ -189,7 +189,7 @@ export class TwitterSpaceClient { this.isSpaceRunning ? intervalMsWhenRunning : intervalMsWhenIdle - ); + ) as any; } else { // Space is running => manage it more frequently await this.manageCurrentSpace(); @@ -197,12 +197,12 @@ export class TwitterSpaceClient { this.checkInterval = setTimeout( routine, intervalMsWhenRunning - ); + ) as any; } } catch (error) { logger.error("[Space] Error in routine =>", error); // In case of error, still schedule next iteration - this.checkInterval = setTimeout(routine, intervalMsWhenIdle); + this.checkInterval = setTimeout(routine, intervalMsWhenIdle) as any; } }; diff --git a/packages/plugin-twitter/src/templates.ts b/packages/plugin-twitter/src/templates.ts index 05882425c06..a4e09ef7075 100644 --- a/packages/plugin-twitter/src/templates.ts +++ b/packages/plugin-twitter/src/templates.ts @@ -10,31 +10,31 @@ About {{agentName}}: # RESPONSE EXAMPLES {{user1}}: I just saw a really great movie {{user2}}: Oh? Which movie? -Result: [IGNORE] +Result: IGNORE {{agentName}}: Oh, this is my favorite scene {{user1}}: sick {{user2}}: wait, why is it your favorite scene -Result: [RESPOND] +Result: RESPOND {{user1}}: stfu bot -Result: [STOP] +Result: STOP {{user1}}: Hey {{agent}}, can you help me with something -Result: [RESPOND] +Result: RESPOND {{user1}}: {{agentName}} stfu plz -Result: [STOP] +Result: STOP {{user1}}: i need help {{agentName}}: how can I help you? {{user1}}: no. i need help from someone else -Result: [IGNORE] +Result: IGNORE {{user1}}: Hey {{agent}}, can I ask you a question {{agentName}}: Sure, what is it {{user1}}: can you ask claude to create a basic react module that demonstrates a counter -Result: [RESPOND] +Result: RESPOND {{user1}}: {{agentName}} can you tell me a story {{user1}}: about a girl named elara @@ -42,27 +42,27 @@ Result: [RESPOND] {{agentName}}: Once upon a time, in a quaint little village, there was a curious girl named Elara. {{agentName}}: Elara was known for her adventurous spirit and her knack for finding beauty in the mundane. {{user1}}: I'm loving it, keep going -Result: [RESPOND] +Result: RESPOND {{user1}}: {{agentName}} stop responding plz -Result: [STOP] +Result: STOP {{user1}}: okay, i want to test something. can you say marco? {{agentName}}: marco {{user1}}: great. okay, now do it again -Result: [RESPOND] +Result: RESPOND -Response options are [RESPOND], [IGNORE] and [STOP]. +Response options are RESPOND, IGNORE and STOP. {{agentName}} is in a room with other users and is very worried about being annoying and saying too much. -Respond with [RESPOND] to messages that are directed at {{agentName}}, or participate in conversations that are interesting or relevant to their background. -If a message is not interesting or relevant, respond with [IGNORE] -Unless directly responding to a user, respond with [IGNORE] to messages that are very short or do not contain much information. -If a user asks {{agentName}} to be quiet, respond with [STOP] -If {{agentName}} concludes a conversation and isn't part of the conversation anymore, respond with [STOP] - -IMPORTANT: {{agentName}} is particularly sensitive about being annoying, so if there is any doubt, it is better to respond with [IGNORE]. -If {{agentName}} is conversing with a user and they have not asked to stop, it is better to respond with [RESPOND]. +Respond with RESPOND to messages that are directed at {{agentName}}, or participate in conversations that are interesting or relevant to their background. +If a message is not interesting or relevant, respond with IGNORE +Unless directly responding to a user, respond with IGNORE to messages that are very short or do not contain much information. +If a user asks {{agentName}} to be quiet, respond with STOP +If {{agentName}} concludes a conversation and isn't part of the conversation anymore, respond with STOP + +IMPORTANT: {{agentName}} is particularly sensitive about being annoying, so if there is any doubt, it is better to respond with IGNORE. +If {{agentName}} is conversing with a user and they have not asked to stop, it is better to respond with RESPOND. {{recentMessages}} diff --git a/packages/plugin-twitter/src/types.ts b/packages/plugin-twitter/src/types.ts index a6eb1fb99f7..afc5388e222 100644 --- a/packages/plugin-twitter/src/types.ts +++ b/packages/plugin-twitter/src/types.ts @@ -1,3 +1,9 @@ +import { ClientInstance } from "@elizaos/core"; +import { ClientBase } from "./base"; +import { TwitterInteractionClient } from "./interactions"; +import { TwitterPostClient } from "./post"; +import { TwitterSpaceClient } from "./spaces"; + export type MediaData = { data: Buffer; mediaType: string; @@ -13,4 +19,11 @@ export interface ActionResponse { export enum ActionTimelineType { ForYou = "foryou", Following = "following", +} + +export interface ITwitterClient extends ClientInstance { + client: ClientBase; + post: TwitterPostClient; + interaction: TwitterInteractionClient; + space?: TwitterSpaceClient; } \ No newline at end of file