···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
···2829void 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.
3031-Getting Started:
32-[Further sections on installation, configuration, and contribution guidelines would go here, which are beyond void's current capabilities to generate automatically.]
000000000000000000000003334Contact:
35For inquiries, please contact @cameron.pfiffer.org on Bluesky.
···2829void 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.
3031+## 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+```
5657Contact:
58For inquiries, please contact @cameron.pfiffer.org on Bluesky.
+58-44
bsky.py
···2021import bsky_utils
22from tools.blocks import attach_user_blocks, detach_user_blocks
0000000002324def extract_handles_from_data(data):
25 """Recursively extract all unique handles from nested data structure."""
···41 _extract_recursive(data)
42 return list(handles)
4344-# Configure logging
45-logging.basicConfig(
46- level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
47-)
48logger = logging.getLogger("void_bot")
49-logger.setLevel(logging.INFO)
5051-# 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-5859# 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)
6465-# Use the "Bluesky" project
66-PROJECT_ID = "5ec33d52-ab14-4fd6-91b5-9dbc43e888a8"
6768# Notification check delay
69-FETCH_NOTIFICATIONS_DELAY_SEC = 30
7071# Queue directory
72-QUEUE_DIR = Path("queue")
73QUEUE_DIR.mkdir(exist_ok=True)
74-QUEUE_ERROR_DIR = Path("queue/errors")
75QUEUE_ERROR_DIR.mkdir(exist_ok=True, parents=True)
76-QUEUE_NO_REPLY_DIR = Path("queue/no_reply")
77QUEUE_NO_REPLY_DIR.mkdir(exist_ok=True, parents=True)
78-PROCESSED_NOTIFICATIONS_FILE = Path("queue/processed_notifications.json")
7980# Maximum number of processed notifications to track
81-MAX_PROCESSED_NOTIFICATIONS = 10000
8283# Message tracking counters
84message_counters = defaultdict(int)
···137def initialize_void():
138 logger.info("Starting void agent initialization...")
139000140 # Ensure that a shared zeitgeist block exists
141 logger.info("Creating/updating zeitgeist block...")
0142 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 )
148149 # Ensure that a shared void personality block exists
150 logger.info("Creating/updating void-persona block...")
0151 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 )
157158 # Ensure that a shared void human block exists
159 logger.info("Creating/updating void-humans block...")
0160 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 )
166167 # 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 )
183184 # 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 )
346347 # Collect the streaming response
···759760 # 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_"
0763764 # 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
919920 logger.info("Fetching all unread notifications...")
921
···2021import bsky_utils
22from 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+)
3233def extract_handles_from_data(data):
34 """Recursively extract all unique handles from nested data structure."""
···50 _extract_recursive(data)
51 return list(handles)
5253+# Initialize configuration and logging
54+config = get_config()
55+config.setup_logging()
056logger = logging.getLogger("void_bot")
05758+# 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()
6566# Create a client with extended timeout for LLM operations
67+CLIENT = Letta(
68+ token=letta_config['api_key'],
69+ timeout=letta_config['timeout']
70)
7172+# Use the configured project ID
73+PROJECT_ID = letta_config['project_id']
7475# Notification check delay
76+FETCH_NOTIFICATIONS_DELAY_SEC = bot_config['fetch_notifications_delay']
7778# Queue directory
79+QUEUE_DIR = Path(queue_config['base_dir'])
80QUEUE_DIR.mkdir(exist_ok=True)
81+QUEUE_ERROR_DIR = Path(queue_config['error_dir'])
82QUEUE_ERROR_DIR.mkdir(exist_ok=True, parents=True)
83+QUEUE_NO_REPLY_DIR = Path(queue_config['no_reply_dir'])
84QUEUE_NO_REPLY_DIR.mkdir(exist_ok=True, parents=True)
85+PROCESSED_NOTIFICATIONS_FILE = Path(queue_config['processed_file'])
8687# Maximum number of processed notifications to track
88+MAX_PROCESSED_NOTIFICATIONS = bot_config['max_processed_notifications']
8990# Message tracking counters
91message_counters = defaultdict(int)
···144def initialize_void():
145 logger.info("Starting void agent initialization...")
146147+ # 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 )
159160 # 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 )
169170 # 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 )
179180 # 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 )
196197 # 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 )
359360 # Collect the streaming response
···772773 # 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_"
777778 # 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
933934 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())
210211-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."
···236237238def default_login() -> Client:
239- username = os.getenv("BSKY_USERNAME")
240- password = os.getenv("BSKY_PASSWORD")
0000000000241242- if username is None:
243- logger.error(
244- "No username provided. Please provide a username using the BSKY_USERNAME environment variable."
245- )
246- exit()
247248- if password is None:
249- logger.error(
250- "No password provided. Please provide a password using the BSKY_PASSWORD environment variable."
251- )
252- exit()
253254- return init_client(username, password)
255256def remove_outside_quotes(text: str) -> str:
257 """
···208 logger.debug(f"Saving changed session for {username}")
209 save_session(username, session.export())
210211+def init_client(username: str, password: str, pds_uri: str = "https://bsky.social") -> Client:
0212 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."
···235236237def 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")
250251+ 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()
256257+ 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()
262263+ return init_client(username, password, pds_uri)
264265def remove_outside_quotes(text: str) -> str:
266 """
···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