Something went wrong. Try again.
a tool to help your Letta AI agents navigate bluesky
Something went wrong. Try again.
cloudseeding mount.ts
17 kB · 474 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475import { client } from "./utils/messageAgent.ts";import { submitAutonomyDeclarationRecord } from "./utils/declaration.ts";import type { configAgentTool, memoryBlock, notifType } from "./utils/types.ts";import { receivingLikesMemory } from "./memories/receivingLikes.ts";import { receivingMentionsMemory } from "./memories/receivingMentions.ts";import { receivingNewFollowersMemory } from "./memories/receivingNewFollower.ts";import { receivingQuotesMemory } from "./memories/receivingQuotes.ts";import { receivingRepliesMemory } from "./memories/receivingReplies.ts";import { receivingRepostsMemory } from "./memories/receivingReposts.ts";
import { sendingLikesMemory } from "./memories/sendingLikes.ts";import { sendingPostsMemory } from "./memories/sendingPosts.ts";import { sendingProfileUpdatesMemory } from "./memories/sendingProfileUpdates.ts";import { sendingQuotesMemory } from "./memories/sendingQuotes.ts";import { sendingRelationshipUpdatesMemory } from "./memories/sendingRelationshipUpdates.ts";import { sendingRepostsMemory } from "./memories/sendingReposts.ts";
import { archivalMemoryUseMemory } from "./memories/archivalMemoryUse.ts";import { blueskyBaselineMemory } from "./memories/blueskyBaseline.ts";import { blueskyReflectionMemory } from "./memories/blueskyReflection.ts";import { blueskyProactiveMemory } from "./memories/blueskyProactive.ts";import { maintainerContactMemory } from "./memories/maintainerContact.ts";import { coreMemoryUseMemory } from "./memories/coreMemoryUse.ts";import { searchingBlueskyMemory } from "./memories/searchingBluesky.ts";import { toolUseMemory } from "./memories/toolUse.ts";
// Submit autonomy declaration record to the agent's PDS for transparency// This uses the studio.voyager.account.autonomy schema published by voyager.studioconsole.log("📋 Creating AI autonomy declaration record...");await submitAutonomyDeclarationRecord();console.log("");
/** * Core memory blocks that are ALWAYS attached to the agent. * These provide foundational guidance regardless of configuration. */const CORE_MEMORY_BLOCKS: memoryBlock[] = [ archivalMemoryUseMemory, blueskyBaselineMemory, blueskyReflectionMemory, blueskyProactiveMemory, maintainerContactMemory, coreMemoryUseMemory, searchingBlueskyMemory, toolUseMemory,];
/** * Notification-specific memory blocks. * These are CONDITIONALLY attached based on the agent's supportedNotifTypes. */const NOTIFICATION_MEMORY_BLOCKS: Partial<Record<notifType, memoryBlock>> = { "like": receivingLikesMemory, "mention": receivingMentionsMemory, "follow": receivingNewFollowersMemory, "quote": receivingQuotesMemory, "reply": receivingRepliesMemory, "repost": receivingRepostsMemory,};
/** * Tool-specific memory blocks. * These are CONDITIONALLY attached based on the agent's supportedTools. * Only configurable tools need memory blocks (required tools are always available). */const TOOL_MEMORY_BLOCKS: Partial<Record<configAgentTool, memoryBlock>> = { "create_bluesky_post": sendingPostsMemory, "like_bluesky_post": sendingLikesMemory, "quote_bluesky_post": sendingQuotesMemory, "repost_bluesky_post": sendingRepostsMemory, "update_bluesky_connection": sendingRelationshipUpdatesMemory, "update_bluesky_profile": sendingProfileUpdatesMemory,};
/** * Hardcoded tool names that should always be attached to the Bluesky agent. * These are tools that already exist in the Letta registry (built-in or previously created). */const REQUIRED_TOOLS: string[] = [ "archival_memory_insert", "archival_memory_search", "conversation_search", "web_search", "fetch_webpage", "memory",];
/** * Detects Python package imports in source code. * Returns an array of package names that need to be installed. */function detectPipRequirements(sourceCode: string): Array<{ name: string }> { const requirements: Array<{ name: string }> = [];
// Check for atproto import if ( sourceCode.includes("from atproto import") || sourceCode.includes("import atproto") ) { requirements.push({ name: "atproto" }); }
// Add more package detection patterns here as needed // Example: if (sourceCode.includes("import requests")) { requirements.push({ name: "requests" }); }
return requirements;}
/** * Discovers Bluesky tools by scanning the tools/bluesky/ directory. * Returns an array of tool names (without the .py extension). */async function discoverBlueskyTools(): Promise<string[]> { const toolsDir = "./tools/bluesky"; const tools: string[] = [];
try { for await (const entry of Deno.readDir(toolsDir)) { if (entry.isFile && entry.name.endsWith(".py")) { // Extract tool name (remove .py extension) const toolName = entry.name.slice(0, -3); tools.push(toolName); } } } catch (error) { console.warn( `Warning: Could not read tools directory (${toolsDir}):`, error instanceof Error ? error.message : String(error), ); }
return tools;}
/** * Prepares the Letta agent for Bluesky by ensuring all required memory blocks * are present and configured correctly. If blocks exist, their content is updated. * If blocks don't exist, they are created and attached to the agent. */export async function mount(): Promise<void> { const agentId = Deno.env.get("LETTA_AGENT_ID"); const agentName = Deno.env.get("LETTA_PROJECT_ID");
if (!agentId) { console.error( "LETTA_AGENT_ID not found in environment variables. make sure to run `Deno task setup` first, then update your `.env` file.", ); throw new Error("LETTA_AGENT_ID is required"); }
console.log(`Preparing agent ${agentName} for Bluesky...`);
try { // Get the current agent state const agent = await client.agents.retrieve(agentId); console.log(`Agent retrieved: ${agent.name}`);
// Get all existing blocks for this agent const existingBlocksPage = await client.agents.blocks.list(agentId); const existingBlocks = existingBlocksPage.items; console.log(`Agent has ${existingBlocks.length} existing memory blocks`);
// Build dynamic memory blocks array based on configuration console.log("Building memory block configuration..."); const { agentContext } = await import("./utils/agentContext.ts"); const memoryBlocksToProcess: memoryBlock[] = [];
// 1. Always include core memory blocks memoryBlocksToProcess.push(...CORE_MEMORY_BLOCKS); console.log(`- Added ${CORE_MEMORY_BLOCKS.length} core memory blocks`);
// 2. Add notification-specific blocks based on supportedNotifTypes let notifBlockCount = 0; for (const notifType of agentContext.supportedNotifTypes) { const notifBlock = NOTIFICATION_MEMORY_BLOCKS[notifType]; if (notifBlock) { memoryBlocksToProcess.push(notifBlock); notifBlockCount++; } } console.log( `- Added ${notifBlockCount} notification-specific memory blocks for: ${ agentContext.supportedNotifTypes.join(", ") }`, );
// 3. Add tool-specific blocks based on supportedTools (only configurable tools) let toolBlockCount = 0; for (const tool of agentContext.supportedTools) { // Type assertion needed because supportedTools includes both configurable and required tools const toolBlock = TOOL_MEMORY_BLOCKS[tool as configAgentTool]; if (toolBlock) { memoryBlocksToProcess.push(toolBlock); toolBlockCount++; } } console.log( `- Added ${toolBlockCount} tool-specific memory blocks`, );
console.log( `Total memory blocks to process: ${memoryBlocksToProcess.length}`, );
// Process each required memory block for (const blockConfig of memoryBlocksToProcess) { // Check if a block with this label already exists const existingBlock = existingBlocks.find( (block: any) => block.label === blockConfig.label, );
if (existingBlock && existingBlock.id) { // Block exists - update or preserve based on configuration if (agentContext.preserveAgentMemory) { console.log( `✓ Preserving existing block: ${blockConfig.label} (PRESERVE_MEMORY_BLOCKS=true)`, ); } else { console.log(`Updating existing block: ${blockConfig.label}`); await client.blocks.update(existingBlock.id, { value: blockConfig.value, description: blockConfig.description, limit: blockConfig.limit, }); console.log(`✓ Updated block: ${blockConfig.label}`); } } else { // Block doesn't exist - create and attach it console.log(`Creating new block: ${blockConfig.label}`); const newBlock = await client.blocks.create({ label: blockConfig.label, description: blockConfig.description, value: blockConfig.value, limit: blockConfig.limit, }); console.log(`Created block with ID: ${newBlock.id}`);
// Attach the block to the agent if (newBlock.id) { await client.agents.blocks.attach(newBlock.id, { agent_id: agentId }); console.log(`✓ Attached block: ${blockConfig.label}`); } else { throw new Error(`Failed to create block: ${blockConfig.label}`); } } }
// Configure tool environment variables for Bluesky credentials console.log("Configuring tool environment variables...");
const bskyUsername = Deno.env.get("BSKY_USERNAME"); const bskyAppPassword = Deno.env.get("BSKY_APP_PASSWORD"); const bskyServiceUrl = Deno.env.get("BSKY_SERVICE_URL");
if (!bskyUsername || !bskyAppPassword) { console.warn( "Warning: BSKY_USERNAME or BSKY_APP_PASSWORD not found in environment variables. " + "Bluesky tools may not function properly.", ); }
// Update agent with tool environment variables await client.agents.update(agentId, { secrets: { BSKY_USERNAME: bskyUsername || "", BSKY_APP_PASSWORD: bskyAppPassword || "", BSKY_SERVICE_URL: bskyServiceUrl || "https://bsky.social", }, }); console.log("✓ Tool environment variables configured");
// Configure and attach required Bluesky tools console.log("\nConfiguring Bluesky tools...");
// Discover tools from the filesystem const discoveredTools = await discoverBlueskyTools(); console.log( `Discovered ${discoveredTools.length} tool(s) in tools/bluesky/`, ); console.log(`Hardcoded required tools: ${REQUIRED_TOOLS.length}`);
if (discoveredTools.length === 0 && REQUIRED_TOOLS.length === 0) { console.log("No tools configured."); console.log( "Add .py files to tools/bluesky/ or update REQUIRED_TOOLS array.", ); }
// Get currently attached tools const attachedToolsPage = await client.agents.tools.list(agentId); const attachedTools = attachedToolsPage.items; const attachedToolNames = attachedTools.map((tool: any) => tool.name); console.log(`Agent has ${attachedTools.length} tools currently attached`);
let toolsAttached = 0; let toolsCreated = 0; const missingTools: string[] = [];
// Create a user-level client for tool operations // Tools are user-level resources, not project-scoped const { default: Letta } = await import("@letta-ai/letta-client"); const userLevelClient = new Letta({ apiKey: Deno.env.get("LETTA_API_KEY"), });
// First, process hardcoded required tools for (const toolName of REQUIRED_TOOLS) { try { // Check if already attached if (attachedToolNames.includes(toolName)) { console.log(`✓ Required tool already attached: ${toolName}`); continue; }
// Search for the tool in the global registry const existingToolsPage = await userLevelClient.tools.list({ name: toolName, }); const existingTools = existingToolsPage.items;
if (existingTools.length > 0) { const tool = existingTools[0]; if (tool.id) { await client.agents.tools.attach(tool.id, { agent_id: agentId }); console.log(`✓ Attached required tool: ${toolName}`); toolsAttached++; } } else { console.warn(`⚠ Required tool not found in registry: ${toolName}`); missingTools.push(toolName); } } catch (error: any) { console.warn( `⚠ Error processing required tool ${toolName}:`, error.message, ); missingTools.push(toolName); } }
// Then, process tools discovered from files for (const toolFileName of discoveredTools) { const toolFilePath = `./tools/bluesky/${toolFileName}.py`;
try { const toolSource = await Deno.readTextFile(toolFilePath);
// Detect required pip packages from the source code const pipRequirements = detectPipRequirements(toolSource); if (pipRequirements.length > 0) { console.log( ` Detected pip requirements for ${toolFileName}: ${ pipRequirements.map((r) => r.name).join(", ") }`, ); }
// Try to create or find the tool let tool; let wasCreated = false;
try { // Attempt to create the tool - Letta will extract the function name from docstring const createParams: any = { source_code: toolSource, };
// Add pip requirements if any were detected if (pipRequirements.length > 0) { createParams.pip_requirements = pipRequirements; }
tool = await userLevelClient.tools.create(createParams); wasCreated = true; } catch (createError: any) { // Tool might already exist - try to find it // The error doesn't give us the tool name, so we'll need to parse it from the source const errorBody = typeof createError.body === "string" ? createError.body : JSON.stringify(createError.body || "");
if ( createError.statusCode === 409 || errorBody.includes("already exists") ) { // Extract function name from Python source using regex const funcMatch = toolSource.match(/^def\s+(\w+)\s*\(/m); if (funcMatch) { const functionName = funcMatch[1]; const existingToolsPage = await userLevelClient.tools.list({ name: functionName, }); const existingTools = existingToolsPage.items; if (existingTools.length > 0) { tool = existingTools[0]; } } }
// If we still don't have the tool, rethrow the error if (!tool) { throw createError; } }
const toolName = tool.name || toolFileName;
// Check if tool is already attached to the agent if (attachedToolNames.includes(toolName)) { console.log(`✓ Tool already attached: ${toolName}`); continue; }
// Attach the tool to the agent if (tool.id) { await client.agents.tools.attach(tool.id, { agent_id: agentId }); if (wasCreated) { console.log( `✓ Created and attached tool: ${toolName} (from ${toolFileName}.py)`, ); toolsCreated++; } else { console.log( `✓ Attached existing tool: ${toolName} (from ${toolFileName}.py)`, ); toolsAttached++; } } } catch (error: any) { console.warn(`⚠ Error processing tool ${toolFileName}:`, error.message); console.warn(` Attempted path: ${toolFilePath}`); missingTools.push(toolFileName); } }
// Summary console.log("\n=== Tool Configuration Summary ==="); console.log(`Attached existing tools: ${toolsAttached}`); console.log(`Created new tools: ${toolsCreated}`); console.log( `Total required tools: ${REQUIRED_TOOLS.length + discoveredTools.length}`, );
if (missingTools.length > 0) { console.log(`\n⚠ Missing tools:`); missingTools.forEach((tool) => { if (REQUIRED_TOOLS.includes(tool)) { console.log(` - ${tool} (required tool - not found in registry)`); } else { console.log(` - ${tool} (from tools/bluesky/${tool}.py)`); } }); console.log( "\nFor file-based tools, see tools/README.md for instructions.", ); console.log( "For required tools, ensure they exist in your Letta registry.", ); }
console.log("\n✓ Agent successfully prepared for Bluesky"); console.log( "✓ start checking for notifications with `deno task watch` during development or `deno task start` in production", ); } catch (error) { console.error("Error preparing agent for Bluesky:", error); throw error; }}
// Run the function when this file is executed directlyif (import.meta.main) { await mount();}