-
Notifications
You must be signed in to change notification settings - Fork 888
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support flow.flex.yaml for csharp serve (#3019)
# Description Allow run `pf flow serve` on below csharp scenario 1. flex flow, including class init 2. use yaml as source Add an util function to infer language of a flow # All Promptflow Contribution checklist: - [x] **The pull request does not introduce [breaking changes].** - [x] **CHANGELOG is updated for new features, bug fixes or other significant changes.** - [x] **I have read the [contribution guidelines](../CONTRIBUTING.md).** - [x] **Create an issue and link to the pull request to get dedicated review from promptflow team. Learn more: [suggested workflow](../CONTRIBUTING.md#suggested-workflow).** ## General Guidelines and Best Practices - [x] Title of the pull request is clear and informative. - [x] There are a small number of commits, each of which have an informative message. This means that previously merged commits do not appear in the history of the PR. For more information on cleaning up the commits in your PR, [see this page](https://github.com/Azure/azure-powershell/blob/master/documentation/development-docs/cleaning-up-commits.md). ### Testing Guidelines - [ ] Pull request includes test coverage for the included changes.
- Loading branch information
Showing
7 changed files
with
259 additions
and
107 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
src/promptflow-devkit/promptflow/_sdk/_utils/chat_utils.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
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 | ||
|
||
|
||
def construct_flow_absolute_path(flow: str) -> str: | ||
flow_dir, flow_file = resolve_flow_path(flow) | ||
return (flow_dir / flow_file).absolute().resolve().as_posix() | ||
|
||
|
||
# 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.update({"enable_internal_features": "true", **url_params}) | ||
query_params = urlencode(query_dict) | ||
return urlunparse(("http", f"127.0.0.1:{port}", "/v1.0/ui/chat", "", query_params, "")) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
167 changes: 167 additions & 0 deletions
167
src/promptflow-devkit/promptflow/_sdk/_utils/serve_utils.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
import contextlib | ||
import json | ||
import logging | ||
import os | ||
import shutil | ||
import socket | ||
import subprocess | ||
import sys | ||
import tempfile | ||
import uuid | ||
import webbrowser | ||
from pathlib import Path | ||
from typing import Any, Dict, Generator | ||
|
||
from promptflow._constants import PROMPT_FLOW_DIR_NAME, FlowLanguage | ||
from promptflow._proxy._csharp_inspector_proxy import EXECUTOR_SERVICE_DLL | ||
from promptflow._sdk._utils.general_utils import resolve_flow_language | ||
from promptflow._utils.flow_utils import resolve_flow_path | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def find_available_port() -> str: | ||
"""Find an available port on localhost""" | ||
# TODO: replace find_available_port in CSharpExecutorProxy with this one | ||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: | ||
s.bind(("localhost", 0)) | ||
_, port = s.getsockname() | ||
return str(port) | ||
|
||
|
||
def _resolve_python_flow_additional_includes(source) -> Path: | ||
# Resolve flow additional includes | ||
from promptflow.client import load_flow | ||
|
||
flow = load_flow(source) | ||
from promptflow._sdk.operations import FlowOperations | ||
|
||
with FlowOperations._resolve_additional_includes(flow.path) as resolved_flow_path: | ||
if resolved_flow_path == flow.path: | ||
return source | ||
# Copy resolved flow to temp folder if additional includes exists | ||
# Note: DO NOT use resolved flow path directly, as when inner logic raise exception, | ||
# temp dir will fail due to file occupied by other process. | ||
temp_flow_path = Path(tempfile.TemporaryDirectory().name) | ||
shutil.copytree(src=resolved_flow_path.parent, dst=temp_flow_path, dirs_exist_ok=True) | ||
|
||
return temp_flow_path | ||
|
||
|
||
def start_flow_service( | ||
*, | ||
source: Path, | ||
static_folder: str = None, | ||
host: str = "localhost", | ||
port: int = 8080, | ||
config: dict = None, | ||
environment_variables: Dict[str, str] = None, | ||
init: Dict[str, Any] = None, | ||
skip_open_browser: bool = True, | ||
): | ||
logger.info( | ||
"Start promptflow server with port %s", | ||
port, | ||
) | ||
language = resolve_flow_language(flow_path=source) | ||
|
||
flow_dir, flow_file_name = resolve_flow_path(source) | ||
if language == FlowLanguage.Python: | ||
serve_python_flow( | ||
flow_file_name=flow_file_name, | ||
flow_dir=flow_dir, | ||
init=init or {}, | ||
port=port, | ||
static_folder=static_folder, | ||
host=host, | ||
config=config or {}, | ||
environment_variables=environment_variables or {}, | ||
skip_open_browser=skip_open_browser, | ||
) | ||
else: | ||
serve_csharp_flow( | ||
flow_file_name=flow_file_name, | ||
flow_dir=flow_dir, | ||
init=init or {}, | ||
port=port, | ||
) | ||
|
||
|
||
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) | ||
|
||
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, | ||
"--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 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 |
Oops, something went wrong.