a digital person for bluesky

Implement X user block management system

- Add X-specific user block functions to tools/blocks.py:
- attach_x_user_blocks() for attaching user blocks by ID
- detach_x_user_blocks() for detaching user blocks
- x_user_note_append/replace/set/view() for block content management
- Block format: x_user_<author_id> as requested

- Add ensure_x_user_blocks_attached() to x.py:
- Automatically creates and attaches user blocks for thread participants
- Sets initial content with handle and name information
- Integrates with existing thread processing

- Update thread_to_yaml_string() to include author_id:
- Enables agent to reference user IDs for block management
- Maintains compatibility with existing YAML structure

- Fix config loading issue:
- Environment variable LETTA_API_KEY was overriding config file
- Now properly uses void-x agent (agent-4f7cc732-36bc-4e55-a5fa-f5d12eec6c5c)
- Successfully tested block creation and attachment

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+432 -1
tools
+360
tools/blocks.py
··· 45 45 handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") 46 46 47 47 48 + # X (Twitter) User Block Management 49 + class AttachXUserBlocksArgs(BaseModel): 50 + user_ids: List[str] = Field(..., description="List of X user IDs (e.g., ['1232326955652931584', '1950680610282094592'])") 51 + 52 + 53 + class DetachXUserBlocksArgs(BaseModel): 54 + user_ids: List[str] = Field(..., description="List of X user IDs (e.g., ['1232326955652931584', '1950680610282094592'])") 55 + 56 + 57 + class XUserNoteAppendArgs(BaseModel): 58 + user_id: str = Field(..., description="X user ID (e.g., '1232326955652931584')") 59 + note: str = Field(..., description="Note to append to the user's memory block (e.g., '\\\\n- Cameron is a person')") 60 + 61 + 62 + class XUserNoteReplaceArgs(BaseModel): 63 + user_id: str = Field(..., description="X user ID (e.g., '1232326955652931584')") 64 + old_text: str = Field(..., description="Text to find and replace in the user's memory block") 65 + new_text: str = Field(..., description="Text to replace the old_text with") 66 + 67 + 68 + class XUserNoteSetArgs(BaseModel): 69 + user_id: str = Field(..., description="X user ID (e.g., '1232326955652931584')") 70 + content: str = Field(..., description="Complete content to set for the user's memory block") 71 + 72 + 73 + class XUserNoteViewArgs(BaseModel): 74 + user_id: str = Field(..., description="X user ID (e.g., '1232326955652931584')") 75 + 76 + 48 77 49 78 def attach_user_blocks(handles: list, agent_state: "AgentState") -> str: 50 79 """ ··· 386 415 raise Exception(f"Error viewing user block: {str(e)}") 387 416 388 417 418 + # X (Twitter) User Block Management Functions 419 + 420 + def attach_x_user_blocks(user_ids: list, agent_state: "AgentState") -> str: 421 + """ 422 + Attach X user-specific memory blocks to the agent. Creates blocks if they don't exist. 423 + 424 + Args: 425 + user_ids: List of X user IDs (e.g., ['1232326955652931584', '1950680610282094592']) 426 + agent_state: The agent state object containing agent information 427 + 428 + Returns: 429 + String with attachment results for each user ID 430 + """ 431 + logger = logging.getLogger(__name__) 432 + 433 + user_ids = list(set(user_ids)) 434 + 435 + try: 436 + client = get_letta_client() 437 + results = [] 438 + 439 + # Get current blocks using the API 440 + current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 441 + current_block_labels = set() 442 + 443 + for block in current_blocks: 444 + current_block_labels.add(block.label) 445 + 446 + for user_id in user_ids: 447 + # Create block label with x_user_ prefix 448 + block_label = f"x_user_{user_id}" 449 + 450 + # Skip if already attached 451 + if block_label in current_block_labels: 452 + results.append(f"✓ {user_id}: Already attached") 453 + continue 454 + 455 + # Check if block exists or create new one 456 + try: 457 + blocks = client.blocks.list(label=block_label) 458 + if blocks and len(blocks) > 0: 459 + block = blocks[0] 460 + logger.debug(f"Found existing block: {block_label}") 461 + else: 462 + block = client.blocks.create( 463 + label=block_label, 464 + value=f"# X User: {user_id}\n\nNo information about this user yet.", 465 + limit=5000 466 + ) 467 + logger.info(f"Created new block: {block_label}") 468 + 469 + # Attach block atomically 470 + client.agents.blocks.attach( 471 + agent_id=str(agent_state.id), 472 + block_id=str(block.id) 473 + ) 474 + 475 + results.append(f"✓ {user_id}: Block attached") 476 + logger.debug(f"Successfully attached block {block_label} to agent") 477 + 478 + except Exception as e: 479 + results.append(f"✗ {user_id}: Error - {str(e)}") 480 + logger.error(f"Error processing block for {user_id}: {e}") 481 + 482 + return f"X user attachment results:\n" + "\n".join(results) 483 + 484 + except Exception as e: 485 + logger.error(f"Error attaching X user blocks: {e}") 486 + raise Exception(f"Error attaching X user blocks: {str(e)}") 487 + 488 + 489 + def detach_x_user_blocks(user_ids: list, agent_state: "AgentState") -> str: 490 + """ 491 + Detach X user-specific memory blocks from the agent. Blocks are preserved for later use. 492 + 493 + Args: 494 + user_ids: List of X user IDs (e.g., ['1232326955652931584', '1950680610282094592']) 495 + agent_state: The agent state object containing agent information 496 + 497 + Returns: 498 + String with detachment results for each user ID 499 + """ 500 + logger = logging.getLogger(__name__) 501 + 502 + try: 503 + client = get_letta_client() 504 + results = [] 505 + 506 + # Build mapping of block labels to IDs using the API 507 + current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 508 + block_label_to_id = {} 509 + 510 + for block in current_blocks: 511 + block_label_to_id[block.label] = str(block.id) 512 + 513 + # Process each user ID and detach atomically 514 + for user_id in user_ids: 515 + block_label = f"x_user_{user_id}" 516 + 517 + if block_label in block_label_to_id: 518 + try: 519 + # Detach block atomically 520 + client.agents.blocks.detach( 521 + agent_id=str(agent_state.id), 522 + block_id=block_label_to_id[block_label] 523 + ) 524 + results.append(f"✓ {user_id}: Detached") 525 + logger.debug(f"Successfully detached block {block_label} from agent") 526 + except Exception as e: 527 + results.append(f"✗ {user_id}: Error during detachment - {str(e)}") 528 + logger.error(f"Error detaching block {block_label}: {e}") 529 + else: 530 + results.append(f"✗ {user_id}: Not attached") 531 + 532 + return f"X user detachment results:\n" + "\n".join(results) 533 + 534 + except Exception as e: 535 + logger.error(f"Error detaching X user blocks: {e}") 536 + raise Exception(f"Error detaching X user blocks: {str(e)}") 537 + 538 + 539 + def x_user_note_append(user_id: str, note: str, agent_state: "AgentState") -> str: 540 + """ 541 + Append a note to an X user's memory block. Creates the block if it doesn't exist. 542 + 543 + Args: 544 + user_id: X user ID (e.g., '1232326955652931584') 545 + note: Note to append to the user's memory block 546 + agent_state: The agent state object containing agent information 547 + 548 + Returns: 549 + String confirming the note was appended 550 + """ 551 + logger = logging.getLogger(__name__) 552 + 553 + try: 554 + client = get_letta_client() 555 + 556 + block_label = f"x_user_{user_id}" 557 + 558 + # Check if block exists 559 + blocks = client.blocks.list(label=block_label) 560 + 561 + if blocks and len(blocks) > 0: 562 + # Block exists, append to it 563 + block = blocks[0] 564 + current_value = block.value 565 + new_value = current_value + note 566 + 567 + # Update the block 568 + client.blocks.modify( 569 + block_id=str(block.id), 570 + value=new_value 571 + ) 572 + logger.info(f"Appended note to existing block: {block_label}") 573 + return f"✓ Appended note to X user {user_id}'s memory block" 574 + 575 + else: 576 + # Block doesn't exist, create it with the note 577 + initial_value = f"# X User: {user_id}\n\n{note}" 578 + block = client.blocks.create( 579 + label=block_label, 580 + value=initial_value, 581 + limit=5000 582 + ) 583 + logger.info(f"Created new block with note: {block_label}") 584 + 585 + # Check if block needs to be attached to agent 586 + current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 587 + current_block_labels = {block.label for block in current_blocks} 588 + 589 + if block_label not in current_block_labels: 590 + # Attach the new block to the agent 591 + client.agents.blocks.attach( 592 + agent_id=str(agent_state.id), 593 + block_id=str(block.id) 594 + ) 595 + logger.info(f"Attached new block to agent: {block_label}") 596 + return f"✓ Created and attached X user {user_id}'s memory block with note" 597 + else: 598 + return f"✓ Created X user {user_id}'s memory block with note" 599 + 600 + except Exception as e: 601 + logger.error(f"Error appending note to X user block: {e}") 602 + raise Exception(f"Error appending note to X user block: {str(e)}") 603 + 604 + 605 + def x_user_note_replace(user_id: str, old_text: str, new_text: str, agent_state: "AgentState") -> str: 606 + """ 607 + Replace text in an X user's memory block. 608 + 609 + Args: 610 + user_id: X user ID (e.g., '1232326955652931584') 611 + old_text: Text to find and replace 612 + new_text: Text to replace the old_text with 613 + agent_state: The agent state object containing agent information 614 + 615 + Returns: 616 + String confirming the text was replaced 617 + """ 618 + logger = logging.getLogger(__name__) 619 + 620 + try: 621 + client = get_letta_client() 622 + 623 + block_label = f"x_user_{user_id}" 624 + 625 + # Check if block exists 626 + blocks = client.blocks.list(label=block_label) 627 + 628 + if not blocks or len(blocks) == 0: 629 + raise Exception(f"No memory block found for X user: {user_id}") 630 + 631 + block = blocks[0] 632 + current_value = block.value 633 + 634 + # Check if old_text exists in the block 635 + if old_text not in current_value: 636 + raise Exception(f"Text '{old_text}' not found in X user {user_id}'s memory block") 637 + 638 + # Replace the text 639 + new_value = current_value.replace(old_text, new_text) 640 + 641 + # Update the block 642 + client.blocks.modify( 643 + block_id=str(block.id), 644 + value=new_value 645 + ) 646 + logger.info(f"Replaced text in block: {block_label}") 647 + return f"✓ Replaced text in X user {user_id}'s memory block" 648 + 649 + except Exception as e: 650 + logger.error(f"Error replacing text in X user block: {e}") 651 + raise Exception(f"Error replacing text in X user block: {str(e)}") 652 + 653 + 654 + def x_user_note_set(user_id: str, content: str, agent_state: "AgentState") -> str: 655 + """ 656 + Set the complete content of an X user's memory block. 657 + 658 + Args: 659 + user_id: X user ID (e.g., '1232326955652931584') 660 + content: Complete content to set for the memory block 661 + agent_state: The agent state object containing agent information 662 + 663 + Returns: 664 + String confirming the content was set 665 + """ 666 + logger = logging.getLogger(__name__) 667 + 668 + try: 669 + client = get_letta_client() 670 + 671 + block_label = f"x_user_{user_id}" 672 + 673 + # Check if block exists 674 + blocks = client.blocks.list(label=block_label) 675 + 676 + if blocks and len(blocks) > 0: 677 + # Block exists, update it 678 + block = blocks[0] 679 + client.blocks.modify( 680 + block_id=str(block.id), 681 + value=content 682 + ) 683 + logger.info(f"Set content for existing block: {block_label}") 684 + return f"✓ Set content for X user {user_id}'s memory block" 685 + 686 + else: 687 + # Block doesn't exist, create it 688 + block = client.blocks.create( 689 + label=block_label, 690 + value=content, 691 + limit=5000 692 + ) 693 + logger.info(f"Created new block with content: {block_label}") 694 + 695 + # Check if block needs to be attached to agent 696 + current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 697 + current_block_labels = {block.label for block in current_blocks} 698 + 699 + if block_label not in current_block_labels: 700 + # Attach the new block to the agent 701 + client.agents.blocks.attach( 702 + agent_id=str(agent_state.id), 703 + block_id=str(block.id) 704 + ) 705 + logger.info(f"Attached new block to agent: {block_label}") 706 + return f"✓ Created and attached X user {user_id}'s memory block" 707 + else: 708 + return f"✓ Created X user {user_id}'s memory block" 709 + 710 + except Exception as e: 711 + logger.error(f"Error setting X user block content: {e}") 712 + raise Exception(f"Error setting X user block content: {str(e)}") 713 + 714 + 715 + def x_user_note_view(user_id: str, agent_state: "AgentState") -> str: 716 + """ 717 + View the content of an X user's memory block. 718 + 719 + Args: 720 + user_id: X user ID (e.g., '1232326955652931584') 721 + agent_state: The agent state object containing agent information 722 + 723 + Returns: 724 + String containing the user's memory block content 725 + """ 726 + logger = logging.getLogger(__name__) 727 + 728 + try: 729 + client = get_letta_client() 730 + 731 + block_label = f"x_user_{user_id}" 732 + 733 + # Check if block exists 734 + blocks = client.blocks.list(label=block_label) 735 + 736 + if not blocks or len(blocks) == 0: 737 + return f"No memory block found for X user: {user_id}" 738 + 739 + block = blocks[0] 740 + logger.info(f"Retrieved content for block: {block_label}") 741 + 742 + return f"Memory block for X user {user_id}:\n\n{block.value}" 743 + 744 + except Exception as e: 745 + logger.error(f"Error viewing X user block: {e}") 746 + raise Exception(f"Error viewing X user block: {str(e)}") 747 + 748 +
+72 -1
x.py
··· 395 395 tweet_obj = { 396 396 'text': tweet.get('text'), 397 397 'created_at': tweet.get('created_at'), 398 - 'author': author_info 398 + 'author': author_info, 399 + 'author_id': author_id # Include user ID for block management 399 400 } 400 401 401 402 simplified_thread["conversation"].append(tweet_obj) 402 403 403 404 return yaml.dump(simplified_thread, default_flow_style=False, sort_keys=False) 405 + 406 + 407 + def ensure_x_user_blocks_attached(thread_data: Dict, agent_id: str) -> None: 408 + """ 409 + Ensure all users in the thread have their X user blocks attached. 410 + Creates blocks with initial content including their handle if they don't exist. 411 + 412 + Args: 413 + thread_data: Dict with 'tweets' and 'users' keys from get_thread_context() 414 + agent_id: The Letta agent ID to attach blocks to 415 + """ 416 + if not thread_data or "users" not in thread_data: 417 + return 418 + 419 + try: 420 + from tools.blocks import attach_x_user_blocks, x_user_note_set 421 + from config_loader import get_letta_config 422 + from letta_client import Letta 423 + 424 + # Get Letta client 425 + config = get_letta_config() 426 + client = Letta(token=config['api_key'], timeout=config['timeout']) 427 + 428 + # Get agent info to create a mock agent_state for the functions 429 + class MockAgentState: 430 + def __init__(self, agent_id): 431 + self.id = agent_id 432 + 433 + agent_state = MockAgentState(agent_id) 434 + 435 + users_data = thread_data["users"] 436 + user_ids = list(users_data.keys()) 437 + 438 + if not user_ids: 439 + return 440 + 441 + logger.info(f"Ensuring X user blocks for {len(user_ids)} users: {user_ids}") 442 + 443 + # Get current blocks to check which users already have blocks with content 444 + current_blocks = client.agents.blocks.list(agent_id=agent_id) 445 + existing_user_blocks = {} 446 + 447 + for block in current_blocks: 448 + if block.label.startswith("x_user_"): 449 + user_id = block.label.replace("x_user_", "") 450 + existing_user_blocks[user_id] = block 451 + 452 + # Attach all user blocks (this will create missing ones with basic content) 453 + attach_result = attach_x_user_blocks(user_ids, agent_state) 454 + logger.info(f"X user block attachment result: {attach_result}") 455 + 456 + # For newly created blocks, update with user handle information 457 + for user_id in user_ids: 458 + if user_id not in existing_user_blocks: 459 + user_info = users_data[user_id] 460 + username = user_info.get('username', 'unknown') 461 + name = user_info.get('name', 'Unknown') 462 + 463 + # Set initial content with handle information 464 + initial_content = f"# X User: {user_id}\n\n**Handle:** @{username}\n**Name:** {name}\n\nNo additional information about this user yet." 465 + 466 + try: 467 + x_user_note_set(user_id, initial_content, agent_state) 468 + logger.info(f"Set initial content for X user {user_id} (@{username})") 469 + except Exception as e: 470 + logger.error(f"Failed to set initial content for X user {user_id}: {e}") 471 + 472 + except Exception as e: 473 + logger.error(f"Error ensuring X user blocks: {e}") 474 + 404 475 405 476 # X Caching and Queue System Functions 406 477