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 f85b43c
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 17 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
33 changes: 31 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
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,31 @@ 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)
request_json = request.get_json()
# TODO
# Here's how to load it with comments:
# from ruamel.yaml import YAML
# yaml = YAML()
# file_contents = read_file_contents(state.aiconfigrc_path)
# yaml.load(file_contents)


@app.route("/api/set_aiconfigrc", methods=["POST"])
def set_aiconfigrc() -> FlaskResponse:
state = get_server_state(app)
request_json = request.get_json()
# TODO:
# 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 = ".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
81 changes: 68 additions & 13 deletions python/src/aiconfig/scripts/aiconfig_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@
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 = ".aiconfigrc"


logging.basicConfig(format=core_utils.LOGGER_FMT)
Expand All @@ -36,28 +40,57 @@ 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)
cli_config = core_utils.parse_args(main_parser, argv[1:], AIConfigCLIConfig)
cli_config.and_then(_process_cli_config)

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]:
"""
Runs editor servers with the given configs (one for the editor server, one for other CLI options)
Since there could have been parser errors, both configs are actually results that have to be unpacked
before we can pass them to _run_editor_servers().
This code block is similar to a nested list comprehension, but for Result types.
It should be read as follows:
- Start at the first `for`. If edit_config is OK, go to the next line.
Otherwise, short circuit and set out to the Err.
- If cli_config is OK, go to the next line.
Otherwise, short circuit and set out to the Err.
- Now both configs are OK, so we can run the editor servers.
- If _run_editor_servers() returns Ok, go to the next line.
Otherwise, short circuit and set out to the Err.
- Finally, we have a list of server outcomes, each corresponding to one of the servers (backend, frontend).
- To create the final result, join them with a comma.
- If everything is Ok, `out` is set to that comma-separated string.
(well, it's a Result, so it's `Ok("this is a comma-separated string")`)
For more information, see here: https://github.com/rustedpy/result?tab=readme-ov-file#do-notation
For more general information about Result and why it's useful,
please see the beginning of that README.
"""
out: Result[str, str] = result.do(
#
Ok(",".join(server_outcomes_ok))
#
for edit_config_ok in edit_config
for cli_config_ok in cli_config
for server_outcomes_ok in _run_editor_servers(edit_config_ok, cli_config_ok.aiconfigrc_path)
)
return out


def _sigint(procs: list[subprocess.Popen[bytes]]) -> Result[str, str]:
LOGGER.info("sigint")
for p in procs:
Expand All @@ -76,7 +109,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 +133,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 @@ -116,6 +149,28 @@ 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)
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 f85b43c

Please sign in to comment.