a tool to help your Letta AI agents navigate bluesky
at main 7.1 kB view raw
1import Letta from "@letta-ai/letta-client"; 2import { agentContext } from "./agentContext.ts"; 3// Helper function to format tool arguments as inline key-value pairs 4const formatArgsInline = (args: unknown, maxValueLength = 50): string => { 5 try { 6 const parsed = typeof args === "string" ? JSON.parse(args) : args; 7 if (typeof parsed !== "object" || parsed === null) { 8 return String(parsed); 9 } 10 return Object.entries(parsed) 11 .map(([key, value]) => { 12 let valueStr = typeof value === "object" 13 ? JSON.stringify(value) 14 : String(value); 15 // Truncate long values at word boundaries 16 if (valueStr.length > maxValueLength) { 17 const truncated = valueStr.slice(0, maxValueLength); 18 const lastSpace = truncated.lastIndexOf(" "); 19 // If we found a space in the last 30% of the truncated string, use it 20 valueStr = lastSpace > maxValueLength * 0.7 21 ? truncated.slice(0, lastSpace) + "..." 22 : truncated + "..."; 23 } 24 return `${key}=${valueStr}`; 25 }) 26 .join(", "); 27 } catch { 28 return String(args); 29 } 30}; 31 32// Helper function to truncate long strings to 500 characters 33const truncateString = (str: string, maxLength = 140): string => { 34 if (str.length <= maxLength) { 35 return str; 36 } 37 return `${str.slice(0, maxLength)}... (truncated, ${str.length} total chars)`; 38}; 39 40// Helper function to extract tool return value from wrapper structure 41const extractToolReturn = (toolReturns: unknown): string => { 42 try { 43 // If it's already a string, return it 44 if (typeof toolReturns === "string") { 45 return toolReturns; 46 } 47 48 // If it's an array, extract the tool_return from first element 49 if (Array.isArray(toolReturns) && toolReturns.length > 0) { 50 const firstReturn = toolReturns[0]; 51 if ( 52 typeof firstReturn === "object" && 53 firstReturn !== null && 54 "tool_return" in firstReturn 55 ) { 56 const toolReturn = firstReturn.tool_return; 57 // If tool_return is already a string, return it 58 if (typeof toolReturn === "string") { 59 return toolReturn; 60 } 61 // Otherwise stringify it 62 return JSON.stringify(toolReturn); 63 } 64 } 65 66 // Fallback: return stringified version of the whole thing 67 return JSON.stringify(toolReturns); 68 } catch { 69 return String(toolReturns); 70 } 71}; 72 73// Helper function to select important params for logging 74const selectImportantParams = (args: unknown): unknown => { 75 try { 76 const parsed = typeof args === "string" ? JSON.parse(args) : args; 77 if (typeof parsed !== "object" || parsed === null) { 78 return parsed; 79 } 80 81 const entries = Object.entries(parsed); 82 83 // Filter out URIs/DIDs and very long values 84 const filtered = entries.filter(([key, value]) => { 85 const str = String(value); 86 // Skip AT URIs and DIDs 87 if (str.includes("at://") || str.includes("did:plc:")) return false; 88 // Skip very long values 89 if (str.length > 60) return false; 90 return true; 91 }); 92 93 // Take first 3, or first entry if none pass filters 94 const selected = filtered.length > 0 95 ? filtered.slice(0, 3) 96 : entries.slice(0, 1); 97 98 return Object.fromEntries(selected); 99 } catch { 100 return args; 101 } 102}; 103 104// Helper function to format tool response for logging 105const formatToolResponse = (returnValue: string): string => { 106 try { 107 // Try to parse as JSON - handle both JSON and Python dict syntax 108 let parsed; 109 try { 110 // First try standard JSON 111 parsed = JSON.parse(returnValue); 112 } catch { 113 // Try to parse Python-style dict (with single quotes and None/True/False) 114 const pythonToJson = returnValue 115 .replace(/'/g, '"') 116 .replace(/\bNone\b/g, "null") 117 .replace(/\bTrue\b/g, "true") 118 .replace(/\bFalse\b/g, "false"); 119 parsed = JSON.parse(pythonToJson); 120 } 121 122 // Handle arrays - show count and sample first item 123 if (Array.isArray(parsed)) { 124 const count = parsed.length; 125 if (count === 0) return "[]"; 126 127 const firstItem = parsed[0]; 128 if (typeof firstItem === "object" && firstItem !== null) { 129 // Format first item's key fields (use 30 for samples to keep concise) 130 const sample = formatArgsInline(firstItem, 30); 131 return `[${count} items] ${sample}`; 132 } 133 return `[${count} items]`; 134 } 135 136 // If parsed successfully and it's an object, format as key=value pairs 137 if (typeof parsed === "object" && parsed !== null) { 138 return `(${formatArgsInline(parsed, 50)})`; 139 } 140 141 // If it's a primitive, return the original string 142 return returnValue; 143 } catch { 144 // If parsing fails, return as-is (it's a simple string) 145 return returnValue; 146 } 147}; 148 149export const client = new Letta({ 150 apiKey: Deno.env.get("LETTA_API_KEY"), 151 // @ts-ignore: Letta SDK type definition might be slightly off or expecting different casing 152 projectId: Deno.env.get("LETTA_PROJECT_ID"), 153}); 154 155export const messageAgent = async (prompt: string) => { 156 const agent = Deno.env.get("LETTA_AGENT_ID"); 157 158 if (agent) { 159 const reachAgent = await client.agents.messages.stream(agent, { 160 messages: [ 161 { 162 role: "system", 163 content: prompt, 164 }, 165 ], 166 stream_tokens: true, 167 }); 168 169 let lastToolName = ""; 170 171 for await (const response of reachAgent) { 172 if (response.message_type === "reasoning_message") { 173 // console.log(`💭 reasoning…`); 174 } else if (response.message_type === "assistant_message") { 175 console.log(`💬 ${agentContext.agentBskyName}: ${response.content}`); 176 } else if (response.message_type === "tool_call_message") { 177 // Use tool_call (singular) or tool_calls (both are objects, not arrays) 178 const toolCall = response.tool_call || response.tool_calls; 179 if (toolCall && toolCall.name) { 180 lastToolName = toolCall.name; 181 const importantParams = selectImportantParams(toolCall.arguments); 182 const formattedArgs = formatArgsInline(importantParams); 183 console.log(`🔧 tool called: ${toolCall.name} (${formattedArgs})`); 184 } 185 } else if (response.message_type === "tool_return_message") { 186 const extractedReturn = extractToolReturn(response.tool_returns); 187 const formattedResponse = formatToolResponse(extractedReturn); 188 189 // Determine separator based on format 190 const separator = formattedResponse.startsWith("(") ? " " : ": "; 191 const logMessage = `↩️ tool response: ${lastToolName}${separator}${formattedResponse}`; 192 193 console.log(truncateString(logMessage, 300)); 194 } else if (response.message_type === "usage_statistics") { 195 console.log(`🔢 total steps: ${response.step_count}`); 196 } else if (response.message_type === "hidden_reasoning_message") { 197 console.log(`hidden reasoning…`); 198 } 199 } 200 } else { 201 console.log( 202 "🔹 Letta agent ID was not a set variable, skipping notification processing…", 203 ); 204 } 205};