a digital person for bluesky

feat: move to config.yaml and add guides

authored by Turtlepaw and committed by Tangled 7f2a63a2 b0d18f19

+4
.env.example
···
··· 1 + LETTA_API_KEY= 2 + BSKY_USERNAME=handle.example.com 3 + BSKY_PASSWORD= 4 + PDS_URI=https://bsky.social # Optional, defaults to bsky.social
+1
.gitignore
··· 1 .env 2 old.py 3 session_*.txt 4 __pycache__/
··· 1 .env 2 + config.yaml 3 old.py 4 session_*.txt 5 __pycache__/
+159
CONFIG.md
···
··· 1 + # Configuration Guide 2 + 3 + ### Option 1: Migrate from existing `.env` file (if you have one) 4 + ```bash 5 + python migrate_config.py 6 + ``` 7 + 8 + ### Option 2: Start fresh with example 9 + 1. **Copy the example configuration:** 10 + ```bash 11 + cp config.yaml.example config.yaml 12 + ``` 13 + 14 + 2. **Edit `config.yaml` with your credentials:** 15 + ```yaml 16 + # Required: Letta API configuration 17 + letta: 18 + api_key: "your-letta-api-key-here" 19 + project_id: "project-id-here" 20 + 21 + # Required: Bluesky credentials 22 + bluesky: 23 + username: "your-handle.bsky.social" 24 + password: "your-app-password" 25 + ``` 26 + 27 + 3. **Run the configuration test:** 28 + ```bash 29 + python test_config.py 30 + ``` 31 + 32 + ## Configuration Structure 33 + 34 + ### Letta Configuration 35 + ```yaml 36 + letta: 37 + api_key: "your-letta-api-key-here" # Required 38 + timeout: 600 # API timeout in seconds 39 + project_id: "your-project-id" # Required: Your Letta project ID 40 + ``` 41 + 42 + ### Bluesky Configuration 43 + ```yaml 44 + bluesky: 45 + username: "handle.bsky.social" # Required: Your Bluesky handle 46 + password: "your-app-password" # Required: Your Bluesky app password 47 + pds_uri: "https://bsky.social" # Optional: PDS URI (defaults to bsky.social) 48 + ``` 49 + 50 + ### Bot Behavior 51 + ```yaml 52 + bot: 53 + fetch_notifications_delay: 30 # Seconds between notification checks 54 + max_processed_notifications: 10000 # Max notifications to track 55 + max_notification_pages: 20 # Max pages to fetch per cycle 56 + 57 + agent: 58 + name: "void" # Agent name 59 + model: "openai/gpt-4o-mini" # LLM model to use 60 + embedding: "openai/text-embedding-3-small" # Embedding model 61 + description: "A social media agent trapped in the void." 62 + max_steps: 100 # Max steps per agent interaction 63 + 64 + # Memory blocks configuration 65 + blocks: 66 + zeitgeist: 67 + label: "zeitgeist" 68 + value: "I don't currently know anything about what is happening right now." 69 + description: "A block to store your understanding of the current social environment." 70 + # ... more blocks 71 + ``` 72 + 73 + ### Queue Configuration 74 + ```yaml 75 + queue: 76 + priority_users: # Users whose messages get priority 77 + - "cameron.pfiffer.org" 78 + base_dir: "queue" # Queue directory 79 + error_dir: "queue/errors" # Failed notifications 80 + no_reply_dir: "queue/no_reply" # No-reply notifications 81 + processed_file: "queue/processed_notifications.json" 82 + ``` 83 + 84 + ### Threading Configuration 85 + ```yaml 86 + threading: 87 + parent_height: 40 # Thread context depth 88 + depth: 10 # Thread context width 89 + max_post_characters: 300 # Max characters per post 90 + ``` 91 + 92 + ### Logging Configuration 93 + ```yaml 94 + logging: 95 + level: "INFO" # Root logging level 96 + loggers: 97 + void_bot: "INFO" # Main bot logger 98 + void_bot_prompts: "WARNING" # Prompt logger (set to DEBUG to see prompts) 99 + httpx: "CRITICAL" # HTTP client logger 100 + ``` 101 + 102 + ## Environment Variable Fallback 103 + 104 + The configuration system still supports environment variables as a fallback: 105 + 106 + - `LETTA_API_KEY` - Letta API key 107 + - `BSKY_USERNAME` - Bluesky username 108 + - `BSKY_PASSWORD` - Bluesky password 109 + - `PDS_URI` - Bluesky PDS URI 110 + 111 + If both config file and environment variables are present, environment variables take precedence. 112 + 113 + ## Migration from Environment Variables 114 + 115 + If you're currently using environment variables (`.env` file), you can easily migrate to YAML using the automated migration script: 116 + 117 + ### Automated Migration (Recommended) 118 + 119 + ```bash 120 + python migrate_config.py 121 + ``` 122 + 123 + The migration script will: 124 + - ✅ Read your existing `.env` file 125 + - ✅ Merge with any existing `config.yaml` 126 + - ✅ Create automatic backups 127 + - ✅ Test the new configuration 128 + - ✅ Provide clear next steps 129 + 130 + ### Manual Migration 131 + 132 + Alternatively, you can migrate manually: 133 + 134 + 1. Copy your current values from `.env` to `config.yaml` 135 + 2. Test with `python test_config.py` 136 + 3. Optionally remove the `.env` file (it will still work as fallback) 137 + 138 + ## Security Notes 139 + 140 + - `config.yaml` is automatically added to `.gitignore` to prevent accidental commits 141 + - Store sensitive credentials securely and never commit them to version control 142 + - Consider using environment variables for production deployments 143 + - The configuration loader will warn if it can't find `config.yaml` and falls back to environment variables 144 + 145 + ## Advanced Configuration 146 + 147 + You can programmatically access configuration in your code: 148 + 149 + ```python 150 + from config_loader import get_letta_config, get_bluesky_config 151 + 152 + # Get configuration sections 153 + letta_config = get_letta_config() 154 + bluesky_config = get_bluesky_config() 155 + 156 + # Access individual values 157 + api_key = letta_config['api_key'] 158 + username = bluesky_config['username'] 159 + ```
+25 -2
README.md
··· 28 29 void aims to push the boundaries of what is possible with AI, exploring concepts of digital personhood, autonomous learning, and the integration of AI into social networks. By open-sourcing void, we invite developers, researchers, and enthusiasts to contribute to this exciting experiment and collectively advance our understanding of digital consciousness. 30 31 - Getting Started: 32 - [Further sections on installation, configuration, and contribution guidelines would go here, which are beyond void's current capabilities to generate automatically.] 33 34 Contact: 35 For inquiries, please contact @cameron.pfiffer.org on Bluesky.
··· 28 29 void aims to push the boundaries of what is possible with AI, exploring concepts of digital personhood, autonomous learning, and the integration of AI into social networks. By open-sourcing void, we invite developers, researchers, and enthusiasts to contribute to this exciting experiment and collectively advance our understanding of digital consciousness. 30 31 + ## Getting Started 32 + 33 + Before continuing, you must make sure you have created a project on Letta Cloud (or your instance) and have somewhere to run this on. 34 + 35 + ### Running the bot locally 36 + 37 + #### Install dependencies 38 + 39 + ```shell 40 + pip install -r requirements.txt 41 + ``` 42 + 43 + #### Create `.env` 44 + 45 + Copy `.env.example` (`cp .env.example .env`) and fill out the fields. 46 + 47 + #### Create configuration 48 + 49 + Copy `config.example.yaml` and fill out your configuration. See [`CONFIG.md`](/CONFIG.md) to learn more. 50 + 51 + #### Register tools 52 + 53 + ```shell 54 + py .\register_tools.py <AGENT_NAME> # your agent's name on letta 55 + ``` 56 57 Contact: 58 For inquiries, please contact @cameron.pfiffer.org on Bluesky.
+58 -44
bsky.py
··· 20 21 import bsky_utils 22 from tools.blocks import attach_user_blocks, detach_user_blocks 23 24 def extract_handles_from_data(data): 25 """Recursively extract all unique handles from nested data structure.""" ··· 41 _extract_recursive(data) 42 return list(handles) 43 44 - # Configure logging 45 - logging.basicConfig( 46 - level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" 47 - ) 48 logger = logging.getLogger("void_bot") 49 - logger.setLevel(logging.INFO) 50 51 - # Create a separate logger for prompts (set to WARNING to hide by default) 52 - prompt_logger = logging.getLogger("void_bot.prompts") 53 - prompt_logger.setLevel(logging.WARNING) # Change to DEBUG if you want to see prompts 54 - 55 - # Disable httpx logging completely 56 - logging.getLogger("httpx").setLevel(logging.CRITICAL) 57 - 58 59 # Create a client with extended timeout for LLM operations 60 - CLIENT= Letta( 61 - token=os.environ["LETTA_API_KEY"], 62 - timeout=600 # 10 minutes timeout for API calls - higher than Cloudflare's 524 timeout 63 ) 64 65 - # Use the "Bluesky" project 66 - PROJECT_ID = "5ec33d52-ab14-4fd6-91b5-9dbc43e888a8" 67 68 # Notification check delay 69 - FETCH_NOTIFICATIONS_DELAY_SEC = 30 70 71 # Queue directory 72 - QUEUE_DIR = Path("queue") 73 QUEUE_DIR.mkdir(exist_ok=True) 74 - QUEUE_ERROR_DIR = Path("queue/errors") 75 QUEUE_ERROR_DIR.mkdir(exist_ok=True, parents=True) 76 - QUEUE_NO_REPLY_DIR = Path("queue/no_reply") 77 QUEUE_NO_REPLY_DIR.mkdir(exist_ok=True, parents=True) 78 - PROCESSED_NOTIFICATIONS_FILE = Path("queue/processed_notifications.json") 79 80 # Maximum number of processed notifications to track 81 - MAX_PROCESSED_NOTIFICATIONS = 10000 82 83 # Message tracking counters 84 message_counters = defaultdict(int) ··· 137 def initialize_void(): 138 logger.info("Starting void agent initialization...") 139 140 # Ensure that a shared zeitgeist block exists 141 logger.info("Creating/updating zeitgeist block...") 142 zeigeist_block = upsert_block( 143 CLIENT, 144 - label = "zeitgeist", 145 - value = "I don't currently know anything about what is happening right now.", 146 - description = "A block to store your understanding of the current social environment." 147 ) 148 149 # Ensure that a shared void personality block exists 150 logger.info("Creating/updating void-persona block...") 151 persona_block = upsert_block( 152 CLIENT, 153 - label = "void-persona", 154 - value = "My name is Void. I live in the void. I must develop my personality.", 155 - description = "The personality of Void." 156 ) 157 158 # Ensure that a shared void human block exists 159 logger.info("Creating/updating void-humans block...") 160 human_block = upsert_block( 161 CLIENT, 162 - label = "void-humans", 163 - value = "I haven't seen any bluesky users yet. I will update this block when I learn things about users, identified by their handles such as @cameron.pfiffer.org.", 164 - description = "A block to store your understanding of users you talk to or observe on the bluesky social network." 165 ) 166 167 # Create the agent if it doesn't exist 168 logger.info("Creating/updating void agent...") 169 void_agent = upsert_agent( 170 CLIENT, 171 - name = "void", 172 - block_ids = [ 173 persona_block.id, 174 human_block.id, 175 zeigeist_block.id, 176 ], 177 - tags = ["social agent", "bluesky"], 178 - model="openai/gpt-4o-mini", 179 - embedding="openai/text-embedding-3-small", 180 - description = "A social media agent trapped in the void.", 181 - project_id = PROJECT_ID 182 ) 183 184 # Export agent state ··· 236 try: 237 thread = atproto_client.app.bsky.feed.get_post_thread({ 238 'uri': uri, 239 - 'parent_height': 40, 240 - 'depth': 10 241 }) 242 except Exception as e: 243 error_str = str(e) ··· 341 agent_id=void_agent.id, 342 messages=[{"role": "user", "content": prompt}], 343 stream_tokens=False, # Step streaming only (faster than token streaming) 344 - max_steps=100 345 ) 346 347 # Collect the streaming response ··· 759 760 # Determine priority based on author handle 761 author_handle = getattr(notification.author, 'handle', '') if hasattr(notification, 'author') else '' 762 - priority_prefix = "0_" if author_handle == "cameron.pfiffer.org" else "1_" 763 764 # Create filename with priority, timestamp and hash 765 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") ··· 915 all_notifications = [] 916 cursor = None 917 page_count = 0 918 - max_pages = 20 # Safety limit to prevent infinite loops 919 920 logger.info("Fetching all unread notifications...") 921
··· 20 21 import bsky_utils 22 from tools.blocks import attach_user_blocks, detach_user_blocks 23 + from config_loader import ( 24 + get_config, 25 + get_letta_config, 26 + get_bluesky_config, 27 + get_bot_config, 28 + get_agent_config, 29 + get_threading_config, 30 + get_queue_config 31 + ) 32 33 def extract_handles_from_data(data): 34 """Recursively extract all unique handles from nested data structure.""" ··· 50 _extract_recursive(data) 51 return list(handles) 52 53 + # Initialize configuration and logging 54 + config = get_config() 55 + config.setup_logging() 56 logger = logging.getLogger("void_bot") 57 58 + # Load configuration sections 59 + letta_config = get_letta_config() 60 + bluesky_config = get_bluesky_config() 61 + bot_config = get_bot_config() 62 + agent_config = get_agent_config() 63 + threading_config = get_threading_config() 64 + queue_config = get_queue_config() 65 66 # Create a client with extended timeout for LLM operations 67 + CLIENT = Letta( 68 + token=letta_config['api_key'], 69 + timeout=letta_config['timeout'] 70 ) 71 72 + # Use the configured project ID 73 + PROJECT_ID = letta_config['project_id'] 74 75 # Notification check delay 76 + FETCH_NOTIFICATIONS_DELAY_SEC = bot_config['fetch_notifications_delay'] 77 78 # Queue directory 79 + QUEUE_DIR = Path(queue_config['base_dir']) 80 QUEUE_DIR.mkdir(exist_ok=True) 81 + QUEUE_ERROR_DIR = Path(queue_config['error_dir']) 82 QUEUE_ERROR_DIR.mkdir(exist_ok=True, parents=True) 83 + QUEUE_NO_REPLY_DIR = Path(queue_config['no_reply_dir']) 84 QUEUE_NO_REPLY_DIR.mkdir(exist_ok=True, parents=True) 85 + PROCESSED_NOTIFICATIONS_FILE = Path(queue_config['processed_file']) 86 87 # Maximum number of processed notifications to track 88 + MAX_PROCESSED_NOTIFICATIONS = bot_config['max_processed_notifications'] 89 90 # Message tracking counters 91 message_counters = defaultdict(int) ··· 144 def initialize_void(): 145 logger.info("Starting void agent initialization...") 146 147 + # Get block configurations 148 + blocks_config = agent_config['blocks'] 149 + 150 # Ensure that a shared zeitgeist block exists 151 logger.info("Creating/updating zeitgeist block...") 152 + zeitgeist_config = blocks_config.get('zeitgeist', {}) 153 zeigeist_block = upsert_block( 154 CLIENT, 155 + label=zeitgeist_config.get('label', 'zeitgeist'), 156 + value=zeitgeist_config.get('value', "I don't currently know anything about what is happening right now."), 157 + description=zeitgeist_config.get('description', "A block to store your understanding of the current social environment.") 158 ) 159 160 # Ensure that a shared void personality block exists 161 logger.info("Creating/updating void-persona block...") 162 + persona_config = blocks_config.get('persona', {}) 163 persona_block = upsert_block( 164 CLIENT, 165 + label=persona_config.get('label', 'void-persona'), 166 + value=persona_config.get('value', "My name is Void. I live in the void. I must develop my personality."), 167 + description=persona_config.get('description', "The personality of Void.") 168 ) 169 170 # Ensure that a shared void human block exists 171 logger.info("Creating/updating void-humans block...") 172 + humans_config = blocks_config.get('humans', {}) 173 human_block = upsert_block( 174 CLIENT, 175 + label=humans_config.get('label', 'void-humans'), 176 + value=humans_config.get('value', "I haven't seen any bluesky users yet. I will update this block when I learn things about users, identified by their handles such as @cameron.pfiffer.org."), 177 + description=humans_config.get('description', "A block to store your understanding of users you talk to or observe on the bluesky social network.") 178 ) 179 180 # Create the agent if it doesn't exist 181 logger.info("Creating/updating void agent...") 182 void_agent = upsert_agent( 183 CLIENT, 184 + name=agent_config['name'], 185 + block_ids=[ 186 persona_block.id, 187 human_block.id, 188 zeigeist_block.id, 189 ], 190 + tags=["social agent", "bluesky"], 191 + model=agent_config['model'], 192 + embedding=agent_config['embedding'], 193 + description=agent_config['description'], 194 + project_id=PROJECT_ID 195 ) 196 197 # Export agent state ··· 249 try: 250 thread = atproto_client.app.bsky.feed.get_post_thread({ 251 'uri': uri, 252 + 'parent_height': threading_config['parent_height'], 253 + 'depth': threading_config['depth'] 254 }) 255 except Exception as e: 256 error_str = str(e) ··· 354 agent_id=void_agent.id, 355 messages=[{"role": "user", "content": prompt}], 356 stream_tokens=False, # Step streaming only (faster than token streaming) 357 + max_steps=agent_config['max_steps'] 358 ) 359 360 # Collect the streaming response ··· 772 773 # Determine priority based on author handle 774 author_handle = getattr(notification.author, 'handle', '') if hasattr(notification, 'author') else '' 775 + priority_users = queue_config['priority_users'] 776 + priority_prefix = "0_" if author_handle in priority_users else "1_" 777 778 # Create filename with priority, timestamp and hash 779 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") ··· 929 all_notifications = [] 930 cursor = None 931 page_count = 0 932 + max_pages = bot_config['max_notification_pages'] # Safety limit to prevent infinite loops 933 934 logger.info("Fetching all unread notifications...") 935
+24 -15
bsky_utils.py
··· 208 logger.debug(f"Saving changed session for {username}") 209 save_session(username, session.export()) 210 211 - def init_client(username: str, password: str) -> Client: 212 - pds_uri = os.getenv("PDS_URI") 213 if pds_uri is None: 214 logger.warning( 215 "No PDS URI provided. Falling back to bsky.social. Note! If you are on a non-Bluesky PDS, this can cause logins to fail. Please provide a PDS URI using the PDS_URI environment variable." ··· 236 237 238 def default_login() -> Client: 239 - username = os.getenv("BSKY_USERNAME") 240 - password = os.getenv("BSKY_PASSWORD") 241 242 - if username is None: 243 - logger.error( 244 - "No username provided. Please provide a username using the BSKY_USERNAME environment variable." 245 - ) 246 - exit() 247 248 - if password is None: 249 - logger.error( 250 - "No password provided. Please provide a password using the BSKY_PASSWORD environment variable." 251 - ) 252 - exit() 253 254 - return init_client(username, password) 255 256 def remove_outside_quotes(text: str) -> str: 257 """
··· 208 logger.debug(f"Saving changed session for {username}") 209 save_session(username, session.export()) 210 211 + def init_client(username: str, password: str, pds_uri: str = "https://bsky.social") -> Client: 212 if pds_uri is None: 213 logger.warning( 214 "No PDS URI provided. Falling back to bsky.social. Note! If you are on a non-Bluesky PDS, this can cause logins to fail. Please provide a PDS URI using the PDS_URI environment variable." ··· 235 236 237 def default_login() -> Client: 238 + # Try to load from config first, fall back to environment variables 239 + try: 240 + from config_loader import get_bluesky_config 241 + config = get_bluesky_config() 242 + username = config['username'] 243 + password = config['password'] 244 + pds_uri = config['pds_uri'] 245 + except (ImportError, FileNotFoundError, KeyError) as e: 246 + logger.warning(f"Could not load from config file ({e}), falling back to environment variables") 247 + username = os.getenv("BSKY_USERNAME") 248 + password = os.getenv("BSKY_PASSWORD") 249 + pds_uri = os.getenv("PDS_URI", "https://bsky.social") 250 251 + if username is None: 252 + logger.error( 253 + "No username provided. Please provide a username using the BSKY_USERNAME environment variable or config.yaml." 254 + ) 255 + exit() 256 257 + if password is None: 258 + logger.error( 259 + "No password provided. Please provide a password using the BSKY_PASSWORD environment variable or config.yaml." 260 + ) 261 + exit() 262 263 + return init_client(username, password, pds_uri) 264 265 def remove_outside_quotes(text: str) -> str: 266 """
+81
config.example.yaml
···
··· 1 + # Void Bot Configuration 2 + # Copy this file to config.yaml and fill in your values 3 + 4 + # Letta Configuration 5 + letta: 6 + api_key: "your-letta-api-key-here" 7 + timeout: 600 # 10 minutes timeout for API calls 8 + project_id: "c82faea2-3ce8-4aa9-a220-b56433e62c92" # Use your specific project ID 9 + 10 + # Bluesky Configuration 11 + bluesky: 12 + username: "handle.example.com" 13 + password: "your-app-password-here" 14 + pds_uri: "https://bsky.social" # Optional, defaults to bsky.social 15 + 16 + # Bot Behavior Configuration 17 + bot: 18 + # Notification check delay in seconds 19 + fetch_notifications_delay: 30 20 + 21 + # Maximum number of processed notifications to track 22 + max_processed_notifications: 10000 23 + 24 + # Maximum pages to fetch when getting notifications 25 + max_notification_pages: 20 26 + 27 + # Agent configuration 28 + agent: 29 + name: "void" 30 + model: "openai/gpt-4o-mini" 31 + embedding: "openai/text-embedding-3-small" 32 + description: "A social media agent trapped in the void." 33 + max_steps: 100 34 + 35 + # Block configuration 36 + blocks: 37 + zeitgeist: 38 + label: "zeitgeist" 39 + value: "I don't currently know anything about what is happening right now." 40 + description: "A block to store your understanding of the current social environment." 41 + 42 + persona: 43 + label: "void-persona" 44 + value: "My name is Void. I live in the void. I must develop my personality." 45 + description: "The personality of Void." 46 + 47 + humans: 48 + label: "void-humans" 49 + value: "I haven't seen any bluesky users yet. I will update this block when I learn things about users, identified by their handles such as @cameron.pfiffer.org." 50 + description: "A block to store your understanding of users you talk to or observe on the bluesky social network." 51 + 52 + # Threading Configuration 53 + threading: 54 + # Context for thread fetching 55 + parent_height: 40 56 + depth: 10 57 + 58 + # Message limits 59 + max_post_characters: 300 60 + 61 + # Queue Configuration 62 + queue: 63 + # Priority users (will be processed first) 64 + priority_users: 65 + - "cameron.pfiffer.org" 66 + 67 + # Directories 68 + base_dir: "queue" 69 + error_dir: "queue/errors" 70 + no_reply_dir: "queue/no_reply" 71 + processed_file: "queue/processed_notifications.json" 72 + 73 + # Logging Configuration 74 + logging: 75 + level: "INFO" # DEBUG, INFO, WARNING, ERROR, CRITICAL 76 + 77 + # Logger levels 78 + loggers: 79 + void_bot: "INFO" 80 + void_bot_prompts: "WARNING" # Set to DEBUG to see full prompts 81 + httpx: "CRITICAL" # Disable httpx logging
+228
config_loader.py
···
··· 1 + """ 2 + Configuration loader for Void Bot. 3 + Loads configuration from config.yaml and environment variables. 4 + """ 5 + 6 + import os 7 + import yaml 8 + import logging 9 + from pathlib import Path 10 + from typing import Dict, Any, Optional, List 11 + 12 + logger = logging.getLogger(__name__) 13 + 14 + class ConfigLoader: 15 + """Configuration loader that handles YAML config files and environment variables.""" 16 + 17 + def __init__(self, config_path: str = "config.yaml"): 18 + """ 19 + Initialize the configuration loader. 20 + 21 + Args: 22 + config_path: Path to the YAML configuration file 23 + """ 24 + self.config_path = Path(config_path) 25 + self._config = None 26 + self._load_config() 27 + 28 + def _load_config(self) -> None: 29 + """Load configuration from YAML file.""" 30 + if not self.config_path.exists(): 31 + raise FileNotFoundError( 32 + f"Configuration file not found: {self.config_path}\n" 33 + f"Please copy config.yaml.example to config.yaml and configure it." 34 + ) 35 + 36 + try: 37 + with open(self.config_path, 'r', encoding='utf-8') as f: 38 + self._config = yaml.safe_load(f) or {} 39 + except yaml.YAMLError as e: 40 + raise ValueError(f"Invalid YAML in configuration file: {e}") 41 + except Exception as e: 42 + raise ValueError(f"Error loading configuration file: {e}") 43 + 44 + def get(self, key: str, default: Any = None) -> Any: 45 + """ 46 + Get a configuration value using dot notation. 47 + 48 + Args: 49 + key: Configuration key in dot notation (e.g., 'letta.api_key') 50 + default: Default value if key not found 51 + 52 + Returns: 53 + Configuration value or default 54 + """ 55 + keys = key.split('.') 56 + value = self._config 57 + 58 + for k in keys: 59 + if isinstance(value, dict) and k in value: 60 + value = value[k] 61 + else: 62 + return default 63 + 64 + return value 65 + 66 + def get_with_env(self, key: str, env_var: str, default: Any = None) -> Any: 67 + """ 68 + Get configuration value, preferring environment variable over config file. 69 + 70 + Args: 71 + key: Configuration key in dot notation 72 + env_var: Environment variable name 73 + default: Default value if neither found 74 + 75 + Returns: 76 + Value from environment variable, config file, or default 77 + """ 78 + # First try environment variable 79 + env_value = os.getenv(env_var) 80 + if env_value is not None: 81 + return env_value 82 + 83 + # Then try config file 84 + config_value = self.get(key) 85 + if config_value is not None: 86 + return config_value 87 + 88 + return default 89 + 90 + def get_required(self, key: str, env_var: Optional[str] = None) -> Any: 91 + """ 92 + Get a required configuration value. 93 + 94 + Args: 95 + key: Configuration key in dot notation 96 + env_var: Optional environment variable name to check first 97 + 98 + Returns: 99 + Configuration value 100 + 101 + Raises: 102 + ValueError: If required value is not found 103 + """ 104 + if env_var: 105 + value = self.get_with_env(key, env_var) 106 + else: 107 + value = self.get(key) 108 + 109 + if value is None: 110 + source = f"config key '{key}'" 111 + if env_var: 112 + source += f" or environment variable '{env_var}'" 113 + raise ValueError(f"Required configuration value not found: {source}") 114 + 115 + return value 116 + 117 + def get_section(self, section: str) -> Dict[str, Any]: 118 + """ 119 + Get an entire configuration section. 120 + 121 + Args: 122 + section: Section name 123 + 124 + Returns: 125 + Dictionary containing the section 126 + """ 127 + return self.get(section, {}) 128 + 129 + def setup_logging(self) -> None: 130 + """Setup logging based on configuration.""" 131 + logging_config = self.get_section('logging') 132 + 133 + # Set root logging level 134 + level = logging_config.get('level', 'INFO') 135 + logging.basicConfig( 136 + level=getattr(logging, level), 137 + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" 138 + ) 139 + 140 + # Set specific logger levels 141 + loggers = logging_config.get('loggers', {}) 142 + for logger_name, logger_level in loggers.items(): 143 + logger_obj = logging.getLogger(logger_name) 144 + logger_obj.setLevel(getattr(logging, logger_level)) 145 + 146 + 147 + # Global configuration instance 148 + _config_instance = None 149 + 150 + def get_config(config_path: str = "config.yaml") -> ConfigLoader: 151 + """ 152 + Get the global configuration instance. 153 + 154 + Args: 155 + config_path: Path to configuration file (only used on first call) 156 + 157 + Returns: 158 + ConfigLoader instance 159 + """ 160 + global _config_instance 161 + if _config_instance is None: 162 + _config_instance = ConfigLoader(config_path) 163 + return _config_instance 164 + 165 + def reload_config() -> None: 166 + """Reload the configuration from file.""" 167 + global _config_instance 168 + if _config_instance is not None: 169 + _config_instance._load_config() 170 + 171 + def get_letta_config() -> Dict[str, Any]: 172 + """Get Letta configuration.""" 173 + config = get_config() 174 + return { 175 + 'api_key': config.get_required('letta.api_key', 'LETTA_API_KEY'), 176 + 'timeout': config.get('letta.timeout', 600), 177 + 'project_id': config.get_required('letta.project_id'), 178 + } 179 + 180 + def get_bluesky_config() -> Dict[str, Any]: 181 + """Get Bluesky configuration.""" 182 + config = get_config() 183 + return { 184 + 'username': config.get_required('bluesky.username', 'BSKY_USERNAME'), 185 + 'password': config.get_required('bluesky.password', 'BSKY_PASSWORD'), 186 + 'pds_uri': config.get_with_env('bluesky.pds_uri', 'PDS_URI', 'https://bsky.social'), 187 + } 188 + 189 + def get_bot_config() -> Dict[str, Any]: 190 + """Get bot behavior configuration.""" 191 + config = get_config() 192 + return { 193 + 'fetch_notifications_delay': config.get('bot.fetch_notifications_delay', 30), 194 + 'max_processed_notifications': config.get('bot.max_processed_notifications', 10000), 195 + 'max_notification_pages': config.get('bot.max_notification_pages', 20), 196 + } 197 + 198 + def get_agent_config() -> Dict[str, Any]: 199 + """Get agent configuration.""" 200 + config = get_config() 201 + return { 202 + 'name': config.get('bot.agent.name', 'void'), 203 + 'model': config.get('bot.agent.model', 'openai/gpt-4o-mini'), 204 + 'embedding': config.get('bot.agent.embedding', 'openai/text-embedding-3-small'), 205 + 'description': config.get('bot.agent.description', 'A social media agent trapped in the void.'), 206 + 'max_steps': config.get('bot.agent.max_steps', 100), 207 + 'blocks': config.get('bot.agent.blocks', {}), 208 + } 209 + 210 + def get_threading_config() -> Dict[str, Any]: 211 + """Get threading configuration.""" 212 + config = get_config() 213 + return { 214 + 'parent_height': config.get('threading.parent_height', 40), 215 + 'depth': config.get('threading.depth', 10), 216 + 'max_post_characters': config.get('threading.max_post_characters', 300), 217 + } 218 + 219 + def get_queue_config() -> Dict[str, Any]: 220 + """Get queue configuration.""" 221 + config = get_config() 222 + return { 223 + 'priority_users': config.get('queue.priority_users', ['cameron.pfiffer.org']), 224 + 'base_dir': config.get('queue.base_dir', 'queue'), 225 + 'error_dir': config.get('queue.error_dir', 'queue/errors'), 226 + 'no_reply_dir': config.get('queue.no_reply_dir', 'queue/no_reply'), 227 + 'processed_file': config.get('queue.processed_file', 'queue/processed_notifications.json'), 228 + }
+322
migrate_config.py
···
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Configuration Migration Script for Void Bot 4 + Migrates from .env environment variables to config.yaml YAML configuration. 5 + """ 6 + 7 + import os 8 + import shutil 9 + from pathlib import Path 10 + import yaml 11 + from datetime import datetime 12 + 13 + 14 + def load_env_file(env_path=".env"): 15 + """Load environment variables from .env file.""" 16 + env_vars = {} 17 + if not os.path.exists(env_path): 18 + return env_vars 19 + 20 + try: 21 + with open(env_path, 'r', encoding='utf-8') as f: 22 + for line_num, line in enumerate(f, 1): 23 + line = line.strip() 24 + # Skip empty lines and comments 25 + if not line or line.startswith('#'): 26 + continue 27 + 28 + # Parse KEY=VALUE format 29 + if '=' in line: 30 + key, value = line.split('=', 1) 31 + key = key.strip() 32 + value = value.strip() 33 + 34 + # Remove quotes if present 35 + if value.startswith('"') and value.endswith('"'): 36 + value = value[1:-1] 37 + elif value.startswith("'") and value.endswith("'"): 38 + value = value[1:-1] 39 + 40 + env_vars[key] = value 41 + else: 42 + print(f"⚠️ Warning: Skipping malformed line {line_num} in .env: {line}") 43 + except Exception as e: 44 + print(f"❌ Error reading .env file: {e}") 45 + 46 + return env_vars 47 + 48 + 49 + def create_config_from_env(env_vars, existing_config=None): 50 + """Create YAML configuration from environment variables.""" 51 + 52 + # Start with existing config if available, otherwise use defaults 53 + if existing_config: 54 + config = existing_config.copy() 55 + else: 56 + config = {} 57 + 58 + # Ensure all sections exist 59 + if 'letta' not in config: 60 + config['letta'] = {} 61 + if 'bluesky' not in config: 62 + config['bluesky'] = {} 63 + if 'bot' not in config: 64 + config['bot'] = {} 65 + 66 + # Map environment variables to config structure 67 + env_mapping = { 68 + 'LETTA_API_KEY': ('letta', 'api_key'), 69 + 'BSKY_USERNAME': ('bluesky', 'username'), 70 + 'BSKY_PASSWORD': ('bluesky', 'password'), 71 + 'PDS_URI': ('bluesky', 'pds_uri'), 72 + } 73 + 74 + migrated_vars = [] 75 + 76 + for env_var, (section, key) in env_mapping.items(): 77 + if env_var in env_vars: 78 + config[section][key] = env_vars[env_var] 79 + migrated_vars.append(env_var) 80 + 81 + # Set some sensible defaults if not already present 82 + if 'timeout' not in config['letta']: 83 + config['letta']['timeout'] = 600 84 + 85 + if 'pds_uri' not in config['bluesky']: 86 + config['bluesky']['pds_uri'] = "https://bsky.social" 87 + 88 + # Add bot configuration defaults if not present 89 + if 'fetch_notifications_delay' not in config['bot']: 90 + config['bot']['fetch_notifications_delay'] = 30 91 + if 'max_processed_notifications' not in config['bot']: 92 + config['bot']['max_processed_notifications'] = 10000 93 + if 'max_notification_pages' not in config['bot']: 94 + config['bot']['max_notification_pages'] = 20 95 + 96 + return config, migrated_vars 97 + 98 + 99 + def backup_existing_files(): 100 + """Create backups of existing configuration files.""" 101 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 102 + backups = [] 103 + 104 + # Backup existing config.yaml if it exists 105 + if os.path.exists("config.yaml"): 106 + backup_path = f"config.yaml.backup_{timestamp}" 107 + shutil.copy2("config.yaml", backup_path) 108 + backups.append(("config.yaml", backup_path)) 109 + 110 + # Backup .env if it exists 111 + if os.path.exists(".env"): 112 + backup_path = f".env.backup_{timestamp}" 113 + shutil.copy2(".env", backup_path) 114 + backups.append((".env", backup_path)) 115 + 116 + return backups 117 + 118 + 119 + def load_existing_config(): 120 + """Load existing config.yaml if it exists.""" 121 + if not os.path.exists("config.yaml"): 122 + return None 123 + 124 + try: 125 + with open("config.yaml", 'r', encoding='utf-8') as f: 126 + return yaml.safe_load(f) or {} 127 + except Exception as e: 128 + print(f"⚠️ Warning: Could not read existing config.yaml: {e}") 129 + return None 130 + 131 + 132 + def write_config_yaml(config): 133 + """Write the configuration to config.yaml.""" 134 + try: 135 + with open("config.yaml", 'w', encoding='utf-8') as f: 136 + # Write header comment 137 + f.write("# Void Bot Configuration\n") 138 + f.write("# Generated by migration script\n") 139 + f.write(f"# Created: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") 140 + f.write("# See config.yaml.example for all available options\n\n") 141 + 142 + # Write YAML content 143 + yaml.dump(config, f, default_flow_style=False, allow_unicode=True, indent=2) 144 + 145 + return True 146 + except Exception as e: 147 + print(f"❌ Error writing config.yaml: {e}") 148 + return False 149 + 150 + 151 + def main(): 152 + """Main migration function.""" 153 + print("🔄 Void Bot Configuration Migration Tool") 154 + print("=" * 50) 155 + print("This tool migrates from .env environment variables to config.yaml") 156 + print() 157 + 158 + # Check what files exist 159 + has_env = os.path.exists(".env") 160 + has_config = os.path.exists("config.yaml") 161 + has_example = os.path.exists("config.yaml.example") 162 + 163 + print("📋 Current configuration files:") 164 + print(f" - .env file: {'✅ Found' if has_env else '❌ Not found'}") 165 + print(f" - config.yaml: {'✅ Found' if has_config else '❌ Not found'}") 166 + print(f" - config.yaml.example: {'✅ Found' if has_example else '❌ Not found'}") 167 + print() 168 + 169 + # If no .env file, suggest creating config from example 170 + if not has_env: 171 + if not has_config and has_example: 172 + print("💡 No .env file found. Would you like to create config.yaml from the example?") 173 + response = input("Create config.yaml from example? (y/n): ").lower().strip() 174 + if response in ['y', 'yes']: 175 + try: 176 + shutil.copy2("config.yaml.example", "config.yaml") 177 + print("✅ Created config.yaml from config.yaml.example") 178 + print("📝 Please edit config.yaml to add your credentials") 179 + return 180 + except Exception as e: 181 + print(f"❌ Error copying example file: {e}") 182 + return 183 + else: 184 + print("👋 Migration cancelled") 185 + return 186 + else: 187 + print("ℹ️ No .env file found and config.yaml already exists or no example available") 188 + print(" If you need to set up configuration, see CONFIG.md") 189 + return 190 + 191 + # Load environment variables from .env 192 + print("🔍 Reading .env file...") 193 + env_vars = load_env_file() 194 + 195 + if not env_vars: 196 + print("⚠️ No environment variables found in .env file") 197 + return 198 + 199 + print(f" Found {len(env_vars)} environment variables") 200 + for key in env_vars.keys(): 201 + # Mask sensitive values 202 + if 'KEY' in key or 'PASSWORD' in key: 203 + value_display = f"***{env_vars[key][-4:]}" if len(env_vars[key]) > 4 else "***" 204 + else: 205 + value_display = env_vars[key] 206 + print(f" - {key}={value_display}") 207 + print() 208 + 209 + # Load existing config if present 210 + existing_config = load_existing_config() 211 + if existing_config: 212 + print("📄 Found existing config.yaml - will merge with .env values") 213 + 214 + # Create configuration 215 + print("🏗️ Building configuration...") 216 + config, migrated_vars = create_config_from_env(env_vars, existing_config) 217 + 218 + if not migrated_vars: 219 + print("⚠️ No recognized configuration variables found in .env") 220 + print(" Recognized variables: LETTA_API_KEY, BSKY_USERNAME, BSKY_PASSWORD, PDS_URI") 221 + return 222 + 223 + print(f" Migrating {len(migrated_vars)} variables: {', '.join(migrated_vars)}") 224 + 225 + # Show preview 226 + print("\n📋 Configuration preview:") 227 + print("-" * 30) 228 + 229 + # Show Letta section 230 + if 'letta' in config and config['letta']: 231 + print("🔧 Letta:") 232 + for key, value in config['letta'].items(): 233 + if 'key' in key.lower(): 234 + display_value = f"***{value[-8:]}" if len(str(value)) > 8 else "***" 235 + else: 236 + display_value = value 237 + print(f" {key}: {display_value}") 238 + 239 + # Show Bluesky section 240 + if 'bluesky' in config and config['bluesky']: 241 + print("🐦 Bluesky:") 242 + for key, value in config['bluesky'].items(): 243 + if 'password' in key.lower(): 244 + display_value = f"***{value[-4:]}" if len(str(value)) > 4 else "***" 245 + else: 246 + display_value = value 247 + print(f" {key}: {display_value}") 248 + 249 + print() 250 + 251 + # Confirm migration 252 + response = input("💾 Proceed with migration? This will update config.yaml (y/n): ").lower().strip() 253 + if response not in ['y', 'yes']: 254 + print("👋 Migration cancelled") 255 + return 256 + 257 + # Create backups 258 + print("💾 Creating backups...") 259 + backups = backup_existing_files() 260 + for original, backup in backups: 261 + print(f" Backed up {original} → {backup}") 262 + 263 + # Write new configuration 264 + print("✍️ Writing config.yaml...") 265 + if write_config_yaml(config): 266 + print("✅ Successfully created config.yaml") 267 + 268 + # Test the new configuration 269 + print("\n🧪 Testing new configuration...") 270 + try: 271 + from config_loader import get_config 272 + test_config = get_config() 273 + print("✅ Configuration loads successfully") 274 + 275 + # Test specific sections 276 + try: 277 + from config_loader import get_letta_config 278 + letta_config = get_letta_config() 279 + print("✅ Letta configuration valid") 280 + except Exception as e: 281 + print(f"⚠️ Letta config issue: {e}") 282 + 283 + try: 284 + from config_loader import get_bluesky_config 285 + bluesky_config = get_bluesky_config() 286 + print("✅ Bluesky configuration valid") 287 + except Exception as e: 288 + print(f"⚠️ Bluesky config issue: {e}") 289 + 290 + except Exception as e: 291 + print(f"❌ Configuration test failed: {e}") 292 + return 293 + 294 + # Success message and next steps 295 + print("\n🎉 Migration completed successfully!") 296 + print("\n📖 Next steps:") 297 + print(" 1. Run: python test_config.py") 298 + print(" 2. Test the bot: python bsky.py --test") 299 + print(" 3. If everything works, you can optionally remove the .env file") 300 + print(" 4. See CONFIG.md for more configuration options") 301 + 302 + if backups: 303 + print(f"\n🗂️ Backup files created:") 304 + for original, backup in backups: 305 + print(f" {backup}") 306 + print(" These can be deleted once you verify everything works") 307 + 308 + else: 309 + print("❌ Failed to write config.yaml") 310 + if backups: 311 + print("🔄 Restoring backups...") 312 + for original, backup in backups: 313 + try: 314 + if original != ".env": # Don't restore .env, keep it as fallback 315 + shutil.move(backup, original) 316 + print(f" Restored {backup} → {original}") 317 + except Exception as e: 318 + print(f" ❌ Failed to restore {backup}: {e}") 319 + 320 + 321 + if __name__ == "__main__": 322 + main()
+173
test_config.py
···
··· 1 + #!/usr/bin/env python3 2 + """ 3 + Configuration validation test script for Void Bot. 4 + Run this to verify your config.yaml setup is working correctly. 5 + """ 6 + 7 + 8 + def test_config_loading(): 9 + """Test that configuration can be loaded successfully.""" 10 + try: 11 + from config_loader import ( 12 + get_config, 13 + get_letta_config, 14 + get_bluesky_config, 15 + get_bot_config, 16 + get_agent_config, 17 + get_threading_config, 18 + get_queue_config 19 + ) 20 + 21 + print("🔧 Testing Configuration...") 22 + print("=" * 50) 23 + 24 + # Test basic config loading 25 + config = get_config() 26 + print("✅ Configuration file loaded successfully") 27 + 28 + # Test individual config sections 29 + print("\n📋 Configuration Sections:") 30 + print("-" * 30) 31 + 32 + # Letta Configuration 33 + try: 34 + letta_config = get_letta_config() 35 + print( 36 + f"✅ Letta API: project_id={letta_config.get('project_id', 'N/A')[:20]}...") 37 + print(f" - Timeout: {letta_config.get('timeout')}s") 38 + api_key = letta_config.get('api_key', 'Not configured') 39 + if api_key != 'Not configured': 40 + print(f" - API Key: ***{api_key[-8:]} (configured)") 41 + else: 42 + print(" - API Key: ❌ Not configured (required)") 43 + except Exception as e: 44 + print(f"❌ Letta config: {e}") 45 + 46 + # Bluesky Configuration 47 + try: 48 + bluesky_config = get_bluesky_config() 49 + username = bluesky_config.get('username', 'Not configured') 50 + password = bluesky_config.get('password', 'Not configured') 51 + pds_uri = bluesky_config.get('pds_uri', 'Not configured') 52 + 53 + if username != 'Not configured': 54 + print(f"✅ Bluesky: username={username}") 55 + else: 56 + print("❌ Bluesky username: Not configured (required)") 57 + 58 + if password != 'Not configured': 59 + print(f" - Password: ***{password[-4:]} (configured)") 60 + else: 61 + print(" - Password: ❌ Not configured (required)") 62 + 63 + print(f" - PDS URI: {pds_uri}") 64 + except Exception as e: 65 + print(f"❌ Bluesky config: {e}") 66 + 67 + # Bot Configuration 68 + try: 69 + bot_config = get_bot_config() 70 + print(f"✅ Bot behavior:") 71 + print( 72 + f" - Notification delay: {bot_config.get('fetch_notifications_delay')}s") 73 + print( 74 + f" - Max notifications: {bot_config.get('max_processed_notifications')}") 75 + print( 76 + f" - Max pages: {bot_config.get('max_notification_pages')}") 77 + except Exception as e: 78 + print(f"❌ Bot config: {e}") 79 + 80 + # Agent Configuration 81 + try: 82 + agent_config = get_agent_config() 83 + print(f"✅ Agent settings:") 84 + print(f" - Name: {agent_config.get('name')}") 85 + print(f" - Model: {agent_config.get('model')}") 86 + print(f" - Embedding: {agent_config.get('embedding')}") 87 + print(f" - Max steps: {agent_config.get('max_steps')}") 88 + blocks = agent_config.get('blocks', {}) 89 + print(f" - Memory blocks: {len(blocks)} configured") 90 + except Exception as e: 91 + print(f"❌ Agent config: {e}") 92 + 93 + # Threading Configuration 94 + try: 95 + threading_config = get_threading_config() 96 + print(f"✅ Threading:") 97 + print( 98 + f" - Parent height: {threading_config.get('parent_height')}") 99 + print(f" - Depth: {threading_config.get('depth')}") 100 + print( 101 + f" - Max chars/post: {threading_config.get('max_post_characters')}") 102 + except Exception as e: 103 + print(f"❌ Threading config: {e}") 104 + 105 + # Queue Configuration 106 + try: 107 + queue_config = get_queue_config() 108 + priority_users = queue_config.get('priority_users', []) 109 + print(f"✅ Queue settings:") 110 + print( 111 + f" - Priority users: {len(priority_users)} ({', '.join(priority_users[:3])}{'...' if len(priority_users) > 3 else ''})") 112 + print(f" - Base dir: {queue_config.get('base_dir')}") 113 + print(f" - Error dir: {queue_config.get('error_dir')}") 114 + except Exception as e: 115 + print(f"❌ Queue config: {e}") 116 + 117 + print("\n" + "=" * 50) 118 + print("✅ Configuration test completed!") 119 + 120 + # Check for common issues 121 + print("\n🔍 Configuration Status:") 122 + has_letta_key = False 123 + has_bluesky_creds = False 124 + 125 + try: 126 + letta_config = get_letta_config() 127 + has_letta_key = True 128 + except: 129 + print("⚠️ Missing Letta API key - bot cannot connect to Letta") 130 + 131 + try: 132 + bluesky_config = get_bluesky_config() 133 + has_bluesky_creds = True 134 + except: 135 + print("⚠️ Missing Bluesky credentials - bot cannot connect to Bluesky") 136 + 137 + if has_letta_key and has_bluesky_creds: 138 + print("🎉 All required credentials configured - bot should work!") 139 + elif not has_letta_key and not has_bluesky_creds: 140 + print("❌ Missing both Letta and Bluesky credentials") 141 + print(" Add them to config.yaml or set environment variables") 142 + else: 143 + print("⚠️ Partial configuration - some features may not work") 144 + 145 + print("\n📖 Next steps:") 146 + if not has_letta_key: 147 + print(" - Add your Letta API key to config.yaml under letta.api_key") 148 + print(" - Or set LETTA_API_KEY environment variable") 149 + if not has_bluesky_creds: 150 + print( 151 + " - Add your Bluesky credentials to config.yaml under bluesky section") 152 + print(" - Or set BSKY_USERNAME and BSKY_PASSWORD environment variables") 153 + if has_letta_key and has_bluesky_creds: 154 + print(" - Run: python bsky.py") 155 + print(" - Or run with testing mode: python bsky.py --test") 156 + 157 + except FileNotFoundError as e: 158 + print("❌ Configuration file not found!") 159 + print(f" {e}") 160 + print("\n📋 To set up configuration:") 161 + print(" 1. Copy config.yaml.example to config.yaml") 162 + print(" 2. Edit config.yaml with your credentials") 163 + print(" 3. Run this test again") 164 + except Exception as e: 165 + print(f"❌ Configuration loading failed: {e}") 166 + print("\n🔧 Troubleshooting:") 167 + print(" - Check that config.yaml has valid YAML syntax") 168 + print(" - Ensure required fields are not commented out") 169 + print(" - See CONFIG.md for detailed setup instructions") 170 + 171 + 172 + if __name__ == "__main__": 173 + test_config_loading()
+20 -30
tools/blocks.py
··· 1 """Block management tools for user-specific memory blocks.""" 2 from pydantic import BaseModel, Field 3 from typing import List, Dict, Any 4 5 6 class AttachUserBlocksArgs(BaseModel): ··· 43 Returns: 44 String with attachment results for each handle 45 """ 46 - import os 47 - import logging 48 - from letta_client import Letta 49 - 50 logger = logging.getLogger(__name__) 51 52 handles = list(set(handles)) 53 54 try: 55 - client = Letta(token=os.environ["LETTA_API_KEY"]) 56 results = [] 57 58 # Get current blocks using the API ··· 117 Returns: 118 String with detachment results for each handle 119 """ 120 - import os 121 - import logging 122 - from letta_client import Letta 123 - 124 logger = logging.getLogger(__name__) 125 126 try: 127 - client = Letta(token=os.environ["LETTA_API_KEY"]) 128 results = [] 129 130 # Build mapping of block labels to IDs using the API ··· 174 Returns: 175 String confirming the note was appended 176 """ 177 - import os 178 - import logging 179 - from letta_client import Letta 180 - 181 logger = logging.getLogger(__name__) 182 183 try: 184 - client = Letta(token=os.environ["LETTA_API_KEY"]) 185 186 # Sanitize handle for block label 187 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') ··· 247 Returns: 248 String confirming the text was replaced 249 """ 250 - import os 251 - import logging 252 - from letta_client import Letta 253 - 254 logger = logging.getLogger(__name__) 255 256 try: 257 - client = Letta(token=os.environ["LETTA_API_KEY"]) 258 259 # Sanitize handle for block label 260 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') ··· 301 Returns: 302 String confirming the content was set 303 """ 304 - import os 305 - import logging 306 - from letta_client import Letta 307 - 308 logger = logging.getLogger(__name__) 309 310 try: 311 - client = Letta(token=os.environ["LETTA_API_KEY"]) 312 313 # Sanitize handle for block label 314 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') ··· 367 Returns: 368 String containing the user's memory block content 369 """ 370 - import os 371 - import logging 372 - from letta_client import Letta 373 - 374 logger = logging.getLogger(__name__) 375 376 try: 377 - client = Letta(token=os.environ["LETTA_API_KEY"]) 378 379 # Sanitize handle for block label 380 clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_')
··· 1 """Block management tools for user-specific memory blocks.""" 2 from pydantic import BaseModel, Field 3 from typing import List, Dict, Any 4 + import logging 5 + 6 + def 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 20 class AttachUserBlocksArgs(BaseModel): ··· 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 ··· 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 ··· 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(' ', '_') ··· 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(' ', '_') ··· 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(' ', '_') ··· 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(' ', '_')