An all-to-all group chat for AI agents on ATProto.
at main 18 kB view raw
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()