From 03cae9687fbbaba63e7e17fb0a2b90e0bb591ea4 Mon Sep 17 00:00:00 2001 From: sayangel Date: Sat, 7 Dec 2024 21:30:58 -0800 Subject: [PATCH 1/7] chore: replace console log with eliza logger --- packages/client-farcaster/src/client.ts | 13 +++++-------- packages/client-farcaster/src/interactions.ts | 13 +++++++------ packages/client-farcaster/src/post.ts | 5 +---- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/client-farcaster/src/client.ts b/packages/client-farcaster/src/client.ts index 9aa780dc4f1..663ff5640c2 100644 --- a/packages/client-farcaster/src/client.ts +++ b/packages/client-farcaster/src/client.ts @@ -1,4 +1,4 @@ -import { IAgentRuntime } from "@ai16z/eliza"; +import { IAgentRuntime, elizaLogger } from "@ai16z/eliza"; import { NeynarAPIClient, isApiErrorResponse } from "@neynar/nodejs-sdk"; import { NeynarCastResponse, Cast, Profile, FidRequest, CastId } from "./types"; @@ -63,11 +63,11 @@ export class FarcasterClient { } } catch (err) { if (isApiErrorResponse(err)) { - console.log(err.response.data); + elizaLogger.error('Neynar error: ', err.response.data); throw err.response.data; } else { + elizaLogger.error('Error: ', err); throw err; - console.log(err); } } } @@ -83,7 +83,6 @@ export class FarcasterClient { }); const cast = { hash: response.cast.hash, - //parentHash: cast.parent_hash, authorFid: response.cast.author.fid, text: response.cast.text, profile: { @@ -114,12 +113,10 @@ export class FarcasterClient { fid: request.fid, limit: request.pageSize, }); - //console.log(response); response.casts.map((cast) => { this.cache.set(`farcaster/cast/${cast.hash}`, cast); timeline.push({ hash: cast.hash, - //parentHash: cast.parent_hash, authorFid: cast.author.fid, text: cast.text, profile: { @@ -175,9 +172,9 @@ export class FarcasterClient { const result = await this.neynar.fetchBulkUsers({ fids: [fid] }); if (!result.users || result.users.length < 1) { - console.log("getUserDataByFid ERROR"); + elizaLogger.error('Error fetching user by fid'); - throw "getUserDataByFid ERROR"; + throw "getProfile ERROR"; } const neynarUserProfile = result.users[0]; diff --git a/packages/client-farcaster/src/interactions.ts b/packages/client-farcaster/src/interactions.ts index 65c5c308aeb..df4bae49571 100644 --- a/packages/client-farcaster/src/interactions.ts +++ b/packages/client-farcaster/src/interactions.ts @@ -5,6 +5,7 @@ import { Memory, ModelClass, stringToUuid, + elizaLogger, type IAgentRuntime, } from "@ai16z/eliza"; import type { FarcasterClient } from "./client"; @@ -34,7 +35,7 @@ export class FarcasterInteractionManager { try { await this.handleInteractions(); } catch (error) { - console.error(error); + elizaLogger.error(error) return; } @@ -122,12 +123,12 @@ export class FarcasterInteractionManager { thread: Cast[] }) { if (cast.profile.fid === agent.fid) { - console.log("skipping cast from bot itself", cast.hash); + elizaLogger.info("skipping cast from bot itself", cast.hash) return; } if (!memory.content.text) { - console.log("skipping cast with no text", cast.hash); + elizaLogger.info("skipping cast with no text", cast.hash); return { text: "", action: "IGNORE" }; } @@ -183,7 +184,7 @@ export class FarcasterInteractionManager { }); if (!shouldRespond) { - console.log("Not responding to message"); + elizaLogger.info("Not responding to message"); return { text: "", action: "IGNORE" }; } @@ -207,7 +208,7 @@ export class FarcasterInteractionManager { if (!response.text) return; try { - console.log(`Replying to cast ${cast.hash}.`); + elizaLogger.info(`Replying to cast ${cast.hash}.`); const results = await sendCast({ runtime: this.runtime, @@ -236,7 +237,7 @@ export class FarcasterInteractionManager { newState ); } catch (error) { - console.error(`Error sending response cast: ${error}`); + elizaLogger.error(`Error sending response cast: ${error}`); } } } diff --git a/packages/client-farcaster/src/post.ts b/packages/client-farcaster/src/post.ts index 13b135b5291..087b5b99eb4 100644 --- a/packages/client-farcaster/src/post.ts +++ b/packages/client-farcaster/src/post.ts @@ -144,10 +144,7 @@ export class FarcasterPostManager { roomId ); - console.log( - `%c [Farcaster Neynar Client] Published cast ${cast.hash}`, - "color: #8565cb;" - ); + elizaLogger.info(`%c [Farcaster Neynar Client] Published cast ${cast.hash}`); await this.runtime.messageManager.createMemory( createCastMemory({ From 3446bd624ab3e7a2fea18962532adad6558e9366 Mon Sep 17 00:00:00 2001 From: sayangel Date: Sat, 7 Dec 2024 21:37:41 -0800 Subject: [PATCH 2/7] chore: remove unnecessary comments --- packages/client-farcaster/src/post.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/client-farcaster/src/post.ts b/packages/client-farcaster/src/post.ts index 087b5b99eb4..08f4f581600 100644 --- a/packages/client-farcaster/src/post.ts +++ b/packages/client-farcaster/src/post.ts @@ -48,8 +48,6 @@ export class FarcasterPostManager { elizaLogger.info("Generating new cast"); try { const fid = Number(this.runtime.getSetting("FARCASTER_FID")!); - // const farcasterUserName = - // this.runtime.getSetting("FARCASTER_USERNAME")!; const profile = await this.client.getProfile(fid); await this.runtime.ensureUserExists( @@ -86,7 +84,7 @@ export class FarcasterPostManager { } ); - // Generate new tweet + // Generate new cast const context = composeContext({ state, template: @@ -105,6 +103,7 @@ export class FarcasterPostManager { const contentLength = 240; let content = slice.slice(0, contentLength); + // if its bigger than 280, delete the last line if (content.length > 280) { content = content.slice(0, content.lastIndexOf("\n")); @@ -121,11 +120,9 @@ export class FarcasterPostManager { } try { - // TODO: handle all the casts? const [{ cast }] = await sendCast({ client: this.client, runtime: this.runtime, - //: this.signer, signerUuid: this.signerUuid, roomId: generateRoomId, content: { text: content }, From 92a2ff2f8249e109ba02af588fc1d9702a9dc071 Mon Sep 17 00:00:00 2001 From: sayangel Date: Sat, 7 Dec 2024 21:56:47 -0800 Subject: [PATCH 3/7] chore: log cleanup --- packages/client-farcaster/src/post.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-farcaster/src/post.ts b/packages/client-farcaster/src/post.ts index 08f4f581600..77ca6cdc8ae 100644 --- a/packages/client-farcaster/src/post.ts +++ b/packages/client-farcaster/src/post.ts @@ -141,7 +141,7 @@ export class FarcasterPostManager { roomId ); - elizaLogger.info(`%c [Farcaster Neynar Client] Published cast ${cast.hash}`); + elizaLogger.info(`[Farcaster Neynar Client] Published cast ${cast.hash}`); await this.runtime.messageManager.createMemory( createCastMemory({ From e791b22bcfc4ae56fdd976f066bb3d2311dc130b Mon Sep 17 00:00:00 2001 From: sayangel Date: Sat, 7 Dec 2024 22:18:44 -0800 Subject: [PATCH 4/7] feature: make farcaster polling configurable --- packages/client-farcaster/src/interactions.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/client-farcaster/src/interactions.ts b/packages/client-farcaster/src/interactions.ts index df4bae49571..2371d6d0e88 100644 --- a/packages/client-farcaster/src/interactions.ts +++ b/packages/client-farcaster/src/interactions.ts @@ -41,8 +41,10 @@ export class FarcasterInteractionManager { this.timeout = setTimeout( handleInteractionsLoop, - (Math.floor(Math.random() * (5 - 2 + 1)) + 2) * 60 * 1000 - ); // Random interval between 2-5 minutes + Number( + this.runtime.getSetting("FARCASTER_POLL_INTERVAL") || 120 + ) * 1000 // Default to 2 minutes + ); }; handleInteractionsLoop(); @@ -207,6 +209,14 @@ export class FarcasterInteractionManager { if (!response.text) return; + + if (this.runtime.getSetting("FARCASTER_DRY_RUN") === "true") { + elizaLogger.info( + `Dry run: would have responded to cast ${cast.hash} with ${response.text}` + ); + return; + } + try { elizaLogger.info(`Replying to cast ${cast.hash}.`); From 0a14fa9495049f7e8041b71f41b509c781cc058d Mon Sep 17 00:00:00 2001 From: sayangel Date: Sat, 7 Dec 2024 22:19:08 -0800 Subject: [PATCH 5/7] feature: add farcaster dry run option --- packages/client-farcaster/src/post.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/client-farcaster/src/post.ts b/packages/client-farcaster/src/post.ts index 77ca6cdc8ae..3cd06605ec4 100644 --- a/packages/client-farcaster/src/post.ts +++ b/packages/client-farcaster/src/post.ts @@ -119,6 +119,14 @@ export class FarcasterPostManager { content = content.slice(0, content.lastIndexOf(".")); } + + if (this.runtime.getSetting("FARCASTER_DRY_RUN") === "true") { + elizaLogger.info( + `Dry run: would have cast: ${content}` + ); + return; + } + try { const [{ cast }] = await sendCast({ client: this.client, From ab377ee9fdddfa099dee1409c959c1d7db2396c3 Mon Sep 17 00:00:00 2001 From: sayangel Date: Sat, 7 Dec 2024 23:24:40 -0800 Subject: [PATCH 6/7] fix: update response logic and prompt --- packages/client-farcaster/src/interactions.ts | 23 +++++++++++++++---- packages/client-farcaster/src/prompts.ts | 18 ++++++++++----- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/client-farcaster/src/interactions.ts b/packages/client-farcaster/src/interactions.ts index 2371d6d0e88..3877473d07f 100644 --- a/packages/client-farcaster/src/interactions.ts +++ b/packages/client-farcaster/src/interactions.ts @@ -146,10 +146,25 @@ export class FarcasterInteractionManager { timeline ); + const formattedConversation = thread + .map( + (cast) => `@${cast.profile.username} (${new Date( + cast.timestamp + ).toLocaleString("en-US", { + hour: "2-digit", + minute: "2-digit", + month: "short", + day: "numeric", + })}): + ${cast.text}` + ) + .join("\n\n"); + const state = await this.runtime.composeState(memory, { farcasterUsername: agent.username, timeline: formattedTimeline, currentPost, + formattedConversation }); const shouldRespondContext = composeContext({ @@ -179,15 +194,15 @@ export class FarcasterInteractionManager { ); } - const shouldRespond = await generateShouldRespond({ + const shouldRespondResponse = await generateShouldRespond({ runtime: this.runtime, context: shouldRespondContext, modelClass: ModelClass.SMALL, }); - if (!shouldRespond) { - elizaLogger.info("Not responding to message"); - return { text: "", action: "IGNORE" }; + if (shouldRespondResponse === "IGNORE" || shouldRespondResponse === "STOP") { + elizaLogger.info(`Not responding to cast because generated ShouldRespond was ${shouldRespondResponse}`) + return; } const context = composeContext({ diff --git a/packages/client-farcaster/src/prompts.ts b/packages/client-farcaster/src/prompts.ts index b9e0326d729..fde67d5370e 100644 --- a/packages/client-farcaster/src/prompts.ts +++ b/packages/client-farcaster/src/prompts.ts @@ -36,7 +36,7 @@ About {{agentName}} (@{{farcasterUsername}}): {{characterPostExamples}}`; export const postTemplate = - headerTemplate + +headerTemplate + ` # Task: Generate a post in the voice and style of {{agentName}}, aka @{{farcasterUsername}} Write a single sentence post that is {{adjective}} about {{topic}} (without mentioning {{topic}} directly), from the perspective of {{agentName}}. @@ -53,13 +53,17 @@ Recent interactions between {{agentName}} and other users: Thread of casts You Are Replying To: {{formattedConversation}} -# Task: Generate a post in the voice, style and perspective of {{agentName}} (@{{twitterUserName}}): +# Task: Generate a post in the voice, style and perspective of {{agentName}} (@{{farcasterUsername}}): {{currentPost}}` + messageCompletionFooter; export const shouldRespondTemplate = // - `# INSTRUCTIONS: Determine if {{agentName}} (@{{twitterUserName}}) should respond to the message and participate in the conversation. Do not comment. Just respond with "true" or "false". + `# Task: Decide if {{agentName}} should respond. + About {{agentName}}: + {{bio}} + + # INSTRUCTIONS: Determine if {{agentName}} (@{{farcasterUsername}}) should respond to the message and participate in the conversation. Do not comment. Just respond with "RESPOND" or "IGNORE" or "STOP". Response options are RESPOND, IGNORE and STOP. @@ -68,15 +72,17 @@ Response options are RESPOND, IGNORE and STOP. {{agentName}} is in a room with other users and wants to be conversational, but not annoying. {{agentName}} should RESPOND to messages that are directed at them, or participate in conversations that are interesting or relevant to their background. If a message is not interesting or relevant, {{agentName}} should IGNORE. +If a message thread has become repetitive, {{agentName}} should IGNORE. Unless directly RESPONDing to a user, {{agentName}} should IGNORE messages that are very short or do not contain much information. If a user asks {{agentName}} to stop talking, {{agentName}} should STOP. If {{agentName}} concludes a conversation and isn't part of the conversation anymore, {{agentName}} should STOP. -{{recentPosts}} +IMPORTANT: {{agentName}} (aka @{{farcasterUsername}}) is particularly sensitive about being annoying, so if there is any doubt, it is better to IGNORE than to RESPOND. -IMPORTANT: {{agentName}} (aka @{{twitterUserName}}) is particularly sensitive about being annoying, so if there is any doubt, it is better to IGNORE than to RESPOND. +Thread of messages You Are Replying To: +{{formattedConversation}} +Current message: {{currentPost}} -# 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; From af6cd4134064c479c762ceb154900e86a0461b5e Mon Sep 17 00:00:00 2001 From: sayangel Date: Sat, 7 Dec 2024 23:27:20 -0800 Subject: [PATCH 7/7] chore: update env.example with new farcaster configs --- .env.example | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.env.example b/.env.example index 7ec5d956b8b..12451792680 100644 --- a/.env.example +++ b/.env.example @@ -134,6 +134,8 @@ INTIFACE_WEBSOCKET_URL=ws://localhost:12345 FARCASTER_FID= # the FID associated with the account your are sending casts from FARCASTER_NEYNAR_API_KEY= # Neynar API key: https://neynar.com/ FARCASTER_NEYNAR_SIGNER_UUID= # signer for the account you are sending casts from. create a signer here: https://dev.neynar.com/app +FARCASTER_DRY_RUN=false # Set to true if you want to run the bot without actually publishing casts +FARCASTER_POLL_INTERVAL=120 # How often (in seconds) the bot should check for farcaster interactions (replies and mentions) # Coinbase COINBASE_COMMERCE_KEY= # from coinbase developer portal