Skip to content

Commit

Permalink
[4/n] Add 'start' command to aiconfig server and CLI
Browse files Browse the repository at this point in the history
The current aiconfig CLI has an 'edit' command which starts both the frontend and backend servers, and also loads the aiconfig from disk.

In the same vein, this diff introduces a 'start' command that is used to just start the aiconfig server. This is used by the vscode extension to spawn a server when an aiconfig file is opened.

Unlike the 'edit' command, 'start':
* Doesn't load the aiconfig on server init. Instead, the /api/load_content endpoint is called to initialize.
* Doesn't search for open ports. The vscode client is responsible for finding an open port. This is because the client needs to be in control of the server lifecycle.

A small change that was also added to this diff: using `dotenv` to load an env file.

Test Plan:
* At the top of stack
  • Loading branch information
saqadri committed Jan 30, 2024
1 parent 157a7e6 commit 088a398
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 20 deletions.
47 changes: 30 additions & 17 deletions python/src/aiconfig/editor/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
OpArgs,
ServerMode,
ServerState,
StartServerConfig,
ValidatedPath,
get_http_response_load_user_parser_module,
get_server_state,
Expand Down Expand Up @@ -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):
Expand Down
38 changes: 35 additions & 3 deletions python/src/aiconfig/editor/server/server_utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dotenv
import importlib
import importlib.util
import logging
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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}")
Expand Down
54 changes: 54 additions & 0 deletions python/src/aiconfig/scripts/aiconfig_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
DEFAULT_AICONFIGRC,
EditServerConfig,
ServerMode,
StartServerConfig,
)
from result import Err, Ok, Result
from ruamel.yaml import YAML
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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]:
Expand Down

0 comments on commit 088a398

Please sign in to comment.