Skip to content

Commit

Permalink
[AIC-py][editor] server: persistent local cfg + telemetry flag, endpo…
Browse files Browse the repository at this point in the history
…ints

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
  • Loading branch information
jonathanlastmileai committed Jan 11, 2024
1 parent 9f5cd3d commit fdede9b
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 19 deletions.
3 changes: 2 additions & 1 deletion python/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ pytest-asyncio
python-dotenv
pyyaml
requests
result
result
ruamel.yaml
37 changes: 35 additions & 2 deletions python/src/aiconfig/editor/server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import lastmile_utils.lib.core.api as core_utils
import result
from ruamel.yaml 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 (
Expand Down Expand Up @@ -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, aiconfigrc_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}")
Expand All @@ -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, aiconfigrc_path)
match res_server_state_init:
case Ok(_):
LOGGER.info("Initialized server state")
Expand Down Expand Up @@ -566,3 +567,35 @@ 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_aiconfigrc", methods=["GET"])
def get_aiconfigrc() -> FlaskResponse:
state = get_server_state(app)
yaml = YAML()

yaml_mapping = core_utils.read_text_file(state.aiconfigrc_path).map(yaml.load)
match yaml_mapping:
case Ok(yaml_mapping_ok):
return FlaskResponse((yaml_mapping_ok, 200))
case Err(e):
return FlaskResponse(({"message": f"Failed to load aiconfigrc: {e}"}, 400))


@app.route("/api/set_aiconfigrc", methods=["POST"])
def set_aiconfigrc() -> FlaskResponse:
state = get_server_state(app)
request_json = request.get_json()
# TODO:
# We might not need to implement this at all.
#
# If so:
# Assuming request_json["aiconfigrc"] is a yaml-formatted string
# (possibly with comments)
# Note that the file might already exist and have contents.
#
# here's how to write it to a file:
# from ruamel.yaml import YAML
# yaml = YAML()
# with open(state.aiconfigrc_path, "w") as f:
# yaml.dump(request_json["aiconfigrc"], f)
4 changes: 3 additions & 1 deletion python/src/aiconfig/editor/server/server_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ def convert_to_mode(cls, value: Any) -> ServerMode: # pylint: disable=no-self-a

@dataclass
class ServerState:
aiconfigrc_path: str = os.path.join(os.path.expanduser("~"), ".aiconfigrc")
aiconfig: AIConfigRuntime | None = None
events: dict[str, Event] = field(default_factory=dict)

Expand Down Expand Up @@ -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, aiconfigrc_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.aiconfigrc_path = aiconfigrc_path

assert state.aiconfig is None
if os.path.exists(edit_config.aiconfig_path):
Expand Down
75 changes: 60 additions & 15 deletions python/src/aiconfig/scripts/aiconfig_cli.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import asyncio
import logging
import os
import signal
import socket
import subprocess
import sys
from textwrap import dedent

import lastmile_utils.lib.core.api as core_utils
import result
from ruamel.yaml 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


class AIConfigCLIConfig(core_utils.Record):
log_level: str | int = "WARNING"
aiconfigrc_path: str = os.path.join(os.path.expanduser("~"), ".aiconfigrc")


logging.basicConfig(format=core_utils.LOGGER_FMT)
Expand All @@ -36,28 +40,39 @@ def run_subcommand(argv: list[str]) -> Result[str, str]:
subparser_record_types = {"edit": EditServerConfig}
main_parser = core_utils.argparsify(AIConfigCLIConfig, subparser_record_types=subparser_record_types)

res_cli_config = core_utils.parse_args(main_parser, argv[1:], AIConfigCLIConfig)
res_cli_config.and_then(_process_cli_config)
# Try to parse the CLI args into a config.
cli_config: Result[AIConfigCLIConfig, str] = core_utils.parse_args(main_parser, argv[1:], AIConfigCLIConfig)

# If cli_config is Ok(), pass its contents to _get_cli_process_result_from_config().
# Otherwise, short circuit and assign process_result to the Err.
# Nothing gets mutated except for log level (see inside _get_cli_process_result_from_config()
process_result = cli_config.and_then(_get_cli_process_result_from_config)
LOGGER.info(f"{process_result=}")

subparser_name = core_utils.get_subparser_name(main_parser, argv[1:])
LOGGER.info(f"Running subcommand: {subparser_name}")

if subparser_name == "edit":
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
)
edit_config = core_utils.parse_args(main_parser, argv[1:], EditServerConfig)
LOGGER.debug(f"{edit_config.is_ok()=}")
out = _run_editor_servers_with_configs(edit_config, cli_config)
return out
else:
return Err(f"Unknown subparser: {subparser_name}")


def _run_editor_servers_with_configs(edit_config: Result[EditServerConfig, str], cli_config: Result[AIConfigCLIConfig, str]) -> Result[str, str]:
if not (edit_config.is_ok() and cli_config.is_ok()):
return Err(f"Something went wrong with configs: {edit_config=}, {cli_config=}")

server_outcomes = _run_editor_servers(edit_config.unwrap(), cli_config.unwrap().aiconfigrc_path)
if server_outcomes.is_err():
return Err(f"Something went wrong with servers: {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 @@ -76,7 +91,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, aiconfigrc_path: str) -> Result[list[str], str]:
port = edit_config.server_port

while is_port_in_use(port):
Expand All @@ -100,7 +115,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, aiconfigrc_path)
match backend_res:
case Ok(_):
pass
Expand All @@ -114,8 +129,38 @@ def _run_editor_servers(edit_config: EditServerConfig) -> Result[list[str], str]
return core_utils.result_reduce_list_all_ok(results)


def _process_cli_config(cli_config: AIConfigCLIConfig) -> Result[bool, str]:
def _get_cli_process_result_from_config(cli_config: AIConfigCLIConfig) -> Result[bool, str]:
"""
This function has 2 jobs (currently):
1. Set the log level
2. Write the default aiconfigrc if it doesn't exist.
It returns Ok(True) if everything went well. Currently, it never returns Ok(False).
As usual, we return an error with a message if something went wrong.
"""
LOGGER.setLevel(cli_config.log_level)
try:
config_path = cli_config.aiconfigrc_path
with open(config_path, "x") as f:
yaml = YAML()
yaml.dump(
yaml.load(
dedent(
"""
# Tip: make sure this file is called .aiconfigrc and is in your home directory.
# Flag allowing or denying telemetry for product development purposes.
allow_usage_data_sharing: true
"""
),
),
f,
)
except FileExistsError:
pass
except Exception as e:
return core_utils.ErrWithTraceback(e)

return Ok(True)


Expand Down

0 comments on commit fdede9b

Please sign in to comment.