Skip to content

Commit

Permalink
Merge pull request #914 from sayangel/farcaster-client-improvements
Browse files Browse the repository at this point in the history
fix: Farcater client cleanup and fixed response logic
  • Loading branch information
jkbrooks authored Dec 8, 2024
2 parents 75a4655 + af6cd41 commit 24ff695
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 34 deletions.
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,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
Expand Down
13 changes: 5 additions & 8 deletions packages/client-farcaster/src/client.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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);
}
}
}
Expand All @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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];
Expand Down
48 changes: 37 additions & 11 deletions packages/client-farcaster/src/interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Memory,
ModelClass,
stringToUuid,
elizaLogger,
type IAgentRuntime,
} from "@ai16z/eliza";
import type { FarcasterClient } from "./client";
Expand Down Expand Up @@ -34,14 +35,16 @@ export class FarcasterInteractionManager {
try {
await this.handleInteractions();
} catch (error) {
console.error(error);
elizaLogger.error(error)
return;
}

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();
Expand Down Expand Up @@ -122,12 +125,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" };
}

Expand All @@ -143,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({
Expand Down Expand Up @@ -176,15 +194,15 @@ export class FarcasterInteractionManager {
);
}

const shouldRespond = await generateShouldRespond({
const shouldRespondResponse = await generateShouldRespond({
runtime: this.runtime,
context: shouldRespondContext,
modelClass: ModelClass.SMALL,
});

if (!shouldRespond) {
console.log("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({
Expand All @@ -206,8 +224,16 @@ 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 {
console.log(`Replying to cast ${cast.hash}.`);
elizaLogger.info(`Replying to cast ${cast.hash}.`);

const results = await sendCast({
runtime: this.runtime,
Expand Down Expand Up @@ -236,7 +262,7 @@ export class FarcasterInteractionManager {
newState
);
} catch (error) {
console.error(`Error sending response cast: ${error}`);
elizaLogger.error(`Error sending response cast: ${error}`);
}
}
}
20 changes: 11 additions & 9 deletions packages/client-farcaster/src/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -86,7 +84,7 @@ export class FarcasterPostManager {
}
);

// Generate new tweet
// Generate new cast
const context = composeContext({
state,
template:
Expand All @@ -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"));
Expand All @@ -120,12 +119,18 @@ 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 {
// 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 },
Expand All @@ -144,10 +149,7 @@ export class FarcasterPostManager {
roomId
);

console.log(
`%c [Farcaster Neynar Client] Published cast ${cast.hash}`,
"color: #8565cb;"
);
elizaLogger.info(`[Farcaster Neynar Client] Published cast ${cast.hash}`);

await this.runtime.messageManager.createMemory(
createCastMemory({
Expand Down
18 changes: 12 additions & 6 deletions packages/client-farcaster/src/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}}.
Expand All @@ -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.
Expand All @@ -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;

0 comments on commit 24ff695

Please sign in to comment.