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
+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)