chore: import upstream snapshot with attribution
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) Has been cancelled
Spell check CI / Spell_Check (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:52 +08:00
commit e768098d0e
4004 changed files with 2804145 additions and 0 deletions
@@ -0,0 +1,97 @@
import argparse
import json
from pathlib import Path
from azure.keyvault.secrets import SecretClient
from azure.identity import ClientSecretCredential, DefaultAzureCredential
CONNECTION_FILE_NAME = "connections.json"
CONNECTION_TPL_FILE_PATH = Path(".") / "src/promptflow" / "dev-connections.json.example"
def get_secret_client(
tenant_id: str, client_id: str, client_secret: str
) -> SecretClient:
try:
if (tenant_id is None) or (client_id is None) or (client_secret is None):
credential = DefaultAzureCredential()
client = SecretClient(
vault_url="https://promptflowprod.vault.azure.net/",
credential=credential,
)
else:
credential = ClientSecretCredential(tenant_id, client_id, client_secret)
client = SecretClient(
vault_url="https://github-promptflow.vault.azure.net/",
credential=credential,
)
except Exception as e:
print(e)
return client
def get_secret(secret_name: str, client: SecretClient):
secret = client.get_secret(secret_name)
return secret.value
def list_secret_names(client: SecretClient) -> list:
secret_properties = client.list_properties_of_secrets()
return [secret.name for secret in secret_properties]
def fill_key_to_dict(template_dict, keys_dict):
if not isinstance(template_dict, dict):
return
for key, val in template_dict.items():
if isinstance(val, str) and val in keys_dict:
template_dict[key] = keys_dict[val]
continue
fill_key_to_dict(val, keys_dict)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--tenant_id", type=str, help="The tenant id of the service principal"
)
parser.add_argument(
"--client_id", type=str, help="The client id of the service principal"
)
parser.add_argument(
"--client_secret", type=str, help="The client secret of the service principal"
)
parser.add_argument(
"--target_folder", type=str, help="The target folder to save the generated file"
)
args = parser.parse_args()
template_dict = json.loads(
open(CONNECTION_TPL_FILE_PATH.resolve().absolute(), "r").read()
)
file_path = (
(Path(".") / args.target_folder / CONNECTION_FILE_NAME)
.resolve()
.absolute()
.as_posix()
)
print(f"file_path: {file_path}")
client = get_secret_client(
tenant_id=args.tenant_id,
client_id=args.client_id,
client_secret=args.client_secret,
)
all_secret_names = list_secret_names(client)
data = {
secret_name: get_secret(secret_name, client) for secret_name in all_secret_names
}
fill_key_to_dict(template_dict, data)
with open(file_path, "w") as f:
json.dump(template_dict, f)
+17
View File
@@ -0,0 +1,17 @@
name: release-env
channels:
- defaults
- conda-forge
dependencies:
- pip
- pip:
- setuptools
- twine==4.0.0
- portalocker~=1.2
- setuptools_rust
- pytest
- pytest-xdist
- pytest-sugar
- pytest-timeout
- azure-keyvault
- azure-identity
+128
View File
@@ -0,0 +1,128 @@
import argparse
import os
import sys
from pathlib import Path
from utils import Color, run_command, print_red
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=Color.RED + "Test Coverage for Promptflow!" + Color.END + "\n")
parser.add_argument("-p", required=True, nargs="+", help="The paths to calculate code coverage")
parser.add_argument("-t", required=True, nargs="+", help="The path to the tests")
parser.add_argument("-l", required=True, help="Location to run tests in")
parser.add_argument(
"-m",
required=True,
help="Pytest marker to identify the tests to run",
default="all",
)
parser.add_argument(
"-o",
required=False,
help="Pytest output file name",
default="test-results.xml",
)
parser.add_argument("-n", help="Pytest number of process to run the tests", default="auto")
parser.add_argument(
"--model-name",
help="The model file name to run the tests",
type=str,
default="",
)
parser.add_argument("--timeout", help="Timeout for individual tests (seconds)", type=str, default="")
parser.add_argument(
"--coverage-config",
help="The path of code coverage config file",
type=str,
default="",
)
parser.add_argument(
"--disable-cov-branch",
action="store_true",
help="Whether to enable branch coverage calculation",
)
parser.add_argument(
"--ignore-glob",
help="The path of ignored test file",
type=str,
default="",
)
args = parser.parse_args()
print("Working directory: " + str(os.getcwd()))
print("Args.p: " + str(args.p))
print("Args.t: " + str(args.t))
print("Args.l: " + str(args.l))
print("Args.m: " + str(args.m))
print("Args.n: " + str(args.n))
print("Args.o: " + str(args.o))
print("Args.model-name: " + str(args.model_name))
print("Args.timeout: " + str(args.timeout))
print("Args.coverage-config: " + str(args.coverage_config))
print("Args.ignore-glob: " + str(args.ignore_glob))
print("Args.disable-cov-branch: " + str(args.disable_cov_branch))
test_paths_list = [str(Path(path).absolute()) for path in args.t]
# display a list of all Python packages installed in the current Python environment
run_command(["pip", "list"])
run_command(["pip", "show", "promptflow", "promptflow-sdk"])
pytest_command = ["pytest", f"--junitxml={args.o}"]
pytest_command += test_paths_list
if args.coverage_config:
if args.p:
cov_path_list = [f"--cov={path}" for path in args.p]
pytest_command += cov_path_list
if not args.disable_cov_branch:
pytest_command += ["--cov-branch"]
pytest_command += [ # noqa: W503
"--cov-report=term",
"--cov-report=html",
"--cov-report=xml",
]
pytest_command = pytest_command + [f"--cov-config={args.coverage_config}"]
if args.ignore_glob:
pytest_command = pytest_command + [f"--ignore-glob={args.ignore_glob}"]
pytest_command += [
"-n",
args.n,
"--dist",
"loadfile",
"--log-level=info",
"--log-format=%(asctime)s %(levelname)s %(message)s",
"--log-date-format=[%Y-%m-%d %H:%M:%S]",
"--durations=5",
"-ra",
"-vv",
]
if args.timeout:
pytest_command = pytest_command + [
"--timeout",
args.timeout,
"--timeout_method",
"thread",
]
if args.m != "all":
pytest_command = pytest_command + ["-m", args.m]
if args.model_name:
pytest_command = pytest_command + ["--model-name", args.model_name]
# pytest --junit-xml=test-results.xml --cov=azure.ai.ml --cov-report=html --cov-report=xml -ra ./tests/*/unittests/
error_code, _ = run_command(pytest_command, throw_on_retcode=False)
# https://docs.pytest.org/en/7.1.x/reference/exit-codes.html
if error_code == 1:
print_red("Tests were collected and run but some of the tests failed.")
elif error_code == 2:
print_red("Test execution was interrupted by the user.")
elif error_code == 3:
print_red("Internal error happened while executing tests.")
elif error_code == 4:
print_red("pytest command line usage error.")
elif error_code == 5:
print_red("No tests were collected.")
sys.exit(error_code)
+112
View File
@@ -0,0 +1,112 @@
import logging
import os
import subprocess
import sys
import time
import traceback
module_logger = logging.getLogger(__name__)
class Color:
PURPLE = "\033[95m"
CYAN = "\033[96m"
DARKCYAN = "\033[36m"
BLUE = "\033[94m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
END = "\033[0m"
def print_red(message):
print(Color.RED + message + Color.END)
def print_blue(message):
print(Color.BLUE + message + Color.END)
def get_test_files(testpath):
if os.path.isfile(testpath):
return [testpath]
else:
res = []
for root, dirs, files in os.walk(testpath):
module_logger.debug("Searching %s for files ending in 'tests.py'", root)
res.extend([os.path.join(root, file) for file in files if file.endswith("tests.py")])
return res
def retry(fn, num_attempts=3):
if num_attempts <= 0:
raise Exception("Illegal num_attempts: {}".format(num_attempts))
count = 0
for _ in range(0, num_attempts):
try:
return fn()
except Exception:
count += 1
print("Execution failed on attempt {} out of {}".format(count, num_attempts))
print("Exception trace:")
traceback.print_exc()
if count == num_attempts:
print("Execution failed after {} attempts".format(count))
raise
def _run_command(
commands,
cwd=None,
stderr=subprocess.STDOUT,
shell=False,
env=None,
stream_stdout=True,
throw_on_retcode=True,
logger=None,
):
if logger is None:
logger = module_logger
if cwd is None:
cwd = os.getcwd()
t0 = time.perf_counter()
try:
logger.debug("[RunCommand]Executing {0} in {1}".format(commands, cwd))
out = ""
p = subprocess.Popen(commands, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd, shell=shell, env=env)
for line in p.stdout:
line = line.decode("utf-8").rstrip()
if line and line.strip():
logger.debug(line)
if stream_stdout:
sys.stdout.write(line)
sys.stdout.write("\n")
out += line
out += "\n"
p.communicate()
retcode = p.poll()
if throw_on_retcode:
if retcode:
raise subprocess.CalledProcessError(retcode, p.args, output=out, stderr=p.stderr)
return retcode, out
finally:
t1 = time.perf_counter()
logger.debug("[RunCommand] Execution took {0}s for {1} in {2}".format(t1 - t0, commands, cwd))
def run_command(
commands, cwd=None, stderr=subprocess.STDOUT, shell=False, stream_stdout=True, throw_on_retcode=True, logger=None
):
return _run_command(
commands,
cwd=cwd,
stderr=stderr,
shell=shell,
stream_stdout=stream_stdout,
throw_on_retcode=throw_on_retcode,
logger=logger,
)
+364
View File
@@ -0,0 +1,364 @@
# Enforce the check of pipelines.
# This script will get the diff of the current branch and main branch, calculate the pipelines that should be triggered.
# Then it will check if the triggered pipelines are successful. This script will loop for 30*loop-times seconds at most.
# How many checks are triggered:
# 1. sdk checks: sdk_cli_tests, sdk_cli_azure_test, sdk_cli_global_config_tests are triggered.
# 2. examples checks: this script calculate the path filters and decide what should be triggered.
# Trigger checks and return the status of the checks:
# 1. If examples are not correctly generated, fail.
# 2. If required pipelines are not triggered within 6 rounds of loops, fail.
# 2.1 (special_care global variable could help on some pipelines that need to bypass the check)
# Check pipelines succeed or not:
# 1. These pipelines should return status within loop-times rounds.
# 2. If there is failed pipeline in the triggered pipelines, fail.
# Import necessary libraries
import os
import fnmatch
import subprocess
import time
import argparse
import json
import sys
# Define variables
github_repository = "microsoft/promptflow"
snippet_debug = os.getenv("SNIPPET_DEBUG", 0)
merge_commit = ""
loop_times = 40 # 40 * 30 seconds = 20 minutes
github_workspace = os.path.expanduser("~/promptflow/")
# Special cases for pipelines that need to be triggered more or less than default value 1.
# If 0, the pipeline will not be ignored in check enforcer.
# Please notice that the key should be the Job Name in the pipeline.
special_care = {
"sdk_cli_tests": 4,
"sdk_cli_azure_test_replay": 4,
"sdk_cli_global_config_tests": 1,
"tracing-e2e-test": 12,
"tracing-unit-test": 12,
"core_test": 4,
"azureml_serving_test": 4,
# "samples_connections_connection": 0,
}
# Copy from original yaml pipelines
checks = {
"sdk_cli_tests": [
"src/promptflow-core/**",
"src/promptflow-devkit/**",
"src/promptflow/**",
"src/promptflow-tracing/**",
"scripts/building/**",
"src/promptflow-recording/**",
],
"sdk_cli_global_config_tests": [
"src/promptflow-core/**",
"src/promptflow-devkit/**",
"src/promptflow-tracing/**",
"src/promptflow-azure/**",
"src/promptflow/**",
"scripts/building/**",
],
"sdk_cli_azure_test_replay": [
"src/promptflow/**",
"scripts/building/**",
"src/promptflow-tracing/**",
"src/promptflow-core/**",
"src/promptflow-devkit/**",
"src/promptflow-azure/**",
],
"tracing-e2e-test": [
"src/promptflow-tracing/**",
],
"tracing-unit-test": [
"src/promptflow-tracing/**",
],
"core_test": [
"src/promptflow-tracing/**",
"src/promptflow-core/**",
],
"azureml_serving_test": [
"src/promptflow-tracing/**",
"src/promptflow-core/**",
],
}
reverse_checks = {}
pipelines = {}
pipelines_count = {}
failed_reason = ""
# Define functions
def trigger_checks(valid_status_array):
global failed_reason
global github_repository
global merge_commit
global snippet_debug
global pipelines
global pipelines_count
output = subprocess.check_output(
f"gh api /repos/{github_repository}/commits/{merge_commit}/check-suites?per_page=100",
shell=True,
)
check_suites = json.loads(output)["check_suites"]
for suite in check_suites:
if snippet_debug != 0:
print(f"check-suites id {suite['id']}")
suite_id = suite["id"]
output = subprocess.check_output(
f"gh api /repos/{github_repository}/check-suites/{suite_id}/check-runs?per_page=100",
shell=True,
)
check_runs = json.loads(output)["check_runs"]
for run in check_runs:
if snippet_debug != 0:
print(f"check runs name {run['name']}")
for key in pipelines.keys():
value = pipelines[key]
if value == 0:
continue
if key in run["name"]:
pipelines_count[key] += 1
valid_status_array.append(run)
for key in pipelines.keys():
if pipelines_count[key] < pipelines[key]:
print(
f"[Failure]Pipeline {key} is triggered {pipelines_count[key]} times, less than {pipelines[key]} times."
)
failed_reason = "Not all pipelines are triggered."
else:
print(
f"Pipeline {key} is triggered {pipelines_count[key]} times, more or equal to {pipelines[key]} times."
)
def status_checks(valid_status_array):
global failed_reason
global pipelines
global pipelines_count
# Basic fact of sdk cli checked pipelines.
failed_reason = ""
# Loop through each valid status array.
for status in valid_status_array:
# Check if the pipeline was successful.
if status["conclusion"] and status["conclusion"].lower() == "success":
# Add 1 to the count of successful pipelines.
pass
# Check if the pipeline failed.
elif status["conclusion"] and status["conclusion"].lower() == "failure":
failed_reason = "Required pipelines are not successful."
# Check if the pipeline is still running.
else:
if failed_reason == "":
failed_reason = "Required pipelines are not finished."
# Print the status of the pipeline to the console.
print(status["name"] + " is checking.")
def trigger_prepare(input_paths):
global github_workspace
global checks
global reverse_checks
global pipelines
global pipelines_count
global failed_reason
global special_care
for input_path in input_paths:
if "samples_connections_connection" in input_path:
continue
if input_path.startswith("migration-guide/"):
continue
if "-maf" in input_path:
continue
# Check if the input path contains "examples" or "samples".
if "examples" in input_path or "samples" in input_path:
sys.path.append(os.path.expanduser(github_workspace + "/scripts/readme"))
from readme import main as readme_main
os.chdir(os.path.expanduser(github_workspace))
# Get the list of pipelines from the readme file.
pipelines_samples = readme_main(check=True)
git_diff_files = [
item
for item in subprocess.check_output(
["git", "diff", "--name-only", "HEAD"]
)
.decode("utf-8")
.split("\n")
if item != ""
]
for _ in git_diff_files:
failed_reason = "Run readme generation before check in"
return
# Merge the pipelines from the readme file with the original list of pipelines.
for key in pipelines_samples.keys():
value = pipelines_samples[key]
checks[key] = value
# Reverse checks.
for key in checks.keys():
value = checks[key]
for path in value:
if path in reverse_checks:
reverse_checks[path].append(key)
else:
reverse_checks[path] = [key]
# Render pipelines and pipelines_count using input_paths.
for input_path in input_paths:
# Input pattern /**: input_path should match in the middle.
# Input pattern /*: input_path should match last but one.
# Other input pattern: input_path should match last.
# Skip if input path is a markdown file.
if input_path.endswith(".md"):
continue
keys = [
key for key in reverse_checks.keys() if fnmatch.fnmatch(input_path, key)
]
# Loop through each key in the list of keys.
for key_item in keys:
# Loop through each pipeline in the list of pipelines.
for key in reverse_checks[key_item]:
# Check if the pipeline is in the list of pipelines.
if key in special_care:
pipelines[key] = special_care[key]
else:
pipelines[key] = 1
# Set the pipeline count to 0.
pipelines_count[key] = 0
def run_checks():
global github_repository
global snippet_debug
global merge_commit
global loop_times
global github_workspace
global failed_reason
if merge_commit == "":
merge_commit = (
subprocess.check_output(["git", "log", "-1"]).decode("utf-8").split("\n")
)
if snippet_debug != 0:
print(merge_commit)
for line in merge_commit:
if "Merge" in line and "into" in line:
merge_commit = line.split(" ")[-3]
break
if snippet_debug != 0:
print("MergeCommit " + merge_commit)
not_started_counter = 5
os.chdir(github_workspace)
# Get diff of current branch and main branch.
try:
git_merge_base = (
subprocess.check_output(["git", "merge-base", "origin/main", "HEAD"])
.decode("utf-8")
.rstrip()
)
git_diff = (
subprocess.check_output(
["git", "diff", "--name-only", "--diff-filter=d", f"{git_merge_base}"],
stderr=subprocess.STDOUT,
)
.decode("utf-8")
.rstrip()
.split("\n")
)
for single_diff in git_diff:
print(single_diff)
except subprocess.CalledProcessError as e:
print("Exception on process, rc=", e.returncode, "output=", e.output)
raise e
# Prepare how many pipelines should be triggered.
trigger_prepare(git_diff)
if failed_reason != "":
raise Exception(failed_reason)
# Loop for 20 minutes at most.
for i in range(loop_times):
# Wait for 30 seconds.
time.sleep(30)
# Reset the failed reason.
failed_reason = ""
# Reset the valid status array.
valid_status_array = []
# Get all triggered pipelines.
# If not all pipelines are triggered, continue.
trigger_checks(valid_status_array)
if failed_reason != "":
if not_started_counter == 0:
raise Exception(failed_reason + " for 6 times.")
print(failed_reason)
not_started_counter -= 1
continue
# Get pipeline conclusion priority:
# 1. Not successful, Fail.
# 2. Not finished, Continue.
# 3. Successful, Break.
status_checks(valid_status_array)
# Check if the failed reason contains "not successful".
if "not successful" in failed_reason.lower():
raise Exception(failed_reason)
# Check if the failed reason contains "not finished".
elif "not finished" in failed_reason.lower():
print(failed_reason)
continue
# Otherwise, print that all required pipelines are successful.
else:
print("All required pipelines are successful.")
break
# Check if the failed reason is not empty.
if failed_reason != "":
raise Exception(failed_reason)
if __name__ == "__main__":
# Run the checks.
parser = argparse.ArgumentParser()
parser.add_argument(
"-m",
"--merge-commit",
help="merge commit sha",
)
parser.add_argument(
"-n",
"--loop-times",
type=int,
help="Loop times",
)
parser.add_argument(
"-t",
"--github-workspace",
help="base path of github workspace",
)
args = parser.parse_args()
if args.merge_commit:
merge_commit = args.merge_commit
if args.loop_times:
loop_times = args.loop_times
if args.github_workspace:
github_workspace = args.github_workspace
run_checks()
+20
View File
@@ -0,0 +1,20 @@
"""Tests checking that azure packages are NOT installed."""
import importlib
import pytest
class TestPackagesNotInstalles():
"""Test imports."""
@pytest.mark.parametrize('package', [
'promptflow.azure',
'azure.ai.ml',
'azure.storage.blob'
])
def test_promptflow_azure(self, package):
"""Test promptflow. azure is not installed."""
try:
importlib.import_module(package)
assert False, f'Package {package} must be uninstalled for local test.'
except (ModuleNotFoundError, ImportError):
pass
+73
View File
@@ -0,0 +1,73 @@
#!/bin/bash
print_usage(){
if [ $# -gt 0 ]; then
echo "Missing argument ${1}"
fi
echo "Usage:"
echo "$0 -r [github run id] -w [github workflow] -a [github action id] -b [github ref id] -e [Optional extras] -f [ should we fail?] -l [instal, time limit]"
echo "Extras should be written as it appears in pip, for example for promptflow-evals[azure], it will be [azure]"
echo "Flag -f does not require parameter."
exit 1
}
run_id=""
workflow=""
action=""
ref=""
fail=0
extras=""
limit=""
while getopts ":r:w:a:b:e:l:f" opt; do
# Parse options
case $opt in
(r) run_id="$OPTARG";;
(w) workflow="$OPTARG";;
(a) action="$OPTARG";;
(b) ref="$OPTARG";;
(e) extras="$OPTARG";;
(f) ((fail++));;
(l) limit="$OPTARG";;
\?) print_usage;;
esac
done
for v in "run_id" "workflow" "action" "ref" "limit"; do
if [ -z ${!v} ]; then
print_usage "$v"
fi
done
ENV_DIR="test_pf_ev"
python -m virtualenv "${ENV_DIR}"
# Make activate command platform independent
ACTIVATE="${ENV_DIR}/bin/activate"
if [ ! -f "$ACTIVATE" ]; then
ACTIVATE="${ENV_DIR}/Scripts/activate"
fi
source "${ACTIVATE}"
# Estimate the installation time.
pf_evals_wheel=`ls -1 promptflow_evals-*`
echo "The downloaded wheel file ${pf_evals_wheel}"
packages=`python -m pip freeze | wc -l`
start_tm=`date +%s`
echo "python -m pip install \"./${pf_evals_wheel}${extras}\" --no-cache-dir"
python -m pip install "./${pf_evals_wheel}${extras}" --no-cache-dir
install_time=$((`date +%s` - ${start_tm}))
packages_installed=$((`python -m pip freeze | wc -l` - packages))
# Log the install time
python `dirname "$0"`/report_to_app_insights.py --activity "install_time_s" --value "{\"install_time_s\": ${install_time}, \"number_of_packages_installed\": ${packages_installed}}" --git-hub-action-run-id "${run_id}" --git-hub-workflow "${workflow}" --git-hub-action "${action}" --git-branch "${ref}"
deactivate
rm -rf test_pf_ev
echo "Installed ${packages_installed} packages per ${install_time} seconds."
if [ $fail -eq 0 ]; then
# Swallow the exit code 1 and just show the warning, understandable by
# github UI.
test ${install_time} -le $limit || echo "::warning file=pyproject.toml,line=40,col=0::The installation took ${install_time} seconds, the limit is ${limit}."
else
test ${install_time} -le $limit
fi
# Return the exit code of test command of of echo i.e. 0.
exit $?
+109
View File
@@ -0,0 +1,109 @@
from typing import Dict, Optional, Union
import argparse
import json
import platform
from promptflow._sdk._configuration import Configuration
from promptflow._sdk._telemetry.telemetry import get_telemetry_logger
from xml.dom import minidom
def parse_junit_xml(fle: str) -> Dict[str, Dict[str, Union[float, str]]]:
"""
Parse the xml in Junit xml format.
:param fle: The file in JUnit xml format.
:type fle: str
:return: The dictionary with tests, their run times and pass/fail status.
"""
test_results = {}
dom = minidom.parse(fle)
# Take node list Document/testsuites/testsuite/
for test in dom.firstChild.firstChild.childNodes:
test_name = f"{test.attributes['classname'].value}::{test.attributes['name'].value}"
test_results[test_name] = {'fail_message': '', 'time': float(test.attributes['time'].value)}
for child in test.childNodes:
if child.nodeName == 'failure':
test_results[test_name]['fail_message'] = child.attributes["message"].value
return test_results
def main(activity_name: str,
value: Union[float, str],
run_id: str,
workflow: str,
action: str,
branch: str,
junit_file: Optional[str]) -> None:
"""
Log the CI-CD event.
:param activity_name: The name of a n activity to be logged, for example, installation time.
:type activity_name: str
:param value: The value of a parameter
:type value: float
:param run_id: The CI-CD run id.
:type run_id: str
:param workflow: The name of a workflow or path to a workflow file.
:type workflow: str
:param action: The name of running action or a step.
:type action: str
:param branch: The branch from which the CI-CD was triggered.
:type branch: str
:param junit_file: The path to junit test file results.
:type junit_file: str
"""
# Enable telemetry
config = Configuration.get_instance()
config.set_config(Configuration.COLLECT_TELEMETRY, True)
logger = get_telemetry_logger()
activity_info = {
"activity_name": activity_name,
"activity_type": "ci_cd_analytics",
"OS": platform.system(),
"OS_release": platform.release(),
"branch": branch,
"git_hub_action_run_id": run_id,
"git_hub_workflow": workflow
}
if junit_file:
junit_dict = parse_junit_xml(junit_file)
for k, v in junit_dict.items():
if v["fail_message"]:
# Do not log time together with fail message.
continue
activity_info[k] = v['time']
else:
if isinstance(value, str):
activity_info.update(json.loads(value))
else:
activity_info["value"] = value
# write information to the application insights.
logger.info(action, extra={"custom_dimensions": activity_info})
if __name__ == '__main__':
parser = argparse.ArgumentParser(
description="Log the value to application insights along with platform characteristics and run ID.")
parser.add_argument('--activity', help='The activity to be logged.',
required=True)
parser.add_argument(
'--value',
help='The floating point value for activity or a set of values in key-value format.',
required=False,
default=-1)
parser.add_argument('--junit-xml', help='The path to junit-xml file.',
dest="junit_xml", required=False, default=None)
parser.add_argument('--git-hub-action-run-id', dest='run_id',
help='The run ID of GitHub action run.', required=True)
parser.add_argument('--git-hub-workflow', dest='workflow',
help='The name of a workflow or a path to workflow file.', required=True)
parser.add_argument('--git-hub-action', dest='action',
help='Git hub action or step.', required=True)
parser.add_argument('--git-branch', dest='branch',
help='Git hub Branch.', required=True)
args = parser.parse_args()
main(args.activity, args.value, args.run_id, args.workflow, args.action, args.branch, args.junit_xml)
+67
View File
@@ -0,0 +1,67 @@
import os
import platform
import pytest
from unittest.mock import patch, MagicMock
import report_to_app_insights
import tempfile
class TestReporter:
"""The set of local tests to test reporting to application insights."""
@pytest.mark.parametrize(
'value,expected_val',
[
(42, {'value': 42.}),
('{"foo": 1, "bar": 2}', {"foo": 1, "bar": 2})
]
)
def test_logging_value(self, value, expected_val):
"""Test loading values from."""
mock_logger = MagicMock()
expected = {
"activity_name": 'test_act',
"activity_type": "ci_cd_analytics",
"OS": platform.system(),
"OS_release": platform.release(),
"branch": "some_branch",
"git_hub_action_run_id": "gh_run_id",
"git_hub_workflow": "gh_wf"
}
expected.update(expected_val)
with patch('report_to_app_insights.get_telemetry_logger', return_value=mock_logger):
report_to_app_insights.main(
'test_act', value, "gh_run_id", "gh_wf", 'my_action', "some_branch", junit_file=None)
mock_logger.info.assert_called_with('my_action', extra={'custom_dimensions': expected})
def test_log_junit_xml(self):
"""Test that we are loading junit xml files as expected."""
content = (
'<?xml version="1.0" encoding="utf-8"?><testsuites><testsuite name="pytest">'
'<testcase classname="MyTestClass1" name="my_successful_test_method" time="4.2"/>'
'<testcase classname="MyTestClass2" name="my_unsuccessful_test_method" time="4.2">'
'<failure message="Everything failed">fail :(</failure></testcase>'
'</testsuite></testsuites>'
)
mock_logger = MagicMock()
expected = {
"activity_name": 'test_act',
"activity_type": "ci_cd_analytics",
"OS": platform.system(),
"OS_release": platform.release(),
"MyTestClass1::my_successful_test_method": 4.2,
"branch": "some_branch",
"git_hub_action_run_id": "gh_run_id",
"git_hub_workflow": "gh_wf"
}
with tempfile.TemporaryDirectory() as d:
file_xml = os.path.join(d, "test-results.xml")
with open(file_xml, 'w') as f:
f.write(content)
with patch('report_to_app_insights.get_telemetry_logger', return_value=mock_logger):
report_to_app_insights.main(
'test_act', -1, "gh_run_id", "gh_wf", 'my_action', "some_branch", junit_file=file_xml)
mock_logger.info.assert_called_with('my_action', extra={'custom_dimensions': expected})
@@ -0,0 +1,24 @@
# Copyright (C) Microsoft Corporation. All rights reserved.
<#
.SYNOPSIS
Check Policheck Scan result.
.DESCRIPTION
Helper script to check the Policheck result.
If there is policheck failure, show the error and throw exception.
#>
[CmdLetbinding()]
param (
[string]$policheckResult,
[string]$raiseError = $true
)
$result = Get-Content -Path $policheckResult | Measure-Object -Line;
Write-Host("Number of errors found in this scan: " + ($result.Lines - 1));
if ($raiseError -and ($result.Lines -gt 1))
{
Get-Content -Path $policheckResult;
throw "Policheck scan completed successfully but there are issues to fix.";
}
# Read-Host "Press enter to finish the process and close this window";
@@ -0,0 +1,7 @@
<PoliCheckExclusions>
<!-- All strings must be UPPER CASE -->
<!--index-xxx.js is an auto-generated javascript file - skipped given it's not expected to be readable -->
<Exclusion Type="FileName">SRC\PROMPTFLOW-CORE\PROMPTFLOW\CORE\_SERVING\STATIC\INDEX.JS</Exclusion>
<Exclusion Type="FileType">.MIN.JS</Exclusion>
<Exclusion Type="FolderPathFull">SRC\PROMPTFLOW-DEVKIT\PROMPTFLOW\_SDK\_SERVICE\STATIC\</Exclusion>
</PoliCheckExclusions>
+160
View File
@@ -0,0 +1,160 @@
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------
import argparse
import os
import site
import typing
import sys
from dataclasses import dataclass
from pathlib import Path
from test_resources import REGISTERED_TEST_RESOURCES_FUNCTIONS
from utils import REPO_ROOT_DIR, change_cwd, print_blue, run_cmd
PROMPT_FLOW_PKGS = [
"promptflow-tracing",
"promptflow-core",
"promptflow-devkit",
"promptflow-azure",
"promptflow[azure]",
"promptflow-tools",
"promptflow-evals",
"promptflow-recording",
]
def set_up_git_hook_scripts(verbose: bool) -> None:
run_cmd(["pip", "install", "pre-commit"], verbose=False) # ensure pre-commit is installed
cmd = ["pre-commit", "install"]
print_blue("Running `pre-commit install` to set up the git hook scripts")
run_cmd(cmd, verbose=verbose)
def collect_and_install_from_pyproject() -> None:
# collect dependencies from pyproject.toml and install
deps = []
# cwd is already changed to the package folder in the context
# so we can safely open pyproject.toml here
with open("pyproject.toml", mode="r", encoding="utf-8") as f:
lines = f.readlines()
collect_flag = False
for line in lines:
line = line.strip()
if line.startswith("["):
if "[tool.poetry.group.dev.dependencies]" in line or "[tool.poetry.group.test.dependencies]" in line:
collect_flag = True
continue
else:
collect_flag = False
if collect_flag and line:
deps.append(line.split("=")[0].strip())
cmd = ["pip", "install"] + deps
run_cmd(cmd)
def inject_pth_file() -> None:
# content of the .pth file will be added to `sys.path`
# for packages installed from pyproject.toml, this file should already be there
# for `promptflow`, inject this, and we can avoid `conda env config vars set`
# reference: https://docs.python.org/3/library/site.html
site_packages_path = Path(site.getsitepackages()[0])
with open(site_packages_path / "promptflow.pth", mode="w", encoding="utf-8") as f:
f.write((REPO_ROOT_DIR / "src" / "promptflow").resolve().absolute().as_posix())
def install_pkg_editable(pkg: str, verbose: bool, is_vscode: bool = False) -> None:
if "[" in pkg:
folder_name, extras = pkg.split("[")
extras = f"[{extras}"
else:
folder_name = pkg
extras = ""
pkg_working_dir = REPO_ROOT_DIR / "src" / folder_name
with change_cwd(pkg_working_dir):
print_blue(f"- Setting up {pkg}")
# pip install -e . from pyproject.toml/setup.py
print_blue(f"- Installing {pkg} from source")
cmd = [sys.executable, "-m", "pip", "install", "--editable", f".{extras}"]
run_cmd(cmd, verbose=verbose)
# dev and test dependencies
print_blue(f"- Installing dev and test dependencies for {pkg}")
# promptflow folder has "dev_requirements.txt", directly install it
if folder_name == "promptflow":
cmd = ["pip", "install", "-r", "dev_requirements.txt"]
run_cmd(cmd, verbose=verbose)
inject_pth_file()
# promptflow-tools
# reference: https://github.com/microsoft/promptflow/blob/main/src/promptflow-tools/README.dev.md
elif pkg == "promptflow-tools":
cmd = ["pip", "install", "-r", "requirements.txt"]
run_cmd(cmd, verbose=verbose)
cmd = ["pip", "install", "pytest", "pytest-mock"]
run_cmd(cmd, verbose=verbose)
# promptflow-* will have "pyproject.toml", parse and install
# we can refine this leveraging `poetry export` later
elif os.path.exists("pyproject.toml"):
collect_and_install_from_pyproject()
# touch __init__.py for the package for VS Code
# NOTE that this is a workaround to enable VS Code to recognize the namespace package
# we should be able to remove this after we fully deprecate promptflow in local development
if is_vscode:
with open(pkg_working_dir / "promptflow" / "__init__.py", mode="w", encoding="utf-8") as f:
f.write("__path__ = __import__('pkgutil').extend_path(__path__, __name__)\n")
@dataclass
class Arguments:
packages: typing.List[str]
verbose: bool = False
vscode: bool = False
@staticmethod
def parse_args() -> "Arguments":
epilog = "Example usage: main.py --packages promptflow-tracing promptflow-core"
parser = argparse.ArgumentParser(
description="Welcome to promptflow dev setup!",
epilog=epilog,
)
parser.add_argument(
"--packages",
required=False,
nargs="+",
type=str,
default=PROMPT_FLOW_PKGS,
help="packages to install in editable mode",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="turn on verbose output",
)
parser.add_argument(
"--vscode",
action="store_true",
help="extra setup step for Visual Studio Code",
)
args = parser.parse_args()
return Arguments(
packages=args.packages,
verbose=args.verbose,
vscode=args.vscode,
)
def main(args: Arguments) -> None:
for pkg in args.packages:
install_pkg_editable(pkg, verbose=args.verbose, is_vscode=args.vscode)
# invoke test resource template creation function if available
if pkg in REGISTERED_TEST_RESOURCES_FUNCTIONS:
REGISTERED_TEST_RESOURCES_FUNCTIONS[pkg]()
set_up_git_hook_scripts(verbose=args.verbose)
if __name__ == "__main__":
main(args=Arguments.parse_args())
+76
View File
@@ -0,0 +1,76 @@
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------
import json
import shutil
from pathlib import Path
from utils import REPO_ROOT_DIR, print_yellow
def _prompt_user_for_test_resources(path: Path) -> None:
prompt_msg = (
f"Created test-required file {path.name!r} at {path.as_posix()!r}, "
"please update with your test resource(s)."
)
print_yellow(prompt_msg)
def create_tracing_test_resource_template() -> None:
working_dir = REPO_ROOT_DIR / "src" / "promptflow-tracing"
connections_filename = "connections.json"
connections_file_path = (working_dir / connections_filename).resolve().absolute()
connections_template = {
"azure_open_ai_connection": {
"value": {
"api_key": "aoai-api-key",
"api_base": "aoai-api-endpoint",
"api_version": "2023-07-01-preview",
}
}
}
with open(connections_file_path, mode="w", encoding="utf-8") as f:
json.dump(connections_template, f, ensure_ascii=False, indent=4)
_prompt_user_for_test_resources(connections_file_path)
def create_evals_test_resource_template() -> None:
working_dir = REPO_ROOT_DIR / "src" / "promptflow-evals"
connections_filename = "connections.json"
connections_file_path = (working_dir / connections_filename).resolve().absolute()
connections_template = {
"azure_openai_model_config": {
"value": {
"azure_endpoint": "aoai-api-endpoint",
"api_key": "aoai-api-key",
"api_version": "2023-07-01-preview",
"azure_deployment": "aoai-deployment"
},
},
"azure_ai_project_scope": {
"value": {
"subscription_id": "subscription-id",
"resource_group_name": "resource-group-name",
"project_name": "project-name"
}
}
}
with open(connections_file_path, mode="w", encoding="utf-8") as f:
json.dump(connections_template, f, ensure_ascii=False, indent=4)
_prompt_user_for_test_resources(connections_file_path)
def create_tools_test_resource_template() -> None:
working_dir = REPO_ROOT_DIR / "src" / "promptflow-tools"
example_file_path = (working_dir / "connections.json.example").resolve().absolute()
target_file_path = (working_dir / "connections.json").resolve().absolute()
shutil.copy(example_file_path, target_file_path)
_prompt_user_for_test_resources(target_file_path)
REGISTERED_TEST_RESOURCES_FUNCTIONS = {
"promptflow-tracing": create_tracing_test_resource_template,
"promptflow-tools": create_tools_test_resource_template,
"promptflow-evals": create_evals_test_resource_template,
}
+52
View File
@@ -0,0 +1,52 @@
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------
import contextlib
import os
import platform
import subprocess
import sys
from pathlib import Path
REPO_ROOT_DIR = Path(__file__).parent.parent.parent
class Color:
BLUE = "\033[94m"
YELLOW = "\033[93m"
END = "\033[0m"
def print_blue(msg: str) -> None:
print(Color.BLUE + msg + Color.END)
def print_yellow(msg: str) -> None:
print(Color.YELLOW + msg + Color.END)
@contextlib.contextmanager
def change_cwd(path):
cwd = os.getcwd()
try:
os.chdir(path)
yield
finally:
os.chdir(cwd)
def run_cmd(cmd, verbose: bool = False) -> None:
print_blue(f"Running {' '.join(cmd)}")
shell = platform.system() == "Windows"
p = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=shell,
)
for line in p.stdout:
line = line.decode("utf-8").rstrip()
if verbose:
sys.stdout.write(f"{line}\n")
p.communicate()
@@ -0,0 +1,28 @@
{
"releases":{
"internal":{
"promptflow-tools-release":{
"index": "internal-index-release"
},
"promptflow-tools-test":{
"index": "internal-index-test"
}
}
},
"targets": {
"internal-index-release": {
"storage_account": "mlsdkfdnprod",
"packages_container": "repo",
"index_container": "wheels",
"blob_prefix": "promptflow",
"endpoint": "fdnpromptflow.azureedge.net"
},
"internal-index-test": {
"storage_account": "mlsdkfdnprod",
"packages_container": "repo",
"index_container": "wheels",
"blob_prefix": "test-promptflow",
"endpoint": "fdnpromptflow.azureedge.net"
}
}
}
@@ -0,0 +1,11 @@
name: release-env
channels:
- defaults
- conda-forge
dependencies:
- python=3.8
- pip
- pip:
- setuptools
- twine==4.0.0
- azure-storage-blob==12.16.0
+188
View File
@@ -0,0 +1,188 @@
import argparse
import json
import os
import re
from datetime import datetime, timedelta
from azure.storage.blob import (
AccountSasPermissions,
BlobServiceClient,
ContentSettings,
ResourceTypes,
generate_account_sas,
)
def get_connection_string(storage_account, storage_key):
return f"DefaultEndpointsProtocol=https;AccountName={storage_account};AccountKey={storage_key};EndpointSuffix=core.windows.net" # noqa: E501
def get_object_sas_token(storage_account, storage_key):
sas_token = generate_account_sas(
account_name=storage_account,
account_key=storage_key,
resource_types=ResourceTypes(object=True),
permission=AccountSasPermissions(read=True),
expiry=datetime.utcnow() + timedelta(days=365),
)
return sas_token
def get_wheel_distribution_name(package_name):
"""The wheel filename is {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl.
The distribution name is normalized from the package name."""
return package_name.replace(".", "_").replace("-", "_").replace(" ", "_")
def package_name_based_blob_prefix(package_name):
"""Convert package name to a valid blob prefix."""
prefix = package_name.replace(".", "-")
prefix = prefix.replace("_", "-")
prefix = prefix.lower()
return prefix
def override_version_with_latest(distribution_name):
return re.sub("-([0-9.]*)-", "-latest-", distribution_name, count=1)
def publish_package_internal(package_dir_path, storage_key, release_config):
index = release_config["index"]
index_config = config_json["targets"][index]
storage_account = index_config["storage_account"]
packages_container = index_config["packages_container"]
index_container = index_config["index_container"]
blob_prefix = index_config["blob_prefix"]
pypi_endpoint = index_config["endpoint"]
account_url = f"https://{storage_account}.blob.core.windows.net"
wheel_pattern = re.compile(r".+\.whl$")
whl_distributions = [d for d in os.listdir(package_dir_path) if wheel_pattern.match(d)]
if len(whl_distributions) != 1:
print(
f"[Error] Found {len(whl_distributions)} wheel distributions in {package_dir_path}. "
"There should be exactly one."
)
exit(1)
whl_distribution = whl_distributions[0]
# Create the BlobServiceClient with connection string
blob_service_client = BlobServiceClient.from_connection_string(get_connection_string(storage_account, storage_key))
container_client = blob_service_client.get_container_client(packages_container)
# Upload the wheel package to blob storage
package_blob = os.path.join(blob_prefix, whl_distribution)
package_blob_client = blob_service_client.get_blob_client(container=packages_container, blob=package_blob)
upload_file_path = os.path.join(package_dir_path, whl_distribution)
with open(file=upload_file_path, mode="rb") as package_file:
print(f"[Debug] Uploading {whl_distribution} to container: {packages_container}, blob: {package_blob}...")
package_blob_client.upload_blob(package_file, overwrite=True)
if upload_as_latest:
latest_distribution = override_version_with_latest(whl_distribution)
latest_package_blob = os.path.join(blob_prefix, latest_distribution)
latest_package_blob_client = blob_service_client.get_blob_client(
container=packages_container, blob=latest_package_blob
)
upload_file_path = os.path.join(package_dir_path, whl_distribution)
with open(file=upload_file_path, mode="rb") as package_file:
print(
f"[Debug] Uploading {whl_distribution} as latest distribution to "
f"container: {packages_container}, blob: {latest_package_blob}..."
)
latest_package_blob_client.upload_blob(package_file, overwrite=True)
# List the blobs and generate download sas urls
sas_token = get_object_sas_token(storage_account, storage_key)
print(f"Listing wheel packages with prefix {blob_prefix} in container...")
blob_list = container_client.list_blobs(name_starts_with=f"{blob_prefix}/")
distribution_blobs = [d for d in blob_list if wheel_pattern.match(d.name)]
# Reverse the list so that the latest distribution is at the top
distribution_blobs.reverse()
packages_indexes = {} # {package_name: [distributions]}
for blob in distribution_blobs:
distribution_name = blob.name.split("/")[-1]
package_name = package_name_based_blob_prefix(distribution_name.split("-")[0])
print(f"[Debug] Blob: {blob.name}. Package distribution: {distribution_name}. Package name: {package_name}")
download_link = f"{account_url}/{blob.container}/{blob.name}?{sas_token}"
index_item = f"<a href='{download_link}' rel='external'>{distribution_name}</a><br/>"
if package_name in packages_indexes:
packages_indexes[package_name].append(index_item)
else:
packages_indexes[package_name] = [index_item]
# Update index.html in the top level blob prefix for the project
project_index_file = "project_index.html"
with open(project_index_file, "w", encoding="utf8") as index_file:
index_file.write("<!DOCTYPE html>\n")
index_file.write(
"<html lang='en'><head><meta charset='utf-8'>"
"<meta name='api-version' value='2'/>"
"<title>Simple Index</title></head><body>\n"
)
for package_name in packages_indexes:
package_index_url = f"https://{pypi_endpoint}/{blob_prefix}/{package_name}"
print(f"[Debug] Updated package_index_url: {package_index_url}")
index_file.write(f"<a href='{package_index_url}'>{package_name}</a><br/>\n")
index_file.write("</body></html>\n")
project_index_blob = os.path.join(blob_prefix, "index.html")
project_index_blob_client = blob_service_client.get_blob_client(container=index_container, blob=project_index_blob)
content_settings = ContentSettings(content_type="text/html")
with open(file=project_index_file, mode="rb") as index:
print(f"Uploading {project_index_file} to container: {index_container}, blob: {project_index_blob}...")
project_index_blob_client.upload_blob(index, overwrite=True, content_settings=content_settings)
# Update index.html for the package distributions
for package_name, distribution_indexes in packages_indexes.items():
package_index_file = f"{package_name}_index.html"
if len(distribution_indexes) > 0:
print(f"{len(distribution_indexes)} distributions found for package {package_name}. Updating index.html...")
with open(package_index_file, "w", encoding="utf8") as index_file:
index_file.write("<!DOCTYPE html>\n")
index_file.write(
f"<html lang='en'><head><meta charset='utf-8'><title>{package_name}</title></head><body>\n"
)
for item in distribution_indexes:
index_file.write(f"{item}\n")
index_file.write("</body></html>\n")
# Update the index.html to the blob with prefix: <blob_prefix>/<normalized package_name>
index_blob = os.path.join(blob_prefix, package_name, "index.html")
index_blob_client = blob_service_client.get_blob_client(container=index_container, blob=index_blob)
content_settings = ContentSettings(content_type="text/html")
with open(file=package_index_file, mode="rb") as index:
print(f"Uploading {package_index_file} to container: {index_container}, blob: {index_blob}...")
index_blob_client.upload_blob(index, overwrite=True, content_settings=content_settings)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--config", type=str)
parser.add_argument("--src_folder_name", type=str)
parser.add_argument("--package_dir_path", type=str)
parser.add_argument("--storage_key", type=str)
parser.add_argument("--upload_as_latest", type=str, default="False")
parser.add_argument("--pypi_type", type=str, default="internal") # internal or public pypi
parser.add_argument("--release_type", type=str, default="release") # release or test
args = parser.parse_args()
print("[Debug] Arguments:")
print(f"[Debug] config: {args.config}")
print(f"[Debug] src_folder_name: {args.src_folder_name}")
print(f"[Debug] package_dir_path: {args.package_dir_path}")
upload_as_latest = args.upload_as_latest.lower() == "true"
print(f"[Debug] upload_as_latest: {args.upload_as_latest}. Boolean upload_as_latest: {upload_as_latest}")
print(f"[Debug] pypi_type: {args.pypi_type}")
print(f"[Debug] release_type: {args.release_type}")
cwd = os.getcwd()
print(f"Current working directory: {cwd}")
with open(os.path.join(os.getcwd(), args.config), "r") as config_file:
config_json = json.load(config_file)
package_dir_path = os.path.join(cwd, args.package_dir_path)
release_config = config_json["releases"][args.pypi_type][f"{args.src_folder_name}-{args.release_type}"]
if args.pypi_type == "internal":
publish_package_internal(package_dir_path, args.storage_key, release_config)
+93
View File
@@ -0,0 +1,93 @@
.title {
font-weight:700;
}
.sd-card-header {
font-weight:700;
font-size: 16px;
}
.bd-page-width {
max-width: 100rem;
}
.bd-sidebar-primary {
flex: 0 0 20%;
}
.bd-main .bd-content .bd-article-container {
max-width: 70em;
}
/* Background color for feature announcement */
html[data-theme="light"] {
--header-announcement-color: #3278e5;
}
html[data-theme="dark"] {
--header-announcement-color: #67d6ed;
}
.bd-header-announcement {
background: var(--header-announcement-color);
}
/* Background color for feature announcement */
/* Background color for warning announcement */
/*html[data-theme="light"] {*/
/* --header-announcement-color: #fff070;*/
/*}*/
/*html[data-theme="dark"] {*/
/* --header-announcement-color: #4d4d00;*/
/*}*/
/*.bd-header-announcement {*/
/* background: var(--header-announcement-color);*/
/*}*/
/* Background color for warning announcement */
/* (A) LIGHTBOX BACKGROUND */
#lightbox {
/* (A1) COVERS FULLSCREEN */
position: fixed; z-index: 1060;
top: 0; left: 0;
width: 100%; height: 100%;
/* (A2) BACKGROUND */
background: rgba(0, 0, 0, 0.5);
/* (A3) CENTER IMAGE ON SCREEN */
display: flex;
align-items: center;
align-items: center;
/* (A4) HIDDEN BY DEFAULT */
visibility: hidden;
opacity: 0;
/* (A5) SHOW/HIDE ANIMATION */
transition: opacity ease 0.4s;
}
/* (A6) TOGGLE VISIBILITY */
#lightbox.show {
visibility: visible;
opacity: 1;
}
/* (B) LIGHTBOX IMAGE */
#lightbox img {
/* (B1) DIMENSIONS */
width: 100%;
height: 100%;
/* (B2) IMAGE FIT */
/* contain | cover | fill | scale-down */
object-fit: contain;
}
.avatar {
border-radius: 50%;
width: 30px;
height: 30px;
}
+61
View File
@@ -0,0 +1,61 @@
// Get the head element
let head = document.getElementsByTagName("head")[0];
// Create the script element
let script = document.createElement("script");
script.async = true;
script.src = "https://www.googletagmanager.com/gtag/js?id=G-KZXK5PFBZY";
// Create another script element for the gtag code
let script2 = document.createElement("script");
script2.innerHTML = ` window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date());gtag('config', 'G-KZXK5PFBZY'); `;
// Insert the script elements after the head element
head.insertAdjacentElement("afterbegin", script2);
head.insertAdjacentElement("afterbegin", script);
// This is used to zoom in images when clicked on
window.onload = () => {
if (document.getElementById("lightbox") === null){
// Append lightbox div to each page
let div = document.createElement('div');
div.innerHTML = '<div id="lightbox"></div>';
document.body.appendChild(div);
}
// (A) GET LIGHTBOX & ALL .ZOOMD IMAGES
let all = document.getElementsByClassName("bd-article")[0].getElementsByTagName("img"),
lightbox = document.getElementById("lightbox");
// (B) CLICK TO SHOW IMAGE IN LIGHTBOX
// * SIMPLY CLONE INTO LIGHTBOX & SHOW
if (all.length>0) { for (let i of all) {
// skip if class contains avatar or img_ev3q(Open on github button)
if (i.classList.contains("avatar") || i.classList.contains("img_ev3q")) {
continue;
}
i.onclick = () => {
let clone = i.cloneNode();
clone.className = "";
lightbox.innerHTML = "";
lightbox.appendChild(clone);
lightbox.className = "show";
};
}}
// (C) CLICK TO CLOSE LIGHTBOX
lightbox.onclick = () => {
lightbox.className = "";
};
};
if (window.location.pathname === "/promptflow/" || window.location.pathname === "/promptflow/index.html") {
// This is used to control homepage background
let observer = new MutationObserver(function(mutations) {
const dark = document.documentElement.dataset.theme == 'dark';
document.body.style.backgroundSize = "100%";
document.body.style.backgroundPositionY = "bottom";
document.body.style.backgroundRepeat = "no-repeat"
})
observer.observe(document.documentElement, {attributes: true, attributeFilter: ['data-theme']});
}
+47
View File
@@ -0,0 +1,47 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_699_15212)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M237 39.0408V461.693C237 469.397 228.655 474.208 221.988 470.346L151.918 429.764C130.306 417.247 117 394.164 117 369.19V148.892C117 123.917 130.306 100.834 151.918 88.3177L237 39.0408Z" fill="url(#paint0_linear_699_15212)"/>
<path d="M395.075 127.51L237 39V167.541L283.451 192.041L395.075 127.51Z" fill="url(#paint1_linear_699_15212)"/>
<path d="M395.075 127.51L237 39V167.541L283.451 192.041L395.075 127.51Z" fill="url(#paint2_linear_699_15212)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M255.5 231.426C255.5 217.184 263.073 204.017 275.382 196.854L395 127.248V216.101C395 241.03 381.742 264.078 360.193 276.611L270.528 328.76C263.861 332.637 255.5 327.828 255.5 320.116L255.5 231.426Z" fill="url(#paint3_linear_699_15212)"/>
</g>
<defs>
<linearGradient id="paint0_linear_699_15212" x1="196.286" y1="183.041" x2="270.786" y2="92.5087" gradientUnits="userSpaceOnUse">
<stop stop-color="#3272ED"/>
<stop offset="1" stop-color="#AF7BD6"/>
</linearGradient>
<linearGradient id="paint1_linear_699_15212" x1="457.98" y1="131.313" x2="260.351" y2="133.014" gradientUnits="userSpaceOnUse">
<stop stop-color="#DA7ED0"/>
<stop offset="0.05" stop-color="#B77BD4"/>
<stop offset="0.11" stop-color="#9079DA"/>
<stop offset="0.18" stop-color="#6E77DF"/>
<stop offset="0.25" stop-color="#5175E3"/>
<stop offset="0.33" stop-color="#3973E7"/>
<stop offset="0.42" stop-color="#2772E9"/>
<stop offset="0.54" stop-color="#1A71EB"/>
<stop offset="0.813361" stop-color="#1371EC"/>
<stop offset="1" stop-color="#064495"/>
</linearGradient>
<linearGradient id="paint2_linear_699_15212" x1="210.18" y1="4.19164" x2="307.181" y2="175.949" gradientUnits="userSpaceOnUse">
<stop stop-color="#712575"/>
<stop offset="0.09" stop-color="#9A2884"/>
<stop offset="0.18" stop-color="#BF2C92"/>
<stop offset="0.27" stop-color="#DA2E9C"/>
<stop offset="0.34" stop-color="#EB30A2"/>
<stop offset="0.4" stop-color="#F131A5"/>
<stop offset="0.5" stop-color="#EC30A3"/>
<stop offset="0.61" stop-color="#DF2F9E"/>
<stop offset="0.72" stop-color="#C92D96"/>
<stop offset="0.83" stop-color="#AA2A8A"/>
<stop offset="0.95" stop-color="#83267C"/>
<stop offset="1" stop-color="#712575"/>
</linearGradient>
<linearGradient id="paint3_linear_699_15212" x1="308" y1="260.041" x2="307.043" y2="133.204" gradientUnits="userSpaceOnUse">
<stop stop-color="#1D5CD6"/>
<stop offset="1" stop-color="#787BE5"/>
</linearGradient>
<clipPath id="clip0_699_15212">
<rect width="512" height="512" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 975 B

@@ -0,0 +1,57 @@
{%- macro automodule(modname, options) -%}
.. automodule:: {{ modname }}
{%- for option in options %}
:{{ option }}:
{%- endfor %}
{%- endmacro %}
{%- macro toctree(docnames) -%}
.. toctree::
:maxdepth: {{ maxdepth }}
{% for docname in docnames %}
{{ docname }}
{%- endfor %}
{%- endmacro %}
{%- if is_namespace %}
{{- [pkgname, "namespace"] | join(" ") | e | heading }}
{% else %}
{{- [pkgname, "module"] | join(" ") | e | heading }}
{% endif %}
{%- if is_namespace %}
.. py:module:: {{ pkgname }}
{% endif %}
{%- if modulefirst and not is_namespace %}
{{ automodule(pkgname, automodule_options) }}
{% endif %}
{%- if subpackages %}
Subpackages
-----------
{{ toctree(subpackages) }}
{% endif %}
{%- if submodules %}
Submodules
----------
{% if separatemodules %}
{{ toctree(submodules) }}
{% else %}
{%- for submodule in submodules %}
{% if show_headings %}
{{- [submodule, "module"] | join(" ") | e | heading(2) }}
{% endif %}
{{ automodule(submodule, automodule_options) }}
{% endfor %}
{%- endif %}
{%- endif %}
{%- if not modulefirst and not is_namespace %}
Module contents
---------------
{{ automodule(pkgname, automodule_options) }}
{% endif %}
+164
View File
@@ -0,0 +1,164 @@
# -- Path setup --------------------------------------------------------------
import sys
# -- Project information -----------------------------------------------------
project = "Prompt flow"
copyright = "2024, Microsoft"
author = "Microsoft"
sys.path.append(".")
from gallery_directive import GalleryDirective # noqa: E402
# -- General configuration ---------------------------------------------------
extensions = [
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
"sphinx.ext.todo",
"sphinxext.rediraffe",
"sphinx_design",
"sphinx_copybutton",
"matplotlib.sphinxext.plot_directive",
"sphinx_togglebutton",
"myst_nb",
# 'myst_parser',
"sphinx.builders.linkcheck",
"jupyter_sphinx",
]
# -- Internationalization ------------------------------------------------
# specifying the natural language populates some key tags
language = "en"
# specify charset as utf-8 to accept chinese punctuation
charset_type = "utf-8"
autosummary_generate = True
# Add any paths that contain templates here, relative to this directory.
# templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
nb_execution_mode = "off"
exclude_patterns = [
"_build",
"Thumbs.db",
".DS_Store",
"**.ipynb_checkpoints",
"**.py",
"**.yml",
"**.sh",
"**.zip",
"**.skip",
]
source_suffix = [".rst", ".md", ".ipynb"]
# Options for the linkcheck builder
linkcheck_ignore = [
# openai related sites blocks the IP of the CI server.
r"https://openai\.com/",
r"https://platform\.openai\.com/",
r"https://help\.openai\.com/",
# These are used in card links, for example 'xx.html', .md can't be resolved.
r"^(?!https?)",
"deploy-using-docker.html",
"deploy-using-kubernetes.html",
"https://portal.azure.com/#create/Microsoft.CognitiveServicesTextAnalytics", # sphinx recognizes #create as an anchor while it's not. # noqa: E501
"https://ms.portal.azure.com/#view/Microsoft_Azure_Marketplace/MarketplaceOffersBlade/searchQuery/machine%20learning", # noqa: E501
]
linkcheck_exclude_documents = [
"contributing",
r".*/tutorials/.*", # ignore link in copied notebooks.
]
# -- Extension options -------------------------------------------------------
# This allows us to use ::: to denote directives, useful for admonitions
myst_enable_extensions = ["colon_fence", "substitution"]
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "pydata_sphinx_theme"
html_logo = "_static/logo.svg"
html_favicon = "_static/logo32.ico"
html_sourcelink_suffix = ""
html_show_sourcelink = False
# Define the json_url for our version switcher.
html_theme_options = {
"github_url": "https://github.com/microsoft/promptflow",
"header_links_before_dropdown": 6,
"icon_links": [
{
"name": "PyPI",
"url": "https://pypi.org/project/promptflow/",
"icon": "fa-solid fa-box",
},
],
"logo": {
"text": "Prompt flow",
"alt_text": "Prompt flow",
},
"use_edit_page_button": True,
"show_toc_level": 1,
"navbar_align": "left", # [left, content, right] For testing that the navbar items align properly
"navbar_center": ["navbar-nav"],
"announcement": "🚀<b style='color:black;'>"
"Promptflow 1.13.0 has released! Try new feature: "
"<a href='https://microsoft.github.io/promptflow/how-to-guides/tracing/index.html' style='color:black;'>"
"<u>tracing interaction with LLMs</u></a>."
"</b>",
"show_nav_level": 1,
}
html_sidebars = {
# "quick_start/README.md": ['localtoc.html', 'relations.html', 'searchbox.html'],
# "examples/persistent-search-field": ["search-field"],
# Blog sidebars
# ref: https://ablog.readthedocs.io/manual/ablog-configuration-options/#blog-sidebars
"features": ["localtoc.html", "relations.html", "searchbox.html"],
# "tutorials": ['localtoc.html', 'relations.html', 'searchbox.html'],
}
html_context = {
"default_mode": "light",
"github_user": "",
"github_repo": "microsoft/promptflow",
"github_version": "main",
"doc_path": "docs",
}
rediraffe_redirects = {}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
html_css_files = ["custom.css"]
html_js_files = ["custom.js"]
todo_include_todos = True
# myst reference config
myst_heading_anchors = 5
# allow annotation for __call__ methods
autodoc_default_options = {
'special-members': '__call__',
}
def setup(app):
# Add the gallery directive
app.add_directive("gallery-grid", GalleryDirective)
+262
View File
@@ -0,0 +1,262 @@
<#
.DESCRIPTION
Script to build doc site
.EXAMPLE
PS> ./doc_generation.ps1 -SkipInstall # skip pip install
PS> ./doc_generation.ps1 -BuildLinkCheck -WarningAsError:$true -SkipInstall
#>
[CmdletBinding()]
param(
[switch]$SkipInstall,
[switch]$WarningAsError = $false,
[switch]$BuildLinkCheck = $false,
[switch]$WithReferenceDoc = $false
)
[string] $ScriptPath = $PSCommandPath | Split-Path -Parent
[string] $RepoRootPath = $ScriptPath | Split-Path -Parent | Split-Path -Parent
[string] $DocPath = [System.IO.Path]::Combine($RepoRootPath, "docs")
[string] $TempDocPath = New-TemporaryFile | % { Remove-Item $_; New-Item -ItemType Directory -Path $_ }
[string] $PkgSrcPath = [System.IO.Path]::Combine($RepoRootPath, "src")
[string] $OutPath = [System.IO.Path]::Combine($ScriptPath, "_build")
[string] $SphinxApiDoc = [System.IO.Path]::Combine($DocPath, "sphinx_apidoc.log")
[string] $SphinxBuildDoc = [System.IO.Path]::Combine($DocPath, "sphinx_build.log")
[string] $WarningErrorPattern = "WARNING:|ERROR:|CRITICAL:| broken "
[System.Collections.ArrayList]$IncludeList = @("promptflow-tracing", "promptflow-core", "promptflow-devkit", "promptflow-azure", "promptflow-rag", "promptflow-evals")
$apidocWarningsAndErrors = $null
$buildWarningsAndErrors = $null
if (-not $SkipInstall){
# Prepare doc generation packages
pip install pydata-sphinx-theme==0.11.0
pip install sphinx==5.1
pip install sphinx-copybutton==0.5.0
pip install sphinx_design==0.3.0
pip install sphinx-sitemap==2.2.0
pip install sphinx-togglebutton==0.3.2
pip install sphinxext-rediraffe==0.2.7
pip install sphinxcontrib-mermaid==0.8.1
pip install ipython-genutils==0.2.0
pip install myst-nb==0.17.1
pip install numpydoc==1.5.0
pip install myst-parser==0.18.1
pip install "numpy<1.25"
pip install matplotlib==3.4.3
pip install jinja2==3.0.1
pip install jupyter-sphinx==0.4.0
Write-Host "===============Finished install requirements==============="
}
function ProcessFiles {
# Exclude files not mean to be in doc site
$exclude_files = "README.md", "dev"
foreach ($f in $exclude_files)
{
$full_path = [System.IO.Path]::Combine($TempDocPath, $f)
Remove-Item -Path $full_path -Recurse
}
}
Write-Host "===============PreProcess Files==============="
Write-Host "Copy doc to: $TempDocPath"
ROBOCOPY $DocPath $TempDocPath /S /NFL /NDL /XD "*.git" [System.IO.Path]::Combine($DocPath, "_scripts\_build")
ProcessFiles
function Update-Sub-Pkg-Index-Title {
param (
[string] $SubPkgRefDocPath,
[string] $SubPkgName
)
# This is used to update the title of the promptflow.rst file in the sub package
# from 'promptflow namespaces' to package name
$IndexRst = [System.IO.Path]::Combine($SubPkgRefDocPath, "promptflow.rst")
$IndexContent = Get-Content $IndexRst
$IndexContent[0] = ("{0} package" -f $SubPkgName)
$IndexContent[1] = "================================="
$IndexContent[2] = ".. py:module:: promptflow"
$IndexContent[3] = " :noindex:"
Set-Content $IndexRst $IndexContent
}
function Add-Changelog {
$ChangelogFolder = [System.IO.Path]::Combine($TempDocPath, "reference\changelog")
New-Item -ItemType Directory -Path $ChangelogFolder -Force
Write-Host "===============Collect Package ChangeLog==============="
$TocTreeContent = @("", "``````{toctree}", ":maxdepth: 1", ":hidden:", "")
foreach($Item in Get-Childitem -path $PkgSrcPath)
{
if((-not ($IncludeList -contains $Item.Name)) -and ($Item.Name -ne "promptflow")){
continue
}
# Collect CHANGELOG, name with package.md
$ChangelogPath = [System.IO.Path]::Combine($Item.FullName, "CHANGELOG.md")
$TargetChangelogPath = [System.IO.Path]::Combine($ChangelogFolder, "{0}.md" -f $Item.Name)
if($Item.Name -ne "promptflow"){
$TocTreeContent += $Item.name
}
Copy-Item -Path $ChangelogPath -Destination $TargetChangelogPath
}
$TocTreeContent += "``````"
# Add subpackage index to promptflow changelog
$PromptflowChangelog = [System.IO.Path]::Combine($ChangelogFolder, "promptflow.md")
$PromptflowChangelogContent = Get-Content $PromptflowChangelog
$PromptflowChangelogContent[0] = "# promptflow package"
$PromptflowChangelogContent += $TocTreeContent
Set-Content $PromptflowChangelog $PromptflowChangelogContent
}
function Add-Api-Reference {
$RefDocRelativePath = "reference\python-library-reference"
$RefDocPath = [System.IO.Path]::Combine($TempDocPath, $RefDocRelativePath)
$PlaceHolderFile = [System.IO.Path]::Combine($RefDocPath, "promptflow.md")
if(!(Test-Path $RefDocPath)){
throw "Reference doc path not found. Please make sure '$RefDocRelativePath' is under '$DocPath'"
}
Remove-Item $PlaceHolderFile -Force
$ApidocWarningsAndErrors = [System.Collections.ArrayList]::new()
foreach($Item in Get-Childitem -path $PkgSrcPath){
if(-not ($IncludeList -contains $Item.Name)){
continue
}
# Build API reference doc
$SubPkgPath = [System.IO.Path]::Combine($Item.FullName, "promptflow")
$SubPkgRefDocPath = [System.IO.Path]::Combine($RefDocPath, $Item.Name)
Write-Host "===============Build $Item Reference Doc==============="
$TemplatePath = [System.IO.Path]::Combine($RepoRootPath, "scripts\docs\api_doc_templates")
sphinx-apidoc --separate --module-first --no-toc --implicit-namespaces "$SubPkgPath" -o "$SubPkgRefDocPath" -t $TemplatePath | Tee-Object -FilePath $SphinxApiDoc
$SubPkgWarningsAndErrors = Select-String -Path $SphinxApiDoc -Pattern $WarningErrorPattern
if($SubPkgWarningsAndErrors){
$ApidocWarningsAndErrors.AddRange($SubPkgWarningsAndErrors)
}
Update-Sub-Pkg-Index-Title $SubPkgRefDocPath $Item.Name
}
}
function Add-Metadata{
param (
[string] $NotebookPath,
[string] $NotebookRepoPath,
[System.Collections.ArrayList] $AuthorList
)
if (-not $AuthorList){
# Skip insert if author list not set
throw "Skip Add Metadata: $NotebookPath - Author list not set"
return
}
$NotebookContent = Get-Content $NotebookPath -Raw | ConvertFrom-Json
# Covert to System.Collections.ArrayList to avoid 'Collection was of a fixed size' error.
$NotebookContent.cells = [System.Collections.ArrayList]::new($NotebookContent.cells)
if($NotebookContent.cells[0].source.Length -gt 1){
# If the first cell length > 1, indicate there are more things than title it self in the first cell
throw "Skip Add Metadata: $NotebookPath - First cell length > 1, only leave title to that cell."
return
}
$MetadataFormat = "Authored by:&nbsp;{0}{1}"
$SingleAuthor = "&nbsp;<a href='https://github.com/{0}' target='_blank'><img src='https://github.com/{0}.png' alt='Avatar' class='avatar dark-light'></a>"
$JumpLink = "<a href='{0}' target='_blank'><img decoding='async' loading='lazy' src='https://img.shields.io/badge/Open%20on%20GitHub-grey?logo=github' alt='Open on GitHub' class='img_ev3q' style='float: right;'></a>" -f $NotebookRepoPath
$Authors = $AuthorList | ForEach-Object { $SingleAuthor -f $_.replace("@github.com", "") }
$Metadata = $MetadataFormat -f ($Authors -join ""), $JumpLink
# Insert metadata to cells
$MetadataCell = @{
"cell_type" = "markdown";
"metadata" = @{};
"source" = @($Metadata)
}
$NotebookContent.cells.Insert(1, $MetadataCell)
$NotebookContent | ConvertTo-Json -Depth 100 | Set-Content $NotebookPath
}
function Add-Notebook
{
Write-Host "===============Collect Package Notebooks==============="
$NotebookRootPath = [System.IO.Path]::Combine($RepoRootPath, "examples")
$TargetNotebookPath = [System.IO.Path]::Combine($TempDocPath, "tutorials")
# Create section list
$SectionNames = "Tracing", "Prompty", "Flow", "Rag"
$Sections = [ordered]@{
Tracing=[System.Collections.ArrayList]::new();
Prompty=[System.Collections.ArrayList]::new();
Flow=[System.Collections.ArrayList]::new()
Rag=[System.Collections.ArrayList]::new()
}
foreach($Item in Get-Childitem -path $NotebookRootPath -Recurse -Filter "*.ipynb")
{
# Notebook to build must have metadata: {"build_doc": {"category": "local/azure"}}
$NotebookContent = Get-Content $Item.FullName -Raw | ConvertFrom-Json
if(-not $NotebookContent.metadata.build_doc){
continue
}
$RepoPath = $Item.FullName.Replace($RepoRootPath, "https://github.com/microsoft/promptflow/tree/main/")
$SectionName = $NotebookContent.metadata.build_doc.section
[int]$Weight = $NotebookContent.metadata.build_doc.weight
$Category = $NotebookContent.metadata.build_doc.category
$AuthorList = $NotebookContent.metadata.build_doc.author
# If category is 'azure', add 1000 to weight
if($Category -eq "azure"){
$Weight += 1000
}
# Add ItemName, Category tuple to sections
$Sections[$SectionName].Add([Tuple]::Create($Item.Name.Replace(".ipynb", ""), $Weight))
# Copy notebook to doc path
Write-Host "Adding Notebook $Item ..."
$MediaDir = $Item.FullName + '\..\media'
Copy-Item -Path $Item.FullName -Destination $TargetNotebookPath
if(Test-Path $MediaDir){
# copy image referenced in notebook
Write-Host "Copying media files from $MediaDir ..."
Copy-Item -Path $MediaDir -Destination $TargetNotebookPath -Recurse -Force
}
# Append metadata to notebook
$CopiedNotebookPath = [System.IO.Path]::Combine($TargetNotebookPath, $Item.Name)
Add-Metadata $CopiedNotebookPath $RepoPath $AuthorList
}
# Reverse sort each section list by Weight
foreach($SectionName in $SectionNames){
$Sections[$SectionName] = $Sections[$SectionName] | Sort-Object -Property { $_.Item2 }
}
$TocTreeContent = @("", "``````{{toctree}}", ":caption: {0}", ":hidden:", ":maxdepth: 1", "", "{1}", "``````")
# Build toctree content for each section, append to tutorials index.md
$TutorialIndex = [System.IO.Path]::Combine($TargetNotebookPath, "index.md")
foreach($SectionName in $SectionNames){
$SectionTocTree = $TocTreeContent -join "`n"
# Join Item1 to a string in list
$ExampleList = ($Sections[$SectionName] | ForEach-Object { $_.Item1 }) -join "`n"
$SectionTocTree = $SectionTocTree -f $SectionName, $ExampleList
Write-Debug $SectionTocTree
Add-Content -Path $TutorialIndex -Value $SectionTocTree
}
}
if($WithReferenceDoc){
Add-Api-Reference
}
# Build subpackage changelog
Add-Changelog
# Build notebook examples
Add-Notebook
Write-Host "===============Build Documentation with internal=${Internal}==============="
$BuildParams = [System.Collections.ArrayList]::new()
if($WarningAsError){
$BuildParams.Add("-W")
$BuildParams.Add("--keep-going")
}
if($BuildLinkCheck){
$BuildParams.Add("-blinkcheck")
}
sphinx-build $TempDocPath $OutPath -c $ScriptPath $BuildParams -v | Tee-Object -FilePath $SphinxBuildDoc
$buildWarningsAndErrors = Select-String -Path $SphinxBuildDoc -Pattern $WarningErrorPattern
Write-Host "Clean path: $TempDocPath"
Remove-Item $TempDocPath -Recurse -Confirm:$False -Force
if ($buildWarningsAndErrors) {
Write-Host "=============== Build warnings and errors ==============="
foreach ($line in $buildWarningsAndErrors) {
Write-Host $line -ForegroundColor Red
}
}
+139
View File
@@ -0,0 +1,139 @@
"""A directive to generate a gallery of images from structured data.
Generating a gallery of images that are all the same size is a common
pattern in documentation, and this can be cumbersome if the gallery is
generated programmatically. This directive wraps this particular use-case
in a helper-directive to generate it with a single YAML configuration file.
It currently exists for maintainers of the pydata-sphinx-theme,
but might be abstracted into a standalone package if it proves useful.
"""
from yaml import safe_load
from typing import List
from pathlib import Path
from docutils import nodes
from docutils.parsers.rst import directives
from sphinx.util.docutils import SphinxDirective
from sphinx.util import logging
logger = logging.getLogger(__name__)
TEMPLATE_GRID = """
`````{{grid}} {grid_columns}
{container_options}
{content}
`````
"""
GRID_CARD = """
````{{grid-item-card}} {title}
{card_options}
{content}
````
"""
class GalleryDirective(SphinxDirective):
"""A directive to show a gallery of images and links in a grid."""
name = "gallery-grid"
has_content = True
required_arguments = 0
optional_arguments = 1
final_argument_whitespace = True
option_spec = {
# A class to be added to the resulting container
"grid-columns": directives.unchanged,
"class-container": directives.unchanged,
"class-card": directives.unchanged,
}
def run(self) -> List[nodes.Node]: # noqa: C901
if self.arguments:
# If an argument is given, assume it's a path to a YAML file
# Parse it and load it into the directive content
path_data_rel = Path(self.arguments[0])
path_doc, _ = self.get_source_info()
path_doc = Path(path_doc).parent
path_data = (path_doc / path_data_rel).resolve()
if not path_data.exists():
logger.warn(f"Could not find grid data at {path_data}.")
nodes.text("No grid data found at {path_data}.")
return
yaml_string = path_data.read_text()
else:
yaml_string = "\n".join(self.content)
# Read in YAML so we can generate the gallery
grid_data = safe_load(yaml_string)
grid_items = []
for item in grid_data:
# Grid card parameters
options = {}
if "website" in item:
options["link"] = item["website"]
if "class-card" in self.options:
options["class-card"] = self.options["class-card"]
if "img-background" in item:
options["img-background"] = item["img-background"]
if "img-top" in item:
options["img-top"] = item["img-top"]
if "img-bottom" in item:
options["img-bottom"] = item["img-bottom"]
options_str = "\n".join(f":{k}: {v}" for k, v in options.items()) + "\n\n"
# Grid card content
content_str = ""
if "header" in item:
content_str += f"{item['header']}\n\n^^^\n\n"
if "image" in item:
content_str += f"![Gallery image]({item['image']})\n\n"
if "content" in item:
content_str += f"{item['content']}\n\n"
if "footer" in item:
content_str += f"+++\n\n{item['footer']}\n\n"
title = item.get("title", "")
content_str += "\n"
grid_items.append(
GRID_CARD.format(
card_options=options_str, content=content_str, title=title
)
)
# Parse the template with Sphinx Design to create an output
container = nodes.container()
# Prep the options for the template grid
container_options = {"gutter": 2, "class-container": "gallery-directive"}
if "class-container" in self.options:
container_options[
"class-container"
] += f' {self.options["class-container"]}'
container_options_str = "\n".join(
f":{k}: {v}" for k, v in container_options.items()
)
# Create the directive string for the grid
grid_directive = TEMPLATE_GRID.format(
grid_columns=self.options.get("grid-columns", "1 2 3 4"),
container_options=container_options_str,
content="\n".join(grid_items),
)
# Parse content as a directive so Sphinx Design processes it
self.state.nested_parse([grid_directive], 0, container)
# Sphinx Design outputs a container too, so just use that
container = container.children[0]
# Add extra classes
if self.options.get("container-class", []):
container.attributes["classes"] += self.options.get("class", [])
return [container]
+56
View File
@@ -0,0 +1,56 @@
import os
import subprocess
import multiprocessing
import importlib
git_base = subprocess.check_output(['git', 'rev-parse', '--show-toplevel']).decode().strip()
def walk_and_ignore_pycache(directory):
list = []
for root, dirnames, files in os.walk(directory, topdown=True):
# This line removes any __pycache__ directories from the list
dirnames[:] = [d for d in dirnames if d != '__pycache__' and d != 'tests' and d != 'data']
filenames = [f for f in files if f.endswith('.py') and not f.startswith('__init__')]
for filename in filenames:
# Process files as you would like
list.append(os.path.join(root, filename))
return list
def file_to_import(file):
push_file = []
head_tail = os.path.split(file)
while head_tail[1] != "promptflow" and head_tail[0] != "":
if head_tail[1].endswith(".py"):
push_file.insert(0, head_tail[1][:-3])
else:
push_file.insert(0, head_tail[1])
file = head_tail[0]
head_tail = os.path.split(file)
push_file.insert(0, "promptflow")
return ".".join(push_file)
# If there is an import error, the process will exit with a non-zero exit code
# Find this importlib.import_module as the keyword to search for the error
# The error below this is the import error / circular import error.
def subprocess_check_python_import(file):
print(f'Checking import of {file} on process ID: {os.getpid()}')
importlib.import_module(file)
def process_file(file):
import_name = file_to_import(file)
subprocess_check_python_import(import_name)
if __name__ == '__main__':
pool = multiprocessing.Pool()
list = walk_and_ignore_pycache(git_base + "/src/promptflow-tracing/")
list.extend(walk_and_ignore_pycache(git_base + "/src/promptflow-core/"))
list.extend(walk_and_ignore_pycache(git_base + "/src/promptflow-devkit/"))
list.extend(walk_and_ignore_pycache(git_base + "/src/promptflow-azure/"))
pool.map(process_file, list)
pool.close()
pool.join()
@@ -0,0 +1,32 @@
# Curl Install Script Information
The scripts in this directory are used for installing through curl and they point to the packages on PyPI.
## Install or update promptflow
curl https://promptflowartifact.blob.core.windows.net/linux-install-scripts/install | bash
The script can also be downloaded and run locally. You may have to restart your shell in order for the changes to take effect.
## Uninstall promptflow
Uninstall the promptflow by directly deleting the files from the location chosen at the time of installation.
1. Remove the installed CLI files.
```bash
# The default install/executable location is the user's home directory ($HOME).
rm -r $HOME/lib/promptflow
```
2. Modify your `$HOME/.bash_profile` or `$HOME/.bashrc` file to remove the following line:
```text
export PATH=$PATH:$HOME/lib/promptflow/bin
```
3. If using `bash` or `zsh`, reload your shell's command cache.
```bash
hash -r
```
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
#---------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
#---------------------------------------------------------------------------------------------
#
# Bash script to install the prompt flow
#
INSTALL_SCRIPT_URL="https://promptflowartifact.blob.core.windows.net/linux-install-scripts/install.py"
_TTY=/dev/tty
install_script=$(mktemp -t promptflow_install_tmp_XXXXXX) || exit
echo "Downloading prompt flow install script from $INSTALL_SCRIPT_URL to $install_script."
curl -# $INSTALL_SCRIPT_URL > $install_script || exit
python_cmd=python3
if ! command -v python3 >/dev/null 2>&1
then
echo "ERROR: python3 not found."
echo "If python3 is available on the system, add it to PATH."
exit 1
fi
chmod 775 $install_script
echo "Running install script."
$python_cmd $install_script < $_TTY
@@ -0,0 +1,262 @@
#!/usr/bin/env python
# --------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See License.txt in the project root for license information.
# --------------------------------------------------------------------------------------------
#
# This script will install the promptflow into a directory and create an executable
# at a specified file path that is the entry point into the promptflow.
#
# The latest versions of all promptflow command packages will be installed.
#
import os
import sys
import platform
import tempfile
import shutil
import subprocess
DEFAULT_INSTALL_DIR = os.path.expanduser(os.path.join('~', 'lib', 'promptflow'))
DEFAULT_EXEC_DIR = os.path.expanduser(os.path.join('~', 'bin'))
PF_EXECUTABLE_NAME = 'pf'
PFAZURE_EXECUTABLE_NAME = 'pfazure'
USER_BASH_RC = os.path.expanduser(os.path.join('~', '.bashrc'))
USER_BASH_PROFILE = os.path.expanduser(os.path.join('~', '.bash_profile'))
class CLIInstallError(Exception):
pass
def print_status(msg=''):
print('-- '+msg)
def prompt_input(msg):
return input('\n===> '+msg)
def prompt_input_with_default(msg, default):
if default:
return prompt_input("{} (leave blank to use '{}'): ".format(msg, default)) or default
else:
return prompt_input('{}: '.format(msg))
def prompt_y_n(msg, default=None):
if default not in [None, 'y', 'n']:
raise ValueError("Valid values for default are 'y', 'n' or None")
y = 'Y' if default == 'y' else 'y'
n = 'N' if default == 'n' else 'n'
while True:
ans = prompt_input('{} ({}/{}): '.format(msg, y, n))
if ans.lower() == n.lower():
return False
if ans.lower() == y.lower():
return True
if default and not ans:
return default == y.lower()
def exec_command(command_list, cwd=None, env=None):
print_status('Executing: '+str(command_list))
subprocess.check_call(command_list, cwd=cwd, env=env)
def create_tmp_dir():
tmp_dir = tempfile.mkdtemp()
return tmp_dir
def create_dir(dir):
if not os.path.isdir(dir):
print_status("Creating directory '{}'.".format(dir))
os.makedirs(dir)
def create_virtualenv(install_dir):
cmd = [sys.executable, '-m', 'venv', install_dir]
exec_command(cmd)
def install_cli(install_dir, tmp_dir):
path_to_pip = os.path.join(install_dir, 'bin', 'pip')
cmd = [path_to_pip, 'install', '--cache-dir', tmp_dir,
'promptflow[azure,executable,azureml-serving,executor-service]', '--upgrade']
exec_command(cmd)
cmd = [path_to_pip, 'install', '--cache-dir', tmp_dir, 'promptflow-tools', '--upgrade']
exec_command(cmd)
cmd = [path_to_pip, 'install', '--cache-dir', tmp_dir, 'keyrings.alt', '--upgrade']
exec_command(cmd)
def get_install_dir():
install_dir = None
while not install_dir:
prompt_message = 'In what directory would you like to place the install?'
install_dir = prompt_input_with_default(prompt_message, DEFAULT_INSTALL_DIR)
install_dir = os.path.realpath(os.path.expanduser(install_dir))
if ' ' in install_dir:
print_status("The install directory '{}' cannot contain spaces.".format(install_dir))
install_dir = None
else:
create_dir(install_dir)
if os.listdir(install_dir):
print_status("'{}' is not empty and may contain a previous installation.".format(install_dir))
ans_yes = prompt_y_n('Remove this directory?', 'n')
if ans_yes:
shutil.rmtree(install_dir)
print_status("Deleted '{}'.".format(install_dir))
create_dir(install_dir)
else:
# User opted to not delete the directory so ask for install directory again
install_dir = None
print_status("We will install at '{}'.".format(install_dir))
return install_dir
def _backup_rc(rc_file):
try:
shutil.copyfile(rc_file, rc_file+'.backup')
print_status("Backed up '{}' to '{}'".format(rc_file, rc_file+'.backup'))
except (OSError, IOError):
pass
def _get_default_rc_file():
bashrc_exists = os.path.isfile(USER_BASH_RC)
bash_profile_exists = os.path.isfile(USER_BASH_PROFILE)
if not bashrc_exists and bash_profile_exists:
return USER_BASH_PROFILE
if bashrc_exists and bash_profile_exists and platform.system().lower() == 'darwin':
return USER_BASH_PROFILE
return USER_BASH_RC if bashrc_exists else None
def _default_rc_file_creation_step():
rcfile = USER_BASH_PROFILE if platform.system().lower() == 'darwin' else USER_BASH_RC
ans_yes = prompt_y_n('Could not automatically find a suitable file to use. Create {} now?'.format(rcfile),
default='y')
if ans_yes:
open(rcfile, 'a').close()
return rcfile
return None
def _find_line_in_file(file_path, search_pattern):
try:
with open(file_path, 'r', encoding="utf-8") as search_file:
for line in search_file:
if search_pattern in line:
return True
except (OSError, IOError):
pass
return False
def _modify_rc(rc_file_path, line_to_add):
if not _find_line_in_file(rc_file_path, line_to_add):
with open(rc_file_path, 'a', encoding="utf-8") as rc_file:
rc_file.write('\n'+line_to_add+'\n')
def get_rc_file_path():
rc_file = None
default_rc_file = _get_default_rc_file()
if not default_rc_file:
rc_file = _default_rc_file_creation_step()
rc_file = rc_file or prompt_input_with_default('Enter a path to an rc file to update', default_rc_file)
if rc_file:
rc_file_path = os.path.realpath(os.path.expanduser(rc_file))
if os.path.isfile(rc_file_path):
return rc_file_path
print_status("The file '{}' could not be found.".format(rc_file_path))
return None
def warn_other_azs_on_path(exec_dir, exec_filepath):
env_path = os.environ.get('PATH')
conflicting_paths = []
if env_path:
for p in env_path.split(':'):
for file in [PF_EXECUTABLE_NAME, PFAZURE_EXECUTABLE_NAME]:
p_to_pf = os.path.join(p, file)
if p != exec_dir and os.path.isfile(p_to_pf):
conflicting_paths.append(p_to_pf)
if conflicting_paths:
print_status()
print_status(f"** WARNING: Other '{PF_EXECUTABLE_NAME}/{PFAZURE_EXECUTABLE_NAME}' "
f"executables are on your $PATH. **")
print_status("Conflicting paths: {}".format(', '.join(conflicting_paths)))
print_status("You can run this installation of the promptflow with '{}'.".format(exec_filepath))
def handle_path_and_tab_completion(exec_filepath, install_dir):
ans_yes = prompt_y_n('Modify profile to update your $PATH now?', 'y')
if ans_yes:
rc_file_path = get_rc_file_path()
if not rc_file_path:
raise CLIInstallError('No suitable profile file found.')
_backup_rc(rc_file_path)
line_to_add = "export PATH=$PATH:{}".format(os.path.join(install_dir, "bin"))
_modify_rc(rc_file_path, line_to_add)
warn_other_azs_on_path(install_dir, exec_filepath)
print_status()
print_status('** Run `exec -l $SHELL` to restart your shell. **')
print_status()
else:
print_status("You can run the promptflow with '{}'.".format(exec_filepath))
def verify_python_version():
print_status('Verifying Python version.')
v = sys.version_info
if v < (3, 8):
raise CLIInstallError('The promptflow does not support Python versions less than 3.9.')
if 'conda' in sys.version:
raise CLIInstallError("This script does not support the Python Anaconda environment. "
"Create an Anaconda virtual environment and install with 'pip'")
print_status('Python version {}.{}.{} okay.'.format(v.major, v.minor, v.micro))
def main():
verify_python_version()
tmp_dir = create_tmp_dir()
install_dir = get_install_dir()
create_virtualenv(install_dir)
install_cli(install_dir, tmp_dir)
exec_filepath = [os.path.join(install_dir, "bin", PF_EXECUTABLE_NAME),
os.path.join(install_dir, "bin", PFAZURE_EXECUTABLE_NAME)]
try:
handle_path_and_tab_completion(exec_filepath, install_dir)
except Exception as e:
print_status("Unable to set up PATH. ERROR: {}".format(str(e)))
shutil.rmtree(tmp_dir)
print_status("Installation successful.")
print_status("Run the CLI with {} --help".format(exec_filepath))
if __name__ == '__main__':
try:
main()
except CLIInstallError as cie:
print('ERROR: '+str(cie), file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
print('\n\nExiting...')
sys.exit(1)
# SIG # Begin signature block
# Op0/tmmNDX4QgQefj28K91e/ClVKWeYaA1w1kb5Hi8ALJZtmvyhwvxYlCRZ9eWT+
# wFBfSvpUIzAWYxEMfVqqWy7g9AzqHGa5vE37zQ7uGkIyR0OsmO0bkauOv5FxuCWX
# U0u9d9sir6sRTb2nrEj2O1EXAcP2xNaW77w1fcOtMX9W6ytHXx/+v5p387+/HBnQ
# y00GvJrrcIJeRj4MboBdDdv0VgGJAAlTJp0hxO0lt5ZQUMJvi/sM4e1cPUTcx7uB
# djCmvSZzZrJO2ZIIWiyiP2XNYWJv4A9klJWMbWKukeZOSjxYPS0pO2mTftkKoL5U
# sOJu9RtRWIIx/kLG/Axliw==
# SIG # End signature block
+4
View File
@@ -0,0 +1,4 @@
**/windows/scripts/build
**/windows/scripts/dist
**/windows/wix
**/windows/out
+35
View File
@@ -0,0 +1,35 @@
# Building the Windows MSI Installer
This document provides instructions on creating the MSI installer.
## Option1: Building with Github Actions
Trigger the [workflow](https://github.com/microsoft/promptflow/actions/workflows/build_msi_installer.yml) manually.
## Option2: Local Building
### Prerequisites
1. Turn on the '.NET Framework 3.5' Windows Feature (required for WIX Toolset).
2. Install 'Microsoft Build Tools 2015'.
https://www.microsoft.com/download/details.aspx?id=48159
3. You need to have curl.exe, unzip.exe and msbuild.exe available under PATH.
4. Install 'WIX Toolset build tools' following the instructions below.
- Enter the directory where the README is located (`cd scripts/installer/windows`), `mkdir wix` and `cd wix`.
- `curl --output wix-archive.zip https://azurecliprod.blob.core.windows.net/msi/wix310-binaries-mirror.zip`
- `unzip wix-archive.zip` and `del wix-archive.zip`
5. We recommend creating a clean virtual Python environment and installing all dependencies using src/promptflow/setup.py.
- `python -m venv venv`
- `venv\Scripts\activate`
- `pip install promptflow[azure,executable,azureml-serving,executor-service] promptflow-tools`
### Building
1. Update the version number `$(env.CLI_VERSION)` and `$(env.FILE_VERSION)` in `product.wxs`, `promptflow.wixproj` and `version_info.txt`.
2. `cd scripts/installer/windows/scripts` and run `python generate_dependency.py`.
3. run `pyinstaller promptflow.spec`.
4. `cd scripts/installer/windows` and Run `msbuild /t:rebuild /p:Configuration=Release /p:Platform=x64 promptflow.wixproj`.
5. The unsigned MSI will be in the `scripts/installer/windows/out` folder.
## Notes
- If you encounter "Access is denied" error when running promptflow. Please follow the [link](https://learn.microsoft.com/en-us/microsoft-365/security/defender-endpoint/attack-surface-reduction-rules-deployment-implement?view=o365-worldwide#customize-attack-surface-reduction-rules) to add the executable to the Windows Defender Attack Surface Reduction (ASR) rule.
Or you can add promptflow installation folder to the Windows Defender exclusion list.
@@ -0,0 +1,101 @@
# Install prompt flow MSI installer on Windows
Prompt flow is a suite of development tools designed to streamline the end-to-end development
cycle of LLM-based AI applications, that can be installed locally on Windows computers.
For Windows, the prompt flow is installed via an MSI, which gives you access to the CLI
through the Windows Command Prompt (CMD) or PowerShell.
## Install or update
The MSI distributable is used for installing or updating the prompt flow on Windows.
You don't need to uninstall current versions before using the MSI installer because
the MSI updates any existing version.
::::{tab-set}
:::{tab-item} Microsoft Installer (MSI)
:sync: Microsoft Installer (MSI)
### Latest version
Download and install the latest release of the prompt flow.
When the installer asks if it can make changes to your computer, select the "Yes" box.
> [Latest release of the promptflow (64-bit)](https://aka.ms/installpromptflowwindowsx64)
)
### Specific version
If you prefer, you can download a specific version of the promptflow by using a URL.
To download the MSI installer for a specific version, change the version segment in URL
https://promptflowartifact.blob.core.windows.net/msi-installer/promptflow-<version>.msi
:::
:::{tab-item} Microsoft Installer (MSI) with PowerShell
:sync: Microsoft Installer (MSI) with PowerShell
### PowerShell
To install the prompt flow using PowerShell, start PowerShell and
run the following command:
```PowerShell
$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest -Uri https://aka.ms/installpromptflowwindowsx64 -OutFile .\promptflow.msi; Start-Process msiexec.exe -Wait -ArgumentList '/I promptflow.msi /quiet'; Remove-Item .\promptflow.msi
```
This will download and install the latest 64-bit installer of the prompt flow for Windows.
To install a specific version, replace the `-Uri` argument with the URL like below.
Here is an example of using the 64-bit installer of the promptflow version 1.0.0 in PowerShell:
```PowerShell
$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest -Uri https://promptflowartifact.blob.core.windows.net/msi-installer/promptflow-1.0.0.msi -OutFile .\promptflow.msi; Start-Process msiexec.exe -Wait -ArgumentList '/I promptflow.msi /quiet'; Remove-Item .\promptflow.msi
```
:::
::::
## Run the prompt flow
You can now run the prompt flow with the `pf` or `pfazure` command from either Windows Command Prompt or PowerShell.
## Upgrade the prompt flow
Beginning with version 1.4.0, the prompt flow provides an in-tool command to upgrade to the latest version.
```commandline
pf upgrade
```
For prompt flow versions prior to 1.4.0, upgrade by reinstalling as described in Install the prompt flow.
## Uninstall
You uninstall the prompt flow from the Windows "Apps and Features" list. To uninstall:
| Platform | Instructions |
|---|---|
| Windows 11 | Start > Settings > Apps > Installed apps |
| Windows 10 | Start > Settings > System > Apps & Features |
| Windows 8 and Windows 7 | Start > Control Panel > Programs > Uninstall a program |
Once on this screen type __promptflow_ into the program search bar.
The program to uninstall is listed as __promptflow (64-bit)__.
Select this application, then select the `Uninstall` button.
## FAQ
### Where is the prompt flow installed?
In Windows, the 64-bit prompt flow installs in `C:\Users\**\AppData\Local\Apps\promptflow` by default.
### What version of the prompt flow is installed?
Type `pf --version` in a terminal window to know what version of the prompt flow is installed.
Your output looks like this:
```output
promptflow x.x.x
Executable '***\python.exe'
Python (Windows) 3.*.* | packaged by conda-forge | *
```
+161
View File
@@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<?define ProductVersion="$(env.CLI_VERSION)" ?>
<?define ProductName = "promptflow" ?>
<?define ProductDescription = "Command-line tools for prompt flow." ?>
<?define ProductAuthor = "Microsoft Corporation" ?>
<?define ProductResources = ".\resources\" ?>
<?define UpgradeCode32 = "8b748161-e07a-48f2-8cdf-401480df4694" ?>
<?if $(var.Platform) = "x64" ?>
<?define PromptflowCliRegistryGuid = "0efd984f-9eec-425b-b230-a3994b69649a" ?>
<?define PromptflowServiceGuid = "d4e99207-77be-4bdf-a430-b08632c5aa2b" ?>
<?define PromptflowSystemPathGuid = "4c321045-d4e0-4446-bda4-8c19eaa42af1" ?>
<?define ProgramFilesFolder = "ProgramFiles64Folder" ?>
<?define RemovePromptflowFolderGuid = "ee843aa5-2b72-4958-be84-53dbac17efc7" ?>
<?define UpgradeCode = "772aa21f-f8d4-4771-b910-1dbce3f1920c" ?>
<?define Architecture = "64-bit" ?>
<?elseif $(var.Platform) = "x86" ?>
<?define PromptflowCliRegistryGuid = "7c2c792d-c395-44a1-8222-8e4ea006abb9" ?>
<?define PromptflowServiceGuid = "f706b208-a15d-4ae7-9185-cfcc43656570" ?>
<?define PromptflowSystemPathGuid = "9661fe6a-ff48-4e7c-a60d-fc34c2d06ef3" ?>
<?define ProgramFilesFolder = "ProgramFilesFolder" ?>
<?define RemovePromptflowFolderGuid = "588ca5e1-38c6-4659-8b38-762df7ed5b28" ?>
<?define UpgradeCode = $(var.UpgradeCode32) ?>
<?define Architecture = "32-bit" ?>
<?else ?>
<?error Unsupported platform "$(var.Platform)" ?>
<?endif ?>
<Product Id="*" Name="$(var.ProductName) ($(var.Architecture))" Language="1033" Version="$(var.ProductVersion)" Manufacturer="$(var.ProductAuthor)" UpgradeCode="$(var.UpgradeCode)">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perUser" />
<Upgrade Id="$(var.UpgradeCode)">
<UpgradeVersion Property="WIX_UPGRADE_DETECTED" Maximum="$(var.ProductVersion)" IncludeMaximum="no" MigrateFeatures="yes" />
<UpgradeVersion Property="WIX_DOWNGRADE_DETECTED" Minimum="$(var.ProductVersion)" IncludeMinimum="no" OnlyDetect="yes" />
</Upgrade>
<InstallExecuteSequence>
<RemoveExistingProducts After="InstallExecute" />
</InstallExecuteSequence>
<!-- New product architectures should upgrade the original x86 product - even of the same version. -->
<?if $(var.UpgradeCode) != $(var.UpgradeCode32) ?>
<Upgrade Id="$(var.UpgradeCode32)">
<UpgradeVersion Property="WIX_X86_UPGRADE_DETECTED" Maximum="$(var.ProductVersion)" IncludeMaximum="yes" MigrateFeatures="yes" />
<UpgradeVersion Property="WIX_X86_DOWNGRADE_DETECTED" Minimum="$(var.ProductVersion)" IncludeMinimum="no" OnlyDetect="yes" />
</Upgrade>
<Condition Message="A newer version of $(var.ProductName) is already installed.">NOT (WIX_DOWNGRADE_DETECTED OR WIX_X86_DOWNGRADE_DETECTED)</Condition>
<?else ?>
<Condition Message="A newer version of $(var.ProductName) is already installed.">NOT WIX_DOWNGRADE_DETECTED</Condition>
<?endif ?>
<Media Id="1" Cabinet="promptflow.cab" EmbedCab="yes" CompressionLevel="high" />
<Icon Id="PromptflowIcon" SourceFile="$(var.ProductResources)logo32.ico" />
<Property Id="ARPPRODUCTICON" Value="PromptflowIcon" />
<Property Id="ARPHELPLINK" Value="https://microsoft.github.io/promptflow/how-to-guides/quick-start.html" />
<Property Id="ARPURLINFOABOUT" Value="https://microsoft.github.io/promptflow/how-to-guides/quick-start.html" />
<Property Id="ARPURLUPDATEINFO" Value="https://microsoft.github.io/promptflow/how-to-guides/quick-start.html" />
<Property Id="MSIFASTINSTALL" Value="7" />
<Property Id="ApplicationFolderName" Value="promptflow" />
<Property Id="WixAppFolder" Value="WixPerUserFolder" />
<Feature Id="ProductFeature" Title="promptflow" Level="1" AllowAdvertise="no">
<ComponentGroupRef Id="ProductComponents" />
</Feature>
<!--Custom action to propagate path env variable change-->
<CustomActionRef Id="WixBroadcastEnvironmentChange" />
<!-- User Interface -->
<WixVariable Id="WixUILicenseRtf" Value="$(var.ProductResources)CLI_LICENSE.rtf"/>
<UIRef Id="WixUI_ErrorProgressText"/>
<!-- Show message to restart any terminals only if the PATH is changed -->
<CustomAction Id="Set_WIXUI_EXITDIALOGOPTIONALTEXT" Property="WIXUI_EXITDIALOGOPTIONALTEXT" Value="Please close and reopen any active terminal window to use prompt flow." />
<InstallUISequence>
<Custom Action="Set_WIXUI_EXITDIALOGOPTIONALTEXT" After="CostFinalize">NOT Installed AND NOT WIX_UPGRADE_DETECTED</Custom>
</InstallUISequence>
<Property Id="WixQuietExec64CmdLine" Value="&quot;[APPLICATIONFOLDER]pfcli.exe&quot; pf service stop"/>
<CustomAction Id="StopPromptFlowService"
Execute="immediate"
Return="ignore"
BinaryKey="WixCA"
DllEntry="WixQuietExec64"
Impersonate="yes"/>
<CustomAction Id="StartPromptFlowService"
Directory="APPLICATIONFOLDER"
Execute="deferred"
ExeCommand="wscript.exe promptflow_service.vbs"
Return="asyncNoWait" />
<InstallExecuteSequence>
<Custom Action="StopPromptFlowService" Before="InstallInitialize">Installed OR WIX_UPGRADE_DETECTED</Custom>
<Custom Action="StartPromptFlowService" Before="InstallFinalize">NOT Installed OR WIX_UPGRADE_DETECTED</Custom>
</InstallExecuteSequence>
</Product>
<Fragment>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="$(var.ProgramFilesFolder)">
<Directory Id="APPLICATIONFOLDER" Name="promptflow" />
</Directory>
<Directory Id="StartupFolder" />
</Directory>
<UIRef Id="WixUI_Advanced" />
</Fragment>
<Fragment>
<ComponentGroup Id="PromptflowCliSettingsGroup">
<Component Id="RemovePromptflowFolder" Directory="APPLICATIONFOLDER" Guid="$(var.RemovePromptflowFolderGuid)">
<RemoveFolder Id="APPLICATIONFOLDER" On="uninstall" />
</Component>
<Component Id="PromptflowSystemPath" Directory="APPLICATIONFOLDER" Guid="$(var.PromptflowSystemPathGuid)">
<Environment Id="PromptflowAddedToPATH"
Name="PATH"
Value="[APPLICATIONFOLDER]"
Permanent="no"
Part="first"
Action="set"
System="no" />
<CreateFolder />
</Component>
<Component Id="promptflow_service.vbs" Directory="APPLICATIONFOLDER" Guid="$(var.PromptflowServiceGuid)">
<File Id="promptflow_service.vbs" Source="scripts\promptflow_service.vbs" KeyPath="yes" Checksum="yes"/>
</Component>
<Component Id="ApplicationShortcut" Directory="StartupFolder" Guid="$(var.PromptflowCliRegistryGuid)">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="Prompt flow service"
Description="Prompt Flow Service"
Target="[#promptflow_service.vbs]"
WorkingDirectory="APPLICATIONFOLDER"
Advertise="no">
<Icon Id="PromptflowServiceIcon" SourceFile="$(var.ProductResources)logo32.ico" />
</Shortcut>
<RemoveFile Id="CleanUpShortCut" Directory="StartupFolder" Name="Prompt flow service" On="uninstall"/>
<RegistryKey Root="HKCU" Key="Software\Microsoft\$(var.ProductName)" Action="createAndRemoveOnUninstall" ForceDeleteOnUninstall="yes">
<RegistryValue Name="installed" Type="integer" Value="1" />
<RegistryValue Name="version" Type="string" Value="$(var.ProductVersion)" KeyPath="yes"/>
</RegistryKey>
</Component>
</ComponentGroup>
<ComponentGroup Id="ProductComponents">
<ComponentGroupRef Id="PromptflowCliComponentGroup"/>
<ComponentGroupRef Id="PromptflowCliSettingsGroup"/>
</ComponentGroup>
</Fragment>
</Wix>
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- Project -->
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">x86</Platform>
<ProductVersion>3.10</ProductVersion>
<ProjectGuid>04ff6707-750d-4474-89b3-7922c84721be</ProjectGuid>
<SchemaVersion>2.0</SchemaVersion>
<OutputName>promptflow-$(env.CLI_VERSION)</OutputName>
<OutputType>Package</OutputType>
<WixTargetsPath Condition=" '$(WixTargetsPath)' == '' AND '$(MSBuildExtensionsPath32)' != '' ">$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\Wix.targets</WixTargetsPath>
<WixTargetsPath Condition=" '$(WixTargetsPath)' == '' ">$(MSBuildExtensionsPath)\Microsoft\WiX\v3.x\Wix.targets</WixTargetsPath>
</PropertyGroup>
<!-- Local WiX -->
<PropertyGroup>
<LocalWixRoot>wix</LocalWixRoot>
<WixToolPath>$(MSBuildThisFileDirectory)$(LocalWixRoot)</WixToolPath>
<WixTargetsPath Condition="Exists('$(WixToolPath)\Wix.targets')">$(WixToolPath)\Wix.targets</WixTargetsPath>
<WixTasksPath Condition="Exists('$(WixToolPath)\wixtasks.dll')">$(WixToolPath)\wixtasks.dll</WixTasksPath>
<PromptflowSource>scripts\dist\promptflow</PromptflowSource>
<LinkerAdditionalOptions>-fv</LinkerAdditionalOptions>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x86' ">
<OutputPath>out\$(Configuration)\</OutputPath>
<IntermediateOutputPath>out\obj\$(Configuration)\</IntermediateOutputPath>
<DefineConstants>Debug;PromptflowSource=$(PromptflowSource)</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x86' ">
<OutputPath>out\</OutputPath>
<IntermediateOutputPath>out\obj\$(Configuration)\</IntermediateOutputPath>
<DefineConstants>PromptflowSource=$(PromptflowSource)</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|x64' ">
<OutputPath>out\$(Configuration)\</OutputPath>
<IntermediateOutputPath>out\obj\$(Configuration)\</IntermediateOutputPath>
<DefineConstants>Debug;PromptflowSource=$(PromptflowSource)</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|x64' ">
<OutputPath>out\</OutputPath>
<IntermediateOutputPath>out\obj\$(Configuration)\</IntermediateOutputPath>
<DefineConstants>PromptflowSource=$(PromptflowSource)</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="out\promptflow.wxs">
<Link>promptflow.wxs</Link>
</Compile>
<Compile Include="product.wxs" />
</ItemGroup>
<ItemGroup>
<None Include=".\resources\logo_pf.png" />
</ItemGroup>
<!-- UI -->
<ItemGroup>
<WixExtension Include="WixUIExtension">
<HintPath>$(WixExtDir)\WixUIExtension.dll</HintPath>
<Name>WixUIExtension</Name>
</WixExtension>
<WixExtension Include="WixUtilExtension">
<HintPath>$(WixExtDir)\WixUtilExtension.dll</HintPath>
<Name>WixUtilExtension</Name>
</WixExtension>
</ItemGroup>
<Import Project="$(WixTargetsPath)" Condition=" '$(WixTargetsPath)' != '' " />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\wix.targets" Condition=" '$(WixTargetsPath)' == '' AND Exists('$(MSBuildExtensionsPath32)\Microsoft\WiX\v3.x\wix.targets') " />
<Target Name="EnsureWixToolsetInstalled" Condition=" '$(WixTargetsImported)' != 'true' ">
<Error Text="The WiX Toolset v3.10 build tools must be installed to build this project. To download the WiX Toolset, see https://wixtoolset.org/releases/v3.10/stable" />
</Target>
<Target Name="BeforeBuild">
<HeatDirectory Directory="$(PromptflowSource)" ToolPath="$(WixToolPath)" AutogenerateGuids="true" ComponentGroupName="PromptflowCliComponentGroup" SuppressRootDirectory="true" DirectoryRefId="APPLICATIONFOLDER" OutputFile="out\promptflow.wxs" PreprocessorVariable="var.PromptflowSource" />
</Target>
</Project>
@@ -0,0 +1,120 @@
{\rtf1\ansi\ansicpg1252\cocoartf1504\cocoasubrtf820
{\fonttbl\f0\fnil\fcharset0 Tahoma;\f1\froman\fcharset0 TimesNewRomanPSMT;\f2\ftech\fcharset77 Symbol;
}
{\colortbl;\red255\green255\blue255;\red0\green0\blue255;}
{\*\expandedcolortbl;;\csgenericrgb\c0\c0\c100000;}
{\*\listtable{\list\listtemplateid1\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}.}{\leveltext\leveltemplateid1\'02\'00.;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid1}
{\list\listtemplateid2\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}.}{\leveltext\leveltemplateid101\'02\'00.;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid2}
{\list\listtemplateid3\listhybrid{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{lower-alpha\}.}{\leveltext\leveltemplateid201\'02\'00.;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid3}
{\list\listtemplateid4\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}.}{\leveltext\leveltemplateid301\'02\'00.;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid4}
{\list\listtemplateid5\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{disc\}}{\leveltext\leveltemplateid401\'01\uc0\u8226 ;}{\levelnumbers;}\fi-360\li720\lin720 }{\listname ;}\listid5}
{\list\listtemplateid6\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}.}{\leveltext\leveltemplateid501\'02\'00.;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid6}
{\list\listtemplateid7\listhybrid{\listlevel\levelnfc4\levelnfcn4\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{lower-alpha\}.}{\leveltext\leveltemplateid601\'02\'00.;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid7}
{\list\listtemplateid8\listhybrid{\listlevel\levelnfc0\levelnfcn0\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace360\levelindent0{\*\levelmarker \{decimal\}.}{\leveltext\leveltemplateid701\'02\'00.;}{\levelnumbers\'01;}\fi-360\li720\lin720 }{\listname ;}\listid8}}
{\*\listoverridetable{\listoverride\listid1\listoverridecount0\ls1}{\listoverride\listid2\listoverridecount0\ls2}{\listoverride\listid3\listoverridecount0\ls3}{\listoverride\listid4\listoverridecount0\ls4}{\listoverride\listid5\listoverridecount0\ls5}{\listoverride\listid6\listoverridecount0\ls6}{\listoverride\listid7\listoverridecount0\ls7}{\listoverride\listid8\listoverridecount0\ls8}}
\margl1440\margr1440\vieww10800\viewh8400\viewkind0
\deftab720
\pard\pardeftab720\ri0\sb120\sa120\partightenfactor0
\f0\b\fs20 \cf0 MICROSOFT SOFTWARE LICENSE TERMS\
Microsoft prompt flow
\f1 \
\pard\pardeftab720\ri0\sb120\sa120\partightenfactor0
\f0\b0 \cf0 These license terms are an agreement between Microsoft Corporation (or based on where you live, one of its affiliates) and you. They apply to the software named above. The terms also apply to any Microsoft services or updates for the software, except to the extent those have different terms.
\f1 \
\pard\pardeftab720\ri0\sb120\sa120\partightenfactor0
\f0\b \cf0 IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW.
\f1 \
\pard\tx360\pardeftab720\li357\fi-357\ri0\sb120\sa120\partightenfactor0
\ls1\ilvl0
\f0 \cf0 1. INSTALLATION AND USE RIGHTS.
\f1\b0 \
\pard\pardeftab720\li357\ri0\sb120\sa120\partightenfactor0
\f0 \cf0 You may install and use any number of copies of the software.\
\pard\tx450\pardeftab720\li447\fi-357\ri0\sb120\sa120\partightenfactor0
\ls2\ilvl0
\b \cf0 2. TERMS FOR SPECIFIC COMPONENTS
\f1 .\
\pard\tx4950\pardeftab720\li720\fi-270\ri0\sb120\sa120\partightenfactor0
\ls3\ilvl0
\f0 \cf0 a. Third Party Components
\f1 .
\f0\b0 The software may include third party components with separate legal notices or governed by other agreements, as may be described in the ThirdPartyNotices file(s) accompanying the software. Even if such components are governed by other agreements, the disclaimers and the limitations on and exclusions of damages below also apply.
\f1 \
\pard\tx450\pardeftab720\li450\fi-357\ri0\sb120\sa120\partightenfactor0
\ls4\ilvl0
\f0\b \cf0 3. DATA.
\b0 The software may collect information about you and your use of the software, and send that to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may opt-out of many of these scenarios, but not all, as described in the product documentation. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications and you should provide a copy of Microsoft\'92s privacy statement to your users. The Microsoft privacy statement is located here {\field{\*\fldinst{HYPERLINK "https://go.microsoft.com/fwlink/?LinkID=824704"}}{\fldrslt \cf2 \ul \ulc2 https://go.microsoft.com/fwlink/?LinkID=824704}}. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.\
\pard\tx360\pardeftab720\li357\fi-357\ri0\sb120\sa120\partightenfactor0
\ls4\ilvl0
\b \cf0 4. SCOPE OF LICENSE.
\b0 The software is licensed, not sold. This agreement only gives you some rights to use the software. Microsoft reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the software only as expressly permitted in this agreement. In doing so, you must comply with any technical limitations in the software that only allow you to use it in certain ways. You may not\
\pard\tx720\pardeftab720\li720\fi-363\ri0\sb120\sa120\partightenfactor0
\ls5\ilvl0
\f2 \cf0 \'a5
\f0 work around any technical limitations in the software;\
\ls5\ilvl0
\f2 \'a5
\f0 reverse engineer, decompile or disassemble the software, or otherwise attempt to derive the source code for the software except, and only to the extent required by third party licensing terms governing the use of certain open source components that may be included in the software;\
\ls5\ilvl0
\f2 \'a5
\f0 remove, minimize, block or modify any notices of Microsoft or its suppliers in the software; \
\ls5\ilvl0
\f2 \'a5
\f0 use the software in any way that is against the law; or\
\ls5\ilvl0
\f2 \'a5
\f0 share, publish, rent or lease the software, or provide the software as a stand-alone hosted as solution for others to use, or transfer the software or this agreement to any third party.\
\pard\tx360\pardeftab720\li357\fi-267\ri0\sb120\sa120\partightenfactor0
\ls6\ilvl0
\b \cf0 5. EXPORT RESTRICTIONS.
\b0 You must comply with all domestic and international export laws and regulations that apply to the software, which include restrictions on destinations, end users, and end use. For further information on export restrictions, visit {\field{\*\fldinst{HYPERLINK "http://www.microsoft.com/exporting"}}{\fldrslt \cf2 \ul \ulc2 www.microsoft.com/exporting}}.
\f1 \cf2 \ul \ulc2 \
\pard\tx450\pardeftab720\li447\fi-357\ri0\sb120\sa120\partightenfactor0
\ls6\ilvl0
\f0\b \cf0 \ulnone 6. SUPPORT SERVICES.
\b0 Because this software is \'93as is,\'94 we may not provide support services for it.\
\ls6\ilvl0
\b 7. ENTIRE AGREEMENT.
\b0 This agreement, and the terms for supplements, updates, Internet-based services and support services that you use, are the entire agreement for the software and support services.\
\ls6\ilvl0
\b 8. APPLICABLE LAW.
\b0 If you acquired the software in the United States, Washington law applies to interpretation of and claims for breach of this agreement, and the laws of the state where you live apply to all other claims. If you acquired the software in any other country, its laws apply.
\f1\b \
\ls6\ilvl0
\f0 9. CONSUMER RIGHTS; REGIONAL VARIATIONS.
\b0 This agreement describes certain legal rights. You may have other rights, including consumer rights, under the laws of your state or country. Separate and apart from your relationship with Microsoft, you may also have rights with respect to the party from which you acquired the software. This agreement does not change those other rights if the laws of your state or country do not permit it to do so. For example, if you acquired the software in one of the below regions, or mandatory country law applies, then the following provisions apply to you:\
\pard\pardeftab720\li720\fi-270\ri0\sb120\sa120\partightenfactor0
\ls7\ilvl0
\b \cf0 b. Australia.
\b0 You have statutory guarantees under the Australian Consumer Law and nothing in this agreement is intended to affect those rights.\
\pard\pardeftab720\li717\fi-267\ri0\sb120\sa120\partightenfactor0
\ls7\ilvl0
\b \cf0 c. Canada.
\b0 If you acquired this software in Canada, you may stop receiving updates by turning off the automatic update feature, disconnecting your device from the Internet (if and when you re-connect to the Internet, however, the software will resume checking for and installing updates), or uninstalling the software. The product documentation, if any, may also specify how to turn off updates for your specific device or software.\
\ls7\ilvl0
\b d. Germany and Austria
\f1\b0 .\
\pard\pardeftab720\li717\ri0\sb120\sa120\partightenfactor0
\f0\b \cf0 (i)
\f1\b0
\f0\b Warranty
\b0 . The properly licensed software will perform substantially as described in any Microsoft materials that accompany the software. However, Microsoft gives no contractual guarantee in relation to the licensed software.\
\b (ii)
\f1\b0
\f0\b Limitation of Liability
\b0 . In case of intentional conduct, gross negligence, claims based on the Product Liability Act, as well as, in case of death or personal or physical injury, Microsoft is liable according to the statutory law.\
Subject to the foregoing clause (ii), Microsoft will only be liable for slight negligence if Microsoft is in breach of such material contractual obligations, the fulfillment of which facilitate the due performance of this agreement, the breach of which would endanger the purpose of this agreement and the compliance with which a party may constantly trust in (so-called "cardinal obligations"). In other cases of slight negligence, Microsoft will not be liable for slight negligence.\
\pard\tx450\pardeftab720\li447\fi-357\ri0\sb120\sa120\partightenfactor0
\ls8\ilvl0
\b \cf0 10. DISCLAIMER OF WARRANTY. THE SOFTWARE IS LICENSED \'93AS-IS.\'94 YOU BEAR THE RISK OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES OR CONDITIONS. TO THE EXTENT PERMITTED UNDER YOUR LOCAL LAWS, MICROSOFT EXCLUDES THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT.\
11. LIMITATION ON AND EXCLUSION OF DAMAGES. YOU CAN RECOVER FROM MICROSOFT AND ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR INCIDENTAL DAMAGES.\
\pard\pardeftab720\li450\ri0\sb120\sa120\partightenfactor0
\b0 \cf0 This limitation applies to (a) anything related to the software, services, content (including code) on third party Internet sites, or third party applications; and (b) claims for breach of contract, breach of warranty, guarantee or condition, strict liability, negligence, or other tort to the extent permitted by applicable law.\
It also applies even if Microsoft knew or should have known about the possibility of the damages. The above limitation or exclusion may not apply to you because your country may not allow the exclusion or limitation of incidental, consequential or other damages.}
Binary file not shown.

After

Width:  |  Height:  |  Size: 975 B

@@ -0,0 +1,142 @@
import ast
import re
import subprocess
import copy
from pip._vendor import tomli as toml
from pathlib import Path
from promptflow._sdk._utilities.general_utils import render_jinja_template
def get_git_base_dir():
return Path(
subprocess.run(['git', 'rev-parse', '--show-toplevel'], stdout=subprocess.PIPE)
.stdout.decode('utf-8').strip())
def is_tool(name):
"""Check whether `name` is on PATH and marked as executable."""
# from whichcraft import which
from shutil import which
return which(name) is not None
def extract_requirements(file_path):
with open(file_path, 'r') as file:
tree = ast.parse(file.read())
install_requires = []
extras_requires = {}
for node in ast.walk(tree):
if isinstance(node, ast.Assign) and node.targets[0].id == 'REQUIRES':
install_requires = [elt.s for elt in node.value.elts]
elif isinstance(node, ast.Call) and getattr(node.func, 'id', None) == 'setup':
for keyword in node.keywords:
if keyword.arg == 'extras_require':
extras_requires = ast.literal_eval(keyword.value)
return install_requires, extras_requires
def extract_package_names(packages):
package_names = []
for package in packages:
match = re.match(r'^([a-zA-Z0-9-_.]+)', package)
if match:
package_names.append(match.group(1))
return package_names
def get_toml_dependencies(packages):
file_list = ["promptflow-tracing", "promptflow-core", "promptflow-devkit", "promptflow-azure"]
dependencies = []
for package in packages:
if package in file_list:
with open(get_git_base_dir() / "src" / package / "pyproject.toml", 'rb') as file:
data = toml.load(file)
extra_package_names = data.get('tool', {}).get('poetry', {}).get('dependencies', {})
dependencies.extend(extra_package_names.keys())
# hard-code promptflow-evals dependency here since it's not added in promptflow setup for now
with open(get_git_base_dir() / "src" / "promptflow-evals" / "pyproject.toml", 'rb') as file:
data = toml.load(file)
extra_package_names = data.get('tool', {}).get('poetry', {}).get('dependencies', {})
dependencies.extend(extra_package_names.keys())
dependencies = [dependency for dependency in dependencies
if not dependency.startswith('promptflow') and not dependency == 'python']
return dependencies
def get_package_dependencies(package_name_list):
dependencies = []
for package_name in package_name_list:
if (is_tool('conda')):
result = subprocess.run('conda activate root | pip show {}'.format(package_name),
shell=True, stdout=subprocess.PIPE)
else:
result = subprocess.run(['pip', 'show', package_name], stdout=subprocess.PIPE)
print("---" + package_name)
print(result.stdout)
lines = result.stdout.decode('utf-8', errors="ignore").splitlines()
for line in lines:
if line.startswith('Requires'):
dependency = line.split(': ')[1].split(', ')
if dependency != ['']:
dependencies.extend(dependency)
break
dependencies = [dependency for dependency in dependencies if not dependency.startswith('promptflow')]
return dependencies
if __name__ == '__main__':
dependencies = []
install_requires, extras_requires = extract_requirements(get_git_base_dir() / 'src/promptflow/setup.py')
install_requires_names = extract_package_names(install_requires)
dependencies.extend(install_requires_names)
for key in extras_requires:
extras_require_names = extract_package_names(extras_requires[key])
dependencies.extend(extras_require_names)
# get toml dependencies
dependencies = list(set(dependencies))
direct_package_dependencies = get_toml_dependencies(dependencies)
# get one step furture for dependencies
dependencies = list(set(direct_package_dependencies))
direct_package_dependencies = get_package_dependencies(dependencies)
# get all dependencies
all_packages = list(set(dependencies) | set(direct_package_dependencies))
# remove all packages starting with promptflow
all_packages = [package for package in all_packages if not package.startswith('promptflow')]
hidden_imports = copy.deepcopy(all_packages)
meta_packages = copy.deepcopy(all_packages)
special_packages = ["streamlit-quill", "flask-cors", "flask-restx"]
for i in range(len(hidden_imports)):
# need special handeling because it use _ to import
if hidden_imports[i] in special_packages:
hidden_imports[i] = hidden_imports[i].replace('-', '_').lower()
else:
hidden_imports[i] = hidden_imports[i].replace('-', '.').lower()
hidden_imports.remove("azure.storage.file.share")
hidden_imports.append("azure.storage.fileshare")
hidden_imports.remove("azure.storage.file.datalake")
hidden_imports.append("azure.storage.filedatalake")
render_context = {
"hidden_imports": hidden_imports,
"all_packages": all_packages,
"meta_packages": meta_packages,
}
# always use unix line ending
Path("./promptflow.spec").write_bytes(
render_jinja_template(
get_git_base_dir() / "scripts/installer/windows/scripts/promptflow.spec.jinja2", **render_context)
.encode("utf-8")
.replace(b"\r\n", b"\n"),)
+19
View File
@@ -0,0 +1,19 @@
@echo off
setlocal
SET PF_INSTALLER=MSI
set MAIN_EXE=%~dp0.\pfcli.exe
REM Check if the first argument is 'start'
if "%~1"=="service" (
REM Check if the second argument is 'start'
if "%~2"=="start" (
cscript //nologo %~dp0.\start_pfs.vbs """%MAIN_EXE%"" pf %*"
REM since we won't wait for vbs to finish, we need to wait for the output file to be flushed to disk
timeout /t 5 >nul
type "%~dp0output.txt"
) else (
"%MAIN_EXE%" pf %*
)
) else (
"%MAIN_EXE%" pf %*
)
@@ -0,0 +1,5 @@
@echo off
setlocal
set MAIN_EXE=%~dp0.\pfcli.exe
"%MAIN_EXE%" pfazure %*
@@ -0,0 +1,17 @@
import sys
import multiprocessing
# use this file as the only entry point for the CLI to avoid packaging the same environment repeatedly
if __name__ == "__main__":
multiprocessing.freeze_support()
command = sys.argv[1] if len(sys.argv) > 1 else None
sys.argv = sys.argv[1:]
if command == 'pf':
from promptflow._cli._pf.entry import main as pf_main
pf_main()
elif command == 'pfazure':
from promptflow.azure._cli.entry import main as pfazure_main
pfazure_main()
else:
print(f"Invalid command {sys.argv}. Please use 'pf', 'pfazure'.")
@@ -0,0 +1,82 @@
# -*- mode: python ; coding: utf-8 -*-
from PyInstaller.utils.hooks import collect_data_files, collect_all, copy_metadata
datas = [('../resources/CLI_LICENSE.rtf', '.'), ('../../../../src/promptflow/NOTICE.txt', '.'),
('../../../../src/promptflow-devkit/promptflow/_sdk/data/executable/', './promptflow/_sdk/data/executable/'),
('../../../../src/promptflow-tools/promptflow/tools/', './promptflow/tools/'),
('./pf.bat', '.'), ('./pfazure.bat', '.'), ('./start_pfs.vbs', '.')]
all_packages = {{all_packages}}
meta_packages = {{meta_packages}}
for package in all_packages:
datas += collect_data_files(package)
for package in meta_packages:
datas += copy_metadata(package)
opentelemetry_datas, opentelemetry_binaries, opentelemetry_hiddenimports = collect_all('opentelemetry')
promptflow_datas, promptflow_binaries, promptflow_hiddenimports = collect_all('promptflow')
datas += opentelemetry_datas
datas += promptflow_datas
datas += collect_data_files('streamlit_quill')
hidden_imports = ['win32timezone', 'promptflow', 'opentelemetry.context.contextvars_context', 'streamlit.runtime.scriptrunner.magic_funcs'] + {{hidden_imports}}
hidden_imports += opentelemetry_hiddenimports
hidden_imports += promptflow_hiddenimports
binaries = []
binaries += opentelemetry_binaries
binaries += promptflow_binaries
block_cipher = None
pfcli_a = Analysis(
['pfcli.py'],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hidden_imports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False,
)
pfcli_pyz = PYZ(pfcli_a.pure, pfcli_a.zipped_data, cipher=block_cipher)
pfcli_exe = EXE(
pfcli_pyz,
pfcli_a.scripts,
[],
exclude_binaries=True,
name='pfcli',
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,
contents_directory='.',
icon='../resources/logo32.ico',
version="./version_info.txt",
)
coll = COLLECT(
pfcli_exe,
pfcli_a.binaries,
pfcli_a.zipfiles,
pfcli_a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='promptflow',
)
@@ -0,0 +1,3 @@
DIM objshell
set objshell = wscript.createobject("wscript.shell")
iReturn = objshell.run("pfcli.exe pf service start --force", 0, true)
@@ -0,0 +1,4 @@
DIM objshell
set objshell = wscript.createobject("wscript.shell")
cmd = WScript.Arguments(0)
iReturn = objshell.run(cmd, 0, false)
@@ -0,0 +1,42 @@
# UTF-8
#
# For more details about fixed file info 'ffi' see:
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
VSVersionInfo(
ffi=FixedFileInfo(
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0.
filevers=($(env.FILE_VERSION)),
prodvers=(1, 0, 0, 0),
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
flags=0x0,
# The operating system for which this file was designed.
# 0x4 - NT and there is no need to change it.
OS=0x4,
# The general type of file.
# 0x1 - the file is an application.
fileType=0x1,
# The function of the file.
# 0x0 - the function is not defined for this fileType
subtype=0x0,
# Creation date and time stamp.
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
'040904E4',
[StringStruct('CompanyName', 'Microsoft Corporation'),
StringStruct('FileDescription', 'Microsoft prompt flow'),
StringStruct('FileVersion', '1.0.0.0'),
StringStruct('InternalName', 'setup'),
StringStruct('LegalCopyright', 'Copyright (c) Microsoft Corporation. All rights reserved.'),
StringStruct('ProductName', 'Microsoft prompt flow'),
StringStruct('ProductVersion', '$(env.CLI_VERSION)')])
]),
VarFileInfo([VarStruct('Translation', [1033, 1252])])
]
)
+67
View File
@@ -0,0 +1,67 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"EagerFlowSchema": {
"properties": {
"additional_includes": {
"title": "additional_includes",
"type": "array",
"items": {
"title": "additional_includes",
"type": "string"
}
},
"description": {
"title": "description",
"type": "string"
},
"display_name": {
"title": "display_name",
"type": "string"
},
"entry": {
"title": "entry",
"type": "string"
},
"environment": {
"title": "environment",
"type": "object",
"additionalProperties": {}
},
"language": {
"title": "language",
"type": "string"
},
"$schema": {
"title": "$schema",
"type": "string",
"readOnly": true
},
"tags": {
"title": "tags",
"type": "object",
"additionalProperties": {
"title": "tags",
"type": "string"
}
},
"type": {
"title": "type",
"type": "string",
"enum": [
"standard",
"evaluation",
"chat"
],
"enumNames": []
}
},
"type": "object",
"required": [
"entry"
],
"additionalProperties": false
}
},
"$ref": "#/definitions/EagerFlowSchema"
}
+204
View File
@@ -0,0 +1,204 @@
{
"type": "object",
"oneOf": [
{
"properties": {
"additional_includes": {
"title": "additional_includes",
"type": "array",
"items": {
"title": "additional_includes",
"type": "string"
}
},
"description": {
"title": "description",
"type": "string"
},
"display_name": {
"title": "display_name",
"type": "string"
},
"environment": {
"title": "environment",
"type": "object",
"additionalProperties": {}
},
"environment_variables": {
"title": "environment_variables",
"type": "object",
"additionalProperties": {}
},
"inputs": {
"title": "inputs",
"type": "object",
"additionalProperties": {
"type": "object",
"$ref": "#/definitions/FlowInputSchema"
}
},
"language": {
"title": "language",
"type": "string",
"default": "python",
"enum": [
"python",
"csharp"
],
"enumNames": []
},
"node_variants": {
"title": "node_variants",
"type": "object",
"additionalProperties": {
"title": "node_variants",
"type": "object",
"additionalProperties": {}
}
},
"nodes": {
"title": "nodes",
"type": "array",
"items": {
"title": "nodes",
"type": "object",
"additionalProperties": {}
}
},
"outputs": {
"title": "outputs",
"type": "object",
"additionalProperties": {
"type": "object",
"$ref": "#/definitions/FlowOutputSchema"
}
},
"$schema": {
"title": "$schema",
"type": "string",
"readOnly": true
},
"tags": {
"title": "tags",
"type": "object",
"additionalProperties": {
"title": "tags",
"type": "string"
}
},
"type": {
"title": "type",
"type": "string",
"enum": [
"standard",
"evaluation",
"chat"
],
"enumNames": []
}
},
"type": "object",
"additionalProperties": false
},
{
"properties": {
"additional_includes": {
"title": "additional_includes",
"type": "array",
"items": {
"title": "additional_includes",
"type": "string"
}
},
"description": {
"title": "description",
"type": "string"
},
"display_name": {
"title": "display_name",
"type": "string"
},
"entry": {
"title": "entry",
"type": "string"
},
"environment": {
"title": "environment",
"type": "object",
"additionalProperties": {}
},
"environment_variables": {
"title": "environment_variables",
"type": "object",
"additionalProperties": {}
},
"init": {
"title": "init",
"type": "object",
"additionalProperties": {
"type": "object",
"$ref": "#/definitions/FlexFlowInitSchema"
}
},
"inputs": {
"title": "inputs",
"type": "object",
"additionalProperties": {
"type": "object",
"$ref": "#/definitions/FlexFlowInputSchema"
}
},
"language": {
"title": "language",
"type": "string",
"default": "python",
"enum": [
"python",
"csharp"
],
"enumNames": []
},
"outputs": {
"title": "outputs",
"type": "object",
"additionalProperties": {
"type": "object",
"$ref": "#/definitions/FlexFlowOutputSchema"
}
},
"sample": {
"title": "sample",
"type": "string"
},
"$schema": {
"title": "$schema",
"type": "string",
"readOnly": true
},
"tags": {
"title": "tags",
"type": "object",
"additionalProperties": {
"title": "tags",
"type": "string"
}
},
"type": {
"title": "type",
"type": "string",
"enum": [
"standard",
"evaluation",
"chat"
],
"enumNames": []
}
},
"type": "object",
"required": [
"entry"
],
"additionalProperties": false
}
]
}
+209
View File
@@ -0,0 +1,209 @@
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------
# flake8: noqa
# This file is part of scripts\generate_json_schema.py in sdk-cli-v2, which is used to generate json schema
# To use this script, run `python <this_file>` in promptflow env,
# and the json schema will be generated in the same folder.
import argparse
from inspect import isclass, getmembers
import json
import sys
from azure.ai.ml._schema import ExperimentalField
from promptflow._sdk.schemas._base import YamlFileSchema
from promptflow._sdk.schemas._fields import UnionField
from marshmallow import Schema, fields, missing
from marshmallow.class_registry import get_class
from marshmallow_jsonschema import JSONSchema
class PatchedJSONSchema(JSONSchema):
required = fields.Method("get_required")
properties = fields.Method("get_properties")
def __init__(self, *args, **kwargs):
"""Setup internal cache of nested fields, to prevent recursion.
:param bool props_ordered: if `True` order of properties will be save as declare in class,
else will using sorting, default is `False`.
Note: For the marshmallow scheme, also need to enable
ordering of fields too (via `class Meta`, attribute `ordered`).
"""
self._nested_schema_classes = {}
self.nested = kwargs.pop("nested", False)
self.props_ordered = kwargs.pop("props_ordered", False)
setattr(self.opts, "ordered", self.props_ordered)
super().__init__(*args, **kwargs)
# cspell: ignore pytype
def _from_python_type(self, obj, field, pytype):
metadata = field.metadata.get("metadata", {})
metadata.update(field.metadata)
# This is in the upcoming release of marshmallow-jsonschema, but not available yet
if isinstance(field, fields.Dict):
values = metadata.get("values", None) or field.value_field
json_schema = {"title": field.attribute or field.data_key or field.name}
json_schema["type"] = "object"
if values:
values.parent = field
json_schema["additionalProperties"] = self._get_schema_for_field(obj, values) if values else {}
return json_schema
if isinstance(field, fields.Raw):
json_schema = {"title": field.attribute or field.data_key or field.name}
return json_schema
return super()._from_python_type(obj, field, pytype)
def _get_schema_for_field(self, obj, field):
"""Get schema and validators for field."""
if hasattr(field, "_jsonschema_type_mapping"):
schema = field._jsonschema_type_mapping() # pylint: disable=protected-access
elif "_jsonschema_type_mapping" in field.metadata:
schema = field.metadata["_jsonschema_type_mapping"]
else:
if isinstance(field, UnionField):
schema = self._get_schema_for_union_field(obj, field)
elif isinstance(field, ExperimentalField):
schema = self._get_schema_for_field(obj, field.experimental_field)
elif isinstance(field, fields.Constant):
schema = {"const": field.constant}
else:
schema = super()._get_schema_for_field(obj, field)
if field.data_key:
schema["title"] = field.data_key
return schema
def _get_schema_for_union_field(self, obj, field):
has_yaml_option = False
schemas = []
for field_item in field._union_fields: # pylint: disable=protected-access
if isinstance(field_item, fields.Nested) and isinstance(field_item.schema, YamlFileSchema):
has_yaml_option = True
schemas.append(self._get_schema_for_field(obj, field_item))
if has_yaml_option:
schemas.append({"type": "string", "pattern": "^file:.*"})
if field.allow_none:
schemas.append({"type": "null"})
if field.is_strict:
schema = {"oneOf": schemas}
else:
schema = {"anyOf": schemas}
# This happens in the super() call to get_schema, doing here to allow for adding
# descriptions and other schema attributes from marshmallow metadata
metadata = field.metadata.get("metadata", {})
for md_key, md_val in metadata.items():
if md_key in ("metadata", "name"):
continue
schema[md_key] = md_val
return schema
def _from_nested_schema(self, obj, field):
"""patch in context for nested field"""
if isinstance(field.nested, (str, bytes)):
nested = get_class(field.nested)
else:
nested = field.nested
if isclass(nested) and issubclass(nested, Schema):
only = field.only
exclude = field.exclude
context = getattr(field.parent, "context", {})
field.nested = nested(only=only, exclude=exclude, context=context)
return super()._from_nested_schema(obj, field)
def get_properties(self, obj):
"""Fill out properties field."""
properties = self.dict_class()
if self.props_ordered:
fields_items_sequence = obj.fields.items()
else:
fields_items_sequence = sorted(obj.fields.items())
for _, field in fields_items_sequence:
schema = self._get_schema_for_field(obj, field)
properties[field.metadata.get("name") or field.data_key or field.name] = schema
return properties
def get_required(self, obj):
"""Fill out required field."""
required = []
for _, field in sorted(obj.fields.items()):
if field.required:
required.append(field.metadata.get("name") or field.data_key or field.name)
return required or missing
from promptflow._sdk.schemas._connection import AzureOpenAIConnectionSchema, OpenAIConnectionSchema, \
QdrantConnectionSchema, CognitiveSearchConnectionSchema, SerpConnectionSchema, AzureContentSafetyConnectionSchema, \
FormRecognizerConnectionSchema, CustomConnectionSchema, WeaviateConnectionSchema, ServerlessConnectionSchema, \
CustomStrongTypeConnectionSchema, AzureAIServicesConnectionSchema
from promptflow._sdk.schemas._run import RunSchema
from promptflow._sdk.schemas._flow import FlowSchema, FlexFlowSchema
def dump_json(file_name, dct):
with open(file_name, "w") as f:
f.write(json.dumps(dct, indent=4))
if __name__ == "__main__":
example_text = """Example usage:
python scripts/json_schema/gen_json_schema.py -o Run Flow # Generate Run.schema.json and Flow.schema.json
python scripts/json_schema/gen_json_schema.py -a # Generate all schema files
"""
parser = argparse.ArgumentParser(epilog=example_text, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('-o', '--output-file', nargs='+', help='Specify output file names, Run, Flow, etc.')
parser.add_argument('-a', '--all', action='store_true', help='Generate all schema files')
args = parser.parse_args()
if args.all:
args.output_file = ["Run", "Flow", "AzureOpenAIConnection", "OpenAIConnection", "QdrantConnection",
"CognitiveSearchConnection", "SerpConnection", "AzureContentSafetyConnection",
"FormRecognizerConnection", "CustomConnection", "WeaviateConnection", "ServerlessConnection",
"CustomStrongTypeConnection", "AzureAIServicesConnection"]
# Special case for Flow and EagerFlow
if "Flow" in args.output_file:
cls_list = [FlowSchema, FlexFlowSchema]
schema_list = []
for cls in cls_list:
target_schema = PatchedJSONSchema().dump(cls(context={"base_path": "./"}))
# print(target_schema)
file_name = cls.__name__
file_name = file_name.replace("Schema", "")
schema_list.append(target_schema["definitions"][cls.__name__])
print(target_schema)
schema = {
"type": "object",
"oneOf": schema_list
}
dump_json("Flow.schema.json", schema)
# Dump another small case first letter one
dump_json("flow.schema.json", schema)
args.output_file.remove("Flow")
prepared_schemas = {}
# get all imported schemas
for (name, cls) in getmembers(sys.modules[__name__]):
if name.endswith("Schema"):
prepared_schemas[name.split("Schema")[0]] = cls
for item in args.output_file:
item_cls = prepared_schemas.get(item, None)
if item_cls is None:
print(f"Schema not found for {item}")
else:
target_schema = PatchedJSONSchema().dump(item_cls(context={"base_path": "./"}))
dump_json(f"{item}.schema.json", target_schema)
# Dump another small case first letter one
item = item[0].lower() + item[1:]
dump_json(f"{item}.schema.json", target_schema)
+50
View File
@@ -0,0 +1,50 @@
# Readme Workflow Generator
These tools is used to generate workflows from README.md and python notebook files in the [examples](../../examples/) folder.
* Generated workflows will be placed in [.github/workflows/samples_*](../../.github/workflows/) folder.
* The script will also generate a new explanation [README.md](../../examples/README.md) for all the examples.
## 1. Install dependencies
```bash
pip install -r ../../examples/requirements.txt
pip install -r ../../examples/dev_requirements.txt
```
## 2. Generate workflows
### (Option 1) One Step Generation
At the **root** of the repository, run the following command:
```bash
python scripts/readme/readme.py
```
### (Option 2) Step by Step Generation
At the **root** of the repository, run the following command:
```bash
# Generate workflow from README.md inside examples folder
python scripts/readme/readme_generator.py -g "examples/**/*.ipynb"
# Generate workflow from python notebook inside examples folder
python scripts/readme/workflow_generator.py -g "examples/flows/**/README.md"
```
Multiple inputs are supported.
## 3. Options to control generations of examples [README.md](../../examples/README.md)
### 3.1 Notebook Workflow Generation
* Each workflow contains metadata area, set `.metadata.description` area will display this message in the corresponding cell in [README.md](../../examples/README.md) file.
* When set `.metadata.no_readme_generation` to value `true`, the script will stop generating for this notebook.
### 3.2 README.md Workflow Generation
* For README.md files, only `bash` cells will be collected and converted to workflow. No cells will produce no workflow.
* Readme descriptions are simply collected from the first sentence in the README.md file just below the title. The script will collect words before the first **.** of the fist paragraph. Multi-line sentence is also supported
* A supported description sentence: `This is a sample workflow for testing.`
* A not supported description sentence: `Please check www.microsoft.com for more details.`
View File
@@ -0,0 +1,60 @@
import argparse
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from ghactions_driver.readme_parse import readme_parser
from ghactions_driver.readme_step import ReadmeStepsManage
def comment_pytest(full_text: str):
# separate full_text into lines, group them by start with pytest or not
full_text_lines = full_text.split("\n")
full_text_pytest = []
full_text_no_pytest = []
for line in full_text_lines:
if line.strip().startswith("python -m unittest"):
full_text_pytest.append(line)
else:
full_text_no_pytest.append(line)
return "\n".join(full_text_no_pytest), "\n".join(full_text_pytest)
def write_readme_shell(readme_path: str, output_folder: str):
full_text = readme_parser(readme_path)
full_text, full_text_pytest = comment_pytest(full_text)
Path(ReadmeStepsManage.git_base_dir())
bash_script_path = (
Path(ReadmeStepsManage.git_base_dir()) / output_folder / "bash_script.sh"
)
bash_script_backup = (
Path(ReadmeStepsManage.git_base_dir()) / output_folder / "bash_script_pytest.sh"
)
template_env = Environment(
loader=FileSystemLoader(
Path(ReadmeStepsManage.git_base_dir())
/ "scripts/readme/ghactions_driver/bash_script"
)
)
bash_script_template = template_env.get_template("bash_script.sh.jinja2")
with open(bash_script_path, "w") as f:
f.write(bash_script_template.render({"command": full_text}))
if full_text_pytest != "":
with open(bash_script_backup, "w") as f:
f.write(bash_script_template.render({"command": full_text_pytest}))
if __name__ == "__main__":
# setup argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"-f",
"--readme-file",
help="Input README.md example 'examples/flows/standard/basic/README.md'",
)
parser.add_argument(
"-o",
"--output-folder",
help="Output folder for bash_script.sh example 'examples/flows/standard/basic/'",
)
args = parser.parse_args()
write_readme_shell(args.readme_file, args.output_folder)
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
set -xe
{{ command }}
@@ -0,0 +1,47 @@
import io
import re
from pathlib import Path
import panflute
import pypandoc
from .readme_step import ReadmeStepsManage
def strip_comments(code):
code = str(code)
code = re.sub(r"(?m)^ *#.*\n?", "", code) # remove comments
splits = [ll.rstrip() for ll in code.splitlines() if ll.strip()] # remove empty
splits_no_interactive = [
split
for split in splits
if "interactive" not in split
and "pf flow serve" not in split
and "pf connection delete" not in split
] # remove --interactive and pf flow serve and pf export docker
text = "\n".join([ll.rstrip() for ll in splits_no_interactive])
# replacements
text = text.replace("<your_api_key>", "$aoai_api_key")
text = text.replace("<your_api_base>", "$aoai_api_endpoint")
text = text.replace("<your_subscription_id>", "$test_workspace_sub_id")
text = text.replace("<your_resource_group_name>", "$test_workspace_rg")
text = text.replace("<your_workspace_name>", "$test_workspace_name")
return text
def prepare(doc):
doc.full_text = ""
def action(elem, doc):
if isinstance(elem, panflute.CodeBlock) and "bash" in elem.classes:
doc.full_text = "\n".join([doc.full_text, strip_comments(elem.text)])
def readme_parser(filename: str):
real_filename = Path(ReadmeStepsManage.git_base_dir()) / filename
data = pypandoc.convert_file(str(real_filename), "json")
f = io.StringIO(data)
doc = panflute.load(f)
panflute.run_filter(action, prepare, doc=doc)
return doc.full_text
@@ -0,0 +1,415 @@
import subprocess
from pathlib import Path
import hashlib
from jinja2 import Environment, FileSystemLoader, Template
from .telemetry_obj import Telemetry
class Step:
"""
StepType in workflow
"""
Environment = None
@staticmethod
def init_jinja_loader() -> Environment:
jinja_folder_path = (
Path(ReadmeStepsManage.git_base_dir())
/ "scripts"
/ "readme"
/ "ghactions_driver"
/ "workflow_steps"
)
Step.Environment = Environment(
loader=FileSystemLoader(jinja_folder_path.resolve())
)
def __init__(self, name: str) -> None:
self.workflow_name = name
def get_workflow_step(self) -> str:
# virtual method for override
return ""
@staticmethod
def get_workflow_template(step_file_name: str) -> Template:
# virtual method for override
if Step.Environment is None:
Step.init_jinja_loader()
template = Step.Environment.get_template(step_file_name)
return template
class AzureLoginStep(Step):
def __init__(self) -> None:
Step.__init__(self, "Azure Login")
def get_workflow_step(self) -> str:
template = Step.get_workflow_template("step_azure_login.yml.jinja2")
return template.render(
{
"step_name": self.workflow_name,
}
)
class LoginAgainStep(Step):
def __init__(self) -> None:
Step.__init__(self, "Azure Login Again")
def get_workflow_step(self) -> str:
template = Step.get_workflow_template("step_login_again.yml.jinja2")
return template.render(
{
"step_name": self.workflow_name,
}
)
class InstallDependenciesStep(Step):
def __init__(self) -> None:
Step.__init__(self, "Prepare requirements")
def get_workflow_step(self) -> str:
template = Step.get_workflow_template("step_install_deps.yml.jinja2")
return template.render(
{
"step_name": self.workflow_name,
"working_dir": ReadmeSteps.working_dir,
}
)
class InstallDevDependenciesStep(Step):
def __init__(self) -> None:
Step.__init__(self, "Prepare dev requirements")
def get_workflow_step(self) -> str:
template = Step.get_workflow_template("step_install_dev_deps.yml.jinja2")
return template.render(
{
"step_name": self.workflow_name,
"working_dir": ReadmeSteps.working_dir,
}
)
class CreateAoaiFromYaml(Step):
def __init__(self, yaml_name: str) -> None:
Step.__init__(self, "Create AOAI Connection from YAML")
self.yaml_name = yaml_name
def get_workflow_step(self) -> str:
template = Step.get_workflow_template("step_yml_create_aoai.yml.jinja2")
return template.render(
{
"step_name": self.workflow_name,
"yaml_name": self.yaml_name,
}
)
class ExtractStepsAndRun(Step):
def __init__(self) -> None:
Step.__init__(self, f"Extract Steps {ReadmeSteps.readme_name}")
def get_workflow_step(self) -> str:
template = Step.get_workflow_template("step_extract_steps_and_run.yml.jinja2")
return template.render(
{
"step_name": self.workflow_name,
"working_dir": ReadmeSteps.working_dir,
"readme_name": ReadmeSteps.readme_name,
}
)
class ExtractStepsAndRunGPTFour(Step):
def __init__(self) -> None:
Step.__init__(self, f"Extract Steps {ReadmeSteps.readme_name}")
def get_workflow_step(self) -> str:
template = Step.get_workflow_template(
"step_extract_steps_and_run_gpt4.yml.jinja2"
)
return template.render(
{
"step_name": self.workflow_name,
"working_dir": ReadmeSteps.working_dir,
"readme_name": ReadmeSteps.readme_name,
}
)
class ExecuteCommand(Step):
def __init__(self) -> None:
Step.__init__(self, "Execute Command")
def get_workflow_step(self) -> str:
template = Step.get_workflow_template(
"step_execute_command.yml.jinja2"
)
return template.render(
{
"step_name": self.workflow_name,
"working_dir": ReadmeSteps.working_dir,
}
)
class CreateEnv(Step):
def __init__(self) -> None:
Step.__init__(self, "Refine .env file")
def get_workflow_step(self) -> str:
template = Step.get_workflow_template("step_create_env.yml.jinja2")
content = template.render(
{"step_name": self.workflow_name, "working_dir": ReadmeSteps.working_dir}
)
return content
class CreateEnvGPTFour(Step):
def __init__(self) -> None:
Step.__init__(self, "Refine .env file")
def get_workflow_step(self) -> str:
template = Step.get_workflow_template("step_create_env_gpt4.yml.jinja2")
content = template.render(
{"step_name": self.workflow_name, "working_dir": ReadmeSteps.working_dir}
)
return content
class CreateAoaiFromEnv(Step):
def __init__(self, connection_name: str) -> None:
Step.__init__(self, "Create AOAI Connection from ENV file")
self.connection_name = connection_name
def get_workflow_step(self) -> str:
template = Step.get_workflow_template("step_env_create_aoai.yml.jinja2")
content = template.render(
{
"step_name": self.workflow_name,
"working_dir": ReadmeSteps.working_dir,
"connection_name": self.connection_name,
}
)
return content
class CreateRunYaml(Step):
def __init__(self) -> None:
Step.__init__(self, "Create run.yml")
def get_workflow_step(self) -> str:
template = Step.get_workflow_template("step_create_run_yml.yml.jinja2")
content = template.render(
{"step_name": self.workflow_name, "working_dir": ReadmeSteps.working_dir}
)
return content
class ReadmeSteps:
"""
Static class to record steps, to be filled in workflow templates and Readme
"""
step_array = [] # Record steps
readme_name = "" # Record readme name
working_dir = "" # the working directory of flow, relative to git_base_dir
template = "" # Select a base template under workflow_templates folder
workflow = "" # Target workflow name to be generated
@staticmethod
def remember_step(step: Step) -> Step:
ReadmeSteps.step_array.append(step)
return step
@staticmethod
def get_length() -> int:
return len(ReadmeSteps.step_array)
# region steps
@staticmethod
def create_env() -> Step:
return ReadmeSteps.remember_step(CreateEnv())
@staticmethod
def create_env_gpt4() -> Step:
return ReadmeSteps.remember_step(CreateEnvGPTFour())
@staticmethod
def yml_create_aoai(yaml_name: str) -> Step:
return ReadmeSteps.remember_step(CreateAoaiFromYaml(yaml_name=yaml_name))
@staticmethod
def env_create_aoai(connection_name: str) -> Step:
return ReadmeSteps.remember_step(
CreateAoaiFromEnv(connection_name=connection_name)
)
@staticmethod
def azure_login() -> Step:
return ReadmeSteps.remember_step(AzureLoginStep())
@staticmethod
def login_again() -> Step:
return ReadmeSteps.remember_step(LoginAgainStep())
@staticmethod
def install_dependencies() -> Step:
return ReadmeSteps.remember_step(InstallDependenciesStep())
@staticmethod
def install_dev_dependencies() -> Step:
return ReadmeSteps.remember_step(InstallDevDependenciesStep())
@staticmethod
def create_run_yaml() -> Step:
return ReadmeSteps.remember_step(CreateRunYaml())
@staticmethod
def extract_steps_and_run() -> Step:
return ReadmeSteps.remember_step(ExtractStepsAndRun())
@staticmethod
def extract_steps_and_run_gpt_four() -> Step:
return ReadmeSteps.remember_step(ExtractStepsAndRunGPTFour())
@staticmethod
def execute_command() -> Step:
return ReadmeSteps.remember_step(ExecuteCommand())
# endregion steps
@staticmethod
def setup_target(
working_dir: str, template: str, target: str, readme_name: str
) -> str:
"""
Used at the very head of jinja template to indicate basic information
"""
ReadmeSteps.working_dir = working_dir
ReadmeSteps.template = template
ReadmeSteps.workflow = target
ReadmeSteps.step_array = []
ReadmeSteps.readme_name = readme_name
return ""
@staticmethod
def cleanup() -> None:
ReadmeSteps.working_dir = ""
ReadmeSteps.template = ""
ReadmeSteps.workflow = ""
ReadmeSteps.step_array = []
class ReadmeStepsManage:
"""
# Static methods for manage all readme steps
"""
repo_base_dir = ""
@staticmethod
def git_base_dir() -> str:
"""
Get the base directory of the git repo
"""
if ReadmeStepsManage.repo_base_dir == "":
try:
ReadmeStepsManage.repo_base_dir = (
subprocess.check_output(["git", "rev-parse", "--show-toplevel"])
.decode("utf-8")
.strip()
)
raise Exception("Not in git repo")
except Exception:
ReadmeStepsManage.repo_base_dir = Path(__file__).parent.parent.parent.parent.resolve()
print(ReadmeStepsManage.repo_base_dir)
return ReadmeStepsManage.repo_base_dir
@staticmethod
def write_workflow(
workflow_name: str, pipeline_name: str, output_telemetry=Telemetry()
) -> None:
# Schedule notebooks at different times to reduce maximum quota usage.
name_hash = int(hashlib.sha512(workflow_name.encode()).hexdigest(), 16)
schedule_minute = name_hash % 60
schedule_hour = (name_hash // 60) % 4 + 19 # 19-22 UTC
if "tutorials" in workflow_name:
# markdown filename has some exceptions, special handle here
if "chat_with_pdf" in workflow_name:
readme_name = "chat-with-pdf.md"
elif (
"fine_tuning_evaluation_promptflow_quality_improvement" in workflow_name
):
readme_name = "promptflow-quality-improvement.md"
else:
readme_name = "README.md"
readme_path = (
Path(ReadmeStepsManage.git_base_dir())
/ ReadmeSteps.working_dir
/ readme_name
)
# local import to avoid circular import
from .resource_resolver import resolve_tutorial_resource
path_filter = resolve_tutorial_resource(
workflow_name, readme_path.resolve(), output_telemetry
)
else:
if (
"flow_with_additional_includes" in workflow_name
or "flow_with_symlinks" in workflow_name
):
# these two flows have dependencies on flow web-classification
# so corresponding workflows should also listen to changes in web-classification
path_filter = (
f"[ {ReadmeSteps.working_dir}/**, "
+ "examples/*requirements.txt, "
+ "examples/flows/standard/web-classification/**, "
+ f".github/workflows/{workflow_name}.yml ]"
)
else:
path_filter = (
f"[ {ReadmeSteps.working_dir}/**, "
+ "examples/*requirements.txt, "
+ f".github/workflows/{workflow_name}.yml ]"
)
replacements = {
"steps": ReadmeSteps.step_array,
"workflow_name": workflow_name,
"ci_name": pipeline_name,
"path_filter": path_filter,
"crontab": f"{schedule_minute} {schedule_hour} * * *",
"crontab_comment": f"Every day starting at {schedule_hour - 16}:{schedule_minute} BJT",
}
workflow_template_path = (
Path(ReadmeStepsManage.git_base_dir())
/ "scripts"
/ "readme"
/ "ghactions_driver"
/ "workflow_templates"
)
target_path = (
Path(ReadmeStepsManage.git_base_dir())
/ ".github"
/ "workflows"
/ f"{workflow_name}.yml"
)
template = Environment(
loader=FileSystemLoader(workflow_template_path.resolve())
).get_template(ReadmeSteps.template)
content = template.render(replacements)
with open(target_path.resolve(), "w", encoding="utf-8") as f:
f.write(content)
print(f"Write readme workflow: {target_path.resolve()}")
output_telemetry.workflow_name = workflow_name
output_telemetry.target_path = target_path
output_telemetry.readme_folder = ReadmeSteps.working_dir
output_telemetry.readme_name = ReadmeSteps.readme_name
output_telemetry.path_filter = path_filter
@@ -0,0 +1,116 @@
# Promptflow examples
[![code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![license: MIT](https://img.shields.io/badge/License-MIT-purple.svg)](../LICENSE)
## Get started
**Install dependencies**
- Bootstrap your python environment.
- e.g: create a new [conda](https://conda.io/projects/conda/en/latest/user-guide/getting-started.html) environment. `conda create -n pf-examples python=3.9`.
- install required packages in python environment : `pip install -r requirements.txt`
- show installed sdk: `pip show promptflow`
**Quick start**
| path | status | description |
------|--------|-------------
{% for quickstart in quickstarts.notebooks %}| [{{ quickstart.name }}]({{ quickstart.path }}) | [![{{quickstart.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{quickstart.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{quickstart.yaml_name}}) | {{ quickstart.description }} |
{% endfor %}
## CLI examples
### Tutorials ([tutorials](tutorials))
| path | status | description |
------|--------|-------------
{% for tutorial in tutorials.readmes %}| [{{ tutorial.name }}]({{ tutorial.path }}) | [![{{tutorial.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{tutorial.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{tutorial.yaml_name}}) | {{ tutorial.description }} |
{% endfor %}
### Prompty ([prompty](prompty))
| path | status | description |
------|--------|-------------
{% for flow in prompty.readmes %}| [{{ flow.name }}]({{ flow.path }}) | [![{{flow.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{flow.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{flow.yaml_name}}) | {{ flow.description }} |
{% endfor %}
### Flex Flows ([flex-flows](flex-flows))
| path | status | description |
------|--------|-------------
{% for flow in flex_flows.readmes %}| [{{ flow.name }}]({{ flow.path }}) | [![{{flow.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{flow.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{flow.yaml_name}}) | {{ flow.description }} |
{% endfor %}
### Flows ([flows](flows))
#### [Standard flows](flows/standard/)
| path | status | description |
------|--------|-------------
{% for flow in flows.readmes %}| [{{ flow.name }}]({{ flow.path }}) | [![{{flow.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{flow.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{flow.yaml_name}}) | {{ flow.description }} |
{% endfor %}
#### [Evaluation flows](flows/evaluation/)
| path | status | description |
------|--------|-------------
{% for evaluation in evaluations.readmes %}| [{{ evaluation.name }}]({{ evaluation.path }}) | [![{{evaluation.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{evaluation.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{evaluation.yaml_name}}) | {{ evaluation.description }} |
{% endfor %}
#### [Chat flows](flows/chat/)
| path | status | description |
------|--------|-------------
{% for chat in chats.readmes %}| [{{ chat.name }}]({{ chat.path }}) | [![{{chat.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{chat.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{chat.yaml_name}}) | {{ chat.description }} |
{% endfor %}
### Tool Use Cases ([Tool Use Cases](tools/use-cases))
| path | status | description |
------|--------|-------------
{% for toolusecase in toolusecases.readmes %}| [{{ toolusecase.name }}]({{ toolusecase.path }}) | [![{{toolusecase.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{toolusecase.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{toolusecase.yaml_name}}) | {{ toolusecase.description }} |
{% endfor %}
### Connections ([connections](connections))
| path | status | description |
------|--------|-------------
{% for connection in connections.readmes %}| [{{ connection.name }}]({{ connection.path }}) | [![{{connection.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{connection.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{connection.yaml_name}}) | {{ connection.description }} |
{% endfor %}
## SDK examples
| path | status | description |
------|--------|-------------
{% for quickstart in quickstarts.notebooks %}| [{{ quickstart.name }}]({{ quickstart.path }}) | [![{{quickstart.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{quickstart.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{quickstart.yaml_name}}) | {{ quickstart.description }} |
{% endfor %}
{%- for tutorial in tutorials.notebooks -%}| [{{ tutorial.name }}]({{ tutorial.path }}) | [![{{tutorial.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{tutorial.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{tutorial.yaml_name}}) | {{ tutorial.description }} |
{% endfor %}
{%- if connections.notebooks|length > 0 -%}{% for connection in connections.notebooks %}| [{{ connection.name }}]({{ connection.path }}) | [![{{connection.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{connection.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{connection.yaml_name}}) | {{ connection.description }} |
{% endfor %}{% endif %}
{%- if flex_flows.notebooks|length > 0 -%}{% for flow in flex_flows.notebooks %}| [{{ flow.name }}]({{ flow.path }}) | [![{{flow.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{flow.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{flow.yaml_name}}) | {{ flow.description }} |
{% endfor %}{% endif %}
{%- if prompty.notebooks|length > 0 -%}{% for flow in prompty.notebooks %}| [{{ flow.name }}]({{ flow.path }}) | [![{{flow.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{flow.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{flow.yaml_name}}) | {{ flow.description }} |
{% endfor %}{% endif %}
{%- if chats.notebooks|length > 0 -%}{% for chat in chats.notebooks %}| [{{ chat.name }}]({{ chat.path }}) | [![{{chat.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{chat.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{chat.yaml_name}}) | {{ chat.description }} |
{% endfor %}{% endif %}
{%- if evaluations.notebooks|length > 0 -%}{% for evaluation in evaluations.notebooks %}| [{{ evaluation.name }}]({{ evaluation.path }}) | [![{{evaluation.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{evaluation.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{evaluation.yaml_name}}) | {{ evaluation.description }} |
{% endfor %}{% endif %}
{%- if flows.notebooks|length > 0 -%}{% for flow in flows.notebooks %}| [{{ flow.name }}]({{ flow.path }}) | [![{{flow.pipeline_name}}](https://github.com/microsoft/promptflow/actions/workflows/{{flow.yaml_name}}/badge.svg?branch={{branch}})](https://github.com/microsoft/promptflow/actions/workflows/{{flow.yaml_name}}) | {{ flow.description }} |
{% endfor %}{% endif %}
## Contributing
We welcome contributions and suggestions! Please see the [contributing guidelines](../CONTRIBUTING.md) for details.
## Code of Conduct
This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). Please see the [code of conduct](../CODE_OF_CONDUCT.md) for details.
## Reference
* [Promptflow documentation](https://microsoft.github.io/promptflow/)
@@ -0,0 +1,10 @@
# Tutorials
This section contains a collection of flow samples and step-by-step tutorials.
|Category|<div style="width:250px">Sample</div>|Description|
|--|--|--|
{% for item in items %}|{{ item.category }}|[{{ item.title }}]({{ item.url }})|{{ item.description }}|
{% endfor %}
Learn more: [Try out more promptflow examples.](https://github.com/microsoft/promptflow/tree/main/examples)
@@ -0,0 +1,68 @@
from pathlib import Path
from .readme_step import ReadmeStepsManage, ReadmeSteps
from ghactions_driver.telemetry_obj import Telemetry
def write_readme_workflow(readme_path, output_telemetry=Telemetry()):
relative_path = Path(readme_path).relative_to(
Path(ReadmeStepsManage.git_base_dir())
)
workflow_path = relative_path.parent.as_posix()
relative_name_path = Path(readme_path).relative_to(
Path(ReadmeStepsManage.git_base_dir()) / "examples"
)
workflow_name = (
relative_name_path.as_posix()
.replace(".md", "")
.replace("/README", "")
.replace("/", "_")
.replace("-", "_")
)
workflow_name = "samples_" + workflow_name
ReadmeSteps.setup_target(
working_dir=workflow_path,
template="basic_workflow_replace_config_json.yml.jinja2"
if "e2e_development_chat_with_pdf" in workflow_name
else "basic_workflow_replace.yml.jinja2",
target=f"{workflow_name}.yml",
readme_name=relative_path.as_posix(),
)
ReadmeSteps.install_dependencies()
ReadmeSteps.install_dev_dependencies()
if (
workflow_name.endswith("flows_chat_chat_with_image")
or workflow_name.endswith("flows_standard_describe_image")
):
ReadmeSteps.create_env_gpt4()
ReadmeSteps.env_create_aoai("aoai_gpt4v_connection")
else:
ReadmeSteps.create_env()
if workflow_name.endswith("pdf"):
ReadmeSteps.env_create_aoai("chat_with_pdf_custom_connection")
ReadmeSteps.create_run_yaml()
if (
workflow_name.endswith("flows_standard_basic_with_builtin_llm")
or workflow_name.endswith("flows_standard_flow_with_symlinks")
or workflow_name.endswith("flows_standard_flow_with_additional_includes")
or workflow_name.endswith("flows_standard_basic_with_connection")
):
ReadmeSteps.yml_create_aoai("examples/connections/azure_openai.yml")
ReadmeSteps.azure_login()
if (
workflow_name.endswith("flows_chat_chat_with_image")
or workflow_name.endswith("flows_standard_describe_image")
):
ReadmeSteps.extract_steps_and_run_gpt_four()
else:
ReadmeSteps.extract_steps_and_run()
if workflow_name.endswith("e2e_development_chat_with_pdf"):
ReadmeSteps.login_again()
ReadmeSteps.execute_command()
ReadmeStepsManage.write_workflow(
workflow_name, "samples_readme_ci", output_telemetry
)
ReadmeSteps.cleanup()
@@ -0,0 +1,120 @@
from pathlib import Path
import markdown
import nbformat
from .readme_step import ReadmeStepsManage
RESOURCES_KEY_NAME = "resources"
RESOURCES_KEY_ERROR_MESSAGE = (
"Please follow examples contributing guide to declare tutorial resources: "
"https://github.com/microsoft/promptflow/blob/main/examples/CONTRIBUTING.md"
)
TITLE_KEY_NAME = "title"
CLOUD_KEY_NAME = "cloud"
CATEGORY_KEY_NAME = "category"
WEIGHT_KEY_NAME = "weight"
def _parse_resources_string_from_notebook(path: Path, output_telemetry):
with open(path, "r", encoding="utf-8") as f:
nb = nbformat.read(f, as_version=4)
obj = {}
if nb.metadata.get('build_doc', False):
if nb.metadata['build_doc'].get(CLOUD_KEY_NAME, None):
output_telemetry.cloud = nb.metadata['build_doc']['category']
if nb.metadata['build_doc'].get(CATEGORY_KEY_NAME, None):
output_telemetry.category = nb.metadata['build_doc']['section']
if nb.metadata['build_doc'].get(WEIGHT_KEY_NAME, None):
output_telemetry.weight = int(nb.metadata['build_doc']['weight'])
cell = nb['cells'][0]
for cell in nb['cells']:
if (cell['cell_type'] == 'markdown'):
break
if (cell['cell_type'] == 'markdown'):
lines = cell.source.split('\n')
for line in lines:
if '#' in line:
output_telemetry.title = line.replace('#', '').strip()
break
if RESOURCES_KEY_NAME not in nb.metadata:
raise Exception(RESOURCES_KEY_ERROR_MESSAGE + f" . Error in {path}")
obj[RESOURCES_KEY_NAME] = nb.metadata[RESOURCES_KEY_NAME]
return obj
def _parse_resources_string_from_markdown(path: Path, output_telemetry):
markdown_content = path.read_text(encoding="utf-8")
md = markdown.Markdown(extensions=["meta"])
md.convert(markdown_content)
obj = {}
for line in md.lines:
if '#' in line:
output_telemetry.title = line.replace('#', '').strip()
break
if CLOUD_KEY_NAME in md.Meta:
output_telemetry.cloud = md.Meta[CLOUD_KEY_NAME][0]
if CATEGORY_KEY_NAME in md.Meta:
output_telemetry.category = md.Meta[CATEGORY_KEY_NAME][0]
if WEIGHT_KEY_NAME in md.Meta:
output_telemetry.weight = int(md.Meta[WEIGHT_KEY_NAME][0])
if RESOURCES_KEY_NAME not in md.Meta:
raise Exception(RESOURCES_KEY_ERROR_MESSAGE + f" . Error in {path}")
obj[RESOURCES_KEY_NAME] = md.Meta[RESOURCES_KEY_NAME][0]
return obj
def _parse_resources(path: Path, output_telemetry):
metadata = {}
if path.suffix == ".ipynb":
metadata = _parse_resources_string_from_notebook(path, output_telemetry)
resources_string = metadata["resources"]
elif path.suffix == ".md":
metadata = _parse_resources_string_from_markdown(path, output_telemetry)
resources_string = metadata["resources"]
else:
raise Exception(f"Unknown file type: {path.suffix!r}")
return [resource.strip() for resource in resources_string.split(",")]
def resolve_tutorial_resource(workflow_name: str, resource_path: Path, output_telemetry):
"""Resolve tutorial resources, so that workflow can be triggered more precisely.
A tutorial workflow should listen to changes of:
1. working directory
2. resources declared in notebook/markdown metadata
3. workflow file
4. examples/requirements.txt (for release verification)
5. examples/connections/azure_openai.yml (fall back as it is the most basic and common connection)
"""
# working directory
git_base_dir = Path(ReadmeStepsManage.git_base_dir())
working_dir = resource_path.parent.relative_to(git_base_dir).as_posix()
path_filter_list = [f"{working_dir}/**"]
# resources declared in text file
resources = _parse_resources(resource_path, output_telemetry)
for resource in resources:
# skip empty line
if len(resource) == 0:
continue
# validate resource path exists
resource_path = (git_base_dir / resource).resolve()
if not resource_path.exists():
raise FileNotFoundError(
f"Please declare tutorial resources path {resource_path} whose base is the git repo root."
)
elif resource_path.is_file():
path_filter_list.append(resource)
else:
path_filter_list.append(f"{resource}/**")
# workflow file
path_filter_list.append(f".github/workflows/{workflow_name}.yml")
# manually add examples/requirements.txt if not exists
examples_req = "examples/requirements.txt"
if examples_req not in path_filter_list:
path_filter_list.append(examples_req)
# manually add examples/connections/azure_openai.yml if not exists
aoai_conn = "examples/connections/azure_openai.yml"
if aoai_conn not in path_filter_list:
path_filter_list.append(aoai_conn)
return "[ " + ", ".join(path_filter_list) + " ]"
@@ -0,0 +1,2 @@
class Telemetry(object):
pass
@@ -0,0 +1,11 @@
- name: Random Wait
uses: AliSajid/random-wait-action@main
with:
minimum: 0
maximum: 99
- name: {{ step_name }}
uses: azure/login@v1
with:
subscription-id: ${{ '{{' }}secrets.AZURE_SUBSCRIPTION_ID}}
tenant-id: ${{ '{{' }}secrets.AZURE_TENANT_ID}}
client-id: ${{ '{{' }}secrets.AZURE_CLIENT_ID}}
@@ -0,0 +1,16 @@
- name: {{ step_name }}
working-directory: {{ working_dir }}
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
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
@@ -0,0 +1,8 @@
- name: {{ step_name }}
working-directory: {{ working_dir }}
run: |
AOAI_API_KEY=${{ '{{' }} secrets.AOAI_GPT_4V_KEY }}
AOAI_API_ENDPOINT=${{ '{{' }} secrets.AOAI_GPT_4V_ENDPOINT }}
AOAI_API_ENDPOINT=$(echo ${AOAI_API_ENDPOINT//\//\\/})
cp ../../../connections/azure_openai.yml ./azure_openai.yml
sed -i -e "s/<user-input>/$AOAI_API_KEY/g" -e "s/aoai-api-endpoint/$AOAI_API_ENDPOINT/g" azure_openai.yml
@@ -0,0 +1,8 @@
- name: {{ step_name }}
working-directory: {{ working_dir }}
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
@@ -0,0 +1,10 @@
- name: {{ step_name }}
working-directory: {{ working_dir }}
run: |
if [[ -e .env ]]; then
pf connection create --file .env --name {{ connection_name }}
fi
if [[ -e azure_openai.yml ]]; then
pf connection create --file azure_openai.yml --name {{ connection_name }}
fi
pf connection list
@@ -0,0 +1,20 @@
- name: Run backup scripts against canary workspace (scheduled runs only)
if: github.event_name == 'schedule'
working-directory: {{ working_dir }}
run: |
export aoai_api_key=${{ '{{' }}secrets.AOAI_GPT_4V_KEY }}
export aoai_api_endpoint=${{ '{{' }} secrets.AOAI_GPT_4V_ENDPOINT }}
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_CANARY }}
bash bash_script_pytest.sh
- name: Run backup scripts against production workspace
if: github.event_name != 'schedule'
working-directory: {{ working_dir }}
run: |
export aoai_api_key=${{ '{{' }}secrets.AOAI_GPT_4V_KEY }}
export aoai_api_endpoint=${{ '{{' }} secrets.AOAI_GPT_4V_ENDPOINT }}
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_PROD }}
bash bash_script_pytest.sh
@@ -0,0 +1,43 @@
- name: {{ step_name }}
working-directory: ${{ '{{' }} github.workspace }}
run: |
python scripts/readme/extract_steps_from_readme.py -f {{ readme_name }} -o {{ working_dir }}
- name: Cat script
working-directory: {{ working_dir }}
run: |
cat bash_script.sh
- name: Run scripts against canary workspace (scheduled runs only)
if: github.event_name == 'schedule'
working-directory: {{ working_dir }}
run: |
export aoai_api_key=${{ '{{' }}secrets.AOAI_API_KEY_TEST }}
export aoai_api_endpoint=${{ '{{' }} secrets.AOAI_API_ENDPOINT_TEST }}
export AZURE_OPENAI_API_KEY=${{ '{{' }}secrets.AOAI_API_KEY_TEST }}
export AZURE_OPENAI_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_CANARY }}
bash bash_script.sh
- name: Run scripts against production workspace
if: github.event_name != 'schedule'
working-directory: {{ working_dir }}
run: |
export aoai_api_key=${{ '{{' }}secrets.AOAI_API_KEY_TEST }}
export aoai_api_endpoint=${{ '{{' }} secrets.AOAI_API_ENDPOINT_TEST }}
export AZURE_OPENAI_API_KEY=${{ '{{' }}secrets.AOAI_API_KEY_TEST }}
export AZURE_OPENAI_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_PROD }}
bash bash_script.sh
- name: Pip List for Debug
if : ${{ '{{' }} always() }}
working-directory: {{ working_dir }}
run: |
pip list
- name: Upload artifact
if: ${{ '{{' }} always() }}
uses: actions/upload-artifact@v4
with:
name: artifact
path: {{ working_dir }}/bash_script.sh
@@ -0,0 +1,39 @@
- name: {{ step_name }}
working-directory: ${{ '{{' }} github.workspace }}
run: |
python scripts/readme/extract_steps_from_readme.py -f {{ readme_name }} -o {{ working_dir }}
- name: Cat script
working-directory: {{ working_dir }}
run: |
cat bash_script.sh
- name: Run scripts against canary workspace (scheduled runs only)
if: github.event_name == 'schedule'
working-directory: {{ working_dir }}
run: |
export aoai_api_key=${{ '{{' }}secrets.AOAI_GPT_4V_KEY }}
export aoai_api_endpoint=${{ '{{' }} secrets.AOAI_GPT_4V_ENDPOINT }}
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_CANARY }}
bash bash_script.sh
- name: Run scripts against production workspace
if: github.event_name != 'schedule'
working-directory: {{ working_dir }}
run: |
export aoai_api_key=${{ '{{' }}secrets.AOAI_GPT_4V_KEY }}
export aoai_api_endpoint=${{ '{{' }} secrets.AOAI_GPT_4V_ENDPOINT }}
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_PROD }}
bash bash_script.sh
- name: Pip List for Debug
if : ${{ '{{' }} always() }}
working-directory: {{ working_dir }}
run: |
pip list
- name: Upload artifact
if: ${{ '{{' }} always() }}
uses: actions/upload-artifact@v4
with:
name: artifact
path: {{ working_dir }}/bash_script.sh
@@ -0,0 +1,7 @@
- name: {{ step_name }}
working-directory: examples
run: |
if [[ -e requirements.txt ]]; then
python -m pip install --upgrade pip
pip install -r requirements.txt
fi
@@ -0,0 +1,5 @@
- name: {{ step_name }}
working-directory: examples
run: |
python -m pip install --upgrade pip
pip install -r dev_requirements.txt
@@ -0,0 +1,12 @@
- name: {{ step_name }}
uses: azure/powershell@v1
with:
azPSVersion: "latest"
inlineScript: |
Disconnect-AzAccount
- name: {{ step_name }}_login
uses: azure/login@v1
with:
subscription-id: ${{ '{{' }}secrets.AZURE_SUBSCRIPTION_ID}}
tenant-id: ${{ '{{' }}secrets.AZURE_TENANT_ID}}
client-id: ${{ '{{' }}secrets.AZURE_CLIENT_ID}}
@@ -0,0 +1,3 @@
- name: {{ step_name }}
working-directory: ${{ '{{' }} github.workspace }}
run: pf connection create --file {{ yaml_name }} --set api_key=${{ '{{' }} secrets.AOAI_API_KEY_TEST }} api_base=${{ '{{' }} secrets.AOAI_API_ENDPOINT_TEST }}
@@ -0,0 +1,73 @@
{% extends "workflow_skeleton.yml.jinja2" %}
{% block steps %}
runs-on: ubuntu-latest
environment:
internal
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Python 3.9 environment
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Prepare requirements
run: |
python -m pip install --upgrade pip
pip install -r ${{ '{{' }} github.workspace }}/examples/requirements.txt
pip install -r ${{ '{{' }} github.workspace }}/examples/dev_requirements.txt
- name: setup .env file
working-directory: {{ gh_working_dir }}
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
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
if [[ -e OAI_CONFIG_LIST.json.example ]]; then
echo "OAI_CONFIG_LIST replacement"
sed -i -e "s/<your_AOAI_key>/$AOAI_API_KEY/g" -e "s/<your_AOAI_endpoint>/$AOAI_API_ENDPOINT/g" OAI_CONFIG_LIST.json.example
mv OAI_CONFIG_LIST.json.example OAI_CONFIG_LIST.json
fi
- name: Create Aoai Connection
run: pf connection create -f ${{ '{{' }} github.workspace }}/examples/connections/azure_openai.yml --set api_key="${{ '{{' }} secrets.AOAI_API_KEY_TEST }}" api_base="${{ '{{' }} secrets.AOAI_API_ENDPOINT_TEST }}"
- name: Random Wait
uses: AliSajid/random-wait-action@main
with:
minimum: 1
maximum: 99
- name: Azure Login
uses: azure/login@v2
with:
subscription-id: ${{ '{{' }}secrets.AZURE_SUBSCRIPTION_ID}}
tenant-id: ${{ '{{' }}secrets.AZURE_TENANT_ID}}
client-id: ${{ '{{' }}secrets.AZURE_CLIENT_ID}}
- name: Fetch OID token every 4 mins
shell: bash
run: |
while true; do
token_request=$ACTIONS_ID_TOKEN_REQUEST_TOKEN
token_uri=$ACTIONS_ID_TOKEN_REQUEST_URL
token=$(curl -H "Authorization: bearer $token_request" "${token_uri}&audience=api://AzureADTokenExchange" | jq .value -r)
az login --service-principal -u ${{ '{{' }}secrets.AZURE_CLIENT_ID}} -t ${{ '{{' }}secrets.AZURE_TENANT_ID}} --federated-token $token --output none
# Sleep for 4 minutes
sleep 240
done &
- name: Test Notebook
working-directory: {{ gh_working_dir }}
run: |
papermill -k python {{ name }}.ipynb {{ name }}.output.ipynb
- name: Upload artifact
if: ${{ '{{' }} always() }}
uses: actions/upload-artifact@v3
with:
name: artifact
path: {{ gh_working_dir }}
{% endblock steps %}
@@ -0,0 +1,68 @@
{% extends "workflow_skeleton.yml.jinja2" %}
{% block steps %}
runs-on: ubuntu-latest
environment:
internal
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Python 3.9 environment
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Prepare requirements
run: |
python -m pip install --upgrade pip
pip install -r ${{ '{{' }} github.workspace }}/examples/requirements.txt
pip install -r ${{ '{{' }} github.workspace }}/examples/dev_requirements.txt
- name: setup .env file
working-directory: {{ gh_working_dir }}
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
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 Aoai Connection
run: pf connection create -f ${{ '{{' }} github.workspace }}/examples/connections/azure_openai.yml --set api_key="${{ '{{' }} secrets.AOAI_API_KEY_TEST }}" api_base="${{ '{{' }} secrets.AOAI_API_ENDPOINT_TEST }}"
- name: Random Wait
uses: AliSajid/random-wait-action@main
with:
minimum: 1
maximum: 99
- name: Azure Login
uses: azure/login@v2
with:
subscription-id: ${{ '{{' }}secrets.AZURE_SUBSCRIPTION_ID}}
tenant-id: ${{ '{{' }}secrets.AZURE_TENANT_ID}}
client-id: ${{ '{{' }}secrets.AZURE_CLIENT_ID}}
- name: Fetch OID token every 4 mins
shell: bash
run: |
while true; do
token_request=$ACTIONS_ID_TOKEN_REQUEST_TOKEN
token_uri=$ACTIONS_ID_TOKEN_REQUEST_URL
token=$(curl -H "Authorization: bearer $token_request" "${token_uri}&audience=api://AzureADTokenExchange" | jq .value -r)
az login --service-principal -u ${{ '{{' }}secrets.AZURE_CLIENT_ID}} -t ${{ '{{' }}secrets.AZURE_TENANT_ID}} --federated-token $token --output none
# Sleep for 4 minutes
sleep 240
done &
- name: Test Notebook
working-directory: {{ gh_working_dir }}
run: |
papermill -k python {{ name }}.ipynb {{ name }}.output.ipynb
- name: Upload artifact
if: ${{ '{{' }} always() }}
uses: actions/upload-artifact@v3
with:
name: artifact
path: {{ gh_working_dir }}
{% endblock steps %}
@@ -0,0 +1,17 @@
{% extends "workflow_skeleton.yml.jinja2" %}
{% block steps %}
runs-on: ubuntu-latest
environment:
internal
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Python 3.9 environment
uses: actions/setup-python@v4
with:
python-version: "3.9"
{%- filter indent(width=2) -%}
{% for step in steps %}
{{ step.get_workflow_step() }}{% endfor %}
{%- endfilter -%}
{% endblock steps %}
@@ -0,0 +1,23 @@
{% extends "workflow_skeleton.yml.jinja2" %}
{% block steps %}
runs-on: ubuntu-latest
environment:
internal
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Python 3.9 environment
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Generate config.json for canary workspace (scheduled runs only)
if: github.event_name == 'schedule'
run: echo '${{ '{{' }} secrets.TEST_WORKSPACE_CONFIG_JSON_CANARY }}' > ${{ '{{' }} github.workspace }}/examples/config.json
- name: Generate config.json for production workspace
if: github.event_name != 'schedule'
run: echo '${{ '{{' }} secrets.EXAMPLE_WORKSPACE_CONFIG_JSON_PROD }}' > ${{ '{{' }} github.workspace }}/examples/config.json
{%- filter indent(width=2) -%}
{% for step in steps %}
{{ step.get_workflow_step() }}{% endfor %}
{%- endfilter -%}
{% endblock steps %}
@@ -0,0 +1,54 @@
{% extends "workflow_skeleton.yml.jinja2" %}
{% block steps %}
runs-on: ubuntu-latest
environment:
internal
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Python 3.9 environment
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Prepare requirements
run: |
python -m pip install --upgrade pip
pip install -r ${{ '{{' }} github.workspace }}/examples/requirements.txt
pip install -r ${{ '{{' }} github.workspace }}/examples/dev_requirements.txt
- name: Create Aoai Connection
run: pf connection create -f ${{ '{{' }} github.workspace }}/examples/connections/azure_openai.yml --set api_key="${{ '{{' }} secrets.AOAI_API_KEY_TEST }}" api_base="${{ '{{' }} secrets.AOAI_API_ENDPOINT_TEST }}"
- name: Create new Aoai Connection
run: pf connection create -f ${{ '{{' }} github.workspace }}/examples/connections/azure_openai.yml --set api_key="${{ '{{' }} secrets.AOAI_API_KEY_TEST }}" api_base="${{ '{{' }} secrets.AOAI_API_ENDPOINT_TEST }}" name=new_ai_connection
- name: Random Wait
uses: AliSajid/random-wait-action@main
with:
minimum: 1
maximum: 99
- name: Azure Login
uses: azure/login@v2
with:
subscription-id: ${{ '{{' }}secrets.AZURE_SUBSCRIPTION_ID}}
tenant-id: ${{ '{{' }}secrets.AZURE_TENANT_ID}}
client-id: ${{ '{{' }}secrets.AZURE_CLIENT_ID}}
- name: Fetch OID token every 4 mins
shell: bash
run: |
while true; do
token_request=$ACTIONS_ID_TOKEN_REQUEST_TOKEN
token_uri=$ACTIONS_ID_TOKEN_REQUEST_URL
token=$(curl -H "Authorization: bearer $token_request" "${token_uri}&audience=api://AzureADTokenExchange" | jq .value -r)
az login --service-principal -u ${{ '{{' }}secrets.AZURE_CLIENT_ID}} -t ${{ '{{' }}secrets.AZURE_TENANT_ID}} --federated-token $token --output none
# Sleep for 4 minutes
sleep 240
done &
- name: Test Notebook
working-directory: {{ gh_working_dir }}
run: |
papermill -k python {{ name }}.ipynb {{ name }}.output.ipynb -p api_key ${{ '{{' }} secrets.AOAI_API_KEY_TEST }} -p api_base ${{ '{{' }} secrets.AOAI_API_ENDPOINT_TEST }} -p api_version 2023-07-01-preview
- name: Upload artifact
if: ${{ '{{' }} always() }}
uses: actions/upload-artifact@v3
with:
name: artifact
path: {{ gh_working_dir }}
{% endblock steps %}
@@ -0,0 +1,83 @@
{% extends "workflow_skeleton.yml.jinja2" %}
{% block steps %}
runs-on: ubuntu-latest
environment:
internal
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Generate config.json for canary workspace (scheduled runs only)
if: github.event_name == 'schedule'
run: echo '${{ '{{' }} secrets.TEST_WORKSPACE_CONFIG_JSON_CANARY }}' > ${{ '{{' }} github.workspace }}/examples/config.json
- name: Generate config.json for production workspace
if: github.event_name != 'schedule'
run: echo '${{ '{{' }} secrets.EXAMPLE_WORKSPACE_CONFIG_JSON_PROD }}' > ${{ '{{' }} github.workspace }}/examples/config.json
- name: Setup Python 3.9 environment
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Prepare requirements
run: |
python -m pip install --upgrade pip
pip install -r ${{ '{{' }} github.workspace }}/examples/requirements.txt
pip install -r ${{ '{{' }} github.workspace }}/examples/dev_requirements.txt
pip freeze
pf --version
shell: bash -el {0}
- name: Prepare sample requirements
working-directory: {{ gh_working_dir }}
run: |
pip install -r requirements.txt
- name: Create Chat With PDF Custom Connection
working-directory: {{ gh_working_dir }}
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
pf connection create --file .env --name chat_with_pdf_custom_connection
fi
- name: Create AOAI Connection
working-directory: examples/connections
run: |
AOAI_API_KEY=${{ '{{' }} secrets.AOAI_API_KEY_TEST }}
AOAI_API_ENDPOINT=${{ '{{' }} secrets.AOAI_API_ENDPOINT_TEST }}
if [[ -e azure_openai.yml ]]; then
pf connection create --file azure_openai.yml --set api_key=$AOAI_API_KEY api_base=$AOAI_API_ENDPOINT
fi
- name: Random Wait
uses: AliSajid/random-wait-action@main
with:
minimum: 1
maximum: 99
- name: Azure Login
uses: azure/login@v2
with:
subscription-id: ${{ '{{' }}secrets.AZURE_SUBSCRIPTION_ID}}
tenant-id: ${{ '{{' }}secrets.AZURE_TENANT_ID}}
client-id: ${{ '{{' }}secrets.AZURE_CLIENT_ID}}
- name: Fetch OID token every 4 mins
shell: bash
run: |
while true; do
token_request=$ACTIONS_ID_TOKEN_REQUEST_TOKEN
token_uri=$ACTIONS_ID_TOKEN_REQUEST_URL
token=$(curl -H "Authorization: bearer $token_request" "${token_uri}&audience=api://AzureADTokenExchange" | jq .value -r)
az login --service-principal -u ${{ '{{' }}secrets.AZURE_CLIENT_ID}} -t ${{ '{{' }}secrets.AZURE_TENANT_ID}} --federated-token $token --output none
# Sleep for 4 minutes
sleep 240
done &
- name: Test Notebook
working-directory: {{ gh_working_dir }}
run: |
papermill -k python {{ name }}.ipynb {{ name }}.output.ipynb
- name: Upload artifact
if: ${{ '{{' }} always() }}
uses: actions/upload-artifact@v3
with:
name: artifact
path: {{ gh_working_dir }}
{% endblock steps %}
@@ -0,0 +1,58 @@
{% extends "workflow_skeleton.yml.jinja2" %}
{% block steps %}
runs-on: ubuntu-latest
environment:
internal
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Generate config.json for canary workspace (scheduled runs only)
if: github.event_name == 'schedule'
run: echo '${{ '{{' }} secrets.TEST_WORKSPACE_CONFIG_JSON_CANARY }}' > ${{ '{{' }} github.workspace }}/examples/config.json
- name: Generate config.json for production workspace
if: github.event_name != 'schedule'
run: echo '${{ '{{' }} secrets.EXAMPLE_WORKSPACE_CONFIG_JSON_PROD }}' > ${{ '{{' }} github.workspace }}/examples/config.json
- name: Setup Python 3.9 environment
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Prepare requirements
run: |
python -m pip install --upgrade pip
pip install -r ${{ '{{' }} github.workspace }}/examples/requirements.txt
pip install -r ${{ '{{' }} github.workspace }}/examples/dev_requirements.txt
- name: Create Aoai Connection
run: pf connection create -f ${{ '{{' }} github.workspace }}/examples/connections/azure_openai.yml --set api_key="${{ '{{' }} secrets.AOAI_API_KEY_TEST }}" api_base="${{ '{{' }} secrets.AOAI_API_ENDPOINT_TEST }}"
- name: Random Wait
uses: AliSajid/random-wait-action@main
with:
minimum: 1
maximum: 99
- name: Azure Login
uses: azure/login@v2
with:
subscription-id: ${{ '{{' }}secrets.AZURE_SUBSCRIPTION_ID}}
tenant-id: ${{ '{{' }}secrets.AZURE_TENANT_ID}}
client-id: ${{ '{{' }}secrets.AZURE_CLIENT_ID}}
- name: Fetch OID token every 4 mins
shell: bash
run: |
while true; do
token_request=$ACTIONS_ID_TOKEN_REQUEST_TOKEN
token_uri=$ACTIONS_ID_TOKEN_REQUEST_URL
token=$(curl -H "Authorization: bearer $token_request" "${token_uri}&audience=api://AzureADTokenExchange" | jq .value -r)
az login --service-principal -u ${{ '{{' }}secrets.AZURE_CLIENT_ID}} -t ${{ '{{' }}secrets.AZURE_TENANT_ID}} --federated-token $token --output none
# Sleep for 4 minutes
sleep 240
done &
- name: Test Notebook
working-directory: {{ gh_working_dir }}
run: |
papermill -k python {{ name }}.ipynb {{ name }}.output.ipynb
- name: Upload artifact
if: ${{ '{{' }} always() }}
uses: actions/upload-artifact@v3
with:
name: artifact
path: {{ gh_working_dir }}
{% endblock steps %}
@@ -0,0 +1,27 @@
# 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: {{ workflow_name }}
on:
schedule:
- cron: "{{ crontab }}" # {{ crontab_comment }}
pull_request:
branches: [ main ]
paths: {{ path_filter }}
workflow_dispatch:
env:
IS_IN_CI_PIPELINE: "true"
permissions:
id-token: write
contents: read
jobs:
{{ workflow_name }}:
{%- filter indent(width=4) -%}
{% block steps %}
{% endblock steps %}
{%- endfilter -%}
+445
View File
@@ -0,0 +1,445 @@
# Generate Readme file for the examples folder
import json
from pathlib import Path
import workflow_generator
import readme_generator
from jinja2 import Environment, FileSystemLoader
from ghactions_driver.readme_step import ReadmeStepsManage
from operator import itemgetter
import argparse
import sys
import os
import re
BRANCH = "main"
def get_notebook_readme_description(notebook) -> str:
"""
Set each ipynb metadata description at .metadata.description
"""
try:
# read in notebook
with open(notebook, "r", encoding="utf-8") as f:
data = json.load(f)
return data["metadata"]["description"]
except Exception:
print(f"{notebook} metadata description not set")
return ""
def get_notebook_buildDoc_description(notebook) -> str:
"""
Set each ipynb metadata description at .metadata.description
"""
try:
# read in notebook
with open(notebook, "r", encoding="utf-8") as f:
data = json.load(f)
return data["metadata"]["build_doc"]
except Exception:
print(f"{notebook} metadata build_doc not set")
return {}
def get_readme_description_first_sentence(readme) -> str:
"""
Get each readme first sentence of first paragraph
"""
try:
with open(readme, "r", encoding="utf-8") as f:
# read first line
line = f.readline()
sentence = ""
while True:
line = f.readline()
if line.startswith("#"):
line = ""
# skip metadata section
if (
line.startswith("---")
or line.startswith("resources:")
or line.startswith("title:")
or line.startswith("cloud:")
or line.startswith("category:")
or line.startswith("weight:")
):
line = ""
if line.strip() == "" and sentence != "":
break
elif "." in line:
sentence += " " + line.split(".")[0].strip()
break
else:
if sentence == "":
sentence += line.strip()
elif line.strip() != "":
sentence += " " + line.strip()
return sentence
except Exception:
print(f"Error during reading {readme}")
return ""
def write_readme(workflow_telemetries, readme_telemetries):
global BRANCH
ReadmeStepsManage.git_base_dir()
readme_file = Path(ReadmeStepsManage.git_base_dir()) / "examples/README.md"
quickstarts = {
"readmes": [],
"notebooks": [],
}
tutorials = {
"readmes": [],
"notebooks": [],
}
flex_flows = {
"readmes": [],
"notebooks": [],
}
prompty = {
"readmes": [],
"notebooks": [],
}
flows = {
"readmes": [],
"notebooks": [],
}
evaluations = {
"readmes": [],
"notebooks": [],
}
chats = {
"readmes": [],
"notebooks": [],
}
toolusecases = {
"readmes": [],
"notebooks": [],
}
connections = {
"readmes": [],
"notebooks": [],
}
for workflow_telemetry in workflow_telemetries:
notebook_name = f"{workflow_telemetry.name}.ipynb"
gh_working_dir = workflow_telemetry.gh_working_dir
pipeline_name = workflow_telemetry.workflow_name
yaml_name = f"{pipeline_name}.yml"
# For workflows, open ipynb as raw json and
# setup description at .metadata.description
description = get_notebook_readme_description(workflow_telemetry.notebook)
build_doc = get_notebook_buildDoc_description(workflow_telemetry.notebook)
notebook_path = gh_working_dir.replace("examples/", "") + f"/{notebook_name}"
default_workflow_item = {
"name": notebook_name,
"path": notebook_path,
"pipeline_name": pipeline_name,
"yaml_name": yaml_name,
"description": description,
"build_doc": build_doc,
"title": workflow_telemetry.title.capitalize()
if hasattr(workflow_telemetry, "title")
else "Empty title",
"cloud": workflow_telemetry.cloud.capitalize()
if hasattr(workflow_telemetry, "cloud")
else "NOT DEFINED",
"category": workflow_telemetry.category.capitalize()
if hasattr(workflow_telemetry, "category")
else "General",
"weight": workflow_telemetry.weight
if hasattr(workflow_telemetry, "weight")
else 0,
}
if gh_working_dir.startswith("examples/flows/standard"):
flows["notebooks"].append(default_workflow_item)
elif gh_working_dir.startswith("examples/connections"):
connections["notebooks"].append(default_workflow_item)
elif gh_working_dir.startswith("examples/flows/evaluation"):
evaluations["notebooks"].append(default_workflow_item)
elif gh_working_dir.startswith("examples/tutorials"):
if "quickstart" in notebook_name:
quickstarts["notebooks"].append(default_workflow_item)
else:
tutorials["notebooks"].append(default_workflow_item)
elif gh_working_dir.startswith("examples/flows/chat"):
chats["notebooks"].append(default_workflow_item)
elif gh_working_dir.startswith("examples/flex-flows"):
flex_flows["notebooks"].append(default_workflow_item)
elif gh_working_dir.startswith("examples/prompty"):
prompty["notebooks"].append(default_workflow_item)
elif gh_working_dir.startswith("examples/tools/use-cases"):
toolusecases["notebooks"].append(default_workflow_item)
else:
print(f"Unknown workflow type: {gh_working_dir}")
# Adjust tutorial names:
no_workflow_readmes = []
for readme_telemetry in readme_telemetries:
if readme_telemetry.readme_name.endswith("README.md"):
notebook_name = readme_telemetry.readme_folder.split("/")[-1]
else:
notebook_name = readme_telemetry.readme_name.split("/")[-1].replace(
".md", ""
)
notebook_path = readme_telemetry.readme_name.replace("examples/", "")
if not hasattr(readme_telemetry, "workflow_name"):
no_workflow_readme_item = {
"name": notebook_name,
"path": notebook_path,
"description": get_readme_description_first_sentence(
readme_telemetry.readme_name
),
"title": readme_telemetry.title.capitalize()
if hasattr(readme_telemetry, "title")
else "Empty title",
"cloud": readme_telemetry.cloud.capitalize()
if hasattr(readme_telemetry, "cloud")
else "NOT DEFINED",
"category": readme_telemetry.category.capitalize()
if hasattr(readme_telemetry, "category")
else "General",
"weight": readme_telemetry.weight
if hasattr(readme_telemetry, "weight")
else 0,
}
no_workflow_readmes.append(no_workflow_readme_item)
continue
pipeline_name = readme_telemetry.workflow_name
yaml_name = f"{readme_telemetry.workflow_name}.yml"
description = get_readme_description_first_sentence(
readme_telemetry.readme_name
)
readme_folder = readme_telemetry.readme_folder
default_readme_item = {
"name": notebook_name,
"path": notebook_path,
"pipeline_name": pipeline_name,
"yaml_name": yaml_name,
"description": description,
"title": readme_telemetry.title.capitalize()
if hasattr(readme_telemetry, "title")
else "Empty title",
"cloud": readme_telemetry.cloud.capitalize()
if hasattr(readme_telemetry, "cloud")
else "NOT DEFINED",
"category": readme_telemetry.category.capitalize()
if hasattr(readme_telemetry, "category")
else "General",
"weight": readme_telemetry.weight
if hasattr(readme_telemetry, "weight")
else 0,
}
if readme_folder.startswith("examples/flows/standard"):
flows["readmes"].append(default_readme_item)
elif readme_folder.startswith("examples/connections"):
connections["readmes"].append(default_readme_item)
elif readme_folder.startswith("examples/flows/evaluation"):
evaluations["readmes"].append(default_readme_item)
elif readme_folder.startswith("examples/tutorials"):
if "quickstart" in notebook_name:
quickstarts["readmes"].append(default_readme_item)
else:
tutorials["readmes"].append(default_readme_item)
elif readme_folder.startswith("examples/flows/chat"):
chats["readmes"].append(default_readme_item)
elif readme_folder.startswith("examples/flex-flows"):
flex_flows["readmes"].append(default_readme_item)
elif readme_folder.startswith("examples/prompty"):
prompty["readmes"].append(default_readme_item)
elif readme_folder.startswith("examples/tools/use-cases"):
toolusecases["readmes"].append(default_readme_item)
else:
print(f"Unknown workflow type: {readme_folder}")
quickstarts["notebooks"] = sorted(
quickstarts["notebooks"],
key=itemgetter("name"),
reverse=True,
)
# Debug this replacement to check if generated correctly
replacement = {
"branch": BRANCH,
"tutorials": tutorials,
"flex_flows": flex_flows,
"prompty": prompty,
"flows": flows,
"evaluations": evaluations,
"chats": chats,
"toolusecases": toolusecases,
"connections": connections,
"quickstarts": quickstarts,
}
print("writing README.md...")
env = Environment(
loader=FileSystemLoader(
Path(ReadmeStepsManage.git_base_dir())
/ "scripts/readme/ghactions_driver/readme_templates"
)
)
template = env.get_template("README.md.jinja2")
with open(readme_file, "w") as f:
f.write(template.render(replacement))
print(f"finished writing {str(readme_file)}")
# Build a table out of replacement
# |Area|Cloud|Category|Sample|Description|
new_items = []
for row in replacement.keys():
if row == "branch":
continue
for item in replacement[row]["notebooks"]:
item[
"url"
] = f"https://github.com/microsoft/promptflow/blob/main/examples/{item['path']}"
item["area"] = "SDK"
if "azure" in item["name"].lower():
item["weight"] += 1000
new_items.append(item)
for item in replacement[row]["readmes"]:
if item.get("category", "General") == "General":
print(
f"Tutorial Index: Skipping {item['path']} for not having a category"
)
continue
item[
"url"
] = f"https://github.com/microsoft/promptflow/blob/main/examples/{item['path']}"
item["area"] = "CLI"
new_items.append(item)
for item in no_workflow_readmes:
if not item["path"].startswith("tutorials"):
print(f"Tutorial Index: Skipping {item['path']} for not being in tutorials")
continue
if item.get("category", "General") == "General":
print(f"Tutorial Index: Skipping {item['path']} for not having a category")
continue
item[
"url"
] = f"https://github.com/microsoft/promptflow/blob/main/examples/{item['path']}"
item["area"] = "CLI"
new_items.append(item)
# sort new_items by category
tracing_category = sorted(
[item for item in new_items if item["category"] == "Tracing"],
key=lambda x: x["weight"],
)
prompty_category = sorted(
[item for item in new_items if item["category"] == "Prompty"],
key=lambda x: x["weight"],
)
flow_category = sorted(
[item for item in new_items if item["category"] == "Flow"],
key=lambda x: x["weight"],
)
deployment_category = sorted(
[item for item in new_items if item["category"] == "Deployment"],
key=lambda x: x["weight"],
)
rag_category = sorted(
[item for item in new_items if item["category"] == "Rag"],
key=lambda x: x["weight"],
)
real_new_items = [
*tracing_category,
*prompty_category,
*flow_category,
*deployment_category,
*rag_category,
]
tutorial_items = {"items": real_new_items}
tutorial_index_file = (
Path(ReadmeStepsManage.git_base_dir()) / "docs/tutorials/index.md"
)
template_tutorial = env.get_template("tutorial_index.md.jinja2")
with open(tutorial_index_file, "w") as f:
f.write(template_tutorial.render(tutorial_items))
print(f"Tutorial Index: finished writing {str(tutorial_index_file)}")
def main(check):
if check:
# Disable print
sys.stdout = open(os.devnull, "w")
input_glob = ["examples/**/*.ipynb"]
workflow_telemetry = []
workflow_generator.main(input_glob, workflow_telemetry, check=check)
input_glob_readme = [
"examples/flows/**/README.md",
"examples/flex-flows/**/README.md",
"examples/prompty/**/README.md",
"examples/connections/**/README.md",
"examples/tutorials/**/*.md",
"examples/tools/use-cases/**/README.md",
]
# exclude the readme since this is 3p integration folder, pipeline generation is not included
input_glob_readme_exclude = ["examples/flows/integrations/**/README.md"]
readme_telemetry = []
readme_generator.main(
input_glob_readme, input_glob_readme_exclude, readme_telemetry
)
write_readme(workflow_telemetry, readme_telemetry)
if check:
output_object = {}
for workflow in workflow_telemetry:
workflow_items = re.split(r"\[|,| |\]", workflow.path_filter)
workflow_items = list(filter(None, workflow_items))
output_object[workflow.workflow_name] = []
for item in workflow_items:
if item == "examples/*requirements.txt":
output_object[workflow.workflow_name].append(
"examples/requirements.txt"
)
output_object[workflow.workflow_name].append(
"examples/dev_requirements.txt"
)
continue
output_object[workflow.workflow_name].append(item)
for readme in readme_telemetry:
if not hasattr(readme, "workflow_name"):
continue
output_object[readme.workflow_name] = []
readme_items = re.split(r"\[|,| |\]", readme.path_filter)
readme_items = list(filter(None, readme_items))
for item in readme_items:
if item == "examples/*requirements.txt":
output_object[readme.workflow_name].append(
"examples/requirements.txt"
)
output_object[readme.workflow_name].append(
"examples/dev_requirements.txt"
)
continue
output_object[readme.workflow_name].append(item)
# enable output
sys.stdout = sys.__stdout__
return output_object
else:
return ""
if __name__ == "__main__":
# setup argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"-c", "--check", action="store_true", help="Check what file is affected"
)
args = parser.parse_args()
output = main(args.check)
print(json.dumps(output))
+90
View File
@@ -0,0 +1,90 @@
import argparse
from pathlib import Path
from functools import reduce
from ghactions_driver.readme_workflow_generate import write_readme_workflow
from ghactions_driver.readme_step import ReadmeStepsManage, ReadmeSteps
from ghactions_driver.readme_parse import readme_parser
from ghactions_driver.telemetry_obj import Telemetry
def local_filter(callback, array: [Path]):
results = []
backups = []
for index, item in enumerate(array):
result = callback(item, index, array)
# if returned true, append item to results
if result:
results.append(item)
else:
backups.append(item)
return results, backups
def no_readme_generation_filter(item: Path, index, array) -> bool:
"""
If there is no steps in the readme, then no generation
"""
try:
if 'build' in str(item): # skip build folder
return False
full_text = readme_parser(item.relative_to(ReadmeStepsManage.git_base_dir()))
if full_text == "":
return False
else:
return True
except Exception as error:
print(error)
return False # generate readme
def main(input_glob, exclude_glob=[], output_files=[]):
def set_add(p, q):
return p | q
def set_difference(p, q):
return p - q
globs = reduce(set_add, [set(Path(ReadmeStepsManage.git_base_dir()).glob(p)) for p in input_glob], set())
globs_exclude = reduce(set_difference,
[set(Path(ReadmeStepsManage.git_base_dir()).glob(p)) for p in exclude_glob],
globs)
readme_items = sorted([i for i in globs_exclude])
readme_items, no_generation_files = local_filter(no_readme_generation_filter, readme_items)
for readme in readme_items:
readme_telemetry = Telemetry()
workflow_name = readme.relative_to(ReadmeStepsManage.git_base_dir())
# Deal with readme
write_readme_workflow(workflow_name.resolve(), readme_telemetry)
ReadmeSteps.cleanup()
output_files.append(readme_telemetry)
for readme in no_generation_files:
readme_telemetry = Telemetry()
from ghactions_driver.resource_resolver import resolve_tutorial_resource
try:
resolve_tutorial_resource(
"TEMP", readme.resolve(), readme_telemetry
)
except Exception:
pass
readme_telemetry.readme_name = str(readme.relative_to(ReadmeStepsManage.git_base_dir()))
readme_telemetry.readme_folder = str(readme.relative_to(ReadmeStepsManage.git_base_dir()).parent)
output_files.append(readme_telemetry)
if __name__ == "__main__":
# setup argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"-g",
"--input-glob",
nargs="+",
help="Input Readme.md glob example 'examples/flows/**/Readme.md'",
)
args = parser.parse_args()
# call main
main(args.input_glob)
+80
View File
@@ -0,0 +1,80 @@
from promptflow._sdk._load_functions import load_yaml
from promptflow._sdk._pf_client import PFClient
from ghactions_driver.readme_step import ReadmeStepsManage
from pathlib import Path
import os
import subprocess
import sys
def install(filename):
subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", filename])
def main(input_glob_flow_dag):
# check if flow.dag.yaml contains schema field.
error = False
globs = set()
pf_client = PFClient()
for p in input_glob_flow_dag:
globs = globs | set(Path(ReadmeStepsManage.git_base_dir()).glob(p))
flow_dag_items = sorted([i for i in globs])
for file in flow_dag_items:
data = load_yaml(file)
if "$schema" not in data.keys():
print(f"{file} does not contain $schema field.")
error = True
if error is False:
new_links = []
if (Path(file).parent / "requirements.txt").exists():
# remove all promptflow lines in requirements.txt
# and save time, or else it will check all dependencies of promptflow time by time
with open(Path(file).parent / "requirements.txt", "r") as f:
lines = f.readlines()
with open(Path(file).parent / "requirements.txt", "w") as f:
for line in lines:
if "promptflow" not in line:
f.write(line)
install(Path(file).parent / "requirements.txt")
if "flow-with-symlinks" in str(file):
saved_path = os.getcwd()
os.chdir(str(file.parent))
source_folder = Path("../web-classification")
for file_name in os.listdir(source_folder):
if not Path(file_name).exists():
os.symlink(
source_folder / file_name,
file_name
)
new_links.append(file_name)
validation_result = pf_client.flows.validate(
flow=file,
)
if "flow-with-symlinks" in str(file):
for link in new_links:
os.remove(link)
os.chdir(saved_path)
print(f"VALIDATE {file}: \n" + repr(validation_result))
if not validation_result.passed:
print(f"{file} is not valid.")
error = True
if len(validation_result._warnings) > 0:
print(f"{file} has warnings.")
error = True
if error:
raise Exception("Some flow.dag.yaml validation failed.")
else:
print("All flow.dag.yaml validation completed.")
if __name__ == "__main__":
input_glob_flow_dag = [
"examples/**/flow.dag.yaml",
]
main(input_glob_flow_dag)
+192
View File
@@ -0,0 +1,192 @@
import os
import glob
import argparse
from pathlib import Path
import ntpath
import re
import hashlib
import json
from jinja2 import Environment, FileSystemLoader
from ghactions_driver.readme_step import ReadmeStepsManage
from ghactions_driver.resource_resolver import resolve_tutorial_resource
from ghactions_driver.telemetry_obj import Telemetry
def format_ipynb(notebooks):
# run code formatter on .ipynb files
for notebook in notebooks:
os.system(f"black-nb --clear-output {notebook}")
def _get_paths(paths_list):
"""
Convert the path list to unix format.
:param paths_list: The input path list.
:returns: The same list with unix-like paths.
"""
paths_list.sort()
if ntpath.sep == os.path.sep:
return [pth.replace(ntpath.sep, "/") for pth in paths_list]
return paths_list
def write_notebook_workflow(notebook, name, output_telemetry=Telemetry()):
temp_name_list = re.split(r"/|\.", notebook)
temp_name_list = [
x
for x in temp_name_list
if x != "tutorials" and x != "examples" and x != "ipynb"
]
temp_name_list = [x.replace("-", "") for x in temp_name_list]
workflow_name = "_".join(["samples"] + temp_name_list)
place_to_write = (
Path(ReadmeStepsManage.git_base_dir())
/ ".github"
/ "workflows"
/ f"{workflow_name}.yml"
)
gh_working_dir = "/".join(notebook.split("/")[:-1])
env = Environment(
loader=FileSystemLoader("./scripts/readme/ghactions_driver/workflow_templates")
)
template = env.get_template("basic_workflow.yml.jinja2")
# Schedule notebooks at different times to reduce maximum quota usage.
name_hash = int(hashlib.sha512(workflow_name.encode()).hexdigest(), 16)
schedule_minute = name_hash % 60
schedule_hour = (name_hash // 60) % 4 + 19 # 19-22 UTC
notebook_path = Path(ReadmeStepsManage.git_base_dir()) / str(notebook)
try:
# resolve tutorial resources
path_filter = resolve_tutorial_resource(workflow_name, notebook_path.resolve(), output_telemetry)
except Exception:
if "examples/tutorials" in gh_working_dir:
raise
else:
pass
if "samples_configuration" in workflow_name:
# exception, samples configuration is very simple and not related to other prompt flow examples
path_filter = (
"[ examples/configuration.ipynb, .github/workflows/samples_configuration.yml ]"
)
else:
path_filter = f"[ {gh_working_dir}/**, examples/*requirements.txt, .github/workflows/{workflow_name}.yml ]"
# these workflows require config.json to init PF/ML client
workflows_require_config_json = [
"configuration",
"runflowwithpipeline",
"quickstartazure",
"cloudrunmanagement",
"chatwithclassbasedflowazure",
]
if any(keyword in workflow_name for keyword in workflows_require_config_json):
template = env.get_template("workflow_config_json.yml.jinja2")
elif "chatwithpdf" in workflow_name:
template = env.get_template("pdf_workflow.yml.jinja2")
elif "flowasfunction" in workflow_name:
template = env.get_template("flow_as_function.yml.jinja2")
elif "traceautogengroupchat" in workflow_name:
template = env.get_template("autogen_workflow.yml.jinja2")
content = template.render(
{
"workflow_name": workflow_name,
"ci_name": "samples_notebook_ci",
"name": name,
"gh_working_dir": gh_working_dir,
"path_filter": path_filter,
"crontab": f"{schedule_minute} {schedule_hour} * * *",
"crontab_comment": f"Every day starting at {schedule_hour - 16}:{schedule_minute} BJT",
}
)
# To customize workflow, add new steps in steps.py
# make another function for special cases.
with open(place_to_write.resolve(), "w") as f:
f.write(content)
print(f"Write workflow: {place_to_write.resolve()}")
output_telemetry.workflow_name = workflow_name
output_telemetry.name = name
output_telemetry.gh_working_dir = gh_working_dir
output_telemetry.path_filter = path_filter
def write_workflows(notebooks, output_telemetries=[]):
# process notebooks
for notebook in notebooks:
# get notebook name
output_telemetry = Telemetry()
nb_path = Path(notebook)
name, _ = os.path.splitext(nb_path.parts[-1])
# write workflow file
write_notebook_workflow(notebook, name, output_telemetry)
output_telemetry.notebook = nb_path
output_telemetries.append(output_telemetry)
def local_filter(callback, array):
results = []
for index, item in enumerate(array):
result = callback(item, index, array)
# if returned true, append item to results
if result:
results.append(item)
return results
def no_readme_generation_filter(item, index, array) -> bool:
"""
Set each ipynb metadata no_readme_generation to "true" to skip readme generation
"""
try:
if item.endswith("test.ipynb"):
return False
if "examples/flows/integrations/" in item:
return False
# read in notebook
with open(item, "r", encoding="utf-8") as f:
data = json.load(f)
try:
if data["metadata"]["no_readme_generation"] is not None:
# no_readme_generate == "true", then no generation
return data["metadata"]["no_readme_generation"] != "true"
except Exception:
return True # generate readme
except Exception:
return False # not generate readme
def main(input_glob, output_files=[], check=False):
# get list of workflows
notebooks = _get_paths(
[j for i in [glob.glob(p, recursive=True) for p in input_glob] for j in i]
)
# check each workflow, get metadata.
notebooks = local_filter(no_readme_generation_filter, notebooks)
# format code
if not check:
format_ipynb(notebooks)
# write workflows
write_workflows(notebooks, output_files)
# run functions
if __name__ == "__main__":
# setup argparse
parser = argparse.ArgumentParser()
parser.add_argument(
"-g", "--input-glob", nargs="+", help="Input glob example 'examples/**/*.ipynb'"
)
args = parser.parse_args()
# call main
main(input_glob=args.input_glob)
@@ -0,0 +1,15 @@
We are pleased to announce the release of promptflow {{VERSION}}.
This release includes some new features, bug fixes, and improvements. We recommend that all users upgrade to this version.
See the [CHANGELOG](https://github.com/microsoft/promptflow/blob/release/promptflow/{{VERSION}}/src/promptflow/CHANGELOG.md) for a list of all the changes.
The release will be available via PyPI:
```bash
pip install --upgrade promptflow
```
Please report any issues with the release on the [promptflow issue tracker](https://github.com/microsoft/promptflow/issues).
Thanks to all the contributors who made this release possible.
@@ -0,0 +1,3 @@
FROM mcr.microsoft.com/azureml/promptflow/promptflow-runtime:latest
COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
@@ -0,0 +1,6 @@
PyPDF2
faiss-cpu
openai
jinja2
python-dotenv
tiktoken
+14
View File
@@ -0,0 +1,14 @@
$schema: https://azuremlschemas.azureedge.net/latest/environment.schema.json
name: chat-with-pdf
build:
path: context
inference_config:
liveness_route:
port: 8080
path: /health
readiness_route:
port: 8080
path: /health
scoring_route:
port: 8080
path: /score
+114
View File
@@ -0,0 +1,114 @@
# ---------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# ---------------------------------------------------------
import argparse
import time
from pathlib import Path
import requests
from azure.ai.ml import MLClient, load_environment
from azure.identity import AzureCliCredential
ENVIRONMENT_YAML = Path(__file__).parent / "runtime-env" / "env.yaml"
EXAMPLE_RUNTIME_NAME = "example-runtime-ci"
TEST_RUNTIME_NAME = "test-runtime-ci"
class PFSRuntimeHelper:
def __init__(self, ml_client: MLClient):
subscription_id = ml_client._operation_scope.subscription_id
resource_group_name = ml_client._operation_scope.resource_group_name
workspace_name = ml_client._operation_scope.workspace_name
location = ml_client.workspaces.get().location
self._request_url_prefix = (
f"https://{location}.api.azureml.ms/flow/api/subscriptions/{subscription_id}"
f"/resourceGroups/{resource_group_name}/providers/Microsoft.MachineLearningServices"
f"/workspaces/{workspace_name}/FlowRuntimes"
)
token = ml_client._credential.get_token("https://management.azure.com/.default").token
self._headers = {"Authorization": f"Bearer {token}"}
def update_runtime(self, name: str, env_asset_id: str) -> None:
body = {
"runtimeDescription": "Runtime hosted on compute instance, serves for examples checks.",
"environment": env_asset_id,
"instanceCount": "",
}
response = requests.put(
f"{self._request_url_prefix}/{name}",
headers=self._headers,
json=body,
)
response.raise_for_status()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--path", help="Path to config.json", type=str)
return parser.parse_args()
def init_ml_client(
subscription_id: str,
resource_group_name: str,
workspace_name: str,
) -> MLClient:
return MLClient(
credential=AzureCliCredential(),
subscription_id=subscription_id,
resource_group_name=resource_group_name,
workspace_name=workspace_name,
)
def create_environment(ml_client: MLClient) -> str:
environment = load_environment(source=ENVIRONMENT_YAML)
env = ml_client.environments.create_or_update(environment)
# have observed delay between environment creation and asset id availability
while True:
try:
ml_client.environments.get(name=env.name, version=env.version)
break
except Exception:
time.sleep(10)
# get workspace id from REST workspace object
resource_group_name = ml_client._operation_scope.resource_group_name
workspace_name = ml_client._operation_scope.workspace_name
location = ml_client.workspaces.get().location
workspace_id = ml_client._workspaces._operation.get(
resource_group_name=resource_group_name, workspace_name=workspace_name
).workspace_id
# concat environment asset id
asset_id = (
f"azureml://locations/{location}/workspaces/{workspace_id}"
f"/environments/{env.name}/versions/{env.version}"
)
return asset_id
def main(args: argparse.Namespace):
subscription_id, resource_group_name, workspace_name = MLClient._get_workspace_info(args.path)
ml_client = init_ml_client(
subscription_id=subscription_id,
resource_group_name=resource_group_name,
workspace_name=workspace_name,
)
pfs_runtime_helper = PFSRuntimeHelper(ml_client=ml_client)
print("creating environment...")
env_asset_id = create_environment(ml_client=ml_client)
print("created environment, asset id:", env_asset_id)
print("updating runtime for test...")
pfs_runtime_helper.update_runtime(name=TEST_RUNTIME_NAME, env_asset_id=env_asset_id)
print("updating runtime for example...")
pfs_runtime_helper.update_runtime(name=EXAMPLE_RUNTIME_NAME, env_asset_id=env_asset_id)
print("runtime updated!")
if __name__ == "__main__":
main(args=parse_args())
+78
View File
@@ -0,0 +1,78 @@
import argparse
import base64
import os
import io
from PIL import Image
SUPPORT_IMAGE_TYPES = ["png", "jpg", "jpeg", "bmp"]
def get_image_size(image_path):
with Image.open(image_path) as img:
width, height = img.size
return width, height
def get_image_storage_size(image_path):
file_size_bytes = os.path.getsize(image_path)
file_size_mb = file_size_bytes / (1024 * 1024)
return file_size_mb
def image_to_data_url(image_path):
with open(image_path, "rb") as image_file:
# Create a BytesIO object from the image file
image_data = io.BytesIO(image_file.read())
# Open the image and resize it
img = Image.open(image_data)
if img.size != (16, 16):
img = img.resize((16, 16), Image.Resampling.LANCZOS)
# Save the resized image to a data URL
buffered = io.BytesIO()
img.save(buffered, format="PNG")
img_str = base64.b64encode(buffered.getvalue())
data_url = 'data:image/png;base64,' + img_str.decode('utf-8')
return data_url
def create_html_file(data_uri, output_path):
html_content = '<html>\n<body>\n<img src="{}" alt="My Image">\n</body>\n</html>'.format(data_uri)
with open(output_path, 'w') as file:
file.write(html_content)
def check_image_type(image_path):
file_extension = image_path.lower().split('.')[-1]
if file_extension not in SUPPORT_IMAGE_TYPES:
raise ValueError("Only png, jpg or bmp image types are supported.")
def check_image_type_and_generate_data_url(image_path):
check_image_type(image_path)
return image_to_data_url(image_path)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--image-path",
type=str,
required=True,
help="Your image input path",
)
parser.add_argument(
"--output",
"-o",
type=str,
required=True,
help="Your image output path",
)
args = parser.parse_args()
data_url = check_image_type_and_generate_data_url(args.image_path)
print("Your image data uri: \n{}".format(data_url))
create_html_file(data_url, args.output)
+12
View File
@@ -0,0 +1,12 @@
import argparse
from utils.repo_utils import create_remote_branch_in_ADO_with_new_tool_pkg_version, deploy_test_endpoint
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--tool_pkg_version", type=str, required=True)
parser.add_argument("--ado_pat", type=str, required=True)
args = parser.parse_args()
print(f"Package version: {args.tool_pkg_version}")
branch_name = create_remote_branch_in_ADO_with_new_tool_pkg_version(args.ado_pat, args.tool_pkg_version)
deploy_test_endpoint(branch_name, ado_pat=args.ado_pat)
+1
View File
@@ -0,0 +1 @@
from .secret_exceptions import SecretNameAlreadyExistsException, SecretNameInvalidException, SecretNoSetPermissionException # noqa: F401, E501
@@ -0,0 +1,10 @@
class SecretNameAlreadyExistsException(Exception):
pass
class SecretNameInvalidException(Exception):
pass
class SecretNoSetPermissionException(Exception):
pass
@@ -0,0 +1,39 @@
import argparse
import json
from pathlib import Path
from utils.secret_manager import get_secret, get_secret_client, list_secret_names
CONNECTION_FILE_NAME = "connections.json"
PROMPTFLOW_TOOLS_ROOT = Path(__file__) / "../../../src/promptflow-tools"
CONNECTION_TPL_FILE_PATH = PROMPTFLOW_TOOLS_ROOT / "connections.json.example"
def fill_key_to_dict(template_dict, keys_dict):
if not isinstance(template_dict, dict):
return
for key, val in template_dict.items():
if isinstance(val, str) and val in keys_dict:
template_dict[key] = keys_dict[val]
continue
fill_key_to_dict(val, keys_dict)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--local", action='store_true', help="local debug mode")
args = parser.parse_args()
template_dict = json.loads(open(CONNECTION_TPL_FILE_PATH.resolve().absolute(), "r").read())
file_path = (PROMPTFLOW_TOOLS_ROOT / CONNECTION_FILE_NAME).resolve().absolute().as_posix()
print(f"file_path: {file_path}")
if not args.local:
client = get_secret_client()
all_secret_names = list_secret_names(client)
data = {secret_name: get_secret(secret_name, client) for secret_name in all_secret_names}
fill_key_to_dict(template_dict, data)
with open(file_path, "w") as f:
json.dump(template_dict, f)
+114
View File
@@ -0,0 +1,114 @@
import argparse
import ast
import importlib
import json
import os
import sys
from ruamel.yaml import YAML
sys.path.append("src/promptflow-tools")
sys.path.append(os.getcwd())
from utils.generate_tool_meta_utils import generate_custom_llm_tools_in_module_as_dict, generate_python_tools_in_module_as_dict # noqa: E402, E501
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Generate meta for a tool.")
parser.add_argument("--module", "-m", help="Module to generate tools.", type=str, required=True)
parser.add_argument("--output", "-o", help="Path to the output tool json file.", required=True)
parser.add_argument(
"--tool-type",
"-t",
help="Provide tool type: 'python' or 'custom_llm'. By default, 'python' will be set as the tool type.",
type=str,
choices=["python", "custom_llm"],
default="python",
)
parser.add_argument(
"--name",
"-n",
help="Provide a custom name for the tool. By default, the function name will be used as the tool name.",
type=str,
)
parser.add_argument("--description", "-d", help="Provide a brief description of the tool.", type=str)
parser.add_argument(
"--icon",
"-i",
type=str,
help="your tool's icon image path, if you need to show different icons in dark and light mode, \n"
"please use `icon-light` and `icon-dark` parameters. \n"
"If these icon parameters are not provided, the system will use the default icon.",
required=False)
parser.add_argument(
"--icon-light",
type=str,
help="your tool's icon image path for light mode, \n"
"if you need to show the same icon in dark and light mode, please use `icon` parameter. \n"
"If these icon parameters are not provided, the system will use the default icon.",
required=False)
parser.add_argument(
"--icon-dark",
type=str,
help="your tool's icon image path for dark mode, \n"
"if you need to show the same icon in dark and light mode, please use `icon` parameter. \n"
"If these icon parameters are not provided, the system will use the default icon.",
required=False)
parser.add_argument(
"--category",
"-c",
type=str,
help="your tool's category, if not provided, the tool will be displayed under the root folder.",
required=False)
parser.add_argument(
"--tags",
type=ast.literal_eval,
help="your tool's tags. It should be a dictionary-like string, e.g.: --tags \"{'tag1':'v1','tag2':'v2'}\".",
required=False)
args = parser.parse_args()
m = importlib.import_module(args.module)
icon = ""
if args.icon:
if args.icon_light or args.icon_dark:
raise ValueError("You cannot provide both `icon` and `icon-light` or `icon-dark`.")
from convert_image_to_data_url import check_image_type_and_generate_data_url # noqa: E402
icon = check_image_type_and_generate_data_url(args.icon)
elif args.icon_light or args.icon_dark:
if args.icon_light:
from convert_image_to_data_url import check_image_type_and_generate_data_url # noqa: E402
if isinstance(icon, dict):
icon["light"] = check_image_type_and_generate_data_url(args.icon_light)
else:
icon = {"light": check_image_type_and_generate_data_url(args.icon_light)}
if args.icon_dark:
from convert_image_to_data_url import check_image_type_and_generate_data_url # noqa: E402
if isinstance(icon, dict):
icon["dark"] = check_image_type_and_generate_data_url(args.icon_dark)
else:
icon = {"dark": check_image_type_and_generate_data_url(args.icon_dark)}
if args.tool_type == "custom_llm":
tools_dict = generate_custom_llm_tools_in_module_as_dict(
m,
name=args.name,
description=args.description,
icon=icon,
category=args.category,
tags=args.tags)
else:
tools_dict = generate_python_tools_in_module_as_dict(
m,
name=args.name,
description=args.description,
icon=icon,
category=args.category,
tags=args.tags)
# The generated dict cannot be dumped as yaml directly since yaml cannot handle string enum.
tools_dict = json.loads(json.dumps(tools_dict))
yaml = YAML()
yaml.preserve_quotes = True
yaml.indent(mapping=2, sequence=4, offset=2)
with open(args.output, "w") as f:
yaml.dump(tools_dict, f)
print(f"Tools meta generated to '{args.output}'.")
@@ -0,0 +1,150 @@
import argparse
import os
import re
from jinja2 import Environment, FileSystemLoader
def make_pythonic_variable_name(input_string):
variable_name = input_string.strip()
variable_name = re.sub(r'\W|^(?=\d)', '_', variable_name)
if not variable_name[0].isalpha() and variable_name[0] != '_':
variable_name = f'_{variable_name}'
return variable_name
def convert_tool_name_to_class_name(tool_name):
return ''.join(word.title() for word in tool_name.split('_'))
def create_file(path):
with open(path, 'w'):
pass
def create_folder(path):
os.makedirs(path, exist_ok=True)
def create_tool_project_structure(destination: str, package_name: str, tool_name: str,
function_name: str, is_class_way=False):
if is_class_way:
class_name = convert_tool_name_to_class_name(tool_name)
# Load templates
templates_abs_path = os.path.join(os.path.dirname(__file__), "templates")
file_loader = FileSystemLoader(templates_abs_path)
env = Environment(loader=file_loader)
# Create new directory
if os.path.exists(destination):
print("Destination already exists. Please choose another one.")
return
os.makedirs(destination, exist_ok=True)
# Generate setup.py
template = env.get_template('setup.py.j2')
output = template.render(package_name=package_name, tool_name=tool_name)
with open(os.path.join(destination, 'setup.py'), 'w') as f:
f.write(output)
# Generate MANIFEST.in
template = env.get_template('MANIFEST.in.j2')
output = template.render(package_name=package_name)
with open(os.path.join(destination, 'MANIFEST.in'), 'w') as f:
f.write(output)
# Create tools folder and __init__.py, tool.py inside it
tools_dir = os.path.join(destination, package_name, 'tools')
create_folder(tools_dir)
create_file(os.path.join(tools_dir, '__init__.py'))
with open(os.path.join(tools_dir, '__init__.py'), 'w') as f:
f.write('__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore\n')
# Generate tool.py
if is_class_way:
template = env.get_template('tool2.py.j2')
output = template.render(class_name=class_name, function_name=function_name)
else:
template = env.get_template('tool.py.j2')
output = template.render(function_name=function_name)
with open(os.path.join(tools_dir, f'{tool_name}.py'), 'w') as f:
f.write(output)
# Generate utils.py
template = env.get_template('utils.py.j2')
output = template.render()
with open(os.path.join(tools_dir, 'utils.py'), 'w') as f:
f.write(output)
create_file(os.path.join(destination, package_name, '__init__.py'))
with open(os.path.join(destination, package_name, '__init__.py'), 'w') as f:
f.write('__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore\n')
# Create yamls folder and __init__.py inside it
yamls_dir = os.path.join(destination, package_name, 'yamls')
create_folder(yamls_dir)
# Create tool yaml
if is_class_way:
template = env.get_template('tool2.yaml.j2')
output = template.render(package_name=package_name, tool_name=tool_name, class_name=class_name,
function_name=function_name)
else:
template = env.get_template('tool.yaml.j2')
output = template.render(package_name=package_name, tool_name=tool_name, function_name=function_name)
with open(os.path.join(yamls_dir, f'{tool_name}.yaml'), 'w') as f:
f.write(output)
# Create test folder and __init__.py inside it
tests_dir = os.path.join(destination, 'tests')
create_folder(tests_dir)
create_file(os.path.join(tests_dir, '__init__.py'))
# Create test_tool.py
if is_class_way:
template = env.get_template('test_tool2.py.j2')
output = template.render(package_name=package_name, tool_name=tool_name, class_name=class_name,
function_name=function_name)
else:
template = env.get_template('test_tool.py.j2')
output = template.render(package_name=package_name, tool_name=tool_name, function_name=function_name)
with open(os.path.join(tests_dir, f'test_{tool_name}.py'), 'w') as f:
f.write(output)
print(f'Generated tool package template for {package_name} at {destination}')
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="promptflow tool template generation arguments.")
parser.add_argument("--package-name", "-p", type=str, help="your tool package's name", required=True)
parser.add_argument("--destination", "-d", type=str,
help="target folder you want to place the generated template", required=True)
parser.add_argument("--tool-name", "-t", type=str,
help="your tool's name, by default is hello_world_tool", required=False)
parser.add_argument("--function-name", "-f", type=str,
help="your tool's function name, by default is your tool's name", required=False)
parser.add_argument("--use-class", action='store_true', help="Specify whether to use a class implementation way.")
args = parser.parse_args()
destination = args.destination
package_name = make_pythonic_variable_name(args.package_name)
package_name = package_name.lower()
if args.tool_name:
tool_name = make_pythonic_variable_name(args.tool_name)
else:
tool_name = 'hello_world_tool'
tool_name = tool_name.lower()
if args.function_name:
function_name = make_pythonic_variable_name(args.function_name)
else:
function_name = tool_name
function_name = function_name.lower()
create_tool_project_structure(destination, package_name, tool_name, function_name, args.use_class)
+1
View File
@@ -0,0 +1 @@
include {{ package_name }}/yamls/*.yaml
+14
View File
@@ -0,0 +1,14 @@
from setuptools import find_packages, setup
PACKAGE_NAME = "{{ package_name }}"
setup(
name=PACKAGE_NAME,
version="0.0.1",
description="This is my tools package",
packages=find_packages(),
entry_points={
"package_tools": ["{{ tool_name }} = {{ package_name }}.tools.utils:list_package_tools"],
},
include_package_data=True, # This line tells setuptools to include files from MANIFEST.in
)
+28
View File
@@ -0,0 +1,28 @@
import pytest
import unittest
from promptflow.connections import CustomConnection
from {{ package_name }}.tools.{{ tool_name }} import {{ function_name }}
@pytest.fixture
def my_custom_connection() -> CustomConnection:
my_custom_connection = CustomConnection(
{
"api-key" : "my-api-key",
"api-secret" : "my-api-secret",
"api-url" : "my-api-url"
}
)
return my_custom_connection
class TestTool:
def test_{{ function_name }}(self, my_custom_connection):
result = {{ function_name }}(my_custom_connection, input_text="Microsoft")
assert result == "Hello Microsoft"
# Run the unit tests
if __name__ == "__main__":
unittest.main()
+27
View File
@@ -0,0 +1,27 @@
import pytest
import unittest
from {{ package_name }}.tools.{{ tool_name }} import {{ class_name }}
@pytest.fixture
def my_url() -> str:
my_url = "https://www.bing.com"
return my_url
@pytest.fixture
def my_tool_provider(my_url) -> {{ class_name }}:
my_tool_provider = {{ class_name }}(my_url)
return my_tool_provider
class TestTool:
def test_{{ tool_name }}(self, my_tool_provider):
result = my_tool_provider.{{ function_name }}(query="Microsoft")
assert result == "Hello Microsoft"
# Run the unit tests
if __name__ == "__main__":
unittest.main()
+11
View File
@@ -0,0 +1,11 @@
from promptflow import tool
from promptflow.connections import CustomConnection
@tool
def {{ function_name }}(connection: CustomConnection, input_text: str) -> str:
# Replace with your tool code.
# Usually connection contains configs to connect to an API.
# Use CustomConnection is a dict. You can use it like: connection.api_key, connection.api_base
# Not all tools need a connection. You can remove it if you don't need it.
return "Hello " + input_text

Some files were not shown because too many files have changed in this diff Show More