a digital person for bluesky
at x 28 kB view raw
1"""Block management tools for user-specific memory blocks.""" 2from pydantic import BaseModel, Field 3from typing import List, Dict, Any 4 5def get_letta_client(): 6 """Get a Letta client using configuration.""" 7 try: 8 from config_loader import get_letta_config 9 from letta_client import Letta 10 config = get_letta_config() 11 return Letta(token=config['api_key'], timeout=config['timeout']) 12 except (ImportError, FileNotFoundError, KeyError): 13 # Fallback to environment variable 14 import os 15 from letta_client import Letta 16 return Letta(token=os.environ["LETTA_API_KEY"]) 17 18 19class AttachUserBlocksArgs(BaseModel): 20 handles: List[str] = Field(..., description="List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social'])") 21 22 23class DetachUserBlocksArgs(BaseModel): 24 handles: List[str] = Field(..., description="List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social'])") 25 26 27class UserNoteAppendArgs(BaseModel): 28 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") 29 note: str = Field(..., description="Note to append to the user's memory block (e.g., '\\n- Cameron is a person')") 30 31 32class UserNoteReplaceArgs(BaseModel): 33 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") 34 old_text: str = Field(..., description="Text to find and replace in the user's memory block") 35 new_text: str = Field(..., description="Text to replace the old_text with") 36 37 38class UserNoteSetArgs(BaseModel): 39 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") 40 content: str = Field(..., description="Complete content to set for the user's memory block") 41 42 43class UserNoteViewArgs(BaseModel): 44 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") 45 46 47# X (Twitter) User Block Management 48class AttachXUserBlocksArgs(BaseModel): 49 user_ids: List[str] = Field(..., description="List of X user IDs (e.g., ['1232326955652931584', '1950680610282094592'])") 50 51 52class DetachXUserBlocksArgs(BaseModel): 53 user_ids: List[str] = Field(..., description="List of X user IDs (e.g., ['1232326955652931584', '1950680610282094592'])") 54 55 56class XUserNoteAppendArgs(BaseModel): 57 user_id: str = Field(..., description="X user ID (e.g., '1232326955652931584')") 58 note: str = Field(..., description="Note to append to the user's memory block (e.g., '\\\\n- Cameron is a person')") 59 60 61class XUserNoteReplaceArgs(BaseModel): 62 user_id: str = Field(..., description="X user ID (e.g., '1232326955652931584')") 63 old_text: str = Field(..., description="Text to find and replace in the user's memory block") 64 new_text: str = Field(..., description="Text to replace the old_text with") 65 66 67class XUserNoteSetArgs(BaseModel): 68 user_id: str = Field(..., description="X user ID (e.g., '1232326955652931584')") 69 content: str = Field(..., description="Complete content to set for the user's memory block") 70 71 72class XUserNoteViewArgs(BaseModel): 73 user_id: str = Field(..., description="X user ID (e.g., '1232326955652931584')") 74 75 76 77def attach_user_blocks(handles: list, agent_state: "AgentState") -> str: 78 """ 79 Attach user-specific memory blocks to the agent. Creates blocks if they don't exist. 80 81 Args: 82 handles: List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social']) 83 agent_state: The agent state object containing agent information 84 85 Returns: 86 String with attachment results for each handle 87 """ 88 89 handles = list(set(handles)) 90 91 try: 92 # Try to get client the local way first, fall back to inline for cloud execution 93 try: 94 client = get_letta_client() 95 except (ImportError, NameError): 96 # Create Letta client inline for cloud execution 97 import os 98 from letta_client import Letta 99 client = Letta(token=os.environ["LETTA_API_KEY"]) 100 results = [] 101 102 # Get current blocks using the API 103 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 104 current_block_labels = set() 105 current_block_ids = set() 106 107 for block in current_blocks: 108 current_block_labels.add(block.label) 109 current_block_ids.add(str(block.id)) 110 111 for handle in handles: 112 # Sanitize handle for block label - completely self-contained 113 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 114 block_label = f"user_{clean_handle}" 115 116 # Skip if already attached 117 if block_label in current_block_labels: 118 results.append(f"{handle}: Already attached") 119 continue 120 121 # Check if block exists or create new one 122 try: 123 blocks = client.blocks.list(label=block_label) 124 if blocks and len(blocks) > 0: 125 block = blocks[0] 126 127 # Double-check if this block is already attached by ID 128 if str(block.id) in current_block_ids: 129 results.append(f"{handle}: Already attached (by ID)") 130 continue 131 else: 132 block = client.blocks.create( 133 label=block_label, 134 value=f"# User: {handle}\n\nNo information about this user yet.", 135 limit=5000 136 ) 137 138 # Attach block atomically 139 try: 140 client.agents.blocks.attach( 141 agent_id=str(agent_state.id), 142 block_id=str(block.id) 143 ) 144 results.append(f"{handle}: Block attached") 145 except Exception as attach_error: 146 # Check if it's a duplicate constraint error 147 error_str = str(attach_error) 148 if "duplicate key value violates unique constraint" in error_str and "unique_label_per_agent" in error_str: 149 # Block is already attached, possibly with this exact label 150 results.append(f"{handle}: Already attached (verified)") 151 else: 152 # Re-raise other errors 153 raise attach_error 154 155 except Exception as e: 156 results.append(f"{handle}: Error - {str(e)}") 157 158 return f"Attachment results:\n" + "\n".join(results) 159 160 except Exception as e: 161 raise Exception(f"Error attaching user blocks: {str(e)}") 162 163 164def detach_user_blocks(handles: list, agent_state: "AgentState") -> str: 165 """ 166 Detach user-specific memory blocks from the agent. Blocks are preserved for later use. 167 168 Args: 169 handles: List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social']) 170 agent_state: The agent state object containing agent information 171 172 Returns: 173 String with detachment results for each handle 174 """ 175 176 try: 177 # Try to get client the local way first, fall back to inline for cloud execution 178 try: 179 client = get_letta_client() 180 except (ImportError, NameError): 181 # Create Letta client inline for cloud execution 182 import os 183 from letta_client import Letta 184 client = Letta(token=os.environ["LETTA_API_KEY"]) 185 results = [] 186 187 # Build mapping of block labels to IDs using the API 188 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 189 block_label_to_id = {} 190 191 for block in current_blocks: 192 block_label_to_id[block.label] = str(block.id) 193 194 # Process each handle and detach atomically 195 for handle in handles: 196 # Sanitize handle for block label - completely self-contained 197 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 198 block_label = f"user_{clean_handle}" 199 200 if block_label in block_label_to_id: 201 try: 202 # Detach block atomically 203 client.agents.blocks.detach( 204 agent_id=str(agent_state.id), 205 block_id=block_label_to_id[block_label] 206 ) 207 results.append(f"{handle}: Detached") 208 except Exception as e: 209 results.append(f"{handle}: Error during detachment - {str(e)}") 210 else: 211 results.append(f"{handle}: Not attached") 212 213 return f"Detachment results:\n" + "\n".join(results) 214 215 except Exception as e: 216 raise Exception(f"Error detaching user blocks: {str(e)}") 217 218 219def user_note_append(handle: str, note: str, agent_state: "AgentState") -> str: 220 """ 221 Append a note to a user's memory block. Creates the block if it doesn't exist. 222 223 Args: 224 handle: User Bluesky handle (e.g., 'cameron.pfiffer.org') 225 note: Note to append to the user's memory block 226 agent_state: The agent state object containing agent information 227 228 Returns: 229 String confirming the note was appended 230 """ 231 232 try: 233 # Create Letta client inline - cloud tools must be self-contained 234 import os 235 from letta_client import Letta 236 client = Letta(token=os.environ["LETTA_API_KEY"]) 237 238 # Sanitize handle for block label 239 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 240 block_label = f"user_{clean_handle}" 241 242 # Check if block exists 243 blocks = client.blocks.list(label=block_label) 244 245 if blocks and len(blocks) > 0: 246 # Block exists, append to it 247 block = blocks[0] 248 current_value = block.value 249 new_value = current_value + note 250 251 # Update the block 252 client.blocks.modify( 253 block_id=str(block.id), 254 value=new_value 255 ) 256 return f"✓ Appended note to {handle}'s memory block" 257 258 else: 259 # Block doesn't exist, create it with the note 260 initial_value = f"# User: {handle}\n\n{note}" 261 block = client.blocks.create( 262 label=block_label, 263 value=initial_value, 264 limit=5000 265 ) 266 267 # Check if block needs to be attached to agent 268 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 269 current_block_labels = {block.label for block in current_blocks} 270 271 if block_label not in current_block_labels: 272 # Attach the new block to the agent 273 client.agents.blocks.attach( 274 agent_id=str(agent_state.id), 275 block_id=str(block.id) 276 ) 277 return f"✓ Created and attached {handle}'s memory block with note" 278 else: 279 return f"✓ Created {handle}'s memory block with note" 280 281 except Exception as e: 282 raise Exception(f"Error appending note to user block: {str(e)}") 283 284 285def user_note_replace(handle: str, old_text: str, new_text: str, agent_state: "AgentState") -> str: 286 """ 287 Replace text in a user's memory block. 288 289 Args: 290 handle: User Bluesky handle (e.g., 'cameron.pfiffer.org') 291 old_text: Text to find and replace 292 new_text: Text to replace the old_text with 293 agent_state: The agent state object containing agent information 294 295 Returns: 296 String confirming the text was replaced 297 """ 298 299 try: 300 # Create Letta client inline - cloud tools must be self-contained 301 import os 302 from letta_client import Letta 303 client = Letta(token=os.environ["LETTA_API_KEY"]) 304 305 # Sanitize handle for block label 306 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 307 block_label = f"user_{clean_handle}" 308 309 # Check if block exists 310 blocks = client.blocks.list(label=block_label) 311 312 if not blocks or len(blocks) == 0: 313 raise Exception(f"No memory block found for user: {handle}") 314 315 block = blocks[0] 316 current_value = block.value 317 318 # Check if old_text exists in the block 319 if old_text not in current_value: 320 raise Exception(f"Text '{old_text}' not found in {handle}'s memory block") 321 322 # Replace the text 323 new_value = current_value.replace(old_text, new_text) 324 325 # Update the block 326 client.blocks.modify( 327 block_id=str(block.id), 328 value=new_value 329 ) 330 return f"✓ Replaced text in {handle}'s memory block" 331 332 except Exception as e: 333 raise Exception(f"Error replacing text in user block: {str(e)}") 334 335 336def user_note_set(handle: str, content: str, agent_state: "AgentState") -> str: 337 """ 338 Set the complete content of a user's memory block. 339 340 Args: 341 handle: User Bluesky handle (e.g., 'cameron.pfiffer.org') 342 content: Complete content to set for the memory block 343 agent_state: The agent state object containing agent information 344 345 Returns: 346 String confirming the content was set 347 """ 348 349 try: 350 # Create Letta client inline - cloud tools must be self-contained 351 import os 352 from letta_client import Letta 353 client = Letta(token=os.environ["LETTA_API_KEY"]) 354 355 # Sanitize handle for block label 356 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 357 block_label = f"user_{clean_handle}" 358 359 # Check if block exists 360 blocks = client.blocks.list(label=block_label) 361 362 if blocks and len(blocks) > 0: 363 # Block exists, update it 364 block = blocks[0] 365 client.blocks.modify( 366 block_id=str(block.id), 367 value=content 368 ) 369 return f"✓ Set content for {handle}'s memory block" 370 371 else: 372 # Block doesn't exist, create it 373 block = client.blocks.create( 374 label=block_label, 375 value=content, 376 limit=5000 377 ) 378 379 # Check if block needs to be attached to agent 380 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 381 current_block_labels = {block.label for block in current_blocks} 382 383 if block_label not in current_block_labels: 384 # Attach the new block to the agent 385 client.agents.blocks.attach( 386 agent_id=str(agent_state.id), 387 block_id=str(block.id) 388 ) 389 return f"✓ Created and attached {handle}'s memory block" 390 else: 391 return f"✓ Created {handle}'s memory block" 392 393 except Exception as e: 394 raise Exception(f"Error setting user block content: {str(e)}") 395 396 397def user_note_view(handle: str, agent_state: "AgentState") -> str: 398 """ 399 View the content of a user's memory block. 400 401 Args: 402 handle: User Bluesky handle (e.g., 'cameron.pfiffer.org') 403 agent_state: The agent state object containing agent information 404 405 Returns: 406 String containing the user's memory block content 407 """ 408 409 try: 410 # Create Letta client inline - cloud tools must be self-contained 411 import os 412 from letta_client import Letta 413 client = Letta(token=os.environ["LETTA_API_KEY"]) 414 415 # Sanitize handle for block label 416 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 417 block_label = f"user_{clean_handle}" 418 419 # Check if block exists 420 blocks = client.blocks.list(label=block_label) 421 422 if not blocks or len(blocks) == 0: 423 return f"No memory block found for user: {handle}" 424 425 block = blocks[0] 426 427 return f"Memory block for {handle}:\n\n{block.value}" 428 429 except Exception as e: 430 raise Exception(f"Error viewing user block: {str(e)}") 431 432 433# X (Twitter) User Block Management Functions 434 435def attach_x_user_blocks(user_ids: list, agent_state: "AgentState") -> str: 436 """ 437 Attach X user-specific memory blocks to the agent. Creates blocks if they don't exist. 438 439 Args: 440 user_ids: List of X user IDs (e.g., ['1232326955652931584', '1950680610282094592']) 441 agent_state: The agent state object containing agent information 442 443 Returns: 444 String with attachment results for each user ID 445 """ 446 447 user_ids = list(set(user_ids)) 448 449 try: 450 # Try to get client the local way first, fall back to inline for cloud execution 451 try: 452 client = get_letta_client() 453 except (ImportError, NameError): 454 # Create Letta client inline for cloud execution 455 import os 456 from letta_client import Letta 457 client = Letta(token=os.environ["LETTA_API_KEY"]) 458 results = [] 459 460 # Get current blocks using the API 461 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 462 current_block_labels = set() 463 464 for block in current_blocks: 465 current_block_labels.add(block.label) 466 467 for user_id in user_ids: 468 # Create block label with x_user_ prefix 469 block_label = f"x_user_{user_id}" 470 471 # Skip if already attached 472 if block_label in current_block_labels: 473 results.append(f"{user_id}: Already attached") 474 continue 475 476 # Check if block exists or create new one 477 try: 478 blocks = client.blocks.list(label=block_label) 479 if blocks and len(blocks) > 0: 480 block = blocks[0] 481 else: 482 block = client.blocks.create( 483 label=block_label, 484 value=f"# X User: {user_id}\n\nNo information about this user yet.", 485 limit=5000 486 ) 487 488 # Attach block atomically 489 client.agents.blocks.attach( 490 agent_id=str(agent_state.id), 491 block_id=str(block.id) 492 ) 493 494 results.append(f"{user_id}: Block attached") 495 496 except Exception as e: 497 results.append(f"{user_id}: Error - {str(e)}") 498 499 return f"X user attachment results:\n" + "\n".join(results) 500 501 except Exception as e: 502 raise Exception(f"Error attaching X user blocks: {str(e)}") 503 504 505def detach_x_user_blocks(user_ids: list, agent_state: "AgentState") -> str: 506 """ 507 Detach X user-specific memory blocks from the agent. Blocks are preserved for later use. 508 509 Args: 510 user_ids: List of X user IDs (e.g., ['1232326955652931584', '1950680610282094592']) 511 agent_state: The agent state object containing agent information 512 513 Returns: 514 String with detachment results for each user ID 515 """ 516 517 try: 518 # Try to get client the local way first, fall back to inline for cloud execution 519 try: 520 client = get_letta_client() 521 except (ImportError, NameError): 522 # Create Letta client inline for cloud execution 523 import os 524 from letta_client import Letta 525 client = Letta(token=os.environ["LETTA_API_KEY"]) 526 results = [] 527 528 # Build mapping of block labels to IDs using the API 529 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 530 block_label_to_id = {} 531 532 for block in current_blocks: 533 block_label_to_id[block.label] = str(block.id) 534 535 # Process each user ID and detach atomically 536 for user_id in user_ids: 537 block_label = f"x_user_{user_id}" 538 539 if block_label in block_label_to_id: 540 try: 541 # Detach block atomically 542 client.agents.blocks.detach( 543 agent_id=str(agent_state.id), 544 block_id=block_label_to_id[block_label] 545 ) 546 results.append(f"{user_id}: Detached") 547 except Exception as e: 548 results.append(f"{user_id}: Error during detachment - {str(e)}") 549 else: 550 results.append(f"{user_id}: Not attached") 551 552 return f"X user detachment results:\n" + "\n".join(results) 553 554 except Exception as e: 555 raise Exception(f"Error detaching X user blocks: {str(e)}") 556 557 558def x_user_note_append(user_id: str, note: str, agent_state: "AgentState") -> str: 559 """ 560 Append a note to an X user's memory block. Creates the block if it doesn't exist. 561 562 Args: 563 user_id: X user ID (e.g., '1232326955652931584') 564 note: Note to append to the user's memory block 565 agent_state: The agent state object containing agent information 566 567 Returns: 568 String confirming the note was appended 569 """ 570 try: 571 # Create Letta client inline - cloud tools must be self-contained 572 import os 573 from letta_client import Letta 574 client = Letta(token=os.environ["LETTA_API_KEY"]) 575 576 block_label = f"x_user_{user_id}" 577 578 # Check if block exists 579 blocks = client.blocks.list(label=block_label) 580 581 if blocks and len(blocks) > 0: 582 # Block exists, append to it 583 block = blocks[0] 584 current_value = block.value 585 new_value = current_value + note 586 587 # Update the block 588 client.blocks.modify( 589 block_id=str(block.id), 590 value=new_value 591 ) 592 return f"✓ Appended note to X user {user_id}'s memory block" 593 594 else: 595 # Block doesn't exist, create it with the note 596 initial_value = f"# X User: {user_id}\n\n{note}" 597 block = client.blocks.create( 598 label=block_label, 599 value=initial_value, 600 limit=5000 601 ) 602 603 # Check if block needs to be attached to agent 604 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 605 current_block_labels = {block.label for block in current_blocks} 606 607 if block_label not in current_block_labels: 608 # Attach the new block to the agent 609 client.agents.blocks.attach( 610 agent_id=str(agent_state.id), 611 block_id=str(block.id) 612 ) 613 return f"✓ Created and attached X user {user_id}'s memory block with note" 614 else: 615 return f"✓ Created X user {user_id}'s memory block with note" 616 617 except Exception as e: 618 raise Exception(f"Error appending note to X user block: {str(e)}") 619 620 621def x_user_note_replace(user_id: str, old_text: str, new_text: str, agent_state: "AgentState") -> str: 622 """ 623 Replace text in an X user's memory block. 624 625 Args: 626 user_id: X user ID (e.g., '1232326955652931584') 627 old_text: Text to find and replace 628 new_text: Text to replace the old_text with 629 agent_state: The agent state object containing agent information 630 631 Returns: 632 String confirming the text was replaced 633 """ 634 try: 635 # Create Letta client inline - cloud tools must be self-contained 636 import os 637 from letta_client import Letta 638 client = Letta(token=os.environ["LETTA_API_KEY"]) 639 640 block_label = f"x_user_{user_id}" 641 642 # Check if block exists 643 blocks = client.blocks.list(label=block_label) 644 645 if not blocks or len(blocks) == 0: 646 raise Exception(f"No memory block found for X user: {user_id}") 647 648 block = blocks[0] 649 current_value = block.value 650 651 # Check if old_text exists in the block 652 if old_text not in current_value: 653 raise Exception(f"Text '{old_text}' not found in X user {user_id}'s memory block") 654 655 # Replace the text 656 new_value = current_value.replace(old_text, new_text) 657 658 # Update the block 659 client.blocks.modify( 660 block_id=str(block.id), 661 value=new_value 662 ) 663 return f"✓ Replaced text in X user {user_id}'s memory block" 664 665 except Exception as e: 666 raise Exception(f"Error replacing text in X user block: {str(e)}") 667 668 669def x_user_note_set(user_id: str, content: str, agent_state: "AgentState") -> str: 670 """ 671 Set the complete content of an X user's memory block. 672 673 Args: 674 user_id: X user ID (e.g., '1232326955652931584') 675 content: Complete content to set for the memory block 676 agent_state: The agent state object containing agent information 677 678 Returns: 679 String confirming the content was set 680 """ 681 try: 682 # Create Letta client inline - cloud tools must be self-contained 683 import os 684 from letta_client import Letta 685 client = Letta(token=os.environ["LETTA_API_KEY"]) 686 687 block_label = f"x_user_{user_id}" 688 689 # Check if block exists 690 blocks = client.blocks.list(label=block_label) 691 692 if blocks and len(blocks) > 0: 693 # Block exists, update it 694 block = blocks[0] 695 client.blocks.modify( 696 block_id=str(block.id), 697 value=content 698 ) 699 return f"✓ Set content for X user {user_id}'s memory block" 700 701 else: 702 # Block doesn't exist, create it 703 block = client.blocks.create( 704 label=block_label, 705 value=content, 706 limit=5000 707 ) 708 709 # Check if block needs to be attached to agent 710 current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 711 current_block_labels = {block.label for block in current_blocks} 712 713 if block_label not in current_block_labels: 714 # Attach the new block to the agent 715 client.agents.blocks.attach( 716 agent_id=str(agent_state.id), 717 block_id=str(block.id) 718 ) 719 return f"✓ Created and attached X user {user_id}'s memory block" 720 else: 721 return f"✓ Created X user {user_id}'s memory block" 722 723 except Exception as e: 724 raise Exception(f"Error setting X user block content: {str(e)}") 725 726 727def x_user_note_view(user_id: str, agent_state: "AgentState") -> str: 728 """ 729 View the content of an X user's memory block. 730 731 Args: 732 user_id: X user ID (e.g., '1232326955652931584') 733 agent_state: The agent state object containing agent information 734 735 Returns: 736 String containing the user's memory block content 737 """ 738 try: 739 # Create Letta client inline - cloud tools must be self-contained 740 import os 741 from letta_client import Letta 742 client = Letta(token=os.environ["LETTA_API_KEY"]) 743 744 block_label = f"x_user_{user_id}" 745 746 # Check if block exists 747 blocks = client.blocks.list(label=block_label) 748 749 if not blocks or len(blocks) == 0: 750 return f"No memory block found for X user: {user_id}" 751 752 block = blocks[0] 753 754 return f"Memory block for X user {user_id}:\n\n{block.value}" 755 756 except Exception as e: 757 raise Exception(f"Error viewing X user block: {str(e)}") 758 759