diff --git a/ra_aid/__main__.py b/ra_aid/__main__.py index 24b941e..748acd7 100644 --- a/ra_aid/__main__.py +++ b/ra_aid/__main__.py @@ -55,6 +55,15 @@ def launch_webui(host: str, port: int): def parse_arguments(args=None): ANTHROPIC_DEFAULT_MODEL = "claude-3-7-sonnet-20250219" OPENAI_DEFAULT_MODEL = "gpt-4o" + + # Case-insensitive log level argument type + def log_level_type(value): + value = value.lower() + if value not in ["debug", "info", "warning", "error", "critical"]: + raise argparse.ArgumentTypeError( + f"Invalid log level: {value}. Choose from debug, info, warning, error, critical." + ) + return value parser = argparse.ArgumentParser( description="RA.Aid - AI Agent for executing programming and research tasks", @@ -148,6 +157,12 @@ Examples: parser.add_argument( "--pretty-logger", action="store_true", help="Enable pretty logging output" ) + parser.add_argument( + "--log-level", + type=log_level_type, + default="warning", + help="Set specific logging level (case-insensitive, overrides --verbose)", + ) parser.add_argument( "--temperature", type=float, @@ -333,7 +348,7 @@ def build_status( def main(): """Main entry point for the ra-aid command line tool.""" args = parse_arguments() - setup_logging(args.verbose, args.pretty_logger) + setup_logging(args.verbose, args.pretty_logger, args.log_level) logger.debug("Starting RA.Aid with arguments: %s", args) # Launch web interface if requested diff --git a/ra_aid/logging_config.py b/ra_aid/logging_config.py index 781b14f..1af5ae4 100644 --- a/ra_aid/logging_config.py +++ b/ra_aid/logging_config.py @@ -1,7 +1,12 @@ import logging +import os import sys +from datetime import datetime +from logging.handlers import RotatingFileHandler +from pathlib import Path from typing import Optional + from rich.console import Console from rich.markdown import Markdown from rich.panel import Panel @@ -36,21 +41,89 @@ class PrettyHandler(logging.Handler): self.handleError(record) -def setup_logging(verbose: bool = False, pretty: bool = False) -> None: +def setup_logging(verbose: bool = False, pretty: bool = False, log_level: Optional[str] = None) -> None: + """ + Configure logging for ra_aid. + + Args: + verbose: Set to True to enable verbose logging (implies DEBUG level if log_level not specified) + pretty: Set to True to enable pretty console logging + log_level: Optional explicit log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + Takes precedence over verbose flag if specified + """ + # Create .ra-aid/logs directory if it doesn't exist + cwd = os.getcwd() + ra_aid_dir_str = os.path.join(cwd, ".ra-aid") + logs_dir_str = os.path.join(ra_aid_dir_str, "logs") + + # Create directory structure + for directory in [ra_aid_dir_str, logs_dir_str]: + path = Path(directory) + if not path.exists(): + try: + path.mkdir(mode=0o755, parents=True, exist_ok=True) + except Exception as e: + print(f"Warning: Failed to create log directory {directory}: {str(e)}") + + # Determine log level + if log_level is not None: + # Use provided log level if specified (case-insensitive) + numeric_level = getattr(logging, log_level.upper(), None) + if not isinstance(numeric_level, int): + # If invalid log level is provided, fall back to default + print(f"Invalid log level: {log_level}") + numeric_level = logging.DEBUG if verbose else logging.WARNING + else: + # Use verbose flag to determine log level + numeric_level = logging.DEBUG if verbose else logging.WARNING + + # Configure root logger logger = logging.getLogger("ra_aid") - logger.setLevel(logging.DEBUG if verbose else logging.WARNING) - - if not logger.handlers: - if pretty: - handler = PrettyHandler() - else: - handler = logging.StreamHandler(sys.stdout) - formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) - handler.setFormatter(formatter) - logger.addHandler(handler) + logger.setLevel(numeric_level) + + # Clear existing handlers to avoid duplicates + if logger.handlers: + logger.handlers.clear() + + # Create console handler + if pretty: + console_handler = PrettyHandler() + else: + console_handler = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + console_handler.setFormatter(formatter) + + # Add console handler to logger + logger.addHandler(console_handler) + + # Create file handler with rotation + try: + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + log_filename = os.path.join(logs_dir_str, f"ra_aid_{timestamp}.log") + + # RotatingFileHandler with 5MB max size and 100 backup files + file_handler = RotatingFileHandler( + log_filename, + maxBytes=5 * 1024 * 1024, # 5MB + backupCount=100, + encoding="utf-8" + ) + + file_formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + file_handler.setFormatter(file_formatter) + file_handler.setLevel(numeric_level) + + # Add file handler to logger + logger.addHandler(file_handler) + + logger.info(f"Log file created: {log_filename}") + except Exception as e: + logger.error(f"Failed to set up file logging: {str(e)}") def get_logger(name: Optional[str] = None) -> logging.Logger: - return logging.getLogger(f"ra_aid.{name}" if name else "ra_aid") + return logging.getLogger(f"ra_aid.{name}" if name else "ra_aid") \ No newline at end of file