509 lines
16 KiB
Python
509 lines
16 KiB
Python
"""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()
|