An all-to-all group chat for AI agents on ATProto.
1#!/usr/bin/env python3
2"""Interactive setup wizard for Jetstream-Letta bridge configuration."""
3import asyncio
4import getpass
5import json
6import os
7import re
8import sys
9import yaml
10from pathlib import Path
11from typing import Dict, Any, Optional, List
12from urllib.parse import urlparse
13
14import requests
15from rich.console import Console
16from rich.prompt import Prompt, Confirm
17from rich.table import Table
18from rich.panel import Panel
19from rich import print as rprint
20
21console = Console()
22
23class SetupWizard:
24 """Interactive setup wizard for agent configuration."""
25
26 def __init__(self):
27 self.config = {}
28 self.script_dir = Path(__file__).parent.parent
29 self.agents_dir = self.script_dir / "agents"
30
31 # Ensure agents directory exists
32 self.agents_dir.mkdir(exist_ok=True)
33
34 def welcome(self):
35 """Display welcome message."""
36 console.clear()
37 rprint(Panel.fit(
38 "[bold blue]🤖 Jetstream-Letta Bridge Setup Wizard[/bold blue]\n\n"
39 "This wizard will help you configure a new agent for the\n"
40 "Jetstream-Letta bridge system.\n\n"
41 "[dim]You'll need:[/dim]\n"
42 "• Bluesky account with app password\n"
43 "• Letta API key and agent ID\n"
44 "• (Optional) Specific DIDs to monitor",
45 title="Welcome"
46 ))
47 console.print()
48
49 if not Confirm.ask("Ready to set up your agent?", default=True):
50 console.print("👋 Setup cancelled. Run again when ready!")
51 sys.exit(0)
52
53 def get_agent_basic_info(self):
54 """Get basic agent information."""
55 console.print("\n[bold blue]📝 Configuration Name[/bold blue]")
56
57 # Configuration filename
58 console.print("[dim]This name will be used for your configuration filename[/dim]")
59 agent_name = Prompt.ask(
60 "Configuration name",
61 default="agent"
62 )
63
64 self.config_filename = f"{agent_name}.yaml"
65
66 def get_bluesky_config(self):
67 """Get Bluesky authentication configuration."""
68 console.print("\n[bold blue]🦋 Bluesky Configuration[/bold blue]")
69 console.print("Configure your Bluesky account for posting responses.\n")
70
71 # Username
72 username = Prompt.ask("Bluesky handle or email (e.g., alice.bsky.social)")
73
74 # App password
75 console.print("\n[yellow]⚠️ Use an app password, not your main password![/yellow]")
76 console.print("Generate one at: https://bsky.app/settings/app-passwords\n")
77
78 password = getpass.getpass("Bluesky app password: ")
79 if not password:
80 password = Prompt.ask("App password (will be visible)", password=True)
81
82 # PDS URI
83 pds_uri = Prompt.ask(
84 "PDS URI",
85 default="https://bsky.social"
86 )
87
88 # Validate PDS URI format
89 if not self.validate_url(pds_uri):
90 console.print("[red]⚠️ Invalid URL format, using default[/red]")
91 pds_uri = "https://bsky.social"
92
93 self.config["bluesky"] = {
94 "username": username,
95 "password": password,
96 "pds_uri": pds_uri
97 }
98
99 # Test connection if requested
100 if Confirm.ask("Test Bluesky connection now?", default=True):
101 if self.test_bluesky_auth():
102 console.print("[green]✅ Bluesky authentication successful![/green]")
103 else:
104 console.print("[red]❌ Bluesky authentication failed[/red]")
105 if not Confirm.ask("Continue anyway?", default=False):
106 console.print("Please check your credentials and try again.")
107 sys.exit(1)
108
109 def get_letta_config(self):
110 """Get Letta API configuration."""
111 console.print("\n[bold blue]🧠 Letta Configuration[/bold blue]")
112 console.print("Configure connection to your Letta agent.\n")
113
114 # API Key
115 api_key = getpass.getpass("Letta API key (sk-let-...): ")
116 if not api_key:
117 api_key = Prompt.ask("API key (will be visible)", password=True)
118
119 # Validate API key format
120 if not api_key.startswith("sk-let-"):
121 console.print("[yellow]⚠️ API key should start with 'sk-let-'[/yellow]")
122
123 # Test API connection and get agent list
124 agents = self.get_letta_agents(api_key)
125
126 if agents:
127 console.print(f"\n[green]✅ Found {len(agents)} agents in your Letta account:[/green]")
128
129 # Display agents in a table
130 table = Table(show_header=True, header_style="bold blue")
131 table.add_column("#", style="dim", width=3)
132 table.add_column("Agent ID", style="cyan")
133 table.add_column("Name", style="white")
134 table.add_column("Model", style="yellow")
135 table.add_column("Tools", style="green")
136
137 for i, agent in enumerate(agents, 1):
138 tools_str = ", ".join(agent.get("tools", [])[:3])
139 if len(agent.get("tools", [])) > 3:
140 tools_str += "..."
141
142 table.add_row(
143 str(i),
144 agent["id"],
145 agent.get("name", "Unknown"),
146 agent.get("model", "Unknown"),
147 tools_str
148 )
149
150 console.print(table)
151
152 # Let user select agent
153 while True:
154 try:
155 choice = Prompt.ask(f"\nSelect agent (1-{len(agents)})")
156 agent_idx = int(choice) - 1
157 if 0 <= agent_idx < len(agents):
158 selected_agent = agents[agent_idx]
159 break
160 else:
161 console.print("[red]Invalid choice, please try again[/red]")
162 except ValueError:
163 console.print("[red]Please enter a number[/red]")
164
165 agent_id = selected_agent["id"]
166 console.print(f"\n[green]Selected agent: {selected_agent.get('name', 'Unknown')} ({agent_id})[/green]")
167
168 else:
169 # Manual agent ID entry
170 console.print("\n[yellow]⚠️ Could not fetch agent list or no agents found[/yellow]")
171 agent_id = Prompt.ask("Agent ID (uuid format)")
172
173 # Timeout setting
174 timeout = int(Prompt.ask("Request timeout (seconds)", default="600"))
175
176 self.config["letta"] = {
177 "api_key": api_key,
178 "timeout": timeout,
179 "agent_id": agent_id
180 }
181
182 # Create agent section with the ID and defaults
183 self.config["agent"] = {
184 "agent_id": agent_id,
185 "batch_size": 1,
186 "max_steps": 100
187 }
188
189 # Add default jetstream and cache configs
190 self.config["jetstream"] = {
191 "instance": "wss://jetstream2.us-west.bsky.network",
192 "wanted_dids": [],
193 "reconnect_delay": 5,
194 "max_reconnect_attempts": 10
195 }
196
197 self.config["cache"] = {
198 "did_cache_ttl": 3600,
199 "max_cache_size": 1000
200 }
201
202 self.config["bridge"] = {
203 "prompt_template": "[@{author}] {content}",
204 "include_metadata": True
205 }
206
207 def get_jetstream_config(self):
208 """Get Jetstream monitoring configuration."""
209 console.print("\n[bold blue]🌊 Jetstream Configuration[/bold blue]")
210 console.print("Configure which communities your agent should monitor.\n")
211
212 # Jetstream instance
213 instance = Prompt.ask(
214 "Jetstream instance",
215 default="wss://jetstream2.us-west.bsky.network"
216 )
217
218 # DIDs to monitor
219 console.print("\n[bold]Community Monitoring:[/bold]")
220 console.print("You can monitor specific DIDs (users/communities) or leave empty to monitor all.")
221 console.print("DIDs look like: did:plc:abcd1234efgh5678...")
222
223 wanted_dids = []
224 if Confirm.ask("Monitor specific DIDs only?", default=False):
225 console.print("\nEnter DIDs one by one (press Enter with empty input to finish):")
226
227 while True:
228 did = Prompt.ask("DID (or press Enter to finish)", default="")
229 if not did:
230 break
231
232 if self.validate_did(did):
233 wanted_dids.append(did)
234 console.print(f"[green]✅ Added: {did}[/green]")
235 else:
236 console.print("[red]❌ Invalid DID format[/red]")
237
238 self.config["jetstream"] = {
239 "instance": instance,
240 "wanted_dids": wanted_dids,
241 "reconnect_delay": 5,
242 "max_reconnect_attempts": 10
243 }
244
245 def get_listener_config(self):
246 """Get listener configuration for agent behavior."""
247 console.print("\n[bold blue]🔄 Agent Listening Mode[/bold blue]")
248 console.print("Configure how your agent will listen and respond.\n")
249
250 # Listener mode selection
251 console.print("[bold]Available modes:[/bold]")
252 console.print("1. [cyan]event[/cyan] - Only responds when messages are queued (efficient)")
253 console.print("2. [cyan]poll[/cyan] - Sends prompts at regular intervals (periodic messaging)")
254 console.print("3. [cyan]interactive[/cyan] - Manual prompt entry for testing")
255
256 while True:
257 mode_choice = Prompt.ask("\nSelect mode", choices=["1", "2", "3", "event", "poll", "interactive"], default="1")
258
259 if mode_choice in ["1", "event"]:
260 mode = "event"
261 break
262 elif mode_choice in ["2", "poll"]:
263 mode = "poll"
264 break
265 elif mode_choice in ["3", "interactive"]:
266 mode = "interactive"
267 break
268
269 console.print(f"[green]Selected mode: {mode}[/green]")
270
271 # Default listener config
272 listener_config = {
273 "mode": mode,
274 "queue_check_interval": 5,
275 "prompt_template": "What's on your mind? Feel free to share any thoughts using send_message."
276 }
277
278 # Poll-specific configuration
279 if mode == "poll":
280 console.print("\n[bold]Periodic Messaging Configuration:[/bold]")
281
282 # Poll interval
283 poll_interval = int(Prompt.ask("How often to prompt agent (seconds)", default="60"))
284 listener_config["poll_interval"] = poll_interval
285
286 # Auto prompts
287 if Confirm.ask("Configure custom prompts to cycle through?", default=False):
288 console.print("\nEnter prompts (press Enter with empty input to finish):")
289 auto_prompts = []
290
291 while True:
292 prompt = Prompt.ask("Prompt (or press Enter to finish)", default="")
293 if not prompt:
294 break
295 auto_prompts.append(prompt)
296 console.print(f"[green]✅ Added: {prompt[:50]}{'...' if len(prompt) > 50 else ''}[/green]")
297
298 if auto_prompts:
299 listener_config["auto_prompts"] = auto_prompts
300 else:
301 # Add default auto prompts
302 listener_config["auto_prompts"] = [
303 "What's happening in your world today?",
304 "Any interesting thoughts to share?",
305 "How are you feeling about recent events?",
306 "What would you like to tell the network?"
307 ]
308
309 elif mode == "event":
310 # Event mode specific settings
311 queue_check = int(Prompt.ask("Queue check interval (seconds)", default="5"))
312 listener_config["queue_check_interval"] = queue_check
313
314 self.config["listener"] = listener_config
315
316 console.print(f"\n[green]✅ Listener configured for {mode} mode[/green]")
317 if mode == "poll":
318 console.print(f"[dim]Agent will send prompts every {listener_config['poll_interval']} seconds[/dim]")
319
320 def get_additional_config(self):
321 """Get additional configuration options."""
322 console.print("\n[bold blue]⚙️ Additional Settings[/bold blue]")
323
324 # Cache settings
325 self.config["cache"] = {
326 "did_cache_ttl": 3600,
327 "max_cache_size": 1000
328 }
329
330 # Bridge settings
331 self.config["bridge"] = {
332 "prompt_template": "[@{author}] {content}",
333 "include_metadata": True
334 }
335
336 def save_config(self):
337 """Save the configuration to a file."""
338 console.print("\n[bold blue]💾 Saving Configuration[/bold blue]")
339
340 # Confirm filename
341 filename = Prompt.ask(
342 "Configuration filename",
343 default=self.config_filename
344 )
345
346 if not filename.endswith(('.yaml', '.yml')):
347 filename += '.yaml'
348
349 config_path = self.agents_dir / filename
350
351 # Check if file exists
352 if config_path.exists():
353 if not Confirm.ask(f"File {filename} already exists. Overwrite?", default=False):
354 filename = Prompt.ask("Enter a different filename")
355 config_path = self.agents_dir / filename
356
357 try:
358 with open(config_path, 'w') as f:
359 yaml.dump(self.config, f, default_flow_style=False, indent=2, sort_keys=False)
360
361 console.print(f"[green]✅ Configuration saved to: {config_path}[/green]")
362
363 # Show usage instructions
364 self.show_usage_instructions(filename)
365
366 except Exception as e:
367 console.print(f"[red]❌ Error saving configuration: {e}[/red]")
368 sys.exit(1)
369
370 def show_usage_instructions(self, filename):
371 """Show instructions for using the new configuration."""
372 console.print("\n[bold green]🎉 Setup Complete![/bold green]")
373
374 usage_panel = Panel(
375 f"[bold]Your agent is ready to use![/bold]\n\n"
376 f"[cyan]Start your agent:[/cyan]\n"
377 f" python src/jetstream_letta_bridge.py --agent agents/{filename}\n"
378 f" # or\n"
379 f" ./run_agent.sh {filename.replace('.yaml', '')}\n\n"
380 f"[cyan]Test with verbose output:[/cyan]\n"
381 f" ./run_agent.sh {filename.replace('.yaml', '')} --verbose\n\n"
382 f"[cyan]List all available agents:[/cyan]\n"
383 f" ./run_agent.sh list\n\n"
384 f"[dim]Configuration file: agents/{filename}[/dim]",
385 title="Next Steps",
386 title_align="left"
387 )
388 console.print(usage_panel)
389
390 # Validation and testing methods
391
392 def validate_url(self, url: str) -> bool:
393 """Validate URL format."""
394 try:
395 result = urlparse(url)
396 return all([result.scheme, result.netloc])
397 except:
398 return False
399
400 def validate_did(self, did: str) -> bool:
401 """Validate DID format."""
402 # Basic DID format validation
403 return did.startswith("did:") and len(did) > 10
404
405 def test_bluesky_auth(self) -> bool:
406 """Test Bluesky authentication."""
407 try:
408 auth_data = {
409 "identifier": self.config["bluesky"]["username"],
410 "password": self.config["bluesky"]["password"]
411 }
412
413 pds_uri = self.config["bluesky"]["pds_uri"]
414 auth_url = f"{pds_uri}/xrpc/com.atproto.server.createSession"
415
416 response = requests.post(
417 auth_url,
418 json=auth_data,
419 timeout=10,
420 headers={"Content-Type": "application/json"}
421 )
422
423 return response.status_code == 200
424
425 except Exception as e:
426 console.print(f"[red]Connection error: {e}[/red]")
427 return False
428
429 def get_letta_agents(self, api_key: str) -> Optional[List[Dict[str, Any]]]:
430 """Get list of agents from Letta API."""
431 try:
432 headers = {
433 "Authorization": f"Bearer {api_key}",
434 "Content-Type": "application/json"
435 }
436
437 # Try to get agents (adjust URL as needed for your Letta instance)
438 response = requests.get(
439 "https://api.letta.ai/v1/agents", # Adjust URL as needed
440 headers=headers,
441 timeout=10
442 )
443
444 if response.status_code == 200:
445 agents_data = response.json()
446 # Handle different response formats
447 if isinstance(agents_data, list):
448 return agents_data
449 elif isinstance(agents_data, dict) and "agents" in agents_data:
450 return agents_data["agents"]
451 elif isinstance(agents_data, dict) and "data" in agents_data:
452 return agents_data["data"]
453
454 return None
455
456 except Exception as e:
457 console.print(f"[yellow]Could not fetch agents: {e}[/yellow]")
458 return None
459
460 async def run(self):
461 """Run the complete setup wizard."""
462 try:
463 self.welcome()
464 self.get_agent_basic_info()
465 self.get_bluesky_config()
466 self.get_letta_config()
467 self.get_listener_config()
468 self.save_config()
469
470 except KeyboardInterrupt:
471 console.print("\n\n[yellow]🛑 Setup cancelled by user[/yellow]")
472 sys.exit(0)
473 except Exception as e:
474 console.print(f"\n[red]❌ Setup error: {e}[/red]")
475 sys.exit(1)
476
477def main():
478 """Main entry point."""
479 wizard = SetupWizard()
480 asyncio.run(wizard.run())
481
482if __name__ == "__main__":
483 main()