"""Main entry point for debate bots application.""" import sys import argparse from pathlib import Path from rich.prompt import Prompt, Confirm from .config import Config from .providers import OpenRouterProvider, LMStudioProvider from .agent import DebateAgent from .debate import DebateOrchestrator from . import ui from .constants import ( DEFAULT_EXCHANGES_PER_ROUND, DEFAULT_CONFIG_FILE, LMSTUDIO_DEFAULT_BASE_URL, ) from .exceptions import ConfigError, ProviderError, DebateError from .logger import setup_logging, get_logger # Initialize logger (will be configured in main) logger = get_logger(__name__) def format_model_name(model: str) -> str: """ Format model name for display as agent name. Examples: 'anthropic/claude-3-haiku' -> 'Claude-3-Haiku' 'meta-llama/llama-3.1-8b-instruct' -> 'Llama-3.1-8B-Instruct' 'gemini-pro' -> 'Gemini-Pro' Args: model: Full model identifier Returns: Formatted model name suitable for display """ # Extract model name after provider prefix (if any) if '/' in model: model = model.split('/')[-1] # Capitalize words and replace common separators parts = model.replace('-', ' ').replace('_', ' ').split() formatted_parts = [] for part in parts: # Keep version numbers and sizes as-is (e.g., "3.1", "8b") if any(char.isdigit() for char in part): formatted_parts.append(part.upper() if part.endswith('b') else part) else: formatted_parts.append(part.capitalize()) return '-'.join(formatted_parts) def create_provider(config: Config, agent_key: str): """ Create an LLM provider from configuration. Args: config: Configuration object agent_key: Agent configuration key Returns: LLM provider instance Raises: ConfigError: If provider configuration is invalid """ provider_type = config.get(f"{agent_key}.provider") model = config.get(f"{agent_key}.model") logger.debug(f"Creating provider for {agent_key}: type={provider_type}, model={model}") if provider_type == "openrouter": api_key = config.get(f"{agent_key}.api_key") if not api_key: raise ConfigError( f"API key not found for {agent_key}. " "Set OPENROUTER_API_KEY environment variable or add to config." ) return OpenRouterProvider(model=model, api_key=api_key) elif provider_type == "lmstudio": base_url = config.get(f"{agent_key}.base_url", LMSTUDIO_DEFAULT_BASE_URL) return LMStudioProvider(model=model, base_url=base_url) else: raise ConfigError(f"Unknown provider type: {provider_type}") def create_agent(config: Config, agent_key: str, name: str = None, max_memory_tokens: int = None) -> DebateAgent: """ Create a debate agent from configuration. Args: config: Configuration object agent_key: Agent configuration key name: Optional agent name (defaults to formatted model name) max_memory_tokens: Optional max memory tokens (from CLI or config) Returns: DebateAgent instance Raises: ConfigError: If agent configuration is invalid """ provider = create_provider(config, agent_key) system_prompt = config.get(f"{agent_key}.system_prompt") # Use model name if no name provided if name is None: model = config.get(f"{agent_key}.model") name = format_model_name(model) # Get max_memory_tokens from config if not provided if max_memory_tokens is None: max_memory_tokens = config.get(f"{agent_key}.max_memory_tokens") agent_kwargs = { "name": name, "provider": provider, "system_prompt": system_prompt, } if max_memory_tokens is not None: agent_kwargs["max_memory_tokens"] = max_memory_tokens logger.info(f"Creating agent: {name} (provider={provider.__class__.__name__})") return DebateAgent(**agent_kwargs) def setup_configuration(config_path: str = DEFAULT_CONFIG_FILE) -> Config: """ Set up configuration from file or interactive prompts. Args: config_path: Path to configuration file Returns: Config object Raises: ConfigError: If configuration is invalid """ logger.info(f"Loading configuration from: {config_path}") # Check for config file if not Path(config_path).exists(): ui.print_info( f"No config file found at {config_path}. " "Starting interactive setup..." ) config = Config() else: config = Config(config_path) # Define required configuration keys required_keys = { "agent1.provider": { "prompt": "Agent 1 provider (openrouter/lmstudio)", "default": "openrouter", }, "agent1.model": { "prompt": "Agent 1 model", "default": "anthropic/claude-3-haiku", }, "agent1.system_prompt": { "prompt": "Agent 1 system prompt", "default": "You are a skilled debater who argues with logic and evidence.", }, "agent2.provider": { "prompt": "Agent 2 provider (openrouter/lmstudio)", "default": "openrouter", }, "agent2.model": { "prompt": "Agent 2 model", "default": "anthropic/claude-3-haiku", }, "agent2.system_prompt": { "prompt": "Agent 2 system prompt", "default": "You are a persuasive debater who uses rhetoric and reasoning.", }, } # Prompt for missing values config.prompt_for_missing(required_keys) # Prompt for API keys if using OpenRouter if config.get("agent1.provider") == "openrouter": if not config.get("agent1.api_key"): config.prompt_for_missing({ "agent1.api_key": { "prompt": "Agent 1 OpenRouter API key", "secret": True, } }) if config.get("agent2.provider") == "openrouter": if not config.get("agent2.api_key"): config.prompt_for_missing({ "agent2.api_key": { "prompt": "Agent 2 OpenRouter API key", "secret": True, } }) # Prompt for LMStudio URLs if needed if config.get("agent1.provider") == "lmstudio": if not config.get("agent1.base_url"): config.prompt_for_missing({ "agent1.base_url": { "prompt": "Agent 1 LMStudio URL", "default": "http://localhost:1234/v1", } }) if config.get("agent2.provider") == "lmstudio": if not config.get("agent2.base_url"): config.prompt_for_missing({ "agent2.base_url": { "prompt": "Agent 2 LMStudio URL", "default": "http://localhost:1234/v1", } }) # Validate configuration try: config.validate_agent_config("agent1") config.validate_agent_config("agent2") logger.info("Configuration validated successfully") except ConfigError as e: ui.print_error(f"Configuration error: {str(e)}") logger.error(f"Configuration validation failed: {e}") sys.exit(1) # Ask if user wants to save config if not Path(config_path).exists(): if Confirm.ask("Save this configuration?", default=True): config.save_to_file(config_path) return config def run_debate_loop(orchestrator: DebateOrchestrator, agent_for, agent_against, auto_save: bool = True): """ Run the debate loop with user interaction. Args: orchestrator: DebateOrchestrator instance agent_for: Agent arguing 'for' agent_against: Agent arguing 'against' auto_save: Whether to auto-save after each round Raises: DebateError: If debate encounters an error """ while True: # Run a round of exchanges ui.print_info( f"Starting round with {orchestrator.exchanges_per_round} exchanges..." ) try: # Run the round (exchanges are displayed as they happen) orchestrator.run_round(agent_for, agent_against) # Auto-save after each round if enabled if auto_save: try: filepath = orchestrator.save_debate() logger.debug(f"Auto-saved debate to {filepath}") except Exception as e: logger.warning(f"Auto-save failed: {e}") # Don't break the debate for auto-save failures except ProviderError as e: ui.print_error(f"Provider error during debate: {str(e)}") logger.error(f"Provider error: {e}", exc_info=True) # Try to save before breaking if auto_save: try: filepath = orchestrator.save_debate() ui.print_info(f"Debate auto-saved to {filepath}") except Exception: pass break except Exception as e: ui.print_error(f"Error during debate: {str(e)}") logger.error(f"Unexpected error during debate: {e}", exc_info=True) # Try to save before breaking if auto_save: try: filepath = orchestrator.save_debate() ui.print_info(f"Debate auto-saved to {filepath}") except Exception: pass break # Round complete - pause for user input ui.print_round_complete(orchestrator.current_exchange) # Show round statistics stats = orchestrator.get_statistics() ui.print_round_statistics(stats) action = ui.get_user_action() if action == "continue": ui.print_info("Continuing debate...") continue elif action == "settle": conclusion = ui.get_custom_instruction() ui.print_info(f"Debate settled: {conclusion}") break elif action == "instruct": instruction = ui.get_custom_instruction() ui.print_info(f"Custom instruction: {instruction}") # Add instruction to both agents agent_for.add_message("user", instruction) agent_against.add_message("user", instruction) continue elif action == "quit": break # Show final statistics ui.print_separator() stats = orchestrator.get_statistics() ui.print_statistics(stats) # Ask if user wants to save the debate (or it's already auto-saved) if not auto_save: if Confirm.ask("\nSave debate history?", default=True): try: filepath = orchestrator.save_debate() ui.print_success(f"Debate saved to {filepath}") except Exception as e: ui.print_error(f"Failed to save debate: {str(e)}") else: ui.print_info("Debate has been automatically saved.") def parse_args(): """ Parse command line arguments. Returns: Parsed arguments """ parser = argparse.ArgumentParser( description="Debate Bots - LLM-powered debate application", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s # Interactive mode with config.yaml %(prog)s --config myconfig.yaml # Use custom config file %(prog)s --topic "AI is beneficial" # Start debate with specific topic %(prog)s --exchanges 5 # Run 5 exchanges per round %(prog)s --no-auto-save # Disable automatic saving %(prog)s --log-level DEBUG # Enable debug logging %(prog)s --log-file debate.log # Log to file """ ) parser.add_argument( "--config", "-c", default=DEFAULT_CONFIG_FILE, help=f"Path to configuration file (default: {DEFAULT_CONFIG_FILE})" ) parser.add_argument( "--topic", "-t", help="Debate topic (if not provided, will prompt)" ) parser.add_argument( "--exchanges", "-e", type=int, default=DEFAULT_EXCHANGES_PER_ROUND, help=f"Number of exchanges per round (default: {DEFAULT_EXCHANGES_PER_ROUND})" ) parser.add_argument( "--no-auto-save", action="store_true", help="Disable automatic debate saving after each round" ) parser.add_argument( "--log-level", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], help="Logging level (default: from environment or INFO)" ) parser.add_argument( "--log-file", help="Log to file (default: console only)" ) parser.add_argument( "--max-memory-tokens", type=int, help="Maximum tokens to keep in agent memory" ) return parser.parse_args() def main(): """Main application entry point.""" # Parse command line arguments args = parse_args() # Set up logging setup_logging(log_level=args.log_level, log_file=args.log_file) logger.info("=" * 50) logger.info("Starting Debate Bots application") logger.info(f"Config file: {args.config}") logger.info(f"Exchanges per round: {args.exchanges}") logger.info(f"Auto-save: {not args.no_auto_save}") logger.info("=" * 50) ui.print_header() # Set up configuration try: config = setup_configuration(args.config) except ConfigError as e: ui.print_error(f"Configuration error: {str(e)}") logger.error(f"Failed to load configuration: {e}") sys.exit(1) # Create agents (names automatically derived from model names) try: ui.print_info("Creating debate agents...") agent1 = create_agent(config, "agent1", max_memory_tokens=args.max_memory_tokens) agent2 = create_agent(config, "agent2", max_memory_tokens=args.max_memory_tokens) ui.print_success(f"Agents created: {agent1.name} vs {agent2.name}") except ConfigError as e: ui.print_error(f"Configuration error: {str(e)}") logger.error(f"Failed to create agents: {e}") sys.exit(1) except ProviderError as e: ui.print_error(f"Provider error: {str(e)}") logger.error(f"Provider error during agent creation: {e}") sys.exit(1) except Exception as e: ui.print_error(f"Failed to create agents: {str(e)}") logger.error(f"Unexpected error creating agents: {e}", exc_info=True) sys.exit(1) # Create orchestrator orchestrator = DebateOrchestrator( agent1=agent1, agent2=agent2, exchanges_per_round=args.exchanges, ) # Get debate topic ui.print_separator() if args.topic: topic = args.topic logger.info(f"Using topic from command line: {topic}") else: topic = Prompt.ask("\n[bold cyan]Enter the debate topic[/bold cyan]") # Start debate ui.print_topic(topic) ui.print_info("Assigning positions (coin flip)...") try: agent_for, agent_against = orchestrator.start_debate(topic) ui.print_position_assignment(agent_for.name, agent_against.name) # Run the debate loop auto_save = not args.no_auto_save run_debate_loop(orchestrator, agent_for, agent_against, auto_save=auto_save) except Exception as e: ui.print_error(f"Error during debate: {str(e)}") logger.error(f"Error during debate: {e}", exc_info=True) # Try to save debate even on error try: filepath = orchestrator.save_debate() ui.print_info(f"Debate saved to {filepath}") except Exception: pass sys.exit(1) # Farewell ui.print_separator() ui.print_info("Thanks for using Debate Bots!") logger.info("Debate Bots session ended") if __name__ == "__main__": main()