Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Doc&Sample]Flow build as executable package. #518

Closed
wants to merge 17 commits into from
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# This code is autogenerated.
# Code is generated by running custom script: python3 readme.py
# Any manual changes to this file may cause incorrect behavior.
# Any manual changes will be overwritten if the code is regenerated.

name: samples_tutorials_flow_deploy_flow_model_packaging
on:
schedule:
- cron: "10 19 * * *" # Every day starting at 3:10 BJT
pull_request:
branches: [ main ]
paths: [ examples/**, .github/workflows/samples_tutorials_flow_deploy_flow_model_packaging.yml ]
workflow_dispatch:

jobs:
samples_readme_ci:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Setup Python 3.9 environment
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Generate config.json
run: echo ${{ secrets.TEST_WORKSPACE_CONFIG_JSON }} > ${{ github.workspace }}/examples/config.json
- name: Prepare requirements
working-directory: examples
run: |
if [[ -e requirements.txt ]]; then
python -m pip install --upgrade pip
pip install -r requirements.txt
fi
- name: Prepare dev requirements
working-directory: examples
run: |
python -m pip install --upgrade pip
pip install -r dev_requirements.txt
- name: Refine .env file
working-directory: examples/tutorials/flow-deploy/flow-model-packaging
run: |
AOAI_API_KEY=${{ secrets.AOAI_API_KEY_TEST }}
AOAI_API_ENDPOINT=${{ secrets.AOAI_API_ENDPOINT_TEST }}
AOAI_API_ENDPOINT=$(echo ${AOAI_API_ENDPOINT//\//\\/})
if [[ -e .env.example ]]; then
echo "env replacement"
sed -i -e "s/<your_AOAI_key>/$AOAI_API_KEY/g" -e "s/<your_AOAI_endpoint>/$AOAI_API_ENDPOINT/g" .env.example
mv .env.example .env
fi
- name: Create run.yml
working-directory: examples/tutorials/flow-deploy/flow-model-packaging
run: |
gpt_base=${{ secrets.AOAI_API_ENDPOINT_TEST }}
gpt_base=$(echo ${gpt_base//\//\\/})
if [[ -e run.yml ]]; then
sed -i -e "s/\${azure_open_ai_connection.api_key}/${{ secrets.AOAI_API_KEY_TEST }}/g" -e "s/\${azure_open_ai_connection.api_base}/$gpt_base/g" run.yml
fi
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Extract Steps examples/tutorials/flow-deploy/flow-model-packaging/README.md
working-directory: ${{ github.workspace }}
run: |
python scripts/readme/extract_steps_from_readme.py -f examples/tutorials/flow-deploy/flow-model-packaging/README.md -o examples/tutorials/flow-deploy/flow-model-packaging
- name: Cat script
working-directory: examples/tutorials/flow-deploy/flow-model-packaging
run: |
cat bash_script.sh
- name: Run scripts
working-directory: examples/tutorials/flow-deploy/flow-model-packaging
run: |
export aoai_api_key=${{secrets.AOAI_API_KEY_TEST }}
export aoai_api_endpoint=${{ secrets.AOAI_API_ENDPOINT_TEST }}
export test_workspace_sub_id=${{ secrets.TEST_WORKSPACE_SUB_ID }}
export test_workspace_rg=${{ secrets.TEST_WORKSPACE_RG }}
export test_workspace_name=${{ secrets.TEST_WORKSPACE_NAME }}
bash bash_script.sh
- name: Pip List for Debug
if : ${{ always() }}
working-directory: examples/tutorials/flow-deploy/flow-model-packaging
run: |
pip list
- name: Upload artifact
if: ${{ always() }}
uses: actions/upload-artifact@v3
with:
name: artifact
path: examples/tutorials/flow-deploy/flow-model-packaging/bash_script.sh
204 changes: 204 additions & 0 deletions docs/how-to-guides/deploy-a-flow/flow-model-packaging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
# Flow model packaging
YingChen1996 marked this conversation as resolved.
Show resolved Hide resolved
:::{admonition} Experimental feature
This is an experimental feature, and may change at any time. Learn [more](../faq.md#stable-vs-experimental).
:::

There are Four steps to package a flow and deploy in locals:
1. Build the flow as docker format.
YingChen1996 marked this conversation as resolved.
Show resolved Hide resolved
2. Prepare an entry file.
YingChen1996 marked this conversation as resolved.
Show resolved Hide resolved
3. Prepare a spec file.
4. Package flow using [Pyinstaller](https://pyinstaller.org/en/stable/requirements.html#).


## Build a flow as docker format

::::{tab-set}
:::{tab-item} CLI
:sync: CLI

Note that all dependent connections must be created before building as docker.
YingChen1996 marked this conversation as resolved.
Show resolved Hide resolved
```bash
# create connection if not created before
pf connection create --file ../../../examples/connections/azure_openai.yml --set api_key=<your_api_key> api_base=<your_api_base> --name open_ai_connection
```

Use the command below to build a flow as docker format:
```bash
pf flow build --source <path-to-your-flow-folder> --output <your-output-dir> --format docker
```
:::
:::{tab-item} VS Code Extension
:sync: VSC

Click the button below to build a flow as docker format:
YingChen1996 marked this conversation as resolved.
Show resolved Hide resolved
![img](../../media/how-to-guides/vscode_export_as_docker.png)
:::
::::

Note that all dependent connections must be created before exporting as docker.

### Docker format folder structure

Exported Dockerfile & its dependencies are located in the same folder. The structure is as below:
- flow: the folder contains all the flow files
- ...
- connections: the folder contains yaml files to create all related connections
- ...
- Dockerfile: the dockerfile to build the image
- start.sh: the script used in `CMD` of `Dockerfile` to start the service
- settings.json: a json file to store the settings of the docker image
- README.md: Simple introduction of the files

## Package flow model
We are going to use the [web-classification](https://github.com/microsoft/promptflow/tree/main/examples/flows/standard/web-classification/) as
an example to show how to package flow model with Pyinstaller.

Please ensure you have [create the connection](../manage-connections.md#create-a-connection) required by flow, if not, you could
refer to [Setup connection for web-classifiction](https://github.com/microsoft/promptflow/tree/main/examples/flows/standard/web-classification).

Additionally, please ensure that you have installed all the required dependencies. You can refer to the "Prerequisites" section in the README of the [web-classification](https://github.com/microsoft/promptflow/tree/main/examples/flows/standard/web-classification/) for a comprehensive list of prerequisites and installation instructions.

### Prepare an entry file
A Python entry file is included as the entry point for the bundled app. We offer a Python file named `start.py`` here, which enables you to serve a flow folder as an endpoint.

```python
import subprocess
import os
import json

def setup_promptflow(requirement_path) -> None:
if os.path.exists(requirement_path):
print("- Setting up the promptflow requirements")
cmds = ["pip", "install", "-q", "-r", requirement_path]
subprocess.run(cmds)
else:
print("- Setting up the promptflow")
cmds = ["pip", "install", "promptflow", "-q"]
subprocess.run(cmds)

print("- Setting up the promptflow-tools")
cmds = ["pip", "install", "promptflow-tools", "-q"]
subprocess.run(cmds)

def create_connection(directory_path) -> None:
for root, dirs, files in os.walk(directory_path):
for file in files:
file_path = os.path.join(root, file)
subprocess.run(["pf", "connection", "create", "--file", file_path])


def set_environment_variable(file_path) -> None:
with open(file_path, "r") as file:
json_data = json.load(file)
environment_variables = list(json_data.keys())
for environment_variable in environment_variables:
# Check if the required environment variable is set
if not os.environ.get(environment_variable):
print(f"{environment_variable} is not set.")
user_input = input(f"Please enter the value for {environment_variable}: ")
# Set the environment variable
os.environ[environment_variable] = user_input

if __name__ == "__main__":
setup_promptflow("./flow/requirements.txt")
create_connection("./connections")
set_environment_variable("./settings.json")
# Execute 'pf flow serve' command
subprocess.run(["pf", "flow", "serve", "--source", "flow", "--host", "0.0.0.0"])
```

### Prepare a spec file
The spec file tells PyInstaller how to process your script. It encodes the script names and most of the options you give to the pyinstaller command. The spec file is actually executable Python code. PyInstaller builds the app by executing the contents of the spec file.

To streamline this process, we offer a `start.spec`` spec file that bundles the application into a single folder. For additional information on spec files, you can refer to the [Using Spec Files](https://pyinstaller.org/en/stable/spec-files.html).

```spec
# -*- mode: python ; coding: utf-8 -*-

block_cipher = None

a = Analysis(
['start.py'],
pathex=[],
binaries=[],
datas=[("./connections", "connections"), ("./flow", "flow"), ("./settings.json", ".")],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='start',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='start',
)

```

### Package flow using Pyinstaller
PyInstaller reads a spec file or Python script written by you. It analyzes your code to discover every other module and library your script needs in order to execute. Then it collects copies of all those files, including the active Python interpreter, and puts them with your script in a single folder, or optionally in a single executable file.

Once you've placed the spec file `start.spec` and Python entry script `start.py` in the <your-output-dir> folder generated in the [Build a flow as docker format](#build-a-flow-as-docker-format), you can package the flow model by using the following command:
YingChen1996 marked this conversation as resolved.
Show resolved Hide resolved
```bash
cd <your-output-dir>
pyinstaller start.spec
```
It will create two folders named `build` and `dist` within your specified output directory, denoted as <your-output-dir>. The `build` folder houses various log and working files, while the `dist` folder contains the `start` executable folder. Inside the `dist` folder, you will discover the bundled application intended for distribution to your users.

#### Connections
If the service involves connections, all related connections will be exported as yaml files and recreated in the executable package.
Secrets in connections won't be exported directly. Instead, we will export them as a reference to environment variables:
```yaml
$schema: https://azuremlschemas.azureedge.net/promptflow/latest/OpenAIConnection.schema.json
type: open_ai
name: open_ai_connection
module: promptflow.connections
api_key: ${env:OPEN_AI_CONNECTION_API_KEY} # env reference
```
We will prompt you to set up the environment variables in the console to make the connections work.

### Test the endpoint
Finaly, You can compress the `dist` folder and distribute the bundle to other people. They can decompress it and execute your program by double clicking the executable file, e.g. `start.exe` in Windows system or excuting the binary file, e.g. `start` in Linux system.
YingChen1996 marked this conversation as resolved.
Show resolved Hide resolved
YingChen1996 marked this conversation as resolved.
Show resolved Hide resolved

Then they can open another terminal to test the endpoint with the following command:
```bash
curl http://localhost:8080/score --data '{"url":"https://play.google.com/store/apps/details?id=com.twitter.android"}' -X POST -H "Content-Type: application/json"
```
Also, the development server has a built-in web page they can use to test the flow by openning 'http://localhost:8080' in the browser. The expected result is as follows if the flow served successfully, and the process will keep alive until it be killed manually.

To your users, the app is self-contained. They do not need to install any particular version of Python or any modules. They do not need to have Python installed at all.

**Note**: The executable generated is not cross-platform. One platform (e.g. Windows) packaged executable can't run on others (Mac, Linux).


## Next steps
- Try the example [here](https://github.com/microsoft/promptflow/blob/main/examples/tutorials/flow-deploy)
1 change: 1 addition & 0 deletions docs/how-to-guides/deploy-a-flow/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ We are working on more official deployment guides for other hosting providers, a
deploy-using-dev-server
deploy-using-docker
deploy-using-kubernetes
flow-model-packaging
```
Loading