Skip to content

Commit 24ff695

Browse files
authored
Merge pull request #914 from sayangel/farcaster-client-improvements
fix: Farcater client cleanup and fixed response logic
2 parents 75a4655 + af6cd41 commit 24ff695

File tree

5 files changed

+67
-34
lines changed

5 files changed

+67
-34
lines changed

.env.example

+2
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ INTIFACE_WEBSOCKET_URL=ws://localhost:12345
158158
FARCASTER_FID= # the FID associated with the account your are sending casts from
159159
FARCASTER_NEYNAR_API_KEY= # Neynar API key: https://neynar.com/
160160
FARCASTER_NEYNAR_SIGNER_UUID= # signer for the account you are sending casts from. create a signer here: https://dev.neynar.com/app
161+
FARCASTER_DRY_RUN=false # Set to true if you want to run the bot without actually publishing casts
162+
FARCASTER_POLL_INTERVAL=120 # How often (in seconds) the bot should check for farcaster interactions (replies and mentions)
161163

162164
# Coinbase
163165
COINBASE_COMMERCE_KEY= # from coinbase developer portal

packages/client-farcaster/src/client.ts

+5-8
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { IAgentRuntime } from "@ai16z/eliza";
1+
import { IAgentRuntime, elizaLogger } from "@ai16z/eliza";
22
import { NeynarAPIClient, isApiErrorResponse } from "@neynar/nodejs-sdk";
33
import { NeynarCastResponse, Cast, Profile, FidRequest, CastId } from "./types";
44

@@ -63,11 +63,11 @@ export class FarcasterClient {
6363
}
6464
} catch (err) {
6565
if (isApiErrorResponse(err)) {
66-
console.log(err.response.data);
66+
elizaLogger.error('Neynar error: ', err.response.data);
6767
throw err.response.data;
6868
} else {
69+
elizaLogger.error('Error: ', err);
6970
throw err;
70-
console.log(err);
7171
}
7272
}
7373
}
@@ -83,7 +83,6 @@ export class FarcasterClient {
8383
});
8484
const cast = {
8585
hash: response.cast.hash,
86-
//parentHash: cast.parent_hash,
8786
authorFid: response.cast.author.fid,
8887
text: response.cast.text,
8988
profile: {
@@ -114,12 +113,10 @@ export class FarcasterClient {
114113
fid: request.fid,
115114
limit: request.pageSize,
116115
});
117-
//console.log(response);
118116
response.casts.map((cast) => {
119117
this.cache.set(`farcaster/cast/${cast.hash}`, cast);
120118
timeline.push({
121119
hash: cast.hash,
122-
//parentHash: cast.parent_hash,
123120
authorFid: cast.author.fid,
124121
text: cast.text,
125122
profile: {
@@ -175,9 +172,9 @@ export class FarcasterClient {
175172

176173
const result = await this.neynar.fetchBulkUsers({ fids: [fid] });
177174
if (!result.users || result.users.length < 1) {
178-
console.log("getUserDataByFid ERROR");
175+
elizaLogger.error('Error fetching user by fid');
179176

180-
throw "getUserDataByFid ERROR";
177+
throw "getProfile ERROR";
181178
}
182179

183180
const neynarUserProfile = result.users[0];

packages/client-farcaster/src/interactions.ts

+37-11
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
Memory,
66
ModelClass,
77
stringToUuid,
8+
elizaLogger,
89
type IAgentRuntime,
910
} from "@ai16z/eliza";
1011
import type { FarcasterClient } from "./client";
@@ -34,14 +35,16 @@ export class FarcasterInteractionManager {
3435
try {
3536
await this.handleInteractions();
3637
} catch (error) {
37-
console.error(error);
38+
elizaLogger.error(error)
3839
return;
3940
}
4041

4142
this.timeout = setTimeout(
4243
handleInteractionsLoop,
43-
(Math.floor(Math.random() * (5 - 2 + 1)) + 2) * 60 * 1000
44-
); // Random interval between 2-5 minutes
44+
Number(
45+
this.runtime.getSetting("FARCASTER_POLL_INTERVAL") || 120
46+
) * 1000 // Default to 2 minutes
47+
);
4548
};
4649

4750
handleInteractionsLoop();
@@ -122,12 +125,12 @@ export class FarcasterInteractionManager {
122125
thread: Cast[]
123126
}) {
124127
if (cast.profile.fid === agent.fid) {
125-
console.log("skipping cast from bot itself", cast.hash);
128+
elizaLogger.info("skipping cast from bot itself", cast.hash)
126129
return;
127130
}
128131

129132
if (!memory.content.text) {
130-
console.log("skipping cast with no text", cast.hash);
133+
elizaLogger.info("skipping cast with no text", cast.hash);
131134
return { text: "", action: "IGNORE" };
132135
}
133136

@@ -143,10 +146,25 @@ export class FarcasterInteractionManager {
143146
timeline
144147
);
145148

149+
const formattedConversation = thread
150+
.map(
151+
(cast) => `@${cast.profile.username} (${new Date(
152+
cast.timestamp
153+
).toLocaleString("en-US", {
154+
hour: "2-digit",
155+
minute: "2-digit",
156+
month: "short",
157+
day: "numeric",
158+
})}):
159+
${cast.text}`
160+
)
161+
.join("\n\n");
162+
146163
const state = await this.runtime.composeState(memory, {
147164
farcasterUsername: agent.username,
148165
timeline: formattedTimeline,
149166
currentPost,
167+
formattedConversation
150168
});
151169

152170
const shouldRespondContext = composeContext({
@@ -176,15 +194,15 @@ export class FarcasterInteractionManager {
176194
);
177195
}
178196

179-
const shouldRespond = await generateShouldRespond({
197+
const shouldRespondResponse = await generateShouldRespond({
180198
runtime: this.runtime,
181199
context: shouldRespondContext,
182200
modelClass: ModelClass.SMALL,
183201
});
184202

185-
if (!shouldRespond) {
186-
console.log("Not responding to message");
187-
return { text: "", action: "IGNORE" };
203+
if (shouldRespondResponse === "IGNORE" || shouldRespondResponse === "STOP") {
204+
elizaLogger.info(`Not responding to cast because generated ShouldRespond was ${shouldRespondResponse}`)
205+
return;
188206
}
189207

190208
const context = composeContext({
@@ -206,8 +224,16 @@ export class FarcasterInteractionManager {
206224

207225
if (!response.text) return;
208226

227+
228+
if (this.runtime.getSetting("FARCASTER_DRY_RUN") === "true") {
229+
elizaLogger.info(
230+
`Dry run: would have responded to cast ${cast.hash} with ${response.text}`
231+
);
232+
return;
233+
}
234+
209235
try {
210-
console.log(`Replying to cast ${cast.hash}.`);
236+
elizaLogger.info(`Replying to cast ${cast.hash}.`);
211237

212238
const results = await sendCast({
213239
runtime: this.runtime,
@@ -236,7 +262,7 @@ export class FarcasterInteractionManager {
236262
newState
237263
);
238264
} catch (error) {
239-
console.error(`Error sending response cast: ${error}`);
265+
elizaLogger.error(`Error sending response cast: ${error}`);
240266
}
241267
}
242268
}

packages/client-farcaster/src/post.ts

+11-9
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ export class FarcasterPostManager {
4848
elizaLogger.info("Generating new cast");
4949
try {
5050
const fid = Number(this.runtime.getSetting("FARCASTER_FID")!);
51-
// const farcasterUserName =
52-
// this.runtime.getSetting("FARCASTER_USERNAME")!;
5351

5452
const profile = await this.client.getProfile(fid);
5553
await this.runtime.ensureUserExists(
@@ -86,7 +84,7 @@ export class FarcasterPostManager {
8684
}
8785
);
8886

89-
// Generate new tweet
87+
// Generate new cast
9088
const context = composeContext({
9189
state,
9290
template:
@@ -105,6 +103,7 @@ export class FarcasterPostManager {
105103
const contentLength = 240;
106104

107105
let content = slice.slice(0, contentLength);
106+
108107
// if its bigger than 280, delete the last line
109108
if (content.length > 280) {
110109
content = content.slice(0, content.lastIndexOf("\n"));
@@ -120,12 +119,18 @@ export class FarcasterPostManager {
120119
content = content.slice(0, content.lastIndexOf("."));
121120
}
122121

122+
123+
if (this.runtime.getSetting("FARCASTER_DRY_RUN") === "true") {
124+
elizaLogger.info(
125+
`Dry run: would have cast: ${content}`
126+
);
127+
return;
128+
}
129+
123130
try {
124-
// TODO: handle all the casts?
125131
const [{ cast }] = await sendCast({
126132
client: this.client,
127133
runtime: this.runtime,
128-
//: this.signer,
129134
signerUuid: this.signerUuid,
130135
roomId: generateRoomId,
131136
content: { text: content },
@@ -144,10 +149,7 @@ export class FarcasterPostManager {
144149
roomId
145150
);
146151

147-
console.log(
148-
`%c [Farcaster Neynar Client] Published cast ${cast.hash}`,
149-
"color: #8565cb;"
150-
);
152+
elizaLogger.info(`[Farcaster Neynar Client] Published cast ${cast.hash}`);
151153

152154
await this.runtime.messageManager.createMemory(
153155
createCastMemory({

packages/client-farcaster/src/prompts.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ About {{agentName}} (@{{farcasterUsername}}):
3636
{{characterPostExamples}}`;
3737

3838
export const postTemplate =
39-
headerTemplate +
39+
headerTemplate +
4040
`
4141
# Task: Generate a post in the voice and style of {{agentName}}, aka @{{farcasterUsername}}
4242
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:
5353
Thread of casts You Are Replying To:
5454
{{formattedConversation}}
5555
56-
# Task: Generate a post in the voice, style and perspective of {{agentName}} (@{{twitterUserName}}):
56+
# Task: Generate a post in the voice, style and perspective of {{agentName}} (@{{farcasterUsername}}):
5757
{{currentPost}}` +
5858
messageCompletionFooter;
5959

6060
export const shouldRespondTemplate =
6161
//
62-
`# INSTRUCTIONS: Determine if {{agentName}} (@{{twitterUserName}}) should respond to the message and participate in the conversation. Do not comment. Just respond with "true" or "false".
62+
`# Task: Decide if {{agentName}} should respond.
63+
About {{agentName}}:
64+
{{bio}}
65+
66+
# 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".
6367
6468
Response options are RESPOND, IGNORE and STOP.
6569
@@ -68,15 +72,17 @@ Response options are RESPOND, IGNORE and STOP.
6872
{{agentName}} is in a room with other users and wants to be conversational, but not annoying.
6973
{{agentName}} should RESPOND to messages that are directed at them, or participate in conversations that are interesting or relevant to their background.
7074
If a message is not interesting or relevant, {{agentName}} should IGNORE.
75+
If a message thread has become repetitive, {{agentName}} should IGNORE.
7176
Unless directly RESPONDing to a user, {{agentName}} should IGNORE messages that are very short or do not contain much information.
7277
If a user asks {{agentName}} to stop talking, {{agentName}} should STOP.
7378
If {{agentName}} concludes a conversation and isn't part of the conversation anymore, {{agentName}} should STOP.
7479
75-
{{recentPosts}}
80+
IMPORTANT: {{agentName}} (aka @{{farcasterUsername}}) is particularly sensitive about being annoying, so if there is any doubt, it is better to IGNORE than to RESPOND.
7681
77-
IMPORTANT: {{agentName}} (aka @{{twitterUserName}}) is particularly sensitive about being annoying, so if there is any doubt, it is better to IGNORE than to RESPOND.
82+
Thread of messages You Are Replying To:
83+
{{formattedConversation}}
7884
85+
Current message:
7986
{{currentPost}}
8087
81-
# 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.
8288
` + shouldRespondFooter;

0 commit comments

Comments
 (0)