From 275f1401c8859241c669089176d6f14e501d2323 Mon Sep 17 00:00:00 2001 From: Jonathan Lessinger Date: Wed, 10 Jan 2024 18:08:02 -0500 Subject: [PATCH] [AIC-py][editor] server: persistent local cfg + telemetry flag, endpoints Test: - Start the server with and without flag and with no file, see that it gets created with correct contents - start the server with and without flag with existing file, with both true and false - hit both endpoints, check responses and that the file contents are changed --- python/src/aiconfig/editor/server/server.py | 50 ++++++++++++++++++- .../aiconfig/editor/server/server_utils.py | 4 +- python/src/aiconfig/scripts/aiconfig_cli.py | 30 +++++++++-- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/python/src/aiconfig/editor/server/server.py b/python/src/aiconfig/editor/server/server.py index fc091cd51..78679f6ae 100644 --- a/python/src/aiconfig/editor/server/server.py +++ b/python/src/aiconfig/editor/server/server.py @@ -12,6 +12,7 @@ import lastmile_utils.lib.core.api as core_utils import result +import yaml from aiconfig.Config import AIConfigRuntime from aiconfig.editor.server.queue_iterator import STOP_STREAMING_SIGNAL, QueueIterator from aiconfig.editor.server.server_utils import ( @@ -57,7 +58,7 @@ CORS(app, resources={r"/api/*": {"origins": "*"}}) -def run_backend_server(edit_config: EditServerConfig) -> Result[str, str]: +def run_backend_server(edit_config: EditServerConfig, cli_config_path: str) -> Result[str, str]: LOGGER.setLevel(edit_config.log_level) LOGGER.info("Edit config: %s", edit_config.model_dump_json()) LOGGER.info(f"Starting server on http://localhost:{edit_config.server_port}") @@ -68,7 +69,7 @@ def run_backend_server(edit_config: EditServerConfig) -> Result[str, str]: LOGGER.warning(f"Failed to open browser: {e}. Please open http://localhost:{port} manually.") app.server_state = ServerState() # type: ignore - res_server_state_init = init_server_state(app, edit_config) + res_server_state_init = init_server_state(app, edit_config, cli_config_path) match res_server_state_init: case Ok(_): LOGGER.info("Initialized server state") @@ -566,3 +567,48 @@ def _op(aiconfig_runtime: AIConfigRuntime, _op_args: OpArgs) -> Result[None, str signature: dict[str, Type[Any]] = {} return run_aiconfig_operation_with_request_json(aiconfig, request_json, f"method_", _op, signature) + + +@app.route("/api/get_cli_config", methods=["GET"]) +def get_cli_config() -> FlaskResponse: + state = get_server_state(app) + + def _yaml_deserialize(yaml_str: str) -> Result[dict[str, Any], str]: + try: + out = yaml.safe_load(yaml_str) + if out is None: + return Err("YAML string not parsed") + else: + return Ok(out) + except Exception as e: + return core_utils.ErrWithTraceback(e) + + cli_config_object = core_utils.read_text_file(state.cli_config_path).and_then(_yaml_deserialize) + match cli_config_object: + case Ok(cli_config_object_): + return FlaskResponse(({"message": "got cli_config", "data": cli_config_object_}, 200)) + case Err(e): + return FlaskResponse(({"message": f"failed to get cli_config: {e}", "data": None}, 400)) + + +@app.route("/api/set_cli_config", methods=["POST"]) +def set_cli_config() -> FlaskResponse: + state = get_server_state(app) + request_json = request.get_json() + try: + new_contents = request_json.get("new_contents") + serialized = yaml.dump(new_contents) + with open(state.cli_config_path, "w") as f: + f.write(serialized) + + return HttpResponseWithAIConfig( + message=f"Successfully set cli_config to {serialized}", + code=200, + aiconfig=None, + ).to_flask_format() + except Exception as e: + return HttpResponseWithAIConfig( + message=f"Failed to set cli_config: {e}", + code=400, + aiconfig=None, + ).to_flask_format() diff --git a/python/src/aiconfig/editor/server/server_utils.py b/python/src/aiconfig/editor/server/server_utils.py index 072e03e9d..fe702b0ba 100644 --- a/python/src/aiconfig/editor/server/server_utils.py +++ b/python/src/aiconfig/editor/server/server_utils.py @@ -75,6 +75,7 @@ def convert_to_mode(cls, value: Any) -> ServerMode: # pylint: disable=no-self-a @dataclass class ServerState: + cli_config_path: str = "aiconfig_cli.config.yaml" aiconfig: AIConfigRuntime | None = None events: dict[str, Event] = field(default_factory=dict) @@ -200,10 +201,11 @@ def safe_load_from_disk(aiconfig_path: ValidatedPath) -> Result[AIConfigRuntime, return core_utils.ErrWithTraceback(e) -def init_server_state(app: Flask, edit_config: EditServerConfig) -> Result[None, str]: +def init_server_state(app: Flask, edit_config: EditServerConfig, cli_config_path: str) -> Result[None, str]: LOGGER.info("Initializing server state") _load_user_parser_module_if_exists(edit_config.parsers_module_path) state = get_server_state(app) + state.cli_config_path = cli_config_path assert state.aiconfig is None if os.path.exists(edit_config.aiconfig_path): diff --git a/python/src/aiconfig/scripts/aiconfig_cli.py b/python/src/aiconfig/scripts/aiconfig_cli.py index f57de74dc..e59e95ccb 100644 --- a/python/src/aiconfig/scripts/aiconfig_cli.py +++ b/python/src/aiconfig/scripts/aiconfig_cli.py @@ -7,6 +7,7 @@ import lastmile_utils.lib.core.api as core_utils import result +import yaml from aiconfig.editor.server.server import run_backend_server from aiconfig.editor.server.server_utils import EditServerConfig, ServerMode from result import Err, Ok, Result @@ -14,6 +15,8 @@ class AIConfigCLIConfig(core_utils.Record): log_level: str | int = "WARNING" + config_path: str = "aiconfig_cli.config.yaml" + allow_usage_data_sharing: bool = False logging.basicConfig(format=core_utils.LOGGER_FMT) @@ -46,12 +49,13 @@ def run_subcommand(argv: list[str]) -> Result[str, str]: LOGGER.debug("Running edit subcommand") res_edit_config = core_utils.parse_args(main_parser, argv[1:], EditServerConfig) LOGGER.debug(f"{res_edit_config.is_ok()=}") - res_servers = res_edit_config.and_then(_run_editor_servers) out: Result[str, str] = result.do( # Ok(",".join(res_servers_ok)) # - for res_servers_ok in res_servers + for edit_config in res_edit_config + for cli_config in res_cli_config + for res_servers_ok in _run_editor_servers(edit_config, cli_config.config_path) ) return out else: @@ -76,7 +80,7 @@ def is_port_in_use(port: int) -> bool: return s.connect_ex(("localhost", port)) == 0 -def _run_editor_servers(edit_config: EditServerConfig) -> Result[list[str], str]: +def _run_editor_servers(edit_config: EditServerConfig, cli_config_path: str) -> Result[list[str], str]: port = edit_config.server_port while is_port_in_use(port): @@ -100,7 +104,7 @@ def _run_editor_servers(edit_config: EditServerConfig) -> Result[list[str], str] return Err(e) results: list[Result[str, str]] = [] - backend_res = run_backend_server(edit_config) + backend_res = run_backend_server(edit_config, cli_config_path) match backend_res: case Ok(_): pass @@ -116,6 +120,24 @@ def _run_editor_servers(edit_config: EditServerConfig) -> Result[list[str], str] def _process_cli_config(cli_config: AIConfigCLIConfig) -> Result[bool, str]: LOGGER.setLevel(cli_config.log_level) + + config_path = cli_config.config_path + file_contents = yaml.dump({"allow_usage_data_sharing": cli_config.allow_usage_data_sharing}) + try: + with open(config_path, "x") as f: + f.write(file_contents) + except FileExistsError: + with open(config_path, "r") as f: + existing_file_contents = f.read() + + existing_config = yaml.safe_load(existing_file_contents) + existing_config["allow_usage_data_sharing"] = cli_config.allow_usage_data_sharing + file_contents = yaml.dump(existing_config) + with open(config_path, "w") as f: + f.write(file_contents) + except Exception as e: + return core_utils.ErrWithTraceback(e) + return Ok(True)