Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Farcater client cleanup and fixed response logic #914

Merged
merged 7 commits into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
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;
Loading