Skip to content

Commit 2be53cb

Browse files
committed
Ability to like messages
1 parent c4ce29a commit 2be53cb

File tree

5 files changed

+53
-12
lines changed

5 files changed

+53
-12
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ FediChatBot
66
[![GitHub Actions][GitHub Actions badge]][GitHub Actions]
77

88
FediChatBot is an LLM-powered chatbot for fediverse, powered by [BotKit] and
9-
[Gemini 2.0 Flash (experimental)]. It consists of only about 300 lines of code
9+
[Gemini 2.0 Flash (experimental)]. It consists of only about 350 lines of code
1010
(except for prompts)!
1111

1212
Please follow `@FediChatBot@fedichatbot.deno.dev` on your fediverse instance to

bot.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import { join } from "@std/path/join";
4747
import { AsyncLocalStorage } from "node:async_hooks";
4848
import { detect } from "tinyld";
4949
import { FilterXSS } from "xss";
50+
import { z } from "zod";
5051
import metadata from "./deno.json" with { type: "json" };
5152

5253
await configure({
@@ -118,13 +119,27 @@ bot.onFollow = async (session, followRequest) => {
118119
);
119120
};
120121

122+
const reactionResponseSchema = z.object({
123+
whetherToLike: z.boolean().describe(
124+
"Whether to press the like button on the message.",
125+
),
126+
});
127+
121128
bot.onMention = async (session, msg) => {
122129
if (msg.replyTarget != null) return;
123130
const actor = msg.actor;
131+
const llmWithStruct = llm.withStructuredOutput(reactionResponseSchema);
132+
const whetherToLike = await llmWithStruct.invoke([
133+
getSystemMessage(session),
134+
await getIntroMessage(session, actor, await getMentionPrompt(actor)),
135+
await getHumanMessage(msg, true),
136+
]);
137+
logger.debug("Whether to like: {whetherToLike}", whetherToLike);
138+
if (whetherToLike.whetherToLike) await msg.like();
124139
const response = await llm.invoke([
125140
getSystemMessage(session),
126141
await getIntroMessage(session, actor, await getMentionPrompt(actor)),
127-
await getHumanMessage(msg),
142+
await getHumanMessage(msg, false),
128143
]);
129144
const message = response.content.toString();
130145
const language = detect(message);
@@ -153,9 +168,16 @@ bot.onReply = async (session, msg) => {
153168
for (const msg of thread) {
154169
const message = msg.actor?.id?.href === session.actorId.href
155170
? new AIMessage(msg.text)
156-
: await getHumanMessage(msg);
171+
: await getHumanMessage(msg, false);
157172
messages.push(message);
158173
}
174+
const llmWithStruct = llm.withStructuredOutput(reactionResponseSchema);
175+
const whetherToLike = await llmWithStruct.invoke([
176+
...messages.slice(0, messages.length - 1),
177+
await getHumanMessage(thread[thread.length - 1], true),
178+
]);
179+
if (whetherToLike.whetherToLike) await msg.like();
180+
logger.debug("Whether to like: {whetherToLike}", whetherToLike);
159181
const response = await llm.invoke(messages);
160182
const message = response.content.toString();
161183
const language = detect(message);
@@ -183,6 +205,9 @@ const FOLLOW_PROMPT_TEMPLATE = await Deno.readTextFile(
183205
const MENTION_PROMPT_TEMPLATE = await Deno.readTextFile(
184206
join(import.meta.dirname!, "prompts", "mention.txt"),
185207
);
208+
const REACTION_PROMPT_TEMPLATE = await Deno.readTextFile(
209+
join(import.meta.dirname!, "prompts", "reaction.txt"),
210+
);
186211

187212
const template = new Template({ isEscape: false });
188213

@@ -224,6 +249,15 @@ async function getFollowPrompt(actor: Actor): Promise<string> {
224249
});
225250
}
226251

252+
function getReactionPrompt(message: Message<MessageClass, void>): string {
253+
const msg = message.text;
254+
return template.render(REACTION_PROMPT_TEMPLATE, {
255+
quotedMessage: msg == null
256+
? "Not available."
257+
: `> ${msg.replaceAll("\n", "\n> ")}`,
258+
});
259+
}
260+
227261
async function getIntroMessage(
228262
session: Session<void>,
229263
actor: Actor,
@@ -281,6 +315,7 @@ async function getMentionPrompt(actor: Actor): Promise<string> {
281315

282316
async function getHumanMessage<T extends MessageClass>(
283317
msg: Message<T, void>,
318+
reaction: boolean,
284319
): Promise<HumanMessage> {
285320
const attachments = msg.attachments.map(async (doc) => {
286321
if (!doc.mediaType?.startsWith("image/")) return null;
@@ -293,7 +328,7 @@ async function getHumanMessage<T extends MessageClass>(
293328
});
294329
return new HumanMessage({
295330
content: [
296-
{ type: "text", text: msg.text },
331+
{ type: "text", text: reaction ? getReactionPrompt(msg) : msg.text },
297332
...(await Promise.all(attachments)).filter((a) => a != null),
298333
],
299334
});

deno.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"exports": "./bot.ts",
66
"imports": {
77
"@deno-library/template": "jsr:@deno-library/template@^0.2.1",
8-
"@fedify/botkit": "jsr:@fedify/botkit@^0.1.0-dev.63+f54fee0a",
8+
"@fedify/botkit": "jsr:@fedify/botkit@^0.1.0-dev.67+94b4841c",
99
"@fedify/fedify": "jsr:@fedify/fedify@^1.3.3",
1010
"@kitsonk/kv-toolbox": "jsr:@kitsonk/kv-toolbox@^0.25.0",
1111
"@langchain/core": "npm:@langchain/core@^0.3.28",
@@ -15,7 +15,8 @@
1515
"@std/encoding": "jsr:@std/encoding@^1.0.6",
1616
"@std/path": "jsr:@std/path@^1.0.8",
1717
"tinyld": "npm:tinyld@^1.3.4",
18-
"xss": "npm:xss@^1.0.15"
18+
"xss": "npm:xss@^1.0.15",
19+
"zod": "npm:zod@^3.24.1"
1920
},
2021
"unstable": [
2122
"kv",

deno.lock

Lines changed: 8 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

prompts/reaction.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Would you like to press the like button to the following message?
2+
3+
{{quotedMessage}}

0 commit comments

Comments
 (0)