"""CLI entry point that launches the evaluation console web server. Run alongside the existing main.py CLI; both share the same rag_eval library and the same runs/ artifacts. Example: python webmain.py python webmain.py --host 0.0.0.0 --port 8800 python webmain.py --host 0.0.0.0 --port 8800 --log-level debug """ from __future__ import annotations import argparse import logging import logging.config from datetime import datetime from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent def _build_log_config(log_file: Path, level: str) -> dict: """Build a uvicorn-compatible logging config dict. Writes to both stderr (console) and a rotating daily log file. All webapp.* and rag_eval.* loggers inherit from root so every logger.info() call in the API routes is captured. """ level_upper = level.upper() return { "version": 1, "disable_existing_loggers": False, "formatters": { "detailed": { "format": "%(asctime)s %(levelname)-8s %(name)s %(message)s", "datefmt": "%Y-%m-%d %H:%M:%S", }, "console": { "format": "%(asctime)s %(levelname)-8s %(name)-30s %(message)s", "datefmt": "%H:%M:%S", }, }, "handlers": { "console": { "class": "logging.StreamHandler", "stream": "ext://sys.stderr", "formatter": "console", "level": level_upper, }, "file": { "class": "logging.handlers.RotatingFileHandler", "filename": str(log_file), "maxBytes": 50 * 1024 * 1024, # 50 MB per file "backupCount": 7, # keep 7 rotated files "encoding": "utf-8", "formatter": "detailed", "level": "DEBUG", # file always captures everything }, }, "loggers": { # Our application loggers — detailed level "webapp": {"handlers": ["console", "file"], "level": level_upper, "propagate": False}, "rag_eval": {"handlers": ["console", "file"], "level": level_upper, "propagate": False}, # uvicorn access log — captured to file, shown on console "uvicorn.access": {"handlers": ["console", "file"], "level": "INFO", "propagate": False}, "uvicorn.error": {"handlers": ["console", "file"], "level": "INFO", "propagate": False}, "uvicorn": {"handlers": ["console", "file"], "level": "INFO", "propagate": False}, }, "root": { "handlers": ["console", "file"], "level": "WARNING", # suppress noisy third-party libs at WARNING }, } def parse_args() -> argparse.Namespace: """Parse host/port/reload options for the console server.""" parser = argparse.ArgumentParser(description="Launch the RAGAS evaluation console.") parser.add_argument("--host", default="127.0.0.1", help="Bind host (default 127.0.0.1).") parser.add_argument("--port", type=int, default=8800, help="Bind port (default 8800).") parser.add_argument( "--reload", action="store_true", help="Enable auto-reload for local development.", ) parser.add_argument( "--log-level", default="info", choices=["debug", "info", "warning", "error"], help="Console log level (default: info). File always captures DEBUG.", ) parser.add_argument( "--log-file", default=None, help="Log file path (default: logs/server_YYYY-MM-DD.log).", ) return parser.parse_args() def main() -> None: """Start uvicorn with the configured application and logging.""" import uvicorn args = parse_args() # Resolve log file path logs_dir = REPO_ROOT / "logs" logs_dir.mkdir(parents=True, exist_ok=True) if args.log_file: log_file = Path(args.log_file) else: date_str = datetime.now().strftime("%Y-%m-%d") log_file = logs_dir / f"server_{date_str}.log" log_config = _build_log_config(log_file, args.log_level) # Apply config before uvicorn starts so our loggers are ready immediately logging.config.dictConfig(log_config) logger = logging.getLogger("webapp.server") logger.info( "Starting RAGAS Console host=%s port=%d log_level=%s log_file=%s", args.host, args.port, args.log_level, log_file, ) uvicorn.run( "webapp.server:app", host=args.host, port=args.port, reload=args.reload, log_config=log_config, # hand our config to uvicorn so it uses same handlers ) if __name__ == "__main__": main()