chore: import upstream snapshot with attribution
Flake8 Lint / flake8 (push) Waiting to run
Spell check CI / Spell_Check (push) Waiting to run
tools_continuous_delivery / Private PyPI non-main branch release (push) Has been skipped
tools_continuous_delivery / Private PyPI main branch release (push) Failing after 2m42s
Publish Promptflow Doc / Build (push) Has been cancelled
Publish Promptflow Doc / Deploy (push) Has been cancelled
Flake8 Lint / flake8 (push) Waiting to run
Spell check CI / Spell_Check (push) Waiting to run
tools_continuous_delivery / Private PyPI non-main branch release (push) Has been skipped
tools_continuous_delivery / Private PyPI main branch release (push) Failing after 2m42s
Publish Promptflow Doc / Build (push) Has been cancelled
Publish Promptflow Doc / Deploy (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
*.sqlite
|
||||
**/build/flow
|
||||
**/build/connections
|
||||
**/build/settings.json
|
||||
@@ -0,0 +1,3 @@
|
||||
# Deploy flow as applications
|
||||
|
||||
This folder contains examples of how to build & deploy flow as applications like Web Application packaged in Docker format.
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
resources: examples/connections/azure_openai.yml, examples/flows/standard/web-classification
|
||||
category: deployment
|
||||
weight: 60
|
||||
---
|
||||
|
||||
# Deploy flow using Azure App Service
|
||||
|
||||
This example demos how to deploy a flow using Azure App Service.
|
||||
|
||||
[Azure App Service](https://learn.microsoft.com/azure/app-service/) is an HTTP-based service for hosting web applications, REST APIs, and mobile back ends.
|
||||
The scripts (`deploy.sh` for bash and `deploy.ps1` for powershell) under this folder are here to help deploy the docker image to Azure App Service.
|
||||
|
||||
We will use [web-classification](../../../flows/standard/web-classification/README.md) as example in this tutorial.
|
||||
|
||||
## Build a flow as docker format app
|
||||
|
||||
Note that all dependent connections must be created before building as docker.
|
||||
```bash
|
||||
# create connection if not created before
|
||||
pf connection create --file ../../../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 app:
|
||||
|
||||
```bash
|
||||
pf flow build --source ../../../flows/standard/web-classification --output dist --format docker
|
||||
```
|
||||
|
||||
|
||||
## Deploy with Azure App Service
|
||||
The two scripts will do the following things:
|
||||
1. Create a resource group if not exists.
|
||||
2. Build and push the image to docker registry.
|
||||
3. Create an app service plan with the give sku.
|
||||
4. Create an app with specified name, set the deployment container image to the pushed docker image.
|
||||
5. Set up the environment variables for the app.
|
||||
|
||||
Example command to use bash script:
|
||||
```shell
|
||||
bash deploy.sh --path dist -i <image_tag> --name my-app-23d8m -r <docker registry> -g <resource_group>
|
||||
```
|
||||
|
||||
Example command to use powershell script:
|
||||
```powershell
|
||||
.\deploy.ps1 -Path dist -i <image_tag> -n my-app-23d8m -r <docker registry> -g <resource_group>
|
||||
```
|
||||
Note that the `name` will produce a unique FQDN as AppName.azurewebsites.net.
|
||||
|
||||
See the full parameters by `bash deploy.sh -h` or `.\deploy.ps1 -h`.
|
||||
|
||||
## View and test the web app
|
||||
The web app can be found via [azure portal](https://portal.azure.com/)
|
||||
|
||||

|
||||
|
||||
After the app created, you will need to go to https://portal.azure.com/ find the app and set up the environment variables
|
||||
at (Settings>Configuration) or (Settings>Environment variables), then restart the app.
|
||||
|
||||

|
||||
|
||||
Browse the app at Overview and see the test page:
|
||||
|
||||

|
||||
|
||||
You can also test the app by sending a POST request to the app like:
|
||||
```shell
|
||||
curl http://<Default-domain-of-app-service>/score --data '{"url":"https://play.google.com/store/apps/details?id=com.twitter.android"}' -X POST -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
|
||||
Tips:
|
||||
- Reach deployment logs at (Deployment>Deployment Central) and app logs at (Monitoring>Log stream).
|
||||
- Reach advanced deployment tools at (Development Tools>Advanced Tools).
|
||||
- Reach more details about app service at [Azure App Service](https://learn.microsoft.com/azure/app-service/).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 82 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,171 @@
|
||||
<#
|
||||
.DESCRIPTION
|
||||
Script to deploy promptflow to Azure App Service.
|
||||
|
||||
.PARAMETER path
|
||||
The folder path to be deployed
|
||||
.PARAMETER image_tag
|
||||
The container image tag.
|
||||
.PARAMETER registry
|
||||
The container registry name, for example 'xx.azurecr.io'.
|
||||
.PARAMETER name
|
||||
The app name to produce a unique FQDN as AppName.azurewebsites.net.
|
||||
.PARAMETER location
|
||||
The app location, default to 'centralus'.
|
||||
.PARAMETER sku
|
||||
The app sku, default to 'F1'(free).
|
||||
.PARAMETER resource_group
|
||||
The app resource group.
|
||||
.PARAMETER subscription
|
||||
The app subscription, default using az account subscription.
|
||||
.PARAMETER verbose
|
||||
verbose mode.
|
||||
|
||||
.EXAMPLE
|
||||
PS> .\deploy.ps1 -Path <folder-path> -i <image_tag> -r <registry> -n <app_name> -g <resource_group>
|
||||
.EXAMPLE
|
||||
PS> .\deploy.ps1 -Path <folder-path> -i <image_tag> -r <registry> -n <app_name> -g <resource_group> -Subscription "xxxx-xxxx-xxxx-xxxx-xxxx" -Verbose
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Path,
|
||||
[Alias("i", "image_tag")][string]$ImageTag,
|
||||
[Alias("r")][string]$Registry,
|
||||
[Alias("n")][string]$Name,
|
||||
[Alias("l")][string]$Location = "eastus",
|
||||
[string]$Sku = "F1",
|
||||
[Alias("g", "resource_group")][string]$ResourceGroup,
|
||||
[string]$Subscription
|
||||
)
|
||||
|
||||
####################### Validate args ############################
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
# fail if image_tag not provided
|
||||
if (!$ImageTag) {
|
||||
Write-Host "***************************"
|
||||
Write-Host "* Error: image_tag is required.*"
|
||||
Write-Host "***************************"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# check if : in image_tag
|
||||
if (!$ImageTag.Contains(":")) {
|
||||
$version="v$(Get-Date -Format 'yyyyMMdd-HHmmss')"
|
||||
$image_tag="${ImageTag}:${version}"
|
||||
}
|
||||
|
||||
Write-Host "image_tag: $ImageTag"
|
||||
|
||||
# fail if Registry not provided
|
||||
if (!$Registry) {
|
||||
Write-Host "***************************"
|
||||
Write-Host "* Error: registry is required.*"
|
||||
Write-Host "***************************"
|
||||
exit
|
||||
}
|
||||
|
||||
# fail if name not provided
|
||||
if (!$Name) {
|
||||
Write-Host "***************************"
|
||||
Write-Host "* Error: name is required.*"
|
||||
Write-Host "***************************"
|
||||
exit
|
||||
}
|
||||
|
||||
# fail if resource_group not provided
|
||||
if (!$ResourceGroup) {
|
||||
Write-Host "***************************"
|
||||
Write-Host "* Error: resource_group is required.*"
|
||||
Write-Host "***************************"
|
||||
exit
|
||||
}
|
||||
|
||||
# fail if image_tag not provided
|
||||
if (!$Path) {
|
||||
Write-Host "***************************"
|
||||
Write-Host "* Error: Path is required.*"
|
||||
Write-Host "***************************"
|
||||
exit 1
|
||||
}
|
||||
|
||||
####################### Build and push image ############################
|
||||
Write-Host "Change working directory to $Path"
|
||||
cd $Path
|
||||
docker build -t "$ImageTag" .
|
||||
|
||||
if ($Registry.Contains("azurecr.io")) {
|
||||
Write-Host "Trying to login to $Registry..."
|
||||
az acr login -n "$Registry"
|
||||
|
||||
$AcrImageTag = $Registry + "/" + $ImageTag
|
||||
Write-Host "ACR image tag: $AcrImageTag"
|
||||
docker tag "$ImageTag" "$AcrImageTag"
|
||||
$ImageTag = $AcrImageTag
|
||||
}
|
||||
else {
|
||||
Write-Host "***************************************************\n"
|
||||
Write-Host "* WARN: Make sure you have docker account login!!!*\n"
|
||||
Write-Host "***************************************************\n"
|
||||
|
||||
$DockerImageTag = $Registry + "/" + $ImageTag
|
||||
|
||||
Write-Host "Docker image tag: $DockerImageTag"
|
||||
docker tag "$ImageTag" "$DockerImageTag"
|
||||
$ImageTag = $DockerImageTag
|
||||
}
|
||||
|
||||
Write-Host "Start pushing image...$ImageTag"
|
||||
docker push "$ImageTag"
|
||||
|
||||
####################### Create and config app ############################
|
||||
|
||||
function Append-To-Command {
|
||||
param (
|
||||
[string] $Command
|
||||
)
|
||||
if ($Subscription) {
|
||||
$Command = "$Command --subscription $Subscription"
|
||||
}
|
||||
if ($VerbosePreference -eq "Continue") {
|
||||
$Command="$Command --debug"
|
||||
}
|
||||
Write-Host "$Command"
|
||||
return $Command
|
||||
}
|
||||
|
||||
function Invoke-Expression-And-Check{
|
||||
param (
|
||||
[string]$Command
|
||||
)
|
||||
$Command=$(Append-To-Command "$Command")
|
||||
Invoke-Expression $Command
|
||||
if ($LASTEXITCODE -gt 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
# Check and create resource group if not exist
|
||||
$Result = (az group exists --name $ResourceGroup)
|
||||
if ($Result -eq "false") {
|
||||
Write-Host "Creating resource group...$ResourceGroup"
|
||||
$Command="az group create --name $ResourceGroup -l $Location"
|
||||
Invoke-Expression-And-Check "$Command"
|
||||
}
|
||||
# Create service plan
|
||||
$ServicePlanName = $Name + "_service_plan"
|
||||
Write-Host "Creating service plan...$ServicePlanName"
|
||||
$Command="az appservice plan create --name $ServicePlanName --sku $Sku --location $location --is-linux -g $ResourceGroup"
|
||||
Invoke-Expression-And-Check "$Command"
|
||||
# Create app
|
||||
Write-Host "Creating app...$Name"
|
||||
$Command="az webapp create --name $Name -p $ServicePlanName --deployment-container-image-name $ImageTag --startup-file 'bash start.sh' -g $ResourceGroup"
|
||||
Invoke-Expression-And-Check "$Command"
|
||||
# Config environment variable
|
||||
Write-Host "Config app...$Name"
|
||||
# Port default to 8080 corresponding to the DockerFile
|
||||
$Command="az webapp config appsettings set -g $ResourceGroup --name $Name --settings USER_AGENT=promptflow-appservice WEBSITES_PORT=8080 ('@settings.json')"
|
||||
Invoke-Expression-And-Check "$Command"
|
||||
Write-Host "Please go to https://portal.azure.com/ to config environment variables and restart the app: $Name at (Settings>Configuration) or (Settings>Environment variables)"
|
||||
Write-Host "Reach deployment logs at (Deployment>Deployment Central) and app logs at (Monitoring>Log stream)"
|
||||
Write-Host "Reach advanced deployment tools at https://$Name.scm.azurewebsites.net/"
|
||||
Write-Host "Reach more details about app service at https://learn.microsoft.com/en-us/azure/app-service/"
|
||||
@@ -0,0 +1,189 @@
|
||||
#! /bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
program_name=$0
|
||||
|
||||
function usage {
|
||||
echo "usage: $program_name [-i|-image_tag|--image_tag]"
|
||||
echo " -i|-image_tag|--image_tag specify container image tag"
|
||||
echo " -r|-registry|--registry specify container registry name, for example 'xx.azurecr.io'"
|
||||
echo " -n|-name|--name specify app name to produce a unique FQDN as AppName.azurewebsites.net."
|
||||
echo " -l|-location|--location specify app location, default to 'centralus'"
|
||||
echo " -sku|--sku specify app sku, default to 'F1'(free)"
|
||||
echo " -g|-resource_group|--resource_group specify app resource group"
|
||||
echo " -subscription|--subscription specify app subscription, default using az account subscription"
|
||||
echo " -v|-verbose|--verbose specify verbose mode"
|
||||
echo " -p|-path|--path specify folder path to be deployed"
|
||||
exit 1
|
||||
}
|
||||
if [ "$1" == "-help" ] || [ "$1" == "-h" ]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
location="eastus"
|
||||
sku="F1"
|
||||
verbose=false
|
||||
|
||||
####################### Parse and validate args ############################
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-i|-image_tag|--image_tag)
|
||||
image_tag="$2"
|
||||
;;
|
||||
-r|-registry|--registry)
|
||||
registry_name="$2"
|
||||
;;
|
||||
-n|-name|--name)
|
||||
name="$2"
|
||||
;;
|
||||
-l|-location|--location)
|
||||
location="$2"
|
||||
;;
|
||||
-sku|--sku)
|
||||
sku="$2"
|
||||
;;
|
||||
-g|-resource_group|--resource_group)
|
||||
resource_group="$2"
|
||||
;;
|
||||
-subscription|--subscription)
|
||||
subscription="$2"
|
||||
;;
|
||||
-v|-verbose|--verbose)
|
||||
verbose=true
|
||||
;;
|
||||
-p|-path|--path)
|
||||
path="$2"
|
||||
;;
|
||||
*)
|
||||
printf "***************************\n"
|
||||
printf "* Error: Invalid argument.*\n"
|
||||
printf "***************************\n"
|
||||
exit 1
|
||||
esac
|
||||
shift
|
||||
shift
|
||||
done
|
||||
|
||||
# fail if image_tag not provided
|
||||
if [ -z "$image_tag" ]; then
|
||||
printf "***************************\n"
|
||||
printf "* Error: image_tag is required.*\n"
|
||||
printf "***************************\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# check if : in image_tag
|
||||
if [[ $image_tag == *":"* ]]; then
|
||||
echo "image_tag: $image_tag"
|
||||
else
|
||||
version="v$(date '+%Y%m%d-%H%M%S')"
|
||||
|
||||
image_tag="$image_tag:$version"
|
||||
echo "image_tag: $image_tag"
|
||||
fi
|
||||
|
||||
# fail if registry_name not provided
|
||||
if [ -z "$registry_name" ]; then
|
||||
printf "***************************\n"
|
||||
printf "* Error: registry is required.*\n"
|
||||
printf "***************************\n"
|
||||
fi
|
||||
|
||||
# fail if name not provided
|
||||
if [ -z "$name" ]; then
|
||||
printf "***************************\n"
|
||||
printf "* Error: name is required.*\n"
|
||||
printf "***************************\n"
|
||||
fi
|
||||
|
||||
# fail if resource_group not provided
|
||||
if [ -z "$resource_group" ]; then
|
||||
printf "***************************\n"
|
||||
printf "* Error: resource_group is required.*\n"
|
||||
printf "***************************\n"
|
||||
fi
|
||||
|
||||
# fail if path not provided
|
||||
if [ -z "$path" ]; then
|
||||
printf "***************************\n"
|
||||
printf "* Error: path is required.*\n"
|
||||
printf "***************************\n"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
####################### Build and push image ############################
|
||||
echo "Change working directory to $path"
|
||||
cd "$path"
|
||||
docker build -t "$image_tag" .
|
||||
|
||||
if [[ $registry_name == *"azurecr.io" ]]; then
|
||||
echo "Trying to login to $registry_name..."
|
||||
az acr login -n "$registry_name"
|
||||
|
||||
acr_image_tag=$registry_name/$image_tag
|
||||
echo "ACR image tag: $acr_image_tag"
|
||||
docker tag "$image_tag" "$acr_image_tag"
|
||||
image_tag=$acr_image_tag
|
||||
else
|
||||
echo "Make sure you have docker account login!!!"
|
||||
printf "***************************************************\n"
|
||||
printf "* WARN: Make sure you have docker account login!!!*\n"
|
||||
printf "***************************************************\n"
|
||||
|
||||
docker_image_tag=$registry_name/$image_tag
|
||||
|
||||
echo "Docker image tag: $docker_image_tag"
|
||||
docker tag "$image_tag" "$docker_image_tag"
|
||||
image_tag=$docker_image_tag
|
||||
fi
|
||||
|
||||
echo "Start pushing image...$image_tag"
|
||||
docker push "$image_tag"
|
||||
|
||||
####################### Create and config app ############################
|
||||
|
||||
function append_to_command {
|
||||
command=$1
|
||||
if [ -n "$subscription" ]; then
|
||||
command="$command --subscription $subscription"
|
||||
fi
|
||||
if $verbose; then
|
||||
command="$command --debug"
|
||||
fi
|
||||
echo "$command"
|
||||
}
|
||||
|
||||
# Check and create resource group if not exist
|
||||
result=$(az group exists --name "$resource_group")
|
||||
if [ "$result" = "false" ]; then
|
||||
echo "Creating resource group...$resource_group"
|
||||
command="az group create --name $resource_group -l $location"
|
||||
command=$(append_to_command "$command")
|
||||
eval "$command"
|
||||
fi
|
||||
# Create service plan
|
||||
service_plan_name=$name"_service_plan"
|
||||
echo "Creating service plan...$service_plan_name"
|
||||
command="az appservice plan create --name $service_plan_name --sku $sku --location $location --is-linux -g $resource_group"
|
||||
command=$(append_to_command "$command")
|
||||
echo "$command"
|
||||
eval "$command"
|
||||
# Create app
|
||||
echo "Creating app...$name"
|
||||
command="az webapp create --name $name -p $service_plan_name --deployment-container-image-name $image_tag --startup-file 'bash start.sh' -g $resource_group"
|
||||
command=$(append_to_command "$command")
|
||||
echo "$command"
|
||||
eval "$command"
|
||||
# Config environment variable
|
||||
echo "Config app...$name"
|
||||
# Port default to 8080 corresponding to the DockerFile
|
||||
command="az webapp config appsettings set -g $resource_group --name $name --settings USER_AGENT=promptflow-appservice WEBSITES_PORT=8080 @settings.json "
|
||||
command=$(append_to_command "$command")
|
||||
echo "$command"
|
||||
eval "$command"
|
||||
echo "Please go to https://portal.azure.com/ to config environment variables and restart the app: $name at (Settings>Configuration) or (Settings>Environment variables)"
|
||||
echo "Reach deployment logs at (Deployment>Deployment Central) and app logs at (Monitoring>Log stream)"
|
||||
echo "Reach advanced deployment tools at https://$name.scm.azurewebsites.net/"
|
||||
echo "Reach more details about app service at https://learn.microsoft.com/en-us/azure/app-service/"
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
resources: examples/tutorials/flow-deploy/create-service-with-flow
|
||||
category: deployment
|
||||
weight: 10
|
||||
---
|
||||
|
||||
# Create service with flow
|
||||
|
||||
This example shows how to create a simple service with flow.
|
||||
|
||||
You can create your own service by utilize `flow-as-function`.
|
||||
|
||||
This folder contains a example on how to build a service with a flow.
|
||||
Reference [here](./simple_score.py) for a minimal service example.
|
||||
The output of score.py will be a json serialized dictionary.
|
||||
You can use json parser to parse the output.
|
||||
|
||||
## 1. Start the service and put in background
|
||||
|
||||
```bash
|
||||
nohup python simple_score.py &
|
||||
# Note: added this to run in our CI pipeline, not needed for user.
|
||||
sleep 10
|
||||
```
|
||||
|
||||
## 2. Test the service with request
|
||||
|
||||
Executing the following command to send a request to execute a flow.
|
||||
|
||||
```bash
|
||||
curl -X POST http://127.0.0.1:5000/score --header "Content-Type: application/json" --data '{"flow_input": "some_flow_input", "node_input": "some_node_input"}'
|
||||
```
|
||||
|
||||
Sample output of the request:
|
||||
|
||||
```json
|
||||
{
|
||||
"output": {
|
||||
"value": "some_flow_input"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reference [here](./simple_score.py) for more.
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
from promptflow.core import tool
|
||||
from promptflow.connections import AzureOpenAIConnection
|
||||
|
||||
|
||||
@tool
|
||||
def echo_connection(flow_input: str, node_input: str, connection: AzureOpenAIConnection):
|
||||
print(f"Flow input: {flow_input}")
|
||||
print(f"Node input: {node_input}")
|
||||
print(f"Flow connection: {connection._to_dict()}")
|
||||
# get from env var
|
||||
return {"value": flow_input}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
$schema: https://azuremlschemas.azureedge.net/promptflow/latest/Flow.schema.json
|
||||
inputs:
|
||||
flow_input:
|
||||
type: string
|
||||
outputs:
|
||||
output:
|
||||
type: object
|
||||
reference: ${echo_connection.output}
|
||||
nodes:
|
||||
- name: echo_connection
|
||||
type: python
|
||||
source:
|
||||
type: code
|
||||
path: echo_connection.py
|
||||
inputs:
|
||||
flow_input: ${inputs.flow_input}
|
||||
node_input: "dummy_node_input"
|
||||
connection: open_ai_connection
|
||||
@@ -0,0 +1,85 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from flask import Flask, jsonify, request
|
||||
|
||||
from promptflow.client import load_flow
|
||||
from promptflow.connections import AzureOpenAIConnection
|
||||
from promptflow.entities import FlowContext
|
||||
from promptflow.exceptions import SystemErrorException, UserErrorException
|
||||
|
||||
|
||||
class SimpleScoreApp(Flask):
|
||||
pass
|
||||
|
||||
|
||||
app = SimpleScoreApp(__name__)
|
||||
logging.basicConfig(format="%(threadName)s:%(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.INFO)
|
||||
# load flow as a function, the function object can be shared accross threads.
|
||||
f = load_flow("./echo_connection_flow/")
|
||||
|
||||
|
||||
@app.errorhandler(Exception)
|
||||
def handle_error(e):
|
||||
if isinstance(e, UserErrorException):
|
||||
return jsonify({"message": e.message, "additional_info": e.additional_info}), 400
|
||||
elif isinstance(e, SystemErrorException):
|
||||
return jsonify({"message": e.message, "additional_info": e.additional_info}), 500
|
||||
else:
|
||||
from promptflow._internal import ErrorResponse, ExceptionPresenter
|
||||
|
||||
# handle other unexpected errors, can use internal class to format them
|
||||
# but interface may change in the future
|
||||
presenter = ExceptionPresenter.create(e)
|
||||
trace_back = presenter.formatted_traceback
|
||||
|
||||
resp = ErrorResponse(presenter.to_dict(include_debug_info=False))
|
||||
response_code = resp.response_code
|
||||
result = resp.to_simplified_dict()
|
||||
result.update({"trace_back": trace_back})
|
||||
return jsonify(result), response_code
|
||||
|
||||
|
||||
@app.route("/health", methods=["GET"])
|
||||
def health():
|
||||
"""Check if the runtime is alive."""
|
||||
return {"status": "Healthy"}
|
||||
|
||||
|
||||
@app.route("/score", methods=["POST"])
|
||||
def score():
|
||||
"""process a flow request in the runtime."""
|
||||
raw_data = request.get_data()
|
||||
logger.info(f"Start loading request data '{raw_data}'.")
|
||||
data = json.loads(raw_data)
|
||||
|
||||
# create a dummy connection object
|
||||
# the connection object will only exist in memory and won't store in local db.
|
||||
llm_connection = AzureOpenAIConnection(
|
||||
name="llm_connection", api_key="[determined by request]", api_base="[determined by request]"
|
||||
)
|
||||
|
||||
# configure flow contexts, create a new context object for each request to make sure they are thread safe.
|
||||
f.context = FlowContext(
|
||||
# override flow connections with connection object created above
|
||||
connections={"echo_connection": {"connection": llm_connection}},
|
||||
# override the flow nodes' inputs or other flow configs, the overrides may come from the request
|
||||
# **Note**: after this change, node "echo_connection" will take input node_input from request
|
||||
overrides={"nodes.echo_connection.inputs.node_input": data["node_input"]} if "node_input" in data else {},
|
||||
)
|
||||
# data in request will be passed to flow as kwargs
|
||||
result_dict = f(**data)
|
||||
# Note: if specified streaming=True in the flow context, the result will be a generator
|
||||
# reference promptflow.core._serving.response_creator.ResponseCreator on how to handle it in app.
|
||||
return jsonify(result_dict)
|
||||
|
||||
|
||||
def create_app(**kwargs):
|
||||
return app
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# test this with curl -X POST http://127.0.0.1:5000/score --header "Content-Type: application/json" --data '{"flow_input": "some_flow_input", "node_input": "some_node_input"}' # noqa: E501
|
||||
create_app().run(debug=True)
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
resources: examples/connections/azure_openai.yml, examples/flows/standard/web-classification
|
||||
category: deployment
|
||||
weight: 50
|
||||
---
|
||||
|
||||
# Distribute flow as executable app
|
||||
This example demos how to package flow as a executable app.
|
||||
We will use [web-classification](../../../flows/standard/web-classification/README.md) as example in this tutorial.
|
||||
|
||||
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.
|
||||
In this example, we use PyInstaller version 5.13.2 and Streamlit version 1.26.0 within a Python 3.10.8 environment.
|
||||
|
||||
## 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 ../../../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 app:
|
||||
```shell
|
||||
pf flow build --source ../../../flows/standard/web-classification --output target --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.
|
||||
- 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.
|
||||
|
||||
We provide a Python entry script named [app.py](https://github.com/microsoft/promptflow/blob/main/src/promptflow-devkit/promptflow/_sdk/data/executable/app.py) as the entry point for the bundled app, which enables you to serve a flow folder as an endpoint.
|
||||
|
||||
|
||||
|
||||
### 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.jinja2](https://github.com/microsoft/promptflow/blob/main/src/promptflow-devkit/promptflow/_sdk/data/executable/app.spec.jinja2) spec template 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.
|
||||
|
||||
|
||||
### 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.
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
resources: examples/connections/azure_openai.yml, examples/flows/standard/web-classification
|
||||
category: deployment
|
||||
weight: 40
|
||||
---
|
||||
|
||||
# Deploy a flow using Docker
|
||||
|
||||
This example demos how to deploy flow as a docker app.
|
||||
We will use [web-classification](../../../flows/standard/web-classification/README.md) as example in this tutorial.
|
||||
|
||||
## Build a flow as docker format app
|
||||
|
||||
Note that all dependent connections must be created before building as docker.
|
||||
```bash
|
||||
# create connection if not created before
|
||||
pf connection create --file ../../../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 app:
|
||||
|
||||
```bash
|
||||
pf flow build --source ../../../flows/standard/web-classification --output dist --format docker
|
||||
```
|
||||
|
||||
## Deploy with Docker
|
||||
### Build Docker image
|
||||
|
||||
Like other Dockerfile, you need to build the image first. You can tag the image with any name you want. In this example, we use `promptflow-serve`.
|
||||
|
||||
Run the command below to build image:
|
||||
|
||||
```shell
|
||||
docker build dist -t web-classification-serve
|
||||
```
|
||||
|
||||
### Run Docker image
|
||||
|
||||
Run the docker image will start a service to serve the flow inside the container.
|
||||
|
||||
#### Connections
|
||||
If the service involves connections, all related connections will be exported as yaml files and recreated in containers.
|
||||
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
|
||||
```
|
||||
You'll need to set up the environment variables in the container to make the connections work.
|
||||
|
||||
### Run with `docker run`
|
||||
|
||||
|
||||
You can run the docker image directly set via below commands:
|
||||
```shell
|
||||
# The started service will listen on port 8080.You can map the port to any port on the host machine as you want.
|
||||
docker run -p 8080:8080 -e OPEN_AI_CONNECTION_API_KEY=<secret-value> web-classification-serve
|
||||
```
|
||||
|
||||
### Test the endpoint
|
||||
After start the service, you can use curl to test it:
|
||||
|
||||
```shell
|
||||
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"
|
||||
```
|
||||
@@ -0,0 +1,162 @@
|
||||
---
|
||||
resources: examples/connections/azure_openai.yml, examples/flows/standard/web-classification
|
||||
category: deployment
|
||||
weight: 70
|
||||
---
|
||||
|
||||
# Deploy flow using Kubernetes
|
||||
This example demos how to deploy flow as a Kubernetes app.
|
||||
We will use [web-classification](../../../flows/standard/web-classification/README.md) as example in this tutorial.
|
||||
|
||||
Please ensure that you have installed all the required dependencies. You can refer to the "Prerequisites" section in the README of the [web-classification](../../../flows/standard/web-classification/README.md#Prerequisites) for a comprehensive list of prerequisites and installation instructions.
|
||||
|
||||
## Build a flow as docker format
|
||||
|
||||
Note that all dependent connections must be created before building as docker.
|
||||
```bash
|
||||
# create connection if not created before
|
||||
pf connection create --file ../../../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 app:
|
||||
|
||||
```bash
|
||||
pf flow build --source ../../../flows/standard/web-classification --output dist --format docker
|
||||
```
|
||||
|
||||
## Deploy with Kubernetes
|
||||
### Build Docker image
|
||||
|
||||
Like other Dockerfile, you need to build the image first. You can tag the image with any name you want. In this example, we use `web-classification-serve`.
|
||||
|
||||
Then run the command below:
|
||||
|
||||
```shell
|
||||
cd dist
|
||||
docker build . -t web-classification-serve
|
||||
```
|
||||
|
||||
### Create Kubernetes deployment yaml.
|
||||
The Kubernetes deployment yaml file acts as a guide for managing your docker container in a Kubernetes pod. It clearly specifies important information like the container image, port configurations, environment variables, and various settings. Below, you'll find a simple deployment template that you can easily customize to meet your needs.
|
||||
|
||||
**Note**: You need encode the secret using base64 firstly and input the <encoded_secret> as 'open-ai-connection-api-key' in the deployment configuration. For example, you can run below commands in linux:
|
||||
```shell
|
||||
encoded_secret=$(echo -n <your_api_key> | base64)
|
||||
```
|
||||
|
||||
```yaml
|
||||
---
|
||||
kind: Namespace
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: web-classification
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: open-ai-connection-api-key
|
||||
namespace: web-classification
|
||||
type: Opaque
|
||||
data:
|
||||
open-ai-connection-api-key: <encoded_secret>
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: web-classification-service
|
||||
namespace: web-classification
|
||||
spec:
|
||||
type: NodePort
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
nodePort: 30123
|
||||
selector:
|
||||
app: web-classification-serve-app
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: web-classification-serve-app
|
||||
namespace: web-classification
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: web-classification-serve-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: web-classification-serve-app
|
||||
spec:
|
||||
containers:
|
||||
- name: web-classification-serve-container
|
||||
image: web-classification-serve
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: OPEN_AI_CONNECTION_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: open-ai-connection-api-key
|
||||
key: open-ai-connection-api-key
|
||||
```
|
||||
|
||||
### Apply the deployment.
|
||||
Before you can deploy your application, ensure that you have set up a Kubernetes cluster and installed [kubectl](https://kubernetes.io/docs/reference/kubectl/) if it's not already installed. In this documentation, we will use [Minikube](https://minikube.sigs.k8s.io/docs/) as an example. To start the cluster, execute the following command:
|
||||
```shell
|
||||
minikube start
|
||||
```
|
||||
Once your Kubernetes cluster is up and running, you can proceed to deploy your application by using the following command:
|
||||
```shell
|
||||
kubectl apply -f deployment.yaml
|
||||
```
|
||||
This command will create the necessary pods to run your application within the cluster.
|
||||
|
||||
**Note**: You need replace <pod_name> below with your specific pod_name. You can retrieve it by running `kubectl get pods -n web-classification`.
|
||||
|
||||
### Retrieve flow service logs of the container
|
||||
The kubectl logs command is used to retrieve the logs of a container running within a pod, which can be useful for debugging, monitoring, and troubleshooting applications deployed in a Kubernetes cluster.
|
||||
|
||||
```shell
|
||||
kubectl -n web-classification logs <pod-name>
|
||||
```
|
||||
|
||||
#### Connections
|
||||
If the service involves connections, all related connections will be exported as yaml files and recreated in containers.
|
||||
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
|
||||
```
|
||||
You'll need to set up the environment variables in the container to make the connections work.
|
||||
|
||||
### Test the endpoint
|
||||
- Option1:
|
||||
|
||||
Once you've started the service, you can establish a connection between a local port and a port on the pod. This allows you to conveniently test the endpoint from your local terminal.
|
||||
To achieve this, execute the following command:
|
||||
|
||||
```shell
|
||||
kubectl port-forward <pod_name> 8080:8080 -n web-classification
|
||||
```
|
||||
With the port forwarding in place, you can use the curl command to initiate the endpoint test:
|
||||
|
||||
```shell
|
||||
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"
|
||||
```
|
||||
|
||||
- Option2:
|
||||
|
||||
`minikube service web-classification-service --url -n web-classification` runs as a process, creating a tunnel to the cluster. The command exposes the service directly to any program running on the host operating system.
|
||||
|
||||
The command above will retrieve the URL of a service running within a Minikube Kubernetes cluster (e.g. http://<ip>:<assigned_port>), which you can click to interact with the flow service in your web browser. Alternatively, you can use the following command to test the endpoint:
|
||||
|
||||
**Note**: Minikube will use its own external port instead of nodePort to listen to the service. So please substitute <assigned_port> with the port obtained above.
|
||||
```shell
|
||||
curl http://localhost:<assigned_port>/score --data '{"url":"https://play.google.com/store/apps/details?id=com.twitter.android"}' -X POST -H "Content-Type: application/json"
|
||||
```
|
||||
@@ -0,0 +1,56 @@
|
||||
---
|
||||
kind: Namespace
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: web-classification
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: open-ai-connection-api-key
|
||||
namespace: web-classification
|
||||
type: Opaque
|
||||
data:
|
||||
open-ai-connection-api-key: <encoded_secret>
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: web-classification-service
|
||||
namespace: web-classification
|
||||
spec:
|
||||
type: NodePort
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
nodePort: 30123
|
||||
selector:
|
||||
app: web-classification-serve-app
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: web-classification-serve-app
|
||||
namespace: web-classification
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: web-classification-serve-app
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: web-classification-serve-app
|
||||
spec:
|
||||
containers:
|
||||
- name: web-classification-serve-container
|
||||
image: web-classification-serve
|
||||
imagePullPolicy: Never
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
env:
|
||||
- name: OPEN_AI_CONNECTION_API_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: open-ai-connection-api-key
|
||||
key: open-ai-connection-api-key
|
||||
Reference in New Issue
Block a user