diff --git a/.cspell.json b/.cspell.json index 0e45432fb3d..0f00c7b250d 100644 --- a/.cspell.json +++ b/.cspell.json @@ -38,8 +38,6 @@ "**/setup.py", "scripts/installer/curl_install_pypi/**", "scripts/installer/windows/**", - "src/promptflow/promptflow/_sdk/_service/pfsvc.py", - "src/promptflow-devkit/promptflow/_sdk/_service/pfsvc.py", ".github/workflows/**", ".github/actions/**", ".github/pipelines/**" @@ -94,7 +92,6 @@ "nunit", "astext", "Likert", - "pfsvc", "geval", "Summ", "Bhavik", diff --git a/.github/workflows/promptflow-sdk-cli-test.yml b/.github/workflows/promptflow-sdk-cli-test.yml index abf8a9055c6..d3077da1201 100644 --- a/.github/workflows/promptflow-sdk-cli-test.yml +++ b/.github/workflows/promptflow-sdk-cli-test.yml @@ -179,4 +179,4 @@ jobs: osVersion: ubuntu-latest pythonVersion: 3.9 coverageThreshold: 40 - context: test/sdk_cli + context: test/sdk_cli \ No newline at end of file diff --git a/scripts/installer/curl_install_pypi/README.md b/scripts/installer/curl_install_pypi/README.md index 90185f83b2f..33c2a9cf3ca 100644 --- a/scripts/installer/curl_install_pypi/README.md +++ b/scripts/installer/curl_install_pypi/README.md @@ -17,15 +17,12 @@ Uninstall the promptflow by directly deleting the files from the location chosen ```bash # The default install/executable location is the user's home directory ($HOME). rm -r $HOME/lib/promptflow - rm $HOME/bin/pf - rm $HOME/bin/pfs - rm $HOME/bin/pfazure ``` 2. Modify your `$HOME/.bash_profile` or `$HOME/.bashrc` file to remove the following line: ```text - export PATH=$PATH:$HOME/bin + export PATH=$PATH:$HOME/lib/promptflow/bin ``` 3. If using `bash` or `zsh`, reload your shell's command cache. diff --git a/scripts/installer/curl_install_pypi/install.py b/scripts/installer/curl_install_pypi/install.py index bbe32963a56..5d4a7499ff5 100644 --- a/scripts/installer/curl_install_pypi/install.py +++ b/scripts/installer/curl_install_pypi/install.py @@ -15,31 +15,15 @@ import os import sys import platform -import stat import tempfile import shutil import subprocess -import hashlib -PF_DISPATCH_TEMPLATE = """#!/usr/bin/env bash -export PF_INSTALLER=Script -{install_dir}/bin/python -m promptflow._cli._pf.entry "$@" -""" - -PFAZURE_DISPATCH_TEMPLATE = """#!/usr/bin/env bash -{install_dir}/bin/python -m promptflow.azure._cli.entry "$@" -""" - -PFS_DISPATCH_TEMPLATE = """#!/usr/bin/env bash -{install_dir}/bin/python -m promptflow._sdk._service.entry "$@" -""" - DEFAULT_INSTALL_DIR = os.path.expanduser(os.path.join('~', 'lib', 'promptflow')) DEFAULT_EXEC_DIR = os.path.expanduser(os.path.join('~', 'bin')) PF_EXECUTABLE_NAME = 'pf' PFAZURE_EXECUTABLE_NAME = 'pfazure' -PFS_EXECUTABLE_NAME = 'pfs' USER_BASH_RC = os.path.expanduser(os.path.join('~', '.bashrc')) @@ -96,14 +80,6 @@ def create_dir(dir): os.makedirs(dir) -def is_valid_sha256sum(a_file, expected_sum): - sha256 = hashlib.sha256() - with open(a_file, 'rb') as f: - sha256.update(f.read()) - computed_hash = sha256.hexdigest() - return expected_sum == computed_hash - - def create_virtualenv(install_dir): cmd = [sys.executable, '-m', 'venv', install_dir] exec_command(cmd) @@ -111,8 +87,8 @@ def create_virtualenv(install_dir): def install_cli(install_dir, tmp_dir): path_to_pip = os.path.join(install_dir, 'bin', 'pip') - cmd = [path_to_pip, 'install', '--cache-dir', tmp_dir, 'promptflow[azure,executable,azureml-serving]', - '--upgrade'] + cmd = [path_to_pip, 'install', '--cache-dir', tmp_dir, + 'promptflow[azure,executable,azureml-serving,executor-service]', '--upgrade'] exec_command(cmd) cmd = [path_to_pip, 'install', '--cache-dir', tmp_dir, 'promptflow-tools', '--upgrade'] exec_command(cmd) @@ -120,22 +96,6 @@ def install_cli(install_dir, tmp_dir): exec_command(cmd) -def create_executable(exec_dir, install_dir): - create_dir(exec_dir) - exec_filepaths = [] - for filename, template in [(PF_EXECUTABLE_NAME, PF_DISPATCH_TEMPLATE), - (PFAZURE_EXECUTABLE_NAME, PFAZURE_DISPATCH_TEMPLATE), - (PFS_EXECUTABLE_NAME, PFS_DISPATCH_TEMPLATE)]: - exec_filepath = os.path.join(exec_dir, filename) - with open(exec_filepath, 'w') as exec_file: - exec_file.write(template.format(install_dir=install_dir)) - cur_stat = os.stat(exec_filepath) - os.chmod(exec_filepath, cur_stat.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) - print_status("The executable is available at '{}'.".format(exec_filepath)) - exec_filepaths.append(exec_filepath) - return exec_filepaths - - def get_install_dir(): install_dir = None while not install_dir: @@ -161,21 +121,6 @@ def get_install_dir(): return install_dir -def get_exec_dir(): - exec_dir = None - while not exec_dir: - prompt_message = (f"In what directory would you like to place the " - f"'{PFS_EXECUTABLE_NAME}/{PFS_EXECUTABLE_NAME}/{PFAZURE_EXECUTABLE_NAME}' executable?") - exec_dir = prompt_input_with_default(prompt_message, DEFAULT_EXEC_DIR) - exec_dir = os.path.realpath(os.path.expanduser(exec_dir)) - if ' ' in exec_dir: - print_status("The executable directory '{}' cannot contain spaces.".format(exec_dir)) - exec_dir = None - create_dir(exec_dir) - print_status("The executable will be in '{}'.".format(exec_dir)) - return exec_dir - - def _backup_rc(rc_file): try: shutil.copyfile(rc_file, rc_file+'.backup') @@ -240,28 +185,28 @@ def warn_other_azs_on_path(exec_dir, exec_filepath): conflicting_paths = [] if env_path: for p in env_path.split(':'): - for file in [PF_EXECUTABLE_NAME, PFAZURE_EXECUTABLE_NAME, PFS_EXECUTABLE_NAME]: + for file in [PF_EXECUTABLE_NAME, PFAZURE_EXECUTABLE_NAME]: p_to_pf = os.path.join(p, file) if p != exec_dir and os.path.isfile(p_to_pf): conflicting_paths.append(p_to_pf) if conflicting_paths: print_status() - print_status(f"** WARNING: Other '{PFS_EXECUTABLE_NAME}/{PFS_EXECUTABLE_NAME}/{PFAZURE_EXECUTABLE_NAME}' " + print_status(f"** WARNING: Other '{PF_EXECUTABLE_NAME}/{PFAZURE_EXECUTABLE_NAME}' " f"executables are on your $PATH. **") print_status("Conflicting paths: {}".format(', '.join(conflicting_paths))) print_status("You can run this installation of the promptflow with '{}'.".format(exec_filepath)) -def handle_path_and_tab_completion(exec_filepath, exec_dir): +def handle_path_and_tab_completion(exec_filepath, install_dir): ans_yes = prompt_y_n('Modify profile to update your $PATH now?', 'y') if ans_yes: rc_file_path = get_rc_file_path() if not rc_file_path: raise CLIInstallError('No suitable profile file found.') _backup_rc(rc_file_path) - line_to_add = "export PATH=$PATH:{}".format(exec_dir) + line_to_add = "export PATH=$PATH:{}".format(os.path.join(install_dir, "bin")) _modify_rc(rc_file_path, line_to_add) - warn_other_azs_on_path(exec_dir, exec_filepath) + warn_other_azs_on_path(install_dir, exec_filepath) print_status() print_status('** Run `exec -l $SHELL` to restart your shell. **') print_status() @@ -280,59 +225,16 @@ def verify_python_version(): print_status('Python version {}.{}.{} okay.'.format(v.major, v.minor, v.micro)) -def _native_dependencies_for_dist(verify_cmd_args, install_cmd_args, dep_list): - try: - print_status("Executing: '{} {}'".format(' '.join(verify_cmd_args), ' '.join(dep_list))) - subprocess.check_output(verify_cmd_args + dep_list, stderr=subprocess.STDOUT) - print_status('Native dependencies okay.') - except subprocess.CalledProcessError: - err_msg = 'One or more of the following native dependencies are not currently installed and may be required.\n' - err_msg += '"{}"'.format(' '.join(install_cmd_args + dep_list)) - print_status(err_msg) - ans_yes = prompt_y_n('Missing native dependencies. Attempt to continue anyway?', 'n') - if not ans_yes: - raise CLIInstallError('Please install the native dependencies and try again.') - - -def _get_linux_distro(): - if platform.system() != 'Linux': - return None, None - - try: - with open('/etc/os-release') as lines: - tokens = [line.strip() for line in lines] - except Exception: - return None, None - - release_info = {} - for token in tokens: - if '=' in token: - k, v = token.split('=', 1) - release_info[k.lower()] = v.strip('"') - - return release_info.get('name', None), release_info.get('version_id', None) - - -def verify_install_dir_exec_path_conflict(install_dir, exec_dir): - for exec_name in [PF_EXECUTABLE_NAME, PFAZURE_EXECUTABLE_NAME, PFS_EXECUTABLE_NAME]: - exec_path = os.path.join(exec_dir, exec_name) - if install_dir == exec_path: - raise CLIInstallError("The executable file '{}' would clash with the install directory of '{}'. Choose " - "either a different install directory or directory to place the " - "executable.".format(exec_path, install_dir)) - - def main(): verify_python_version() tmp_dir = create_tmp_dir() install_dir = get_install_dir() - exec_dir = get_exec_dir() - verify_install_dir_exec_path_conflict(install_dir, exec_dir) create_virtualenv(install_dir) install_cli(install_dir, tmp_dir) - exec_filepath = create_executable(exec_dir, install_dir) + exec_filepath = [os.path.join(install_dir, "bin", PF_EXECUTABLE_NAME), + os.path.join(install_dir, "bin", PFAZURE_EXECUTABLE_NAME)] try: - handle_path_and_tab_completion(exec_filepath, exec_dir) + handle_path_and_tab_completion(exec_filepath, install_dir) except Exception as e: print_status("Unable to set up PATH. ERROR: {}".format(str(e))) shutil.rmtree(tmp_dir) @@ -351,10 +253,10 @@ def main(): sys.exit(1) # SIG # Begin signature block -# Z1F07ShfIJ7kejST2NXwW1QcFPEya4xaO2xZz6vLT847zaMzbc/PaEa1RKFlD881 -# 4J+i6Au2wtbHzOXDisyH6WeLQ3gh0X2gxFRa4EzW7Nzjcvwm4+WogiTcnPVVxlk3 -# qafM/oyVqs3695K7W5XttOiq2guv/yedsf/TW2BKSEKruFQh9IwDfIiBoi9Zv3wa -# iuzQulRR8KyrCtjEPDV0t4WnZVB/edQea6xJZeTlMG+uLR/miBTbPhUb/VZkVjBf -# qHBv623oLXICzoTNuaPTln9OWvL2NZpisGYvNzebKO7/Ho6AOWZNs5XOVnjs0Ax2 -# aeXvlwBzIQyfyxd25487/Q== +# Op0/tmmNDX4QgQefj28K91e/ClVKWeYaA1w1kb5Hi8ALJZtmvyhwvxYlCRZ9eWT+ +# wFBfSvpUIzAWYxEMfVqqWy7g9AzqHGa5vE37zQ7uGkIyR0OsmO0bkauOv5FxuCWX +# U0u9d9sir6sRTb2nrEj2O1EXAcP2xNaW77w1fcOtMX9W6ytHXx/+v5p387+/HBnQ +# y00GvJrrcIJeRj4MboBdDdv0VgGJAAlTJp0hxO0lt5ZQUMJvi/sM4e1cPUTcx7uB +# djCmvSZzZrJO2ZIIWiyiP2XNYWJv4A9klJWMbWKukeZOSjxYPS0pO2mTftkKoL5U +# sOJu9RtRWIIx/kLG/Axliw== # SIG # End signature block diff --git a/scripts/installer/windows/scripts/pf.bat b/scripts/installer/windows/scripts/pf.bat index 549ba723590..b4319cce62d 100644 --- a/scripts/installer/windows/scripts/pf.bat +++ b/scripts/installer/windows/scripts/pf.bat @@ -3,4 +3,17 @@ setlocal SET PF_INSTALLER=MSI set MAIN_EXE=%~dp0.\pfcli.exe -"%MAIN_EXE%" pf %* \ No newline at end of file +REM Check if the first argument is 'start' +if "%~1"=="service" ( + REM Check if the second argument is 'start' + if "%~2"=="start" ( + cscript //nologo %~dp0.\start_pfs.vbs """%MAIN_EXE%"" pf %*" + REM since we won't wait for vbs to finish, we need to wait for the output file to be flushed to disk + timeout /t 5 >nul + type "%~dp0output.txt" + ) else ( + "%MAIN_EXE%" pf %* + ) +) else ( + "%MAIN_EXE%" pf %* +) \ No newline at end of file diff --git a/scripts/installer/windows/scripts/pfcli.py b/scripts/installer/windows/scripts/pfcli.py index 41b3c1b9e84..6ced55dd24e 100644 --- a/scripts/installer/windows/scripts/pfcli.py +++ b/scripts/installer/windows/scripts/pfcli.py @@ -13,11 +13,5 @@ elif command == 'pfazure': from promptflow.azure._cli.entry import main as pfazure_main pfazure_main() - elif command == 'pfs': - from promptflow._sdk._service.entry import main as pfs_main - pfs_main() - elif command == 'pfsvc': - from promptflow._sdk._service.pfsvc import init as pfsvc_init - pfsvc_init() else: - print(f"Invalid command {sys.argv}. Please use 'pf', 'pfazure', 'pfs' or 'pfsvc'.") + print(f"Invalid command {sys.argv}. Please use 'pf', 'pfazure'.") diff --git a/scripts/installer/windows/scripts/pfs.bat b/scripts/installer/windows/scripts/pfs.bat deleted file mode 100644 index 2c1388e90ed..00000000000 --- a/scripts/installer/windows/scripts/pfs.bat +++ /dev/null @@ -1,13 +0,0 @@ -@echo off -setlocal - -set MAIN_EXE=%~dp0.\pfcli.exe -REM Check if the first argument is 'start' -if "%~1"=="start" ( - cscript //nologo %~dp0.\start_pfs.vbs """%MAIN_EXE%"" pfs %*" -@REM since we won't wait for vbs to finish, we need to wait for the output file to be flushed to disk - timeout /t 5 >nul - type "%~dp0output.txt" -) else ( - "%MAIN_EXE%" pfs %* -) \ No newline at end of file diff --git a/scripts/installer/windows/scripts/pfsvc.bat b/scripts/installer/windows/scripts/pfsvc.bat deleted file mode 100644 index 957ae245cb7..00000000000 --- a/scripts/installer/windows/scripts/pfsvc.bat +++ /dev/null @@ -1,5 +0,0 @@ -@echo off -setlocal - -set MAIN_EXE=%~dp0.\pfcli.exe -"%MAIN_EXE%" pfsvc %* \ No newline at end of file diff --git a/scripts/installer/windows/scripts/promptflow.spec.jinja2 b/scripts/installer/windows/scripts/promptflow.spec.jinja2 index 3840a1056aa..4ea0181e045 100644 --- a/scripts/installer/windows/scripts/promptflow.spec.jinja2 +++ b/scripts/installer/windows/scripts/promptflow.spec.jinja2 @@ -4,7 +4,7 @@ from PyInstaller.utils.hooks import collect_data_files, collect_all, copy_metada datas = [('../resources/CLI_LICENSE.rtf', '.'), ('../../../../src/promptflow/NOTICE.txt', '.'), ('../../../../src/promptflow/promptflow/_sdk/data/executable/', './promptflow/_sdk/data/executable/'), ('../../../../src/promptflow-tools/promptflow/tools/', './promptflow/tools/'), -('./pf.bat', '.'), ('./pfs.bat', '.'), ('./pfazure.bat', '.'), ('./pfsvc.bat', '.'), ('./start_pfs.vbs', '.')] +('./pf.bat', '.'), ('./pfazure.bat', '.'), ('./start_pfs.vbs', '.')] all_packages = {{all_packages}} diff --git a/scripts/installer/windows/scripts/promptflow_service.vbs b/scripts/installer/windows/scripts/promptflow_service.vbs index d06faab67a4..0d01c144ffa 100644 --- a/scripts/installer/windows/scripts/promptflow_service.vbs +++ b/scripts/installer/windows/scripts/promptflow_service.vbs @@ -1,3 +1,3 @@ DIM objshell set objshell = wscript.createobject("wscript.shell") -iReturn = objshell.run("pfcli.exe pfs start --force", 0, true) \ No newline at end of file +iReturn = objshell.run("pfcli.exe pf service start --force", 0, true) \ No newline at end of file diff --git a/src/promptflow-devkit/pf b/src/promptflow-devkit/pf index 333a91a041d..ebd8b842155 100644 --- a/src/promptflow-devkit/pf +++ b/src/promptflow-devkit/pf @@ -1,10 +1,11 @@ #!/usr/bin/env python -import sys import os +import sys + def main(): - if os.environ.get('PF_INSTALLER') is None: - os.environ['PF_INSTALLER'] = 'PIP' + if os.environ.get("PF_INSTALLER") is None: + os.environ["PF_INSTALLER"] = "PIP" - os.execl(sys.executable, sys.executable, '-m', 'promptflow._cli._pf.entry', *sys.argv[1:]) + os.execl(sys.executable, sys.executable, "-m", "promptflow._cli._pf.entry", *sys.argv[1:]) diff --git a/src/promptflow-devkit/promptflow/_sdk/_service/entry.py b/src/promptflow-devkit/promptflow/_cli/_pf/_service.py similarity index 65% rename from src/promptflow-devkit/promptflow/_sdk/_service/entry.py rename to src/promptflow-devkit/promptflow/_cli/_pf/_service.py index c1c21f3f799..f543beecf95 100644 --- a/src/promptflow-devkit/promptflow/_sdk/_service/entry.py +++ b/src/promptflow-devkit/promptflow/_cli/_pf/_service.py @@ -1,8 +1,8 @@ # --------------------------------------------------------- # Copyright (c) Microsoft Corporation. All rights reserved. # --------------------------------------------------------- + import argparse -import json import logging import os import platform @@ -11,20 +11,27 @@ import waitress -from promptflow._cli._utils import _get_cli_activity_name, cli_exception_and_telemetry_handler +from promptflow._cli._params import base_params +from promptflow._cli._utils import activate_action from promptflow._constants import PF_NO_INTERACTIVE_LOGIN -from promptflow._sdk._constants import PF_SERVICE_DEBUG, PF_SERVICE_WORKER_NUM +from promptflow._sdk._constants import ( + HOME_PROMPT_FLOW_DIR, + PF_SERVICE_DEBUG, + PF_SERVICE_LOG_FILE, + PF_SERVICE_WORKER_NUM, +) from promptflow._sdk._service.app import create_app from promptflow._sdk._service.utils.utils import ( check_pfs_service_status, dump_port_to_config, + get_current_env_pfs_file, get_port_from_config, get_started_service_info, is_port_in_use, is_run_from_built_binary, kill_exist_service, ) -from promptflow._sdk._utils import get_promptflow_sdk_version, print_pf_version +from promptflow._sdk._utils import get_promptflow_sdk_version from promptflow._utils.logger_utils import get_cli_sdk_logger # noqa: E402 from promptflow.exceptions import UserErrorException @@ -45,52 +52,100 @@ def get_app(environ, start_response): return app.wsgi_app(environ, start_response) -def add_start_service_action(subparsers): - """Add action to start pfs.""" - start_pfs_parser = subparsers.add_parser( - "start", - description="Start promptflow service.", - help="pfs start", +def add_service_parser(subparsers): + """Add service parser to the pf subparsers.""" + service_parser = subparsers.add_parser( + "service", + description="Manage the PromptFlow service, which offers chat and trace UI functionalities.", + help="pf service", + ) + service_subparsers = service_parser.add_subparsers() + add_parser_start_service(service_subparsers) + add_parser_stop_service(service_subparsers) + add_parser_show_service(service_subparsers) + service_parser.set_defaults(action="service") + + +def dispatch_service_commands(args: argparse.Namespace): + if args.sub_action == "start": + start_service(args) + elif args.sub_action == "stop": + stop_service() + elif args.sub_action == "show-status": + show_service() + + +def add_parser_start_service(subparsers): + """Add service start parser to the pf service subparsers.""" + epilog = """ + Examples: + + # Start promptflow service: + pf service start + # Force restart promptflow service: + pf service start --force + # Start promptflow service with specific port: + pf service start --port 65553 + """ # noqa: E501 + add_param_port = lambda parser: parser.add_argument( # noqa: E731 + "-p", "--port", type=int, help="port of the promptflow service" ) - start_pfs_parser.add_argument("-p", "--port", type=int, help="port of the promptflow service") - start_pfs_parser.add_argument( + add_param_force = lambda parser: parser.add_argument( # noqa: E731 "--force", action="store_true", help="If the port is used, the existing service will be terminated and restart a new service.", ) - start_pfs_parser.add_argument( - "-d", - "--debug", - action="store_true", - help="The flag to turn on debug mode for pfs.", + activate_action( + name="start", + description="Start promptflow service.", + epilog=epilog, + add_params=[ + add_param_port, + add_param_force, + ] + + base_params, + subparsers=subparsers, + help_message="pf service start", + action_param_name="sub_action", ) - start_pfs_parser.set_defaults(action="start") -def add_stop_service_action(subparsers): - """Add action to stop pfs.""" - stop_pfs_parser = subparsers.add_parser( - "stop", +def add_parser_stop_service(subparsers): + """Add service stop parser to the pf service subparsers.""" + epilog = """ + Examples: + + # Stop promptflow service: + pf service stop + """ # noqa: E501 + activate_action( + name="stop", description="Stop promptflow service.", - help="pfs stop", - ) - stop_pfs_parser.add_argument( - "-d", - "--debug", - action="store_true", - help="The flag to turn on debug mode for pfs.", + epilog=epilog, + add_params=base_params, + subparsers=subparsers, + help_message="pf service stop", + action_param_name="sub_action", ) - stop_pfs_parser.set_defaults(action="stop") -def add_show_status_action(subparsers): - """Add action to show pfs status.""" - show_status_parser = subparsers.add_parser( - "show-status", +def add_parser_show_service(subparsers): + """Add service show parser to the pf service subparsers.""" + epilog = """ + Examples: + + # Display the started promptflow service info.: + pf service show-status + """ # noqa: E501 + activate_action( + name="show-status", description="Display the started promptflow service info.", - help="pfs show-status", + epilog=epilog, + add_params=base_params, + subparsers=subparsers, + help_message="pf service show-status", + action_param_name="sub_action", ) - show_status_parser.set_defaults(action="show-status") def start_service(args): @@ -161,7 +216,7 @@ def validate_port(port, force_start): ) command = ( f"waitress-serve --listen=127.0.0.1:{port} --threads={PF_SERVICE_WORKER_NUM} " - "promptflow._sdk._service.entry:get_app" + "promptflow._cli._pf._service:get_app" ) startupinfo = win32process.STARTUPINFO() startupinfo.dwFlags |= win32process.STARTF_USESHOWWINDOW @@ -192,7 +247,7 @@ def validate_port(port, force_start): "waitress-serve", f"--listen=127.0.0.1:{port}", f"--threads={PF_SERVICE_WORKER_NUM}", - "promptflow._sdk._service.entry:get_app", + "promptflow._cli._pf._service:get_app", ] subprocess.Popen(cmd, stdout=subprocess.DEVNULL, start_new_session=True) is_healthy = check_pfs_service_status(port) @@ -215,59 +270,17 @@ def stop_service(): print(message) -def main(): - command_args = sys.argv[1:] - if len(command_args) == 1 and command_args[0] == "version": - version_dict = {"promptflow": get_promptflow_sdk_version()} - return json.dumps(version_dict, ensure_ascii=False, indent=2, sort_keys=True, separators=(",", ": ")) + "\n" - if len(command_args) == 0: - command_args.append("-h") - entry(command_args) - - -def entry(command_args): - parser = argparse.ArgumentParser( - prog="pfs", - formatter_class=argparse.RawDescriptionHelpFormatter, - description="Prompt Flow Service", - ) - - parser.add_argument( - "-v", "--version", dest="version", action="store_true", help="show current PromptflowService version and exit" - ) - subparsers = parser.add_subparsers() - add_start_service_action(subparsers) - add_show_status_action(subparsers) - add_stop_service_action(subparsers) - - args = parser.parse_args(command_args) - - activity_name = _get_cli_activity_name(cli=parser.prog, args=args) - cli_exception_and_telemetry_handler(run_command, activity_name)(args) - - -def run_command(args): - # --debug, enable debug logging - if hasattr(args, "debug") and args.debug: - for handler in logger.handlers: - handler.setLevel(logging.DEBUG) - if args.version: - print_pf_version() +def show_service(): + port = get_port_from_config() + status = get_started_service_info(port) + if is_run_from_built_binary(): + log_file = HOME_PROMPT_FLOW_DIR / PF_SERVICE_LOG_FILE + else: + log_file = get_current_env_pfs_file(PF_SERVICE_LOG_FILE) + if status: + status.update({"log_file": log_file.as_posix()}) + print(status) return - elif args.action == "show-status": - port = get_port_from_config() - status = get_started_service_info(port) - if status: - print(status) - return - else: - logger.warning("Promptflow service is not started.") - sys.exit(1) - elif args.action == "start": - start_service(args) - elif args.action == "stop": - stop_service() - - -if __name__ == "__main__": - main() + else: + logger.warning(f"Promptflow service is not started. log_file: {log_file.as_posix()}") + sys.exit(1) diff --git a/src/promptflow-devkit/promptflow/_cli/_pf/entry.py b/src/promptflow-devkit/promptflow/_cli/_pf/entry.py index ecd8091bef6..c4d719c5f15 100644 --- a/src/promptflow-devkit/promptflow/_cli/_pf/entry.py +++ b/src/promptflow-devkit/promptflow/_cli/_pf/entry.py @@ -22,6 +22,7 @@ from promptflow._cli._pf._connection import add_connection_parser, dispatch_connection_commands # noqa: E402 from promptflow._cli._pf._flow import add_flow_parser, dispatch_flow_commands # noqa: E402 from promptflow._cli._pf._run import add_run_parser, dispatch_run_commands # noqa: E402 +from promptflow._cli._pf._service import add_service_parser, dispatch_service_commands # noqa: E402 from promptflow._cli._pf._tool import add_tool_parser, dispatch_tool_commands # noqa: E402 from promptflow._cli._pf._upgrade import add_upgrade_parser, upgrade_version # noqa: E402 from promptflow._cli._pf.help import show_privacy_statement, show_welcome_message # noqa: E402 @@ -63,6 +64,8 @@ def run_command(args): upgrade_version(args) elif args.action == "experiment": dispatch_experiment_commands(args) + elif args.action == "service": + dispatch_service_commands(args) except KeyboardInterrupt as ex: logger.debug("Keyboard interrupt is captured.") raise ex @@ -100,6 +103,7 @@ def get_parser_args(argv): add_run_parser(subparsers) add_config_parser(subparsers) add_tool_parser(subparsers) + add_service_parser(subparsers) if Configuration.get_instance().is_internal_features_enabled(): add_experiment_parser(subparsers) diff --git a/src/promptflow-devkit/promptflow/_sdk/_service/app.py b/src/promptflow-devkit/promptflow/_sdk/_service/app.py index d6b891d3ead..efa65a6da6f 100644 --- a/src/promptflow-devkit/promptflow/_sdk/_service/app.py +++ b/src/promptflow-devkit/promptflow/_sdk/_service/app.py @@ -150,7 +150,7 @@ def monitor_request(): port = get_port_from_config() if port: app.logger.info( - f"Try auto stop pfs service in port {port} since no request to app within " + f"Try auto stop promptflow service in port {port} since no request to app within " f"{PF_SERVICE_HOUR_TIMEOUT}h" ) kill_exist_service(port) diff --git a/src/promptflow-devkit/promptflow/_sdk/_service/pfsvc.py b/src/promptflow-devkit/promptflow/_sdk/_service/pfsvc.py deleted file mode 100644 index c5ee19e7e93..00000000000 --- a/src/promptflow-devkit/promptflow/_sdk/_service/pfsvc.py +++ /dev/null @@ -1,56 +0,0 @@ -# --------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# --------------------------------------------------------- -from promptflow._sdk._service.entry import main -import sys - -import win32serviceutil # ServiceFramework and commandline helper -import win32service # Events -import servicemanager # Simple setup and logging - - -class PromptFlowService: - """Silly little application stub""" - - def stop(self): - """Stop the service""" - self.running = False - - def run(self): - """Main service loop. This is where work is done!""" - self.running = True - while self.running: - main() # Important work - servicemanager.LogInfoMsg("Service running...") - - -class PromptFlowServiceFramework(win32serviceutil.ServiceFramework): - _svc_name_ = "PromptFlowService" - _svc_display_name_ = "Prompt Flow Service" - - def SvcStop(self): - """Stop the service""" - self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) - self.service_impl.stop() - self.ReportServiceStatus(win32service.SERVICE_STOPPED) - - def SvcDoRun(self): - """Start the service; does not return until stopped""" - self.ReportServiceStatus(win32service.SERVICE_START_PENDING) - self.service_impl = PromptFlowService() - self.ReportServiceStatus(win32service.SERVICE_RUNNING) - # Run the service - self.service_impl.run() - - -def init(): - if len(sys.argv) == 1: - servicemanager.Initialize() - servicemanager.PrepareToHostSingle(PromptFlowServiceFramework) - servicemanager.StartServiceCtrlDispatcher() - else: - win32serviceutil.HandleCommandLine(PromptFlowServiceFramework) - - -if __name__ == "__main__": - init() diff --git a/src/promptflow-devkit/promptflow/_sdk/_tracing.py b/src/promptflow-devkit/promptflow/_sdk/_tracing.py index fdd095e2a93..2cef2e0c2f0 100644 --- a/src/promptflow-devkit/promptflow/_sdk/_tracing.py +++ b/src/promptflow-devkit/promptflow/_sdk/_tracing.py @@ -14,6 +14,7 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor +from promptflow._cli._pf.entry import entry from promptflow._constants import ( OTEL_RESOURCE_SERVICE_NAME, SpanAttributeFieldName, @@ -28,7 +29,6 @@ AzureMLWorkspaceTriad, ContextAttributeKey, ) -from promptflow._sdk._service.entry import entry from promptflow._sdk._service.utils.utils import get_port_from_config, is_pfs_service_healthy, is_port_in_use from promptflow._sdk._utils import extract_workspace_triad_from_trace_provider from promptflow._utils.logger_utils import get_cli_sdk_logger @@ -61,7 +61,7 @@ def _inject_attrs_to_op_ctx(attrs: typing.Dict[str, str]) -> None: def _invoke_pf_svc() -> str: port = get_port_from_config(create_if_not_exists=True) port = str(port) - cmd_args = ["start", "--port", port] + cmd_args = ["service", "start", "--port", port] hint_stop_message = ( f"You can stop the Prompt flow Tracing Server with the following command:'\033[1m pf service stop\033[0m'.\n" f"Alternatively, if no requests are made within {PF_SERVICE_HOUR_TIMEOUT} " diff --git a/src/promptflow-devkit/pyproject.toml b/src/promptflow-devkit/pyproject.toml index 1d5f3cd84cc..1c09145041b 100644 --- a/src/promptflow-devkit/pyproject.toml +++ b/src/promptflow-devkit/pyproject.toml @@ -102,7 +102,6 @@ build-backend = "poetry.core.masonry.api" [tool.poetry.scripts] pf = "promptflow._cli._pf.entry:main" -pfs = "promptflow._sdk._service.entry:main" [tool.pytest.ini_options] markers = [ diff --git a/src/promptflow/tests/sdk_pfs_test/e2etests/test_cli.py b/src/promptflow/tests/sdk_pfs_test/e2etests/test_cli.py index 48a2d189695..5740180293d 100644 --- a/src/promptflow/tests/sdk_pfs_test/e2etests/test_cli.py +++ b/src/promptflow/tests/sdk_pfs_test/e2etests/test_cli.py @@ -8,7 +8,7 @@ import pytest import requests -from promptflow._sdk._service.entry import main +from promptflow._cli._pf.entry import main from promptflow._sdk._service.utils.utils import get_port_from_config, get_random_port, kill_exist_service @@ -18,20 +18,20 @@ def _run_pfs_command(self, *args): """Run a pfs command with the given arguments.""" origin_argv = sys.argv try: - sys.argv = ["pfs"] + list(args) + sys.argv = ["pf", "service"] + list(args) main() finally: sys.argv = origin_argv def _test_start_service(self, port=None, force=False): - command = f"pfs start --port {port}" if port else "pfs start" + command = f"pf service start --port {port}" if port else "pf service start" if force: command = f"{command} --force" start_pfs = subprocess.Popen(command, shell=True) # Wait for service to be started start_pfs.wait() assert self._is_service_healthy() - stop_command = "pfs stop" + stop_command = "pf service stop" stop_pfs = subprocess.Popen(stop_command, shell=True) stop_pfs.wait() @@ -49,7 +49,7 @@ def test_start_service(self): self._test_start_service(port=random_port, force=True) # start pfs - start_pfs = subprocess.Popen("pfs start", shell=True) + start_pfs = subprocess.Popen("pf service start", shell=True) # Wait for service to be started start_pfs.wait() assert self._is_service_healthy() @@ -63,7 +63,7 @@ def test_start_service(self): def test_show_service_status(self, capsys): with pytest.raises(SystemExit): self._run_pfs_command("show-status") - start_pfs = subprocess.Popen("pfs start", shell=True) + start_pfs = subprocess.Popen("pf service start", shell=True) # Wait for service to be started start_pfs.wait() assert self._is_service_healthy()