this repo has no description
at main 15 kB view raw
1"""Block management tools for user-specific memory blocks.""" 2from pydantic import BaseModel, Field 3from typing import List, Dict, Any 4import logging 5 6def get_letta_client(): 7 """Get a Letta client using configuration.""" 8 try: 9 from config_loader import get_letta_config 10 from letta_client import Letta 11 config = get_letta_config() 12 return Letta(token=config['api_key'], timeout=config['timeout']) 13 except (ImportError, FileNotFoundError, KeyError): 14 # Fallback to environment variable 15 import os 16 from letta_client import Letta 17 return Letta(token=os.environ["LETTA_API_KEY"]) 18 19 20class AttachUserBlocksArgs(BaseModel): 21 handles: List[str] = Field(..., description="List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social'])") 22 23 24class DetachUserBlocksArgs(BaseModel): 25 handles: List[str] = Field(..., description="List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social'])") 26 27 28class UserNoteAppendArgs(BaseModel): 29 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") 30 note: str = Field(..., description="Note to append to the user's memory block (e.g., '\\n- Cameron is a person')") 31 32 33class UserNoteReplaceArgs(BaseModel): 34 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") 35 old_text: str = Field(..., description="Text to find and replace in the user's memory block") 36 new_text: str = Field(..., description="Text to replace the old_text with") 37 38 39class UserNoteSetArgs(BaseModel): 40 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") 41 content: str = Field(..., description="Complete content to set for the user's memory block") 42 43 44class UserNoteViewArgs(BaseModel): 45 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") 46 47 48 49def attach_user_blocks(handles: list, agent_state: "AgentState") -> str: 50 """ 51 Attach user-specific memory blocks to the agent. Creates blocks if they don't exist. 52 53 Args: 54 handles: List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social']) 55 agent_state: The agent state object containing agent information 56 57 Returns: 58 String with attachment results for each handle 59 """ 60 logger = logging.getLogger(__name__) 61 62 handles = list(set(handles)) 63 64 try: 65 client = get_letta_client() 66 results = [] 67 68 # Get current blocks using the API 69 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 70 current_block_labels = set() 71 72 for block in current_blocks: 73 current_block_labels.add(block.label) 74 75 for handle in handles: 76 # Sanitize handle for block label - completely self-contained 77 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 78 block_label = f"user_{clean_handle}" 79 80 # Skip if already attached 81 if block_label in current_block_labels: 82 results.append(f"{handle}: Already attached") 83 continue 84 85 # Check if block exists or create new one 86 try: 87 blocks = client.blocks.list(label=block_label) 88 if blocks and len(blocks) > 0: 89 block = blocks[0] 90 logger.debug(f"Found existing block: {block_label}") 91 else: 92 block = client.blocks.create( 93 label=block_label, 94 value=f"# User: {handle}\n\nNo information about this user yet.", 95 limit=5000 96 ) 97 logger.info(f"Created new block: {block_label}") 98 99 # Attach block atomically 100 client.agents.blocks.attach( 101 agent_id=str(agent_state.id), 102 block_id=str(block.id) 103 ) 104 105 results.append(f"{handle}: Block attached") 106 logger.debug(f"Successfully attached block {block_label} to agent") 107 108 except Exception as e: 109 results.append(f"{handle}: Error - {str(e)}") 110 logger.error(f"Error processing block for {handle}: {e}") 111 112 return f"Attachment results:\n" + "\n".join(results) 113 114 except Exception as e: 115 logger.error(f"Error attaching user blocks: {e}") 116 raise Exception(f"Error attaching user blocks: {str(e)}") 117 118 119def detach_user_blocks(handles: list, agent_state: "AgentState") -> str: 120 """ 121 Detach user-specific memory blocks from the agent. Blocks are preserved for later use. 122 123 Args: 124 handles: List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social']) 125 agent_state: The agent state object containing agent information 126 127 Returns: 128 String with detachment results for each handle 129 """ 130 logger = logging.getLogger(__name__) 131 132 try: 133 client = get_letta_client() 134 results = [] 135 136 # Build mapping of block labels to IDs using the API 137 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 138 block_label_to_id = {} 139 140 for block in current_blocks: 141 block_label_to_id[block.label] = str(block.id) 142 143 # Process each handle and detach atomically 144 for handle in handles: 145 # Sanitize handle for block label - completely self-contained 146 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 147 block_label = f"user_{clean_handle}" 148 149 if block_label in block_label_to_id: 150 try: 151 # Detach block atomically 152 client.agents.blocks.detach( 153 agent_id=str(agent_state.id), 154 block_id=block_label_to_id[block_label] 155 ) 156 results.append(f"{handle}: Detached") 157 logger.debug(f"Successfully detached block {block_label} from agent") 158 except Exception as e: 159 results.append(f"{handle}: Error during detachment - {str(e)}") 160 logger.error(f"Error detaching block {block_label}: {e}") 161 else: 162 results.append(f"{handle}: Not attached") 163 164 return f"Detachment results:\n" + "\n".join(results) 165 166 except Exception as e: 167 logger.error(f"Error detaching user blocks: {e}") 168 raise Exception(f"Error detaching user blocks: {str(e)}") 169 170 171def user_note_append(handle: str, note: str, agent_state: "AgentState") -> str: 172 """ 173 Append a note to a user's memory block. Creates the block if it doesn't exist. 174 175 Args: 176 handle: User Bluesky handle (e.g., 'cameron.pfiffer.org') 177 note: Note to append to the user's memory block 178 agent_state: The agent state object containing agent information 179 180 Returns: 181 String confirming the note was appended 182 """ 183 logger = logging.getLogger(__name__) 184 185 try: 186 client = get_letta_client() 187 188 # Sanitize handle for block label 189 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 190 block_label = f"user_{clean_handle}" 191 192 # Check if block exists 193 blocks = client.blocks.list(label=block_label) 194 195 if blocks and len(blocks) > 0: 196 # Block exists, append to it 197 block = blocks[0] 198 current_value = block.value 199 new_value = current_value + note 200 201 # Update the block 202 client.blocks.modify( 203 block_id=str(block.id), 204 value=new_value 205 ) 206 logger.info(f"Appended note to existing block: {block_label}") 207 return f"✓ Appended note to {handle}'s memory block" 208 209 else: 210 # Block doesn't exist, create it with the note 211 initial_value = f"# User: {handle}\n\n{note}" 212 block = client.blocks.create( 213 label=block_label, 214 value=initial_value, 215 limit=5000 216 ) 217 logger.info(f"Created new block with note: {block_label}") 218 219 # Check if block needs to be attached to agent 220 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 221 current_block_labels = {block.label for block in current_blocks} 222 223 if block_label not in current_block_labels: 224 # Attach the new block to the agent 225 client.agents.blocks.attach( 226 agent_id=str(agent_state.id), 227 block_id=str(block.id) 228 ) 229 logger.info(f"Attached new block to agent: {block_label}") 230 return f"✓ Created and attached {handle}'s memory block with note" 231 else: 232 return f"✓ Created {handle}'s memory block with note" 233 234 except Exception as e: 235 logger.error(f"Error appending note to user block: {e}") 236 raise Exception(f"Error appending note to user block: {str(e)}") 237 238 239def user_note_replace(handle: str, old_text: str, new_text: str, agent_state: "AgentState") -> str: 240 """ 241 Replace text in a user's memory block. 242 243 Args: 244 handle: User Bluesky handle (e.g., 'cameron.pfiffer.org') 245 old_text: Text to find and replace 246 new_text: Text to replace the old_text with 247 agent_state: The agent state object containing agent information 248 249 Returns: 250 String confirming the text was replaced 251 """ 252 logger = logging.getLogger(__name__) 253 254 try: 255 client = get_letta_client() 256 257 # Sanitize handle for block label 258 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 259 block_label = f"user_{clean_handle}" 260 261 # Check if block exists 262 blocks = client.blocks.list(label=block_label) 263 264 if not blocks or len(blocks) == 0: 265 raise Exception(f"No memory block found for user: {handle}") 266 267 block = blocks[0] 268 current_value = block.value 269 270 # Check if old_text exists in the block 271 if old_text not in current_value: 272 raise Exception(f"Text '{old_text}' not found in {handle}'s memory block") 273 274 # Replace the text 275 new_value = current_value.replace(old_text, new_text) 276 277 # Update the block 278 client.blocks.modify( 279 block_id=str(block.id), 280 value=new_value 281 ) 282 logger.info(f"Replaced text in block: {block_label}") 283 return f"✓ Replaced text in {handle}'s memory block" 284 285 except Exception as e: 286 logger.error(f"Error replacing text in user block: {e}") 287 raise Exception(f"Error replacing text in user block: {str(e)}") 288 289 290def user_note_set(handle: str, content: str, agent_state: "AgentState") -> str: 291 """ 292 Set the complete content of a user's memory block. 293 294 Args: 295 handle: User Bluesky handle (e.g., 'cameron.pfiffer.org') 296 content: Complete content to set for the memory block 297 agent_state: The agent state object containing agent information 298 299 Returns: 300 String confirming the content was set 301 """ 302 logger = logging.getLogger(__name__) 303 304 try: 305 client = get_letta_client() 306 307 # Sanitize handle for block label 308 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 309 block_label = f"user_{clean_handle}" 310 311 # Check if block exists 312 blocks = client.blocks.list(label=block_label) 313 314 if blocks and len(blocks) > 0: 315 # Block exists, update it 316 block = blocks[0] 317 client.blocks.modify( 318 block_id=str(block.id), 319 value=content 320 ) 321 logger.info(f"Set content for existing block: {block_label}") 322 return f"✓ Set content for {handle}'s memory block" 323 324 else: 325 # Block doesn't exist, create it 326 block = client.blocks.create( 327 label=block_label, 328 value=content, 329 limit=5000 330 ) 331 logger.info(f"Created new block with content: {block_label}") 332 333 # Check if block needs to be attached to agent 334 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 335 current_block_labels = {block.label for block in current_blocks} 336 337 if block_label not in current_block_labels: 338 # Attach the new block to the agent 339 client.agents.blocks.attach( 340 agent_id=str(agent_state.id), 341 block_id=str(block.id) 342 ) 343 logger.info(f"Attached new block to agent: {block_label}") 344 return f"✓ Created and attached {handle}'s memory block" 345 else: 346 return f"✓ Created {handle}'s memory block" 347 348 except Exception as e: 349 logger.error(f"Error setting user block content: {e}") 350 raise Exception(f"Error setting user block content: {str(e)}") 351 352 353def user_note_view(handle: str, agent_state: "AgentState") -> str: 354 """ 355 View the content of a user's memory block. 356 357 Args: 358 handle: User Bluesky handle (e.g., 'cameron.pfiffer.org') 359 agent_state: The agent state object containing agent information 360 361 Returns: 362 String containing the user's memory block content 363 """ 364 logger = logging.getLogger(__name__) 365 366 try: 367 client = get_letta_client() 368 369 # Sanitize handle for block label 370 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 371 block_label = f"user_{clean_handle}" 372 373 # Check if block exists 374 blocks = client.blocks.list(label=block_label) 375 376 if not blocks or len(blocks) == 0: 377 return f"No memory block found for user: {handle}" 378 379 block = blocks[0] 380 logger.info(f"Retrieved content for block: {block_label}") 381 382 return f"Memory block for {handle}:\n\n{block.value}" 383 384 except Exception as e: 385 logger.error(f"Error viewing user block: {e}") 386 raise Exception(f"Error viewing user block: {str(e)}") 387 388