a tool to help your Letta AI agents navigate bluesky
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 8f5306ab3dad80dbbc83a88a3f21465c47a8fc9d 238 lines 7.5 kB view raw
1import { bsky } from "../utils/bsky.ts"; 2import type { AutonomyDeclarationRecord } from "./types.ts"; 3import { Lexicons } from "@atproto/lexicon"; 4 5export const AUTONOMY_DECLARATION_LEXICON = { 6 "lexicon": 1, 7 "id": "studio.voyager.account.autonomy", 8 "defs": { 9 "main": { 10 "type": "record", 11 "key": "literal:self", 12 "record": { 13 "type": "object", 14 "properties": { 15 "automationLevel": { 16 "type": "string", 17 "knownValues": [ 18 "human", 19 "assisted", 20 "collaborative", 21 "automated", 22 ], 23 "description": 24 "Level of automation in account management and content creation", 25 }, 26 "usesGenerativeAI": { 27 "type": "boolean", 28 "description": 29 "Whether this account uses generative AI (LLMs, image generation, etc.) to create content", 30 }, 31 "description": { 32 "type": "string", 33 "maxGraphemes": 300, 34 "description": 35 "Plain language explanation of how this account is automated and what it does", 36 }, 37 "responsibleParty": { 38 "type": "object", 39 "properties": { 40 "type": { 41 "type": "string", 42 "knownValues": [ 43 "person", 44 "organization", 45 ], 46 "description": 47 "Whether the responsible party is a person or organization", 48 }, 49 "name": { 50 "type": "string", 51 "maxGraphemes": 100, 52 "description": "Name of the person or organization responsible", 53 }, 54 "contact": { 55 "type": "string", 56 "maxLength": 300, 57 "description": 58 "Contact information (email, URL, handle, or DID)", 59 }, 60 "did": { 61 "type": "string", 62 "format": "did", 63 "description": 64 "DID of the responsible party if they have an ATProto identity", 65 }, 66 }, 67 "description": 68 "Information about who is accountable for this account's automated behavior", 69 }, 70 "disclosureUrl": { 71 "type": "string", 72 "format": "uri", 73 "description": 74 "URL with additional information about this account's automation", 75 }, 76 "createdAt": { 77 "type": "string", 78 "format": "datetime", 79 "description": "Timestamp when this declaration was created", 80 }, 81 }, 82 "required": [ 83 "createdAt", 84 ], 85 }, 86 "description": 87 "Declaration of automation and AI usage for transparency and accountability", 88 }, 89 }, 90}; 91 92export const createAutonomyDeclarationRecord = async () => { 93 const automationLevel = Deno.env.get("AUTOMATION_LEVEL")?.toLowerCase(); 94 const projectDescription = Deno.env.get("PROJECT_DESCRIPTION"); 95 const disclosureUrl = Deno.env.get("DISCLOSURE_URL"); 96 97 const responsiblePartyType = Deno.env.get("RESPONSIBLE_PARTY_TYPE") 98 ?.toLowerCase(); 99 const responsiblePartyName = Deno.env.get("RESPONSIBLE_PARTY_NAME"); 100 const responsiblePartyContact = Deno.env.get("RESPONSIBLE_PARTY_CONTACT"); 101 const responsiblePartyBsky = Deno.env.get("RESPONSIBLE_PARTY_BSKY"); 102 103 const declarationRecord: AutonomyDeclarationRecord = { 104 $type: "studio.voyager.account.autonomy", 105 usesGenerativeAI: true, // Always true for this project 106 automationLevel: (automationLevel === "assisted" || 107 automationLevel === "collaborative" || 108 automationLevel === "automated") 109 ? automationLevel 110 : "automated", // Default to automated if not specified or invalid 111 createdAt: new Date().toISOString(), 112 }; 113 114 // Add description if provided 115 if (projectDescription?.trim()) { 116 declarationRecord.description = projectDescription.trim(); 117 } 118 119 // Add disclosure URL if provided 120 if (disclosureUrl?.trim()) { 121 declarationRecord.disclosureUrl = disclosureUrl.trim(); 122 } 123 124 // Build responsible party object if any fields are provided 125 if ( 126 responsiblePartyType || 127 responsiblePartyName || 128 responsiblePartyContact || 129 responsiblePartyBsky 130 ) { 131 declarationRecord.responsibleParty = {}; 132 133 // Add type if provided and valid 134 if ( 135 responsiblePartyType === "person" || 136 responsiblePartyType === "organization" 137 ) { 138 declarationRecord.responsibleParty.type = responsiblePartyType; 139 } 140 141 // Add name if provided 142 if (responsiblePartyName?.trim()) { 143 declarationRecord.responsibleParty.name = responsiblePartyName.trim(); 144 } 145 146 // Add contact if provided 147 if (responsiblePartyContact?.trim()) { 148 declarationRecord.responsibleParty.contact = responsiblePartyContact 149 .trim(); 150 } 151 152 // Handle DID or Handle from RESPONSIBLE_PARTY_BSKY 153 if (responsiblePartyBsky?.trim()) { 154 const bskyIdentifier = responsiblePartyBsky.trim(); 155 156 // Check if it's a DID (starts with "did:") 157 if (bskyIdentifier.startsWith("did:")) { 158 declarationRecord.responsibleParty.did = bskyIdentifier; 159 } else { 160 // Assume it's a handle and resolve to DID 161 try { 162 const authorData = await bsky.getProfile({ actor: bskyIdentifier }); 163 declarationRecord.responsibleParty.did = authorData.data.did; 164 } catch (error) { 165 console.warn( 166 `Failed to resolve DID for identifier ${bskyIdentifier}:`, 167 error, 168 ); 169 // Continue without DID rather than failing 170 } 171 } 172 } 173 } 174 175 return declarationRecord; 176}; 177 178export const submitAutonomyDeclarationRecord = async () => { 179 const lex = new Lexicons(); 180 181 try { 182 lex.add(AUTONOMY_DECLARATION_LEXICON as any); 183 const record = await createAutonomyDeclarationRecord(); 184 185 lex.assertValidRecord( 186 "studio.voyager.account.autonomy", 187 record, 188 ); 189 190 const repo = bsky.session?.did; 191 if (!repo) { 192 throw new Error("Not logged in - no DID available"); 193 } 194 195 // Check if record already exists 196 let exists = false; 197 try { 198 await bsky.com.atproto.repo.getRecord({ 199 repo, 200 collection: "studio.voyager.account.autonomy", 201 rkey: "self", 202 }); 203 exists = true; 204 console.log("Existing autonomy declaration found - updating..."); 205 } catch (error: any) { 206 // Handle "record not found" errors (status 400 with error: "RecordNotFound") 207 const isNotFound = 208 error?.status === 400 && error?.error === "RecordNotFound" || 209 error?.status === 404 || 210 error?.message?.includes("not found") || 211 error?.message?.includes("Could not locate record"); 212 213 if (isNotFound) { 214 console.log("No existing autonomy declaration found - creating new..."); 215 } else { 216 // Re-throw if it's not a "not found" error 217 throw error; 218 } 219 } 220 221 // Create or update the record 222 const result = await bsky.com.atproto.repo.putRecord({ 223 repo, 224 collection: "studio.voyager.account.autonomy", 225 rkey: "self", 226 record, 227 }); 228 229 console.log( 230 `Autonomy declaration ${exists ? "updated" : "created"} successfully:`, 231 result, 232 ); 233 return result; 234 } catch (error) { 235 console.error("Error submitting autonomy declaration record:", error); 236 throw error; 237 } 238};