From 98c38f741d68ed4aa21ae5d20e715fa3bddacd01 Mon Sep 17 00:00:00 2001 From: zhangxingzhi Date: Thu, 25 Apr 2024 19:42:08 +0800 Subject: [PATCH] feat: import resolve_flow_language in _internal --- .../promptflow/_utils/flow_utils.py | 21 -- .../promptflow/_cli/_pf/_flow.py | 16 +- .../promptflow/_internal/__init__.py | 1 + .../promptflow/_sdk/_utils/chat_utils.py | 20 +- .../promptflow/_sdk/_utils/general_utils.py | 21 +- .../promptflow/_sdk/_utils/serve_utils.py | 192 ++++++++---------- 6 files changed, 115 insertions(+), 156 deletions(-) diff --git a/src/promptflow-core/promptflow/_utils/flow_utils.py b/src/promptflow-core/promptflow/_utils/flow_utils.py index f997ee5c8d8..58fb7839edc 100644 --- a/src/promptflow-core/promptflow/_utils/flow_utils.py +++ b/src/promptflow-core/promptflow/_utils/flow_utils.py @@ -16,10 +16,8 @@ FLOW_DAG_YAML, FLOW_FILE_SUFFIX, FLOW_FLEX_YAML, - LANGUAGE_KEY, PROMPT_FLOW_DIR_NAME, PROMPTY_EXTENSION, - FlowLanguage, ) from promptflow._core._errors import MetaFileNotFound, MetaFileReadError from promptflow._utils.logger_utils import LoggerFactory @@ -187,25 +185,6 @@ def is_flex_flow( return isinstance(yaml_dict, dict) and "entry" in yaml_dict -def resolve_flow_language( - *, - flow_path: Union[str, Path, PathLike, None] = None, - yaml_dict: Optional[dict] = None, - working_dir: Union[str, Path, PathLike, None] = None, -) -> str: - """Get language of a flow. Will return 'python' for Prompty.""" - if flow_path is None and yaml_dict is None: - raise UserErrorException("Either file_path or yaml_dict should be provided.") - if flow_path is not None and yaml_dict is not None: - raise UserErrorException("Only one of file_path and yaml_dict should be provided.") - if flow_path is not None: - flow_path, flow_file = resolve_flow_path(flow_path, base_path=working_dir, check_flow_exist=False) - file_path = flow_path / flow_file - if file_path.is_file() and file_path.suffix.lower() in (".yaml", ".yml"): - yaml_dict = load_yaml(file_path) - return yaml_dict.get(LANGUAGE_KEY, FlowLanguage.Python) - - def is_prompty_flow(file_path: Union[str, Path], raise_error: bool = False): """Check if the flow is a prompty flow by extension of the flow file is .prompty.""" if not file_path or not Path(file_path).exists(): diff --git a/src/promptflow-devkit/promptflow/_cli/_pf/_flow.py b/src/promptflow-devkit/promptflow/_cli/_pf/_flow.py index fd4ccd885d5..c84c96a18a2 100644 --- a/src/promptflow-devkit/promptflow/_cli/_pf/_flow.py +++ b/src/promptflow-devkit/promptflow/_cli/_pf/_flow.py @@ -42,7 +42,7 @@ from promptflow._sdk._constants import PROMPT_FLOW_DIR_NAME from promptflow._sdk._pf_client import PFClient from promptflow._sdk._utils import generate_yaml_entry_without_recover -from promptflow._sdk._utils.chat_utils import construct_chat_page_url, construct_flow_absolute_path +from promptflow._sdk._utils.chat_utils import construct_chat_page_url from promptflow._sdk._utils.serve_utils import start_flow_service from promptflow._utils.flow_utils import is_flex_flow from promptflow._utils.logger_utils import get_cli_sdk_logger @@ -512,22 +512,16 @@ def _test_flow_multi_modal(args, pf_client): logger.info("Start streamlit with main script generated at: %s", main_script_path) pf_client.flows._chat_with_ui(script=main_script_path, skip_open_browser=args.skip_open_browser) else: - from promptflow._sdk._load_functions import load_flow from promptflow._sdk._tracing import _invoke_pf_svc pfs_port = _invoke_pf_svc() flow = generate_yaml_entry_without_recover(entry=args.flow) # flex flow without yaml file doesn't support /eval in chat window - enable_internal_features = flow != args.flow or Configuration.get_instance().is_internal_features_enabled() - - # for entry like "package:entry_function", a temp flow.flex.yaml will be generated at flow - flow_entity = load_flow(flow) - flow_absolute_path = construct_flow_absolute_path(flow) + enable_internal_features = Configuration.get_instance().is_internal_features_enabled() or flow != args.flow chat_page_url = construct_chat_page_url( - flow_absolute_path, - flow_dir=flow_entity.code, - pfs_port=pfs_port, - url_params=list_of_dict_to_dict(args.url_params), + flow, + pfs_port, + list_of_dict_to_dict(args.url_params), enable_internal_features=enable_internal_features, ) print(f"You can begin chat flow on {chat_page_url}") diff --git a/src/promptflow-devkit/promptflow/_internal/__init__.py b/src/promptflow-devkit/promptflow/_internal/__init__.py index 6046edd88fe..81e9960c1e0 100644 --- a/src/promptflow-devkit/promptflow/_internal/__init__.py +++ b/src/promptflow-devkit/promptflow/_internal/__init__.py @@ -50,6 +50,7 @@ from promptflow._sdk._constants import LOCAL_MGMT_DB_PATH, CreatedByFieldName from promptflow._sdk._service.apis.collector import trace_collector from promptflow._sdk._tracing import process_otlp_trace_request +from promptflow._sdk._utils.general_utils import resolve_flow_language from promptflow._sdk._version import VERSION from promptflow._utils.context_utils import _change_working_dir, inject_sys_path from promptflow._utils.credential_scrubber import CredentialScrubber diff --git a/src/promptflow-devkit/promptflow/_sdk/_utils/chat_utils.py b/src/promptflow-devkit/promptflow/_sdk/_utils/chat_utils.py index 7687b58ea89..aaaa73c719f 100644 --- a/src/promptflow-devkit/promptflow/_sdk/_utils/chat_utils.py +++ b/src/promptflow-devkit/promptflow/_sdk/_utils/chat_utils.py @@ -1,6 +1,6 @@ -from pathlib import Path from urllib.parse import urlencode, urlunparse +from promptflow._sdk._service.utils.utils import encrypt_flow_path from promptflow._utils.flow_utils import resolve_flow_path @@ -9,15 +9,13 @@ def construct_flow_absolute_path(flow: str) -> str: return (flow_dir / flow_file).absolute().resolve().as_posix() -def construct_chat_page_url( - flow_path: str, flow_dir: Path, pfs_port, url_params: dict, enable_internal_features: bool -) -> str: - from promptflow._sdk._service.utils.utils import encrypt_flow_path - - # Todo: use base64 encode for now, will consider whether need use encryption or use db to store flow path info - query_dict = {"flow": encrypt_flow_path(flow_path), **url_params} +# Todo: use base64 encode for now, will consider whether need use encryption or use db to store flow path info +def construct_chat_page_url(flow, port, url_params, enable_internal_features=False): + flow_path_dir, flow_path_file = resolve_flow_path(flow) + flow_path = str(flow_path_dir / flow_path_file) + encrypted_flow_path = encrypt_flow_path(flow_path) + query_dict = {"flow": encrypted_flow_path} if enable_internal_features: - query_dict["enable_internal_features"] = "true" + query_dict.update({"enable_internal_features": "true", **url_params}) query_params = urlencode(query_dict) - - return urlunparse(("http", f"127.0.0.1:{pfs_port}", "/v1.0/ui/chat", "", query_params, "")) + return urlunparse(("http", f"127.0.0.1:{port}", "/v1.0/ui/chat", "", query_params, "")) diff --git a/src/promptflow-devkit/promptflow/_sdk/_utils/general_utils.py b/src/promptflow-devkit/promptflow/_sdk/_utils/general_utils.py index afb33415baf..2b8a4464d08 100644 --- a/src/promptflow-devkit/promptflow/_sdk/_utils/general_utils.py +++ b/src/promptflow-devkit/promptflow/_sdk/_utils/general_utils.py @@ -32,7 +32,7 @@ from keyring.errors import NoKeyringError from marshmallow import ValidationError -from promptflow._constants import ENABLE_MULTI_CONTAINER_KEY, EXTENSION_UA, FLOW_FLEX_YAML, FlowLanguage +from promptflow._constants import ENABLE_MULTI_CONTAINER_KEY, EXTENSION_UA, FLOW_FLEX_YAML, LANGUAGE_KEY, FlowLanguage from promptflow._core.entry_meta_generator import generate_flow_meta as _generate_flow_meta from promptflow._sdk._constants import ( AZURE_WORKSPACE_REGEX_FORMAT, @@ -1103,3 +1103,22 @@ def load_input_data(data_path): return json.load(f) else: raise ValueError("Only support jsonl or json file as input.") + + +def resolve_flow_language( + *, + flow_path: Union[str, Path, PathLike, None] = None, + yaml_dict: Optional[dict] = None, + working_dir: Union[str, Path, PathLike, None] = None, +) -> str: + """Get language of a flow. Will return 'python' for Prompty.""" + if flow_path is None and yaml_dict is None: + raise UserErrorException("Either file_path or yaml_dict should be provided.") + if flow_path is not None and yaml_dict is not None: + raise UserErrorException("Only one of file_path and yaml_dict should be provided.") + if flow_path is not None: + flow_path, flow_file = resolve_flow_path(flow_path, base_path=working_dir, check_flow_exist=False) + file_path = flow_path / flow_file + if file_path.is_file() and file_path.suffix.lower() in (".yaml", ".yml"): + yaml_dict = load_yaml(file_path) + return yaml_dict.get(LANGUAGE_KEY, FlowLanguage.Python) diff --git a/src/promptflow-devkit/promptflow/_sdk/_utils/serve_utils.py b/src/promptflow-devkit/promptflow/_sdk/_utils/serve_utils.py index 765c66dea34..044bbbc8676 100644 --- a/src/promptflow-devkit/promptflow/_sdk/_utils/serve_utils.py +++ b/src/promptflow-devkit/promptflow/_sdk/_utils/serve_utils.py @@ -1,4 +1,3 @@ -import abc import contextlib import json import logging @@ -8,13 +7,15 @@ import subprocess import sys import tempfile +import uuid import webbrowser from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, Generator -from promptflow._constants import FlowLanguage +from promptflow._constants import PROMPT_FLOW_DIR_NAME, FlowLanguage from promptflow._proxy._csharp_inspector_proxy import EXECUTOR_SERVICE_DLL -from promptflow._utils.flow_utils import resolve_flow_language, resolve_flow_path +from promptflow._sdk._utils.general_utils import resolve_flow_language +from promptflow._utils.flow_utils import resolve_flow_path logger = logging.getLogger(__name__) @@ -65,10 +66,8 @@ def start_flow_service( language = resolve_flow_language(flow_path=source) flow_dir, flow_file_name = resolve_flow_path(source) - - os.environ["PROMPTFLOW_PROJECT_PATH"] = flow_dir.absolute().as_posix() if language == FlowLanguage.Python: - helper = PythonFlowServiceHelper( + serve_python_flow( flow_file_name=flow_file_name, flow_dir=flow_dir, init=init or {}, @@ -80,120 +79,89 @@ def start_flow_service( skip_open_browser=skip_open_browser, ) else: - helper = CSharpFlowServiceHelper( + serve_csharp_flow( flow_file_name=flow_file_name, flow_dir=flow_dir, init=init or {}, port=port, ) - helper.run() -class BaseFlowServiceHelper: - def __init__(self): - pass - - @abc.abstractmethod - def run(self): - pass +def serve_python_flow( + *, + flow_file_name, + flow_dir, + port, + host, + static_folder, + config, + environment_variables, + init, + skip_open_browser: bool, +): + from promptflow._sdk._configuration import Configuration + from promptflow.core._serving.app import create_app + flow_dir = _resolve_python_flow_additional_includes(flow_dir / flow_file_name) -class PythonFlowServiceHelper(BaseFlowServiceHelper): - def __init__( - self, - *, + pf_config = Configuration(overrides=config) + logger.info(f"Promptflow config: {pf_config}") + connection_provider = pf_config.get_connection_provider() + os.environ["PROMPTFLOW_PROJECT_PATH"] = flow_dir.absolute().as_posix() + logger.info(f"Change working directory to model dir {flow_dir}") + os.chdir(flow_dir) + app = create_app( + static_folder=Path(static_folder).absolute().as_posix() if static_folder else None, + environment_variables=environment_variables, + connection_provider=connection_provider, + init=init, + ) + if not skip_open_browser: + target = f"http://{host}:{port}" + logger.info(f"Opening browser {target}...") + webbrowser.open(target) + # Debug is not supported for now as debug will rerun command, and we changed working directory. + app.run(port=port, host=host) + + +@contextlib.contextmanager +def construct_csharp_service_start_up_command( + *, port: int, flow_file_name: str, flow_dir: Path, init: Dict[str, Any] = None +) -> Generator[str, None, None]: + cmd = [ + "dotnet", + EXECUTOR_SERVICE_DLL, + "--port", + str(port), + "--yaml_path", flow_file_name, - flow_dir, - port, - host, - static_folder, - config, - environment_variables, - init, - skip_open_browser: bool, - ): - self._static_folder = static_folder - self.flow_file_name = flow_file_name - self.flow_dir = flow_dir - self.host = host - self.port = port - self.config = config - self.environment_variables = environment_variables - self.init = init - self.skip_open_browser = skip_open_browser - super().__init__() - - @property - def static_folder(self): - if self._static_folder is None: - return None - return Path(self._static_folder).absolute().as_posix() - - def run(self): - from promptflow._sdk._configuration import Configuration - from promptflow.core._serving.app import create_app - - flow_dir = _resolve_python_flow_additional_includes(self.flow_dir / self.flow_file_name) - - pf_config = Configuration(overrides=self.config) - logger.info(f"Promptflow config: {pf_config}") - connection_provider = pf_config.get_connection_provider() - os.environ["PROMPTFLOW_PROJECT_PATH"] = flow_dir.absolute().as_posix() - logger.info(f"Change working directory to model dir {flow_dir}") - os.chdir(flow_dir) - app = create_app( - static_folder=self.static_folder, - environment_variables=self.environment_variables, - connection_provider=connection_provider, - init=self.init, - ) - if not self.skip_open_browser: - target = f"http://{self.host}:{self.port}" - logger.info(f"Opening browser {target}...") - webbrowser.open(target) - # Debug is not supported for now as debug will rerun command, and we changed working directory. - app.run(port=self.port, host=self.host) - - -class CSharpFlowServiceHelper(BaseFlowServiceHelper): - def __init__(self, *, flow_file_name, flow_dir, init, port): - self.port = port - self._init = init - self.flow_dir, self.flow_file_name = flow_dir, flow_file_name - super().__init__() - - @contextlib.contextmanager - def _construct_command(self): - cmd = [ - "dotnet", - EXECUTOR_SERVICE_DLL, - "--port", - str(self.port), - "--yaml_path", - self.flow_file_name, - "--assembly_folder", - ".", - "--connection_provider_url", - "", - "--log_path", - "", - "--serving", - ] - if self._init: - init_json_path = Path(tempfile.mktemp()).with_suffix(".json") - with open(init_json_path, "w") as f: - json.dump(self._init, f) - cmd.extend(["--init", init_json_path.as_posix()]) - try: - yield cmd - finally: - os.remove(init_json_path) - else: + "--assembly_folder", + ".", + "--connection_provider_url", + "", + "--log_path", + "", + "--serving", + ] + if init: + init_json_path = flow_dir / PROMPT_FLOW_DIR_NAME / f"init-{uuid.uuid4()}.json" + init_json_path.parent.mkdir(parents=True, exist_ok=True) + with open(init_json_path, "w") as f: + json.dump(init, f) + cmd.extend(["--init", init_json_path.as_posix()]) + try: yield cmd + finally: + os.remove(init_json_path) + else: + yield cmd - def run(self): - try: - with self._construct_command() as command: - subprocess.run(command, cwd=self.flow_dir, stdout=sys.stdout, stderr=sys.stderr) - except KeyboardInterrupt: - pass + +def serve_csharp_flow(flow_dir: Path, port: int, flow_file_name: str, init: Dict[str, Any] = None): + try: + with construct_csharp_service_start_up_command( + port=port, flow_file_name=flow_file_name, flow_dir=flow_dir, init=init + ) as command: + subprocess.run(command, cwd=flow_dir, stdout=sys.stdout, stderr=sys.stderr) + except KeyboardInterrupt: + pass