+4
.env.example
+4
.env.example
+1
.gitignore
+1
.gitignore
+159
CONFIG.md
+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
+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
+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
+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
+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
+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
+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
+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
+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(' ', '_')