From e622b59e1601492a97400df4f6c5e7376dcbc108 Mon Sep 17 00:00:00 2001 From: Jonathan Lessinger Date: Wed, 10 Jan 2024 18:43:58 -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 https://github.com/lastmile-ai/aiconfig/assets/148090348/b36c3c00-c14b-4cde-8449-60386f2be1eb --- 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..a51834dba 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, editor_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, editor_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_settings", methods=["GET"]) +def get_settings() -> 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(f"Cannot parse string from YAML file '{state.settings_path}' into YAML format: dict[str, Any]") + else: + return Ok(out) + except Exception as e: + return core_utils.ErrWithTraceback(e) + + settings_object = core_utils.read_text_file(state.settings_path).and_then(_yaml_deserialize) + match settings_object: + case Ok(settings_object_): + return FlaskResponse(({"message": "got settings", "data": settings_object_}, 200)) + case Err(e): + return FlaskResponse(({"message": f"failed to get settings: {e}", "data": None}, 400)) + + +@app.route("/api/set_settings", methods=["POST"]) +def set_settings() -> FlaskResponse: + state = get_server_state(app) + request_json = request.get_json() + try: + contents = request_json.get("contents") + yaml_contents = yaml.dump(contents) + with open(state.settings_path, "w") as f: + f.write(yaml_contents) + + return HttpResponseWithAIConfig( + message=f"Successfully set settings to {yaml_contents}", + code=200, + aiconfig=None, + ).to_flask_format() + except Exception as e: + return HttpResponseWithAIConfig( + message=f"Failed to set settings: {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..5551274f8 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: + settings_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, settings_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.settings_path = settings_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..2d5c2bc1a 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" + settings_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.settings_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, settings_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, settings_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.settings_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)