+4
.env.example
+4
.env.example
+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
28
29
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
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.]
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
+
```
33
56
34
57
Contact:
35
58
For inquiries, please contact @cameron.pfiffer.org on Bluesky.
+58
-44
bsky.py
+58
-44
bsky.py
···
20
20
21
21
import bsky_utils
22
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
+
)
23
32
24
33
def extract_handles_from_data(data):
25
34
"""Recursively extract all unique handles from nested data structure."""
···
41
50
_extract_recursive(data)
42
51
return list(handles)
43
52
44
-
# Configure logging
45
-
logging.basicConfig(
46
-
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
47
-
)
53
+
# Initialize configuration and logging
54
+
config = get_config()
55
+
config.setup_logging()
48
56
logger = logging.getLogger("void_bot")
49
-
logger.setLevel(logging.INFO)
50
57
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
+
# 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()
58
65
59
66
# 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
67
+
CLIENT = Letta(
68
+
token=letta_config['api_key'],
69
+
timeout=letta_config['timeout']
63
70
)
64
71
65
-
# Use the "Bluesky" project
66
-
PROJECT_ID = "5ec33d52-ab14-4fd6-91b5-9dbc43e888a8"
72
+
# Use the configured project ID
73
+
PROJECT_ID = letta_config['project_id']
67
74
68
75
# Notification check delay
69
-
FETCH_NOTIFICATIONS_DELAY_SEC = 30
76
+
FETCH_NOTIFICATIONS_DELAY_SEC = bot_config['fetch_notifications_delay']
70
77
71
78
# Queue directory
72
-
QUEUE_DIR = Path("queue")
79
+
QUEUE_DIR = Path(queue_config['base_dir'])
73
80
QUEUE_DIR.mkdir(exist_ok=True)
74
-
QUEUE_ERROR_DIR = Path("queue/errors")
81
+
QUEUE_ERROR_DIR = Path(queue_config['error_dir'])
75
82
QUEUE_ERROR_DIR.mkdir(exist_ok=True, parents=True)
76
-
QUEUE_NO_REPLY_DIR = Path("queue/no_reply")
83
+
QUEUE_NO_REPLY_DIR = Path(queue_config['no_reply_dir'])
77
84
QUEUE_NO_REPLY_DIR.mkdir(exist_ok=True, parents=True)
78
-
PROCESSED_NOTIFICATIONS_FILE = Path("queue/processed_notifications.json")
85
+
PROCESSED_NOTIFICATIONS_FILE = Path(queue_config['processed_file'])
79
86
80
87
# Maximum number of processed notifications to track
81
-
MAX_PROCESSED_NOTIFICATIONS = 10000
88
+
MAX_PROCESSED_NOTIFICATIONS = bot_config['max_processed_notifications']
82
89
83
90
# Message tracking counters
84
91
message_counters = defaultdict(int)
···
137
144
def initialize_void():
138
145
logger.info("Starting void agent initialization...")
139
146
147
+
# Get block configurations
148
+
blocks_config = agent_config['blocks']
149
+
140
150
# Ensure that a shared zeitgeist block exists
141
151
logger.info("Creating/updating zeitgeist block...")
152
+
zeitgeist_config = blocks_config.get('zeitgeist', {})
142
153
zeigeist_block = upsert_block(
143
154
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."
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.")
147
158
)
148
159
149
160
# Ensure that a shared void personality block exists
150
161
logger.info("Creating/updating void-persona block...")
162
+
persona_config = blocks_config.get('persona', {})
151
163
persona_block = upsert_block(
152
164
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."
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.")
156
168
)
157
169
158
170
# Ensure that a shared void human block exists
159
171
logger.info("Creating/updating void-humans block...")
172
+
humans_config = blocks_config.get('humans', {})
160
173
human_block = upsert_block(
161
174
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."
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.")
165
178
)
166
179
167
180
# Create the agent if it doesn't exist
168
181
logger.info("Creating/updating void agent...")
169
182
void_agent = upsert_agent(
170
183
CLIENT,
171
-
name = "void",
172
-
block_ids = [
184
+
name=agent_config['name'],
185
+
block_ids=[
173
186
persona_block.id,
174
187
human_block.id,
175
188
zeigeist_block.id,
176
189
],
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
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
182
195
)
183
196
184
197
# Export agent state
···
236
249
try:
237
250
thread = atproto_client.app.bsky.feed.get_post_thread({
238
251
'uri': uri,
239
-
'parent_height': 40,
240
-
'depth': 10
252
+
'parent_height': threading_config['parent_height'],
253
+
'depth': threading_config['depth']
241
254
})
242
255
except Exception as e:
243
256
error_str = str(e)
···
341
354
agent_id=void_agent.id,
342
355
messages=[{"role": "user", "content": prompt}],
343
356
stream_tokens=False, # Step streaming only (faster than token streaming)
344
-
max_steps=100
357
+
max_steps=agent_config['max_steps']
345
358
)
346
359
347
360
# Collect the streaming response
···
759
772
760
773
# Determine priority based on author handle
761
774
author_handle = getattr(notification.author, 'handle', '') if hasattr(notification, 'author') else ''
762
-
priority_prefix = "0_" if author_handle == "cameron.pfiffer.org" else "1_"
775
+
priority_users = queue_config['priority_users']
776
+
priority_prefix = "0_" if author_handle in priority_users else "1_"
763
777
764
778
# Create filename with priority, timestamp and hash
765
779
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
···
915
929
all_notifications = []
916
930
cursor = None
917
931
page_count = 0
918
-
max_pages = 20 # Safety limit to prevent infinite loops
932
+
max_pages = bot_config['max_notification_pages'] # Safety limit to prevent infinite loops
919
933
920
934
logger.info("Fetching all unread notifications...")
921
935
+24
-15
bsky_utils.py
+24
-15
bsky_utils.py
···
208
208
logger.debug(f"Saving changed session for {username}")
209
209
save_session(username, session.export())
210
210
211
-
def init_client(username: str, password: str) -> Client:
212
-
pds_uri = os.getenv("PDS_URI")
211
+
def init_client(username: str, password: str, pds_uri: str = "https://bsky.social") -> Client:
213
212
if pds_uri is None:
214
213
logger.warning(
215
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."
···
236
235
237
236
238
237
def default_login() -> Client:
239
-
username = os.getenv("BSKY_USERNAME")
240
-
password = os.getenv("BSKY_PASSWORD")
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")
241
250
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()
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()
247
256
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()
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()
253
262
254
-
return init_client(username, password)
263
+
return init_client(username, password, pds_uri)
255
264
256
265
def remove_outside_quotes(text: str) -> str:
257
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
1
"""Block management tools for user-specific memory blocks."""
2
2
from pydantic import BaseModel, Field
3
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"])
4
18
5
19
6
20
class AttachUserBlocksArgs(BaseModel):
···
43
57
Returns:
44
58
String with attachment results for each handle
45
59
"""
46
-
import os
47
-
import logging
48
-
from letta_client import Letta
49
-
50
60
logger = logging.getLogger(__name__)
51
61
52
62
handles = list(set(handles))
53
63
54
64
try:
55
-
client = Letta(token=os.environ["LETTA_API_KEY"])
65
+
client = get_letta_client()
56
66
results = []
57
67
58
68
# Get current blocks using the API
···
117
127
Returns:
118
128
String with detachment results for each handle
119
129
"""
120
-
import os
121
-
import logging
122
-
from letta_client import Letta
123
-
124
130
logger = logging.getLogger(__name__)
125
131
126
132
try:
127
-
client = Letta(token=os.environ["LETTA_API_KEY"])
133
+
client = get_letta_client()
128
134
results = []
129
135
130
136
# Build mapping of block labels to IDs using the API
···
174
180
Returns:
175
181
String confirming the note was appended
176
182
"""
177
-
import os
178
-
import logging
179
-
from letta_client import Letta
180
-
181
183
logger = logging.getLogger(__name__)
182
184
183
185
try:
184
-
client = Letta(token=os.environ["LETTA_API_KEY"])
186
+
client = get_letta_client()
185
187
186
188
# Sanitize handle for block label
187
189
clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_')
···
247
249
Returns:
248
250
String confirming the text was replaced
249
251
"""
250
-
import os
251
-
import logging
252
-
from letta_client import Letta
253
-
254
252
logger = logging.getLogger(__name__)
255
253
256
254
try:
257
-
client = Letta(token=os.environ["LETTA_API_KEY"])
255
+
client = get_letta_client()
258
256
259
257
# Sanitize handle for block label
260
258
clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_')
···
301
299
Returns:
302
300
String confirming the content was set
303
301
"""
304
-
import os
305
-
import logging
306
-
from letta_client import Letta
307
-
308
302
logger = logging.getLogger(__name__)
309
303
310
304
try:
311
-
client = Letta(token=os.environ["LETTA_API_KEY"])
305
+
client = get_letta_client()
312
306
313
307
# Sanitize handle for block label
314
308
clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_')
···
367
361
Returns:
368
362
String containing the user's memory block content
369
363
"""
370
-
import os
371
-
import logging
372
-
from letta_client import Letta
373
-
374
364
logger = logging.getLogger(__name__)
375
365
376
366
try:
377
-
client = Letta(token=os.environ["LETTA_API_KEY"])
367
+
client = get_letta_client()
378
368
379
369
# Sanitize handle for block label
380
370
clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_')