A simple Bluesky bot to make sense of the noise, with responses powered by Gemini, similar to Grok.

update inference structure (closes #3)

Changed files
+61 -69
src
handlers
utils
+1
docker-compose.yml
··· 12 12 - "HANDLE=${HANDLE:?}" 13 13 - "APP_PASSWORD=${APP_PASSWORD:?}" 14 14 - "GEMINI_API_KEY=${GEMINI_API_KEY:?}" 15 + - "USE_JETSTREAM=${USE_JETSTREAM:-false}" 15 16 volumes: 16 17 - aero_db:/sqlite.db
+39 -43
src/handlers/messages.ts
··· 18 18 19 19 type SupportedFunctionCall = typeof c.SUPPORTED_FUNCTION_CALLS[number]; 20 20 21 - async function generateAIResponse(parsedConversation: string) { 21 + async function generateAIResponse(parsedContext: string, messages: { 22 + role: string; 23 + parts: { 24 + text: string; 25 + }[]; 26 + }[]) { 22 27 const config = { 23 28 model: env.GEMINI_MODEL, 24 29 config: { ··· 37 42 ], 38 43 }, 39 44 { 40 - role: "user" as const, 45 + role: "model" as const, 41 46 parts: [ 42 47 { 43 - text: 44 - `Below is the yaml for the current conversation. The last message is the one to respond to. The post is the current one you are meant to be analyzing. 45 - 46 - ${parsedConversation}`, 48 + text: parsedContext, 47 49 }, 48 50 ], 49 51 }, 52 + ...messages, 50 53 ]; 51 54 52 55 let inference = await c.ai.models.generateContent({ ··· 99 102 return inference; 100 103 } 101 104 102 - async function sendResponse( 103 - conversation: Conversation, 104 - text: string, 105 - ): Promise<void> { 106 - if (exceedsGraphemes(text)) { 107 - multipartResponse(conversation, text); 108 - } else { 109 - conversation.sendMessage({ 110 - text, 111 - }); 112 - } 113 - } 114 - 115 105 export async function handler(message: ChatMessage): Promise<void> { 116 106 const conversation = await message.getConversation(); 117 107 // ? Conversation should always be able to be found, but just in case: ··· 132 122 return; 133 123 } 134 124 135 - const today = new Date(); 136 - today.setHours(0, 0, 0, 0); 137 - const tomorrow = new Date(today); 138 - tomorrow.setDate(tomorrow.getDate() + 1); 125 + if (message.senderDid != env.ADMIN_DID) { 126 + const todayStart = new Date(); 127 + todayStart.setHours(0, 0, 0, 0); 139 128 140 - const dailyCount = await db 141 - .select({ count: count(messages.id) }) 142 - .from(messages) 143 - .where( 144 - and( 145 - eq(messages.did, message.senderDid), 146 - gte(messages.created_at, today), 147 - lt(messages.created_at, tomorrow), 148 - ), 149 - ); 129 + const dailyCount = await db 130 + .select({ count: count(messages.id) }) 131 + .from(messages) 132 + .where( 133 + and( 134 + eq(messages.did, message.senderDid), 135 + gte(messages.created_at, todayStart), 136 + ), 137 + ); 150 138 151 - if (dailyCount[0]!.count >= env.DAILY_QUERY_LIMIT) { 152 - conversation.sendMessage({ 153 - text: c.QUOTA_EXCEEDED_MESSAGE, 154 - }); 155 - return; 139 + if (dailyCount[0]!.count >= env.DAILY_QUERY_LIMIT) { 140 + conversation.sendMessage({ 141 + text: c.QUOTA_EXCEEDED_MESSAGE, 142 + }); 143 + return; 144 + } 156 145 } 157 146 158 147 logger.success("Found conversation"); ··· 160 149 text: "...", 161 150 }); 162 151 163 - const parsedConversation = await parseConversation(conversation); 164 - 165 - logger.info("Parsed conversation: ", parsedConversation); 152 + const parsedConversation = await parseConversation(conversation, message); 166 153 167 154 try { 168 - const inference = await generateAIResponse(parsedConversation); 155 + const inference = await generateAIResponse( 156 + parsedConversation.context, 157 + parsedConversation.messages, 158 + ); 169 159 if (!inference) { 170 160 throw new Error("Failed to generate text. Returned undefined."); 171 161 } ··· 176 166 logger.success("Generated text:", inference.text); 177 167 saveMessage(conversation, env.DID, inference.text!); 178 168 179 - await sendResponse(conversation, responseText); 169 + if (exceedsGraphemes(responseText)) { 170 + multipartResponse(conversation, responseText); 171 + } else { 172 + conversation.sendMessage({ 173 + text: responseText, 174 + }); 175 + } 180 176 } 181 177 } catch (error) { 182 178 logger.error("Error in post handler:", error);
+21 -26
src/utils/conversation.ts
··· 14 14 /* 15 15 Utilities 16 16 */ 17 - const resolveDid = (convo: Conversation, did: string) => 18 - convo.members.find((actor) => actor.did == did)!; 19 - 20 17 const getUserDid = (convo: Conversation) => 21 18 convo.members.find((actor) => actor.did != env.DID)!; 22 19 ··· 29 26 /* 30 27 Conversations 31 28 */ 32 - async function initConvo(convo: Conversation) { 29 + async function initConvo(convo: Conversation, initialMessage: ChatMessage) { 33 30 const user = getUserDid(convo); 34 - 35 - const initialMessage = (await convo.getMessages()).messages[0] as 36 - | ChatMessage 37 - | undefined; 38 - if (!initialMessage) { 39 - throw new Error("Failed to get initial message of conversation"); 40 - } 41 31 42 32 const postUri = await parseMessagePostUri(initialMessage); 43 33 if (!postUri) { ··· 87 77 return convo; 88 78 } 89 79 90 - export async function parseConversation(convo: Conversation) { 80 + export async function parseConversation( 81 + convo: Conversation, 82 + latestMessage: ChatMessage, 83 + ) { 91 84 let row = await getConvo(convo.id); 92 85 if (!row) { 93 - row = await initConvo(convo); 86 + row = await initConvo(convo, latestMessage); 94 87 } else { 95 - const latestMessage = (await convo.getMessages()) 96 - .messages[0] as ChatMessage; 97 - 98 88 const postUri = await parseMessagePostUri(latestMessage); 99 89 if (postUri) { 100 90 const [updatedRow] = await db ··· 128 118 129 119 let parseResult = null; 130 120 try { 131 - parseResult = yaml.dump({ 132 - post: await parsePost(post, true), 121 + parseResult = { 122 + context: yaml.dump({ 123 + post: await parsePost(post, true), 124 + }), 133 125 messages: convoMessages.map((message) => { 134 - const profile = resolveDid(convo, message.did); 126 + const role = message.did == env.DID ? "model" : "user"; 135 127 136 128 return { 137 - user: profile.displayName 138 - ? `${profile.displayName} (${profile.handle})` 139 - : `Handle: ${profile.handle}`, 140 - text: message.text, 129 + role, 130 + parts: [ 131 + { 132 + text: message.text, 133 + }, 134 + ], 141 135 }; 142 136 }), 143 - }); 137 + }; 144 138 } catch (e) { 145 139 convo.sendMessage({ 146 140 text: ··· 169 163 .where( 170 164 and( 171 165 eq(messages.conversationId, convo.id), 172 - eq(messages.postUri, convo!.postUri), 166 + eq(messages.postUri, convo.postUri), 167 + eq(messages.revision, convo.revision), 173 168 ), 174 169 ) 175 170 .limit(15); ··· 192 187 .values({ 193 188 conversationId: _convo.id, 194 189 postUri: _convo.postUri, 195 - revision: _convo.postUri, 190 + revision: _convo.revision, 196 191 did, 197 192 text, 198 193 });