diff --git a/python/src/aiconfig/editor/server/server.py b/python/src/aiconfig/editor/server/server.py index 32383d1c8..c44701899 100644 --- a/python/src/aiconfig/editor/server/server.py +++ b/python/src/aiconfig/editor/server/server.py @@ -25,6 +25,7 @@ OpArgs, ServerMode, ServerState, + StartServerConfig, ValidatedPath, get_http_response_load_user_parser_module, get_server_state, @@ -62,37 +63,49 @@ def run_backend_server( - edit_config: EditServerConfig, aiconfigrc_path: str + initialization_settings: StartServerConfig | EditServerConfig, + aiconfigrc_path: str, ) -> Result[str, str]: - LOGGER.setLevel(edit_config.log_level) - LOGGER.info("Edit config: %s", edit_config.model_dump_json()) + LOGGER.setLevel(initialization_settings.log_level) + LOGGER.info("Edit config: %s", initialization_settings.model_dump_json()) LOGGER.info( - f"Starting server on http://localhost:{edit_config.server_port}" + f"Starting server on http://localhost:{initialization_settings.server_port}" ) - try: - LOGGER.info( - f"Opening browser at http://localhost:{edit_config.server_port}" - ) - webbrowser.open(f"http://localhost:{edit_config.server_port}") - except Exception as e: - LOGGER.warning( - f"Failed to open browser: {e}. Please open http://localhost:{port} manually." - ) + + if isinstance(initialization_settings, EditServerConfig): + try: + LOGGER.info( + f"Opening browser at http://localhost:{initialization_settings.server_port}" + ) + webbrowser.open( + f"http://localhost:{initialization_settings.server_port}" + ) + except Exception as e: + LOGGER.warning( + f"Failed to open browser: {e}. Please open http://localhost:{initialization_settings.server_port} manually." + ) + else: + # In the case of the 'start' command, just the webserver is started up, and there's no need to open the browser + pass app.server_state = ServerState() # type: ignore res_server_state_init = init_server_state( - app, edit_config, aiconfigrc_path + app, initialization_settings, aiconfigrc_path ) match res_server_state_init: case Ok(_): LOGGER.info("Initialized server state") - debug = edit_config.server_mode in [ + debug = initialization_settings.server_mode in [ ServerMode.DEBUG_BACKEND, ServerMode.DEBUG_SERVERS, ] - LOGGER.info(f"Running in {edit_config.server_mode} mode") + LOGGER.info( + f"Running in {initialization_settings.server_mode} mode" + ) app.run( - port=edit_config.server_port, debug=debug, use_reloader=debug + port=initialization_settings.server_port, + debug=debug, + use_reloader=debug, ) return Ok("Done") case Err(e): diff --git a/python/src/aiconfig/editor/server/server_utils.py b/python/src/aiconfig/editor/server/server_utils.py index 3e2cbaf55..d780090f3 100644 --- a/python/src/aiconfig/editor/server/server_utils.py +++ b/python/src/aiconfig/editor/server/server_utils.py @@ -1,3 +1,4 @@ +import dotenv import importlib import importlib.util import logging @@ -29,6 +30,7 @@ logging.basicConfig(format=core_utils.LOGGER_FMT) LOGGER = logging.getLogger(__name__) +# TODO: saqadri - use logs directory to save logs log_handler = logging.FileHandler("editor_flask_server.log", mode="a") formatter = logging.Formatter(core_utils.LOGGER_FMT) log_handler.setFormatter(formatter) @@ -77,6 +79,24 @@ def convert_to_mode( return value +class StartServerConfig(core_utils.Record): + server_port: int = 8080 + log_level: str | int = "INFO" + server_mode: ServerMode = ServerMode.PROD + parsers_module_path: str = "aiconfig_model_registry.py" + + @field_validator("server_mode", mode="before") + def convert_to_mode( + cls, value: Any + ) -> ServerMode: # pylint: disable=no-self-argument + if isinstance(value, str): + try: + return ServerMode[value.upper()] + except KeyError as e: + raise ValueError(f"Unexpected value for mode: {value}") from e + return value + + @dataclass class ServerState: aiconfigrc_path: str = os.path.join(os.path.expanduser("~"), ".aiconfigrc") @@ -268,13 +288,25 @@ def safe_load_from_disk( def init_server_state( - app: Flask, edit_config: EditServerConfig, aiconfigrc_path: str + app: Flask, + initialization_settings: StartServerConfig | EditServerConfig, + aiconfigrc_path: str, ) -> Result[None, str]: - LOGGER.info("Initializing server state") - _load_user_parser_module_if_exists(edit_config.parsers_module_path) + LOGGER.info("Initializing server state for 'edit' command") + # TODO: saqadri - load specific .env file if specified + dotenv.load_dotenv() + _load_user_parser_module_if_exists( + initialization_settings.parsers_module_path + ) state = get_server_state(app) state.aiconfigrc_path = aiconfigrc_path + if isinstance(initialization_settings, StartServerConfig): + # The aiconfig will be loaded later, when the editor sends the payload. + return Ok(None) + # else: + edit_config = initialization_settings + assert state.aiconfig is None if os.path.exists(edit_config.aiconfig_path): LOGGER.info(f"Loading AIConfig from {edit_config.aiconfig_path}") diff --git a/python/src/aiconfig/scripts/aiconfig_cli.py b/python/src/aiconfig/scripts/aiconfig_cli.py index a2ad2ad64..29b4c14bd 100644 --- a/python/src/aiconfig/scripts/aiconfig_cli.py +++ b/python/src/aiconfig/scripts/aiconfig_cli.py @@ -13,6 +13,7 @@ DEFAULT_AICONFIGRC, EditServerConfig, ServerMode, + StartServerConfig, ) from result import Err, Ok, Result from ruamel.yaml import YAML @@ -43,6 +44,7 @@ def run_subcommand(argv: list[str]) -> Result[str, str]: subparser_record_types = { "edit": EditServerConfig, "rage": rage.RageConfig, + "start": StartServerConfig, } main_parser = core_utils.argparsify( AIConfigCLIConfig, subparser_record_types=subparser_record_types @@ -72,6 +74,14 @@ def run_subcommand(argv: list[str]) -> Result[str, str]: LOGGER.debug(f"{edit_config.is_ok()=}") out = _run_editor_servers_with_configs(edit_config, cli_config) return out + elif subparser_name == "start": + LOGGER.debug("Running start subcommand") + start_config = core_utils.parse_args( + main_parser, argv[1:], StartServerConfig + ) + LOGGER.debug(f"{start_config.is_ok()=}") + out = _start_editor_servers_with_configs(start_config, cli_config) + return out elif subparser_name == "rage": res_rage_config = core_utils.parse_args( main_parser, argv[1:], rage.RageConfig @@ -104,6 +114,26 @@ def _run_editor_servers_with_configs( return Ok(",".join(server_outcomes.unwrap())) +def _start_editor_servers_with_configs( + start_config: Result[StartServerConfig, str], + cli_config: Result[AIConfigCLIConfig, str], +) -> Result[str, str]: + if not (start_config.is_ok() and cli_config.is_ok()): + return Err( + f"Something went wrong with configs: {start_config=}, {cli_config=}" + ) + + server_outcomes = _start_server( + start_config.unwrap(), cli_config.unwrap().aiconfigrc_path + ) + if server_outcomes.is_err(): + return Err( + f"Something went wrong with starting the aiconfig server: {server_outcomes=}" + ) + + return Ok(",".join(server_outcomes.unwrap())) + + def _sigint(procs: list[subprocess.Popen[bytes]]) -> Result[str, str]: LOGGER.info("sigint") for p in procs: @@ -122,6 +152,30 @@ def is_port_in_use(port: int) -> bool: return s.connect_ex(("localhost", port)) == 0 +def _start_server( + start_config: StartServerConfig, aiconfigrc_path: str +) -> Result[list[str], str]: + port = start_config.server_port + + if is_port_in_use(port): + err = f"Port {port} is in use. Cannot start server." + LOGGER.error(err) + return Err(err) + + LOGGER.warning(f"Using {port} to start aiconfig server.") + + results: list[Result[str, str]] = [] + backend_res = run_backend_server(start_config, aiconfigrc_path) + match backend_res: + case Ok(_): + pass + case Err(e): + return Err(e) + + results.append(backend_res) + return core_utils.result_reduce_list_all_ok(results) + + def _run_editor_servers( edit_config: EditServerConfig, aiconfigrc_path: str ) -> Result[list[str], str]: