this repo has no description
at screaming 161 lines 5.3 kB view raw
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");