this repo has no description
1import { AppBskyActorDefs, ComAtprotoLabelDefs } from "@atproto/api";
2import { DID, SIGNING_KEY, DELETE, PORT } from "./constants.js";
3import { LabelerServer } from "@skyware/labeler";
4import { createCanvas, loadImage } from "canvas";
5import { generateText, tool } from "ai";
6import { openai } from "@ai-sdk/openai";
7import { z } from "zod";
8import { AtpAgent } from "@atproto/api";
9import "dotenv/config";
10import fs from "node:fs";
11
12console.log("Starting labeler application");
13
14const agent = new AtpAgent({
15 service: process.env.BSKY_SERVICE ?? "https://bsky.social",
16});
17
18await agent.login({
19 identifier: process.env.BSKY_IDENTIFIER!,
20 password: process.env.BSKY_PASSWORD!,
21});
22
23console.log("Logged in to BlueSky");
24
25const server = new LabelerServer({ did: DID, signingKey: SIGNING_KEY });
26
27server.start(PORT, (error, address) => {
28 if (error) {
29 console.error("Failed to start labeler server:", error);
30 } else {
31 console.log(`Labeler server listening on ${address}`);
32 }
33});
34
35const HOUSES = ["gryffindor", "slytherin", "ravenclaw", "hufflepuff"];
36
37export const label = async (
38 subject: string | AppBskyActorDefs.ProfileView,
39 rkey: string
40) => {
41 console.log(`Labeling subject: ${typeof subject === 'string' ? subject : subject.did}, rkey: ${rkey}`);
42
43 const did = AppBskyActorDefs.isProfileView(subject) ? subject.did : subject;
44 console.log(`DID: ${did}`);
45
46 const query = server.db
47 .prepare<unknown[], ComAtprotoLabelDefs.Label>(
48 `SELECT * FROM labels WHERE uri = ? AND neg = false`
49 )
50 .all(did);
51 console.log(`Found ${query.length} existing labels for ${did}`);
52
53 const labels = query.reduce((set, label) => {
54 if (!label.neg) set.add(label.val);
55 else set.delete(label.val);
56 return set;
57 }, new Set<string>());
58
59 // const currentLabel = query.find(
60 // (label) => !label.neg && HOUSES.includes(label.val)
61 // );
62 // console.log(`Current house label: ${currentLabel ? currentLabel.val : 'None'}`);
63
64 if (rkey.includes(DELETE)) {
65 console.log(`Deleting label for ${did}`);
66 if (labels.size > 0) {
67 await server
68 .createLabels({ uri: did }, { negate: [...labels] })
69 .catch((err) => console.error(`Error deleting label: ${err}`))
70 .then(() => console.log(`Deleted label for ${did}`));
71 } else {
72 console.log(`No label to delete for ${did}`);
73 }
74 } else {
75 if (labels.size > 0) {
76 console.log(`${did} already has a house: ${[...labels].join(', ')}`);
77 return;
78 }
79
80 console.log(`Fetching avatar for ${did}`);
81 let avatarBuffer: Buffer;
82 const avatar = `avatars/${subject}.png`;
83
84 if (typeof subject === "string") {
85 console.log(`Fetching profile for ${subject}`);
86 const { data } = await agent.getProfile({ actor: subject });
87 if (!data) {
88 console.error(`Profile not found for ${subject}`);
89 throw new Error("Profile not found");
90 }
91 subject = data;
92 }
93
94 if (AppBskyActorDefs.isProfileView(subject) && subject.avatar) {
95 console.log(`Loading avatar from URL: ${subject.avatar}`);
96 const image = await loadImage(subject.avatar);
97 const canvas = createCanvas(100, 100);
98 const ctx = canvas.getContext("2d");
99 ctx.drawImage(image, 0, 0, 100, 100);
100 avatarBuffer = canvas.toBuffer();
101 fs.writeFileSync(avatar, avatarBuffer);
102 console.log(`Avatar saved to ${avatar}`);
103 } else {
104 console.log(`No avatar found, using default 1x1 white image`);
105 const canvas = createCanvas(1, 1);
106 const ctx = canvas.getContext("2d");
107 ctx.fillStyle = "white";
108 ctx.fillRect(0, 0, 1, 1);
109 avatarBuffer = canvas.toBuffer();
110 }
111
112 console.log(`Generating prompt for ${did}`);
113 const promptTemplate = `
114You're the Sorting Hat from Harry Potter, operating as a bot on the microblogging social network BlueSky on data from user profiles.
115Which Hogwarts house would this user belong to?
116${AppBskyActorDefs.isProfileView(subject) ? `
117The user's name is ${subject.displayName || subject.handle} (@${subject.handle}).
118${subject.description ? `Their bio is: "${subject.description}"` : ''}
119` : ''}
120If the user has an avatar, it's been attached to the message. If it's a 1x1 white image, please ignore it and focus on the name and bio.
121`;
122
123 console.log(`Calling AI to decide house for ${did}`);
124 await generateText({
125 model: openai("gpt-4o"),
126 messages: [
127 {
128 role: "user",
129 content: [
130 {
131 type: "text",
132 text: promptTemplate,
133 },
134 {
135 type: "image",
136 image: avatarBuffer,
137 },
138 ],
139 },
140 ],
141 toolChoice: "required",
142 tools: {
143 decideHouse: tool({
144 parameters: z.object({
145 answer: z.union([z.literal("gryffindor"), z.literal("slytherin"), z.literal("ravenclaw"), z.literal("hufflepuff")]),
146 }),
147 execute: async ({ answer }) => {
148 await server
149 .createLabel({ uri: did, val: answer })
150 .catch((err) => console.log(err))
151 .then(() => console.log(`Labeled ${did} with ${answer}`));
152 return answer;
153 },
154 }),
155 },
156 });
157 console.log(`AI decision complete for ${did}`);
158 }
159};
160
161console.log("Labeler application initialized");