Skip to content

Commit

Permalink
feat: import resolve_flow_language in _internal
Browse files Browse the repository at this point in the history
  • Loading branch information
elliotzh committed Apr 26, 2024
1 parent 05d5558 commit 98c38f7
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 156 deletions.
21 changes: 0 additions & 21 deletions src/promptflow-core/promptflow/_utils/flow_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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():
Expand Down
16 changes: 5 additions & 11 deletions src/promptflow-devkit/promptflow/_cli/_pf/_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}")
Expand Down
1 change: 1 addition & 0 deletions src/promptflow-devkit/promptflow/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 9 additions & 11 deletions src/promptflow-devkit/promptflow/_sdk/_utils/chat_utils.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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, ""))
21 changes: 20 additions & 1 deletion src/promptflow-devkit/promptflow/_sdk/_utils/general_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
192 changes: 80 additions & 112 deletions src/promptflow-devkit/promptflow/_sdk/_utils/serve_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import abc
import contextlib
import json
import logging
Expand All @@ -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__)

Expand Down Expand Up @@ -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 {},
Expand All @@ -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

0 comments on commit 98c38f7

Please sign in to comment.