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

Flow build as executable package using streamlit. #578

Merged
merged 54 commits into from
Oct 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e9b4be8
update
Sep 15, 2023
addbda6
Merge branch 'main' into chenyin/build_executable_package
Sep 15, 2023
e50d1a1
add workflow file
Sep 15, 2023
a5bc519
update
Sep 15, 2023
7df8ed7
Merge branch 'main' into chenyin/build_executable_package
Sep 15, 2023
220e76d
update
Sep 15, 2023
042981d
Merge branch 'main' into chenyin/build_executable_package
Sep 15, 2023
19f994c
update
Sep 18, 2023
92ddcfc
update
Sep 18, 2023
579fc10
Merge branch 'main' into chenyin/build_executable_package
Sep 18, 2023
aa54bfc
flake8
Sep 18, 2023
e4009cc
add known issue
Sep 18, 2023
18f0774
update
Sep 19, 2023
e840bdd
update
Sep 19, 2023
3227a45
Merge branch 'main' into chenyin/build_executable_package
Sep 19, 2023
bb8bd31
update
Sep 19, 2023
daec690
cspell
Sep 19, 2023
6a68903
update
Sep 21, 2023
3f5ffc4
update
Sep 21, 2023
0619d07
update
Sep 21, 2023
6edc16b
merge
Sep 21, 2023
4338bc7
update
Sep 21, 2023
b14d193
flake8
Sep 21, 2023
720bba5
update
Sep 22, 2023
006e271
update
Sep 22, 2023
91503b8
fix linux error
Sep 22, 2023
f5e7dce
update
Sep 22, 2023
d31e81a
add keyrings.alt for linux
Sep 22, 2023
0875d44
add doc
Sep 25, 2023
c0ecd14
Merge branch 'main' into chenyin/build_executable_package_streamlit
Sep 25, 2023
08906bf
spell check
Sep 25, 2023
688df18
flake8
Sep 25, 2023
39b5294
doc fail
Sep 25, 2023
7e93892
Merge branch 'main' into chenyin/build_executable_package_streamlit
Sep 25, 2023
fefc834
Merge branch 'main' into chenyin/build_executable_package_streamlit
Sep 26, 2023
138ef9f
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Sep 26, 2023
96fe263
update
Sep 27, 2023
6ae26ae
Merge branch 'main' into chenyin/build_executable_package_streamlit
Sep 27, 2023
da0b8cf
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Sep 27, 2023
1983c1f
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Sep 27, 2023
5fd86e2
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Sep 27, 2023
d25eb16
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Sep 27, 2023
c8c88f9
Merge branch 'main' into chenyin/build_executable_package_streamlit
Oct 7, 2023
c4b3cb5
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Oct 9, 2023
394976b
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Oct 10, 2023
2166951
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Oct 10, 2023
dc9d9c6
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Oct 10, 2023
8a16b26
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Oct 10, 2023
006ce83
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Oct 11, 2023
eeffca4
Merge branch 'main' into chenyin/build_executable_package_streamlit
Oct 11, 2023
463e8c5
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Oct 11, 2023
3529e5d
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Oct 12, 2023
58447de
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Oct 12, 2023
c260369
Merge branch 'main' into chenyin/build_executable_package_streamlit
YingChen1996 Oct 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"tcsetattr",
"pysqlite",
"AADSTS700082",
"Pyinstaller",
"runsvdir",
"runsv",
"levelno",
Expand Down
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_distribute_flow_as_executable_app
on:
schedule:
- cron: "10 19 * * *" # Every day starting at 3:10 BJT
pull_request:
branches: [ main ]
paths: [ examples/**, .github/workflows/samples_tutorials_flow_deploy_distribute_flow_as_executable_app.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/distribute-flow-as-executable-app
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/distribute-flow-as-executable-app
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/distribute-flow-as-executable-app/README.md
working-directory: ${{ github.workspace }}
run: |
python scripts/readme/extract_steps_from_readme.py -f examples/tutorials/flow-deploy/distribute-flow-as-executable-app/README.md -o examples/tutorials/flow-deploy/distribute-flow-as-executable-app
- name: Cat script
working-directory: examples/tutorials/flow-deploy/distribute-flow-as-executable-app
run: |
cat bash_script.sh
- name: Run scripts
working-directory: examples/tutorials/flow-deploy/distribute-flow-as-executable-app
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/distribute-flow-as-executable-app
run: |
pip list
- name: Upload artifact
if: ${{ always() }}
uses: actions/upload-artifact@v3
with:
name: artifact
path: examples/tutorials/flow-deploy/distribute-flow-as-executable-app/bash_script.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# Distribute flow as executable app
:::{admonition} Experimental feature
This is an experimental feature, and may change at any time. Learn [more](../faq.md#stable-vs-experimental).
:::

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 distribute flow as executable app with [Pyinstaller](https://pyinstaller.org/en/stable/requirements.html#).


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. And we recommend you to add a `requirements.txt` to indicate all the required dependencies for each flow.

[Pyinstaller](https://pyinstaller.org/en/stable/installation.html) is a popular tool used for converting Python applications into standalone executables. It allows you to package your Python scripts into a single executable file, which can be run on a target machine without requiring the Python interpreter to be installed.
[Streamlit](https://docs.streamlit.io/library/get-started) is an open-source Python library used for creating web applications quickly and easily. It's designed for data scientists and engineers who want to turn data scripts into shareable web apps with minimal effort.
We use Pyinstaller to package the flow and Streamlit to create custom web apps. Prior to distributing the workflow, kindly ensure that you have installed them.


## Build a flow as executable format
Note that all dependent connections must be created before building as executable.
```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 executable format:
```bash
pf flow build --source <path-to-your-flow-folder> --output <your-output-dir> --format executable
```

## Executable format folder structure

Exported files & its dependencies are located in the same folder. The structure is as below:
- flow: the folder contains all the flow files.
YingChen1996 marked this conversation as resolved.
Show resolved Hide resolved
- connections: the folder contains yaml files to create all related connections.
- app.py: the entry file is included as the entry point for the bundled application.
- app.spec: the spec file tells PyInstaller how to process your script.
- main.py: it will start streamlit service and be called by the entry file.
- settings.json: a json file to store the settings of the executable application.
- build: a folder contains various log and working files.
- dist: a folder contains the executable application.
- README.md: Simple introduction of the files.


### A template script of the entry file
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.

::::{tab-set}
:::{tab-item} app.py
:sync: app.py
We provide a Python entry script named `app.py` as the entry point for the bundled app, which enables you to serve a flow folder as an endpoint.

```python
import os
import sys

from promptflow._cli._pf._connection import create_connection
from streamlit.web import cli as st_cli
from streamlit.runtime import exists

from main import start

def is_yaml_file(file_path):
_, file_extension = os.path.splitext(file_path)
return file_extension.lower() in ('.yaml', '.yml')

def create_connections(directory_path) -> None:
for root, dirs, files in os.walk(directory_path):
for file in files:
file_path = os.path.join(root, file)
if is_yaml_file(file_path):
create_connection(file_path)


if __name__ == "__main__":
create_connections(os.path.join(os.path.dirname(__file__), "connections"))
if exists():
start()
else:
main_script = os.path.join(os.path.dirname(__file__), "main.py")
sys.argv = ["streamlit", "run", main_script, "--global.developmentMode=false"]
st_cli.main(prog_name="streamlit")

```
:::

:::{tab-item} main.py
:sync: main.py
The `main.py` file will start streamlit service and be called by the entry file.

```python
import json
import os
import streamlit as st
from pathlib import Path

from promptflow._sdk._utils import print_yellow_warning
from promptflow._sdk._serving.flow_invoker import FlowInvoker


invoker = None


def start():
def clear_chat() -> None:
st.session_state.messages = []

def show_conversation() -> None:
if "messages" not in st.session_state:
st.session_state.messages = []
if st.session_state.messages:
for role, message in st.session_state.messages:
st.chat_message(role).write(message)


def submit(**kwargs) -> None:
container.chat_message("user").write(json.dumps(kwargs))
st.session_state.messages.append(("user", json.dumps(kwargs)))
response = run_flow(kwargs)
container.chat_message("assistant").write(response)
st.session_state.messages.append(("assistant", response))


def run_flow(data: dict) -> dict:
global invoker
if not invoker:
flow = Path(__file__).parent / "flow"
os.chdir(flow)
invoker = FlowInvoker(flow, connection_provider="local")
result = invoker.invoke(data)
print_yellow_warning(f"Result: {result}")
return result


st.title("web-classification APP")
st.chat_message("assistant").write("Hello, please input following flow inputs and connection keys.")
container = st.container()
with container:
show_conversation()

with st.form(key='input_form', clear_on_submit=True):
with open(os.path.join(os.path.dirname(__file__), "settings.json"), "r") as file:
json_data = json.load(file)
environment_variables = list(json_data.keys())
for environment_variable in environment_variables:
secret_input = st.text_input(label=environment_variable, type="password", placeholder=f"Please input {environment_variable} here. If you input before, you can leave it blank.")
if secret_input != "":
os.environ[environment_variable] = secret_input

url = st.text_input(label='url', placeholder='https://play.google.com/store/apps/details?id=com.twitter.android')
cols = st.columns(7)
submit_bt = cols[0].form_submit_button(label='Submit')
clear_bt = cols[1].form_submit_button(label='Clear')

if submit_bt:
submit(url=url)

if clear_bt:
clear_chat()

if __name__ == "__main__":
start()
```
:::
::::

### A template script of the 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 `app.spec` spec file that bundles the application into a single file. For additional information on spec files, you can refer to the [Using Spec Files](https://pyinstaller.org/en/stable/spec-files.html). Please replace `streamlit_runtime_interpreter_path` with the path of streamlit runtime interpreter in your environment.

```spec
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_data_files
from PyInstaller.utils.hooks import copy_metadata

datas = [('connections', 'connections'), ('flow', 'flow'), ('settings.json', '.'), ('main.py', '.'), ('{{streamlit_runtime_interpreter_path}}', './streamlit/runtime')]
datas += collect_data_files('streamlit')
datas += copy_metadata('streamlit')
datas += collect_data_files('keyrings.alt', include_py_files=True)
datas += copy_metadata('keyrings.alt')

block_cipher = None


a = Analysis(
['app.py', 'main.py'],
pathex=[],
binaries=[],
datas=datas,
hiddenimports=['bs4'],
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,
a.binaries,
a.zipfiles,
a.datas,
[],
name='app',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)
```

### The bundled application using Pyinstaller
Once you've build a flow as executable format following [Build a flow as executable format](#build-a-flow-as-executable-format).
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 `app` executable application.

### 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
```
## Test the endpoint
Finally, You can distribute the bundled application `app` to other people. They can execute your program by double clicking the executable file, e.g. `app.exe` in Windows system or running the binary file, e.g. `app` in Linux system.

The development server has a built-in web page they can use to test the flow by opening 'http://localhost:8501' in the browser. The expected result is as follows: if the flow served successfully, the process will keep alive until it is 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).


## Known issues
1. Note that Python 3.10.0 contains a bug making it unsupportable by PyInstaller. PyInstaller will also not work with beta releases of Python 3.13.

## 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
distribute-flow-as-executable-app
```
Loading
Loading