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
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:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
include {{ package_name }}/yamls/*.yaml
|
||||
@@ -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
|
||||
)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -0,0 +1,13 @@
|
||||
{{ package_name }}.tools.{{ tool_name }}.{{ function_name }}:
|
||||
function: {{ function_name }}
|
||||
inputs:
|
||||
connection:
|
||||
type:
|
||||
- CustomConnection
|
||||
input_text:
|
||||
type:
|
||||
- string
|
||||
module: {{ package_name }}.tools.{{ tool_name }}
|
||||
name: Hello World Tool
|
||||
description: This is hello world tool
|
||||
type: python
|
||||
@@ -0,0 +1,15 @@
|
||||
from promptflow import ToolProvider, tool
|
||||
import urllib.request
|
||||
|
||||
|
||||
class {{ class_name }}(ToolProvider):
|
||||
|
||||
def __init__(self, url: str):
|
||||
super().__init__()
|
||||
# Load content from url might be slow, so we do it in __init__ method to make sure it is loaded only once.
|
||||
self.content = urllib.request.urlopen(url).read()
|
||||
|
||||
@tool
|
||||
def {{ function_name }}(self, query: str) -> str:
|
||||
# Replace with your tool code.
|
||||
return "Hello " + query
|
||||
@@ -0,0 +1,14 @@
|
||||
{{ package_name }}.tools.{{ tool_name }}.{{ class_name }}.{{ function_name }}:
|
||||
class_name: {{ class_name }}
|
||||
function: {{ function_name }}
|
||||
inputs:
|
||||
url:
|
||||
type:
|
||||
- string
|
||||
query:
|
||||
type:
|
||||
- string
|
||||
module: {{ package_name }}.tools.{{ tool_name }}
|
||||
name: Hello World Tool
|
||||
description: This is hello world tool
|
||||
type: python
|
||||
@@ -0,0 +1,19 @@
|
||||
from ruamel.yaml import YAML
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def collect_tools_from_directory(base_dir) -> dict:
|
||||
tools = {}
|
||||
yaml = YAML()
|
||||
for f in Path(base_dir).glob("**/*.yaml"):
|
||||
with open(f, "r") as f:
|
||||
tools_in_file = yaml.load(f)
|
||||
for identifier, tool in tools_in_file.items():
|
||||
tools[identifier] = tool
|
||||
return tools
|
||||
|
||||
|
||||
def list_package_tools():
|
||||
"""List package tools"""
|
||||
yaml_dir = Path(__file__).parents[1] / "yamls"
|
||||
return collect_tools_from_directory(yaml_dir)
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"stagesToSkip": [],
|
||||
"resources": {
|
||||
"repositories": {
|
||||
"self": {
|
||||
"refName": "refs/heads/dev-branch"
|
||||
}
|
||||
}
|
||||
},
|
||||
"templateParameters": {
|
||||
"deployEndpoint": "True"
|
||||
},
|
||||
"variables": {
|
||||
"model-file": {
|
||||
"value": "promptflow-gallery-tool-test.yaml",
|
||||
"isSecret": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
storage:
|
||||
storage_account: promptflowgall5817910653
|
||||
deployment:
|
||||
subscription_id: 96aede12-2f73-41cb-b983-6d11a904839b
|
||||
resource_group: promptflow
|
||||
workspace_name: promptflow-gallery
|
||||
endpoint_name: tool-test638236049123389546
|
||||
deployment_name: blue
|
||||
mt_service_endpoint: https://eastus2euap.api.azureml.ms
|
||||
@@ -0,0 +1,136 @@
|
||||
"""
|
||||
This file can generate a meta file for the given prompt template or a python file.
|
||||
"""
|
||||
import inspect
|
||||
import types
|
||||
from dataclasses import asdict
|
||||
|
||||
from utils.tool_utils import function_to_interface
|
||||
|
||||
from promptflow.contracts.tool import Tool, ToolType
|
||||
# Avoid circular dependencies: Use import 'from promptflow._internal' instead of 'from promptflow'
|
||||
# since the code here is in promptflow namespace as well
|
||||
from promptflow._internal import ToolProvider
|
||||
from promptflow.exceptions import ErrorTarget, UserErrorException
|
||||
|
||||
|
||||
def asdict_without_none(obj):
|
||||
return asdict(obj, dict_factory=lambda x: {k: v for (k, v) in x if v})
|
||||
|
||||
|
||||
def asdict_with_advanced_features_without_none(obj, **advanced_features):
|
||||
dict_without_none = asdict_without_none(obj)
|
||||
dict_without_none.update({k: v for k, v in advanced_features.items() if v})
|
||||
return dict_without_none
|
||||
|
||||
|
||||
def is_tool(f):
|
||||
if not isinstance(f, types.FunctionType):
|
||||
return False
|
||||
if not hasattr(f, "__tool"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def collect_tool_functions_in_module(m):
|
||||
tools = []
|
||||
for _, obj in inspect.getmembers(m):
|
||||
if is_tool(obj):
|
||||
# Note that the tool should be in defined in exec but not imported in exec,
|
||||
# so it should also have the same module with the current function.
|
||||
if getattr(obj, "__module__", "") != m.__name__:
|
||||
continue
|
||||
tools.append(obj)
|
||||
return tools
|
||||
|
||||
|
||||
def collect_tool_methods_in_module(m):
|
||||
tools = []
|
||||
for _, obj in inspect.getmembers(m):
|
||||
if isinstance(obj, type) and issubclass(obj, ToolProvider) and obj.__module__ == m.__name__:
|
||||
for _, method in inspect.getmembers(obj):
|
||||
if is_tool(method):
|
||||
initialize_inputs = obj.get_initialize_inputs()
|
||||
tools.append((method, initialize_inputs))
|
||||
return tools
|
||||
|
||||
|
||||
def _parse_tool_from_function(f, initialize_inputs=None, tool_type=ToolType.PYTHON, name=None, description=None):
|
||||
if hasattr(f, "__tool") and isinstance(f.__tool, Tool):
|
||||
return f.__tool
|
||||
if hasattr(f, "__original_function"):
|
||||
f = f.__original_function
|
||||
try:
|
||||
inputs, _, _ = function_to_interface(f, tool_type=tool_type, initialize_inputs=initialize_inputs)
|
||||
except Exception as e:
|
||||
raise BadFunctionInterface(f"Failed to parse interface for tool {f.__name__}, reason: {e}") from e
|
||||
class_name = None
|
||||
if "." in f.__qualname__:
|
||||
class_name = f.__qualname__.replace(f".{f.__name__}", "")
|
||||
# Construct the Tool structure
|
||||
return Tool(
|
||||
name=name or f.__qualname__,
|
||||
description=description or inspect.getdoc(f),
|
||||
inputs=inputs,
|
||||
type=tool_type,
|
||||
class_name=class_name,
|
||||
function=f.__name__,
|
||||
module=f.__module__,
|
||||
)
|
||||
|
||||
|
||||
def generate_python_tools_in_module(module, name, description):
|
||||
tool_functions = collect_tool_functions_in_module(module)
|
||||
tool_methods = collect_tool_methods_in_module(module)
|
||||
return [_parse_tool_from_function(f, name=name, description=description) for f in tool_functions] + [
|
||||
_parse_tool_from_function(f, initialize_inputs, name=name, description=description)
|
||||
for (f, initialize_inputs) in tool_methods
|
||||
]
|
||||
|
||||
|
||||
def generate_python_tools_in_module_as_dict(module, name=None, description=None, **advanced_features):
|
||||
tools = generate_python_tools_in_module(module, name, description)
|
||||
return _construct_tool_dict(tools, **advanced_features)
|
||||
|
||||
|
||||
def generate_custom_llm_tools_in_module(module, name, description):
|
||||
tool_functions = collect_tool_functions_in_module(module)
|
||||
tool_methods = collect_tool_methods_in_module(module)
|
||||
return [
|
||||
_parse_tool_from_function(f, tool_type=ToolType.CUSTOM_LLM, name=name, description=description)
|
||||
for f in tool_functions
|
||||
] + [
|
||||
_parse_tool_from_function(
|
||||
f, initialize_inputs, tool_type=ToolType.CUSTOM_LLM, name=name, description=description
|
||||
)
|
||||
for (f, initialize_inputs) in tool_methods
|
||||
]
|
||||
|
||||
|
||||
def generate_custom_llm_tools_in_module_as_dict(module, name=None, description=None, **advanced_features):
|
||||
tools = generate_custom_llm_tools_in_module(module, name, description)
|
||||
return _construct_tool_dict(tools, **advanced_features)
|
||||
|
||||
|
||||
def _construct_tool_dict(tools, **advanced_features):
|
||||
return {
|
||||
f"{t.module}.{t.class_name}.{t.function}"
|
||||
if t.class_name is not None
|
||||
else f"{t.module}.{t.function}": asdict_with_advanced_features_without_none(t, **advanced_features)
|
||||
for t in tools
|
||||
}
|
||||
|
||||
|
||||
class ToolValidationError(UserErrorException):
|
||||
"""Base exception raised when failed to validate tool."""
|
||||
|
||||
def __init__(self, message):
|
||||
super().__init__(message, target=ErrorTarget.TOOL)
|
||||
|
||||
|
||||
class PythonParsingError(ToolValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class BadFunctionInterface(PythonParsingError):
|
||||
pass
|
||||
@@ -0,0 +1,96 @@
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
scripts_dir = os.path.join(os.getcwd(), "scripts")
|
||||
index_url = "https://fdnpromptflow.azureedge.net/test-promptflow/promptflow-tools"
|
||||
ado_promptflow_repo_url_format = "https://{0}@dev.azure.com/msdata/Vienna/_git/PromptFlow"
|
||||
|
||||
|
||||
def replace_lines_from_file_under_hint(file_path, hint: str, lines_to_replace: list):
|
||||
lines_count = len(lines_to_replace)
|
||||
with open(file_path, "r") as f:
|
||||
lines = f.readlines()
|
||||
has_hint = False
|
||||
for i in range(len(lines)):
|
||||
if lines[i].strip() == hint:
|
||||
has_hint = True
|
||||
lines[i + 1 : i + 1 + lines_count] = lines_to_replace
|
||||
if not has_hint:
|
||||
lines.append(hint + "\n")
|
||||
lines += lines_to_replace
|
||||
with open(file_path, "w") as f:
|
||||
f.writelines(lines)
|
||||
|
||||
|
||||
def create_remote_branch_in_ADO_with_new_tool_pkg_version(
|
||||
ado_pat: str, tool_pkg_version: str, blob_prefix="test-promptflow"
|
||||
) -> str:
|
||||
# Clone the Azure DevOps repo
|
||||
parent_dir = os.path.abspath(os.path.join(os.getcwd(), os.pardir))
|
||||
tmp_dir = os.path.join(parent_dir, "temp")
|
||||
if not os.path.exists(tmp_dir):
|
||||
os.mkdir(tmp_dir)
|
||||
|
||||
subprocess.run(["git", "config", "--global", "user.email", "github-promptflow@dummy.com"])
|
||||
subprocess.run(["git", "config", "--global", "user.name", "github-promptflow"])
|
||||
|
||||
# Change directory to the 'tmp' directory
|
||||
os.chdir(tmp_dir)
|
||||
repo_dir = os.path.join(tmp_dir, "PromptFlow")
|
||||
repo_url = ado_promptflow_repo_url_format.format(ado_pat)
|
||||
subprocess.run(["git", "clone", repo_url, repo_dir])
|
||||
# Change directory to the repo directory
|
||||
os.chdir(repo_dir)
|
||||
# Pull the devs/test branch
|
||||
subprocess.run(["git", "reset", "."])
|
||||
subprocess.run(["git", "checkout", "."])
|
||||
subprocess.run(["git", "clean", "-f", "."])
|
||||
subprocess.run(["git", "checkout", "main"])
|
||||
subprocess.run(["git", "fetch"])
|
||||
subprocess.run(["git", "pull"])
|
||||
|
||||
# Make changes
|
||||
# 1. add test endpoint 'promptflow-gallery-tool-test.yaml'
|
||||
# 2. update tool package version
|
||||
source_file = Path(scripts_dir) / "tool/utils/configs/promptflow-gallery-tool-test.yaml"
|
||||
destination_folder = "deploy/model"
|
||||
shutil.copy(source_file, destination_folder)
|
||||
|
||||
new_lines = [
|
||||
f"--extra-index-url https://fdnpromptflow.azureedge.net/{blob_prefix}\n",
|
||||
f"promptflow_tools=={tool_pkg_version}\n",
|
||||
]
|
||||
replace_lines_from_file_under_hint(
|
||||
file_path="docker_build/linux/extra_requirements.txt",
|
||||
hint="# Prompt-flow tool package",
|
||||
lines_to_replace=new_lines,
|
||||
)
|
||||
|
||||
# Create a new remote branch
|
||||
new_branch_name = f"devs/test_tool_pkg_{tool_pkg_version}_{datetime.now().strftime('%Y%m%d%H%M%S')}"
|
||||
subprocess.run(["git", "branch", "-D", "origin", new_branch_name])
|
||||
subprocess.run(["git", "checkout", "-b", new_branch_name])
|
||||
subprocess.run(["git", "add", "."])
|
||||
subprocess.run(["git", "commit", "-m", f"Update tool package version to {tool_pkg_version}"])
|
||||
subprocess.run(["git", "push", "-u", repo_url, new_branch_name])
|
||||
|
||||
return new_branch_name
|
||||
|
||||
|
||||
def deploy_test_endpoint(branch_name: str, ado_pat: str):
|
||||
# PromptFlow-deploy-endpoint pipeline in ADO: https://msdata.visualstudio.com/Vienna/_build?definitionId=24767&_a=summary # noqa: E501
|
||||
url = "https://dev.azure.com/msdata/Vienna/_apis/pipelines/24767/runs?api-version=7.0-preview.1"
|
||||
request_body_file = Path(scripts_dir) / "tool/utils/configs/deploy-endpoint-request-body.json"
|
||||
with open(request_body_file, "r") as f:
|
||||
body = json.load(f)
|
||||
body["resources"]["repositories"]["self"]["refName"] = f"refs/heads/{branch_name}"
|
||||
print(f"request body: {body}")
|
||||
response = requests.post(url, json=body, auth=("dummy_user_name", ado_pat))
|
||||
print(response.status_code)
|
||||
print(response.content)
|
||||
@@ -0,0 +1,24 @@
|
||||
from azure.identity import AzureCliCredential
|
||||
from azure.keyvault.secrets import SecretClient
|
||||
|
||||
key_vault_name = "github-promptflow"
|
||||
KVUri = f"https://{key_vault_name}.vault.azure.net"
|
||||
|
||||
|
||||
def get_secret_client() -> SecretClient:
|
||||
credential = AzureCliCredential()
|
||||
client = SecretClient(vault_url=KVUri, credential=credential)
|
||||
|
||||
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]
|
||||
@@ -0,0 +1,123 @@
|
||||
import inspect
|
||||
from enum import Enum, EnumMeta
|
||||
from typing import Callable, Union, get_args, get_origin
|
||||
from promptflow.contracts.tool import ConnectionType, InputDefinition, ValueType, ToolType
|
||||
from promptflow.contracts.types import PromptTemplate
|
||||
|
||||
|
||||
def value_to_str(val):
|
||||
if val is inspect.Parameter.empty:
|
||||
# For empty case, default field will be skipped when dumping to json
|
||||
return None
|
||||
if val is None:
|
||||
# Dump default: "" in json to avoid UI validation error
|
||||
return ""
|
||||
if isinstance(val, Enum):
|
||||
return val.value
|
||||
return str(val)
|
||||
|
||||
|
||||
def resolve_annotation(anno) -> Union[str, list]:
|
||||
"""Resolve the union annotation to type list."""
|
||||
origin = get_origin(anno)
|
||||
if origin != Union:
|
||||
return anno
|
||||
# Optional[Type] is Union[Type, NoneType], filter NoneType out
|
||||
args = [arg for arg in get_args(anno) if arg != type(None)] # noqa: E721
|
||||
return args[0] if len(args) == 1 else args
|
||||
|
||||
|
||||
def param_to_definition(param, value_type) -> (InputDefinition, bool):
|
||||
default_value = param.default
|
||||
enum = None
|
||||
custom_type = None
|
||||
# Get value type and enum from default if no annotation
|
||||
if default_value is not inspect.Parameter.empty and value_type == inspect.Parameter.empty:
|
||||
value_type = default_value.__class__ if isinstance(default_value, Enum) else type(default_value)
|
||||
# Extract enum for enum class
|
||||
if isinstance(value_type, EnumMeta):
|
||||
enum = [str(option.value) for option in value_type]
|
||||
value_type = str
|
||||
is_connection = False
|
||||
if ConnectionType.is_connection_value(value_type):
|
||||
if ConnectionType.is_custom_strong_type(value_type):
|
||||
typ = ["CustomConnection"]
|
||||
custom_type = [value_type.__name__]
|
||||
else:
|
||||
typ = [value_type.__name__]
|
||||
is_connection = True
|
||||
elif isinstance(value_type, list):
|
||||
if not all(ConnectionType.is_connection_value(t) for t in value_type):
|
||||
typ = [ValueType.OBJECT]
|
||||
else:
|
||||
custom_connection_added = False
|
||||
typ = []
|
||||
custom_type = []
|
||||
for t in value_type:
|
||||
if ConnectionType.is_custom_strong_type(t):
|
||||
if not custom_connection_added:
|
||||
custom_connection_added = True
|
||||
typ.append("CustomConnection")
|
||||
custom_type.append(t.__name__)
|
||||
else:
|
||||
typ.append(t.__name__)
|
||||
is_connection = True
|
||||
else:
|
||||
typ = [ValueType.from_type(value_type)]
|
||||
return InputDefinition(type=typ, default=value_to_str(default_value),
|
||||
description=None, enum=enum, custom_type=custom_type), is_connection
|
||||
|
||||
|
||||
def function_to_interface(f: Callable, tool_type, initialize_inputs=None) -> tuple:
|
||||
sign = inspect.signature(f)
|
||||
all_inputs = {}
|
||||
input_defs = {}
|
||||
connection_types = []
|
||||
# Initialize the counter for prompt template
|
||||
prompt_template_count = 0
|
||||
# Collect all inputs from class and func
|
||||
if initialize_inputs:
|
||||
if any(k for k in initialize_inputs if k in sign.parameters):
|
||||
raise Exception(f'Duplicate inputs found from {f.__name__!r} and "__init__()"!')
|
||||
all_inputs = {**initialize_inputs}
|
||||
all_inputs.update(
|
||||
{
|
||||
k: v
|
||||
for k, v in sign.parameters.items()
|
||||
if k != "self" and v.kind != v.VAR_KEYWORD and v.kind != v.VAR_POSITIONAL # TODO: Handle these cases
|
||||
}
|
||||
)
|
||||
# Resolve inputs to definitions.
|
||||
for k, v in all_inputs.items():
|
||||
# Get value type from annotation
|
||||
value_type = resolve_annotation(v.annotation)
|
||||
if value_type is PromptTemplate:
|
||||
# custom llm tool has prompt template as input, skip it
|
||||
prompt_template_count += 1
|
||||
continue
|
||||
input_def, is_connection = param_to_definition(v, value_type)
|
||||
input_defs[k] = input_def
|
||||
if is_connection:
|
||||
connection_types.append(input_def.type)
|
||||
|
||||
# Check PromptTemplate input:
|
||||
# a. For custom llm tool, there should be exactly one PromptTemplate input
|
||||
# b. For python tool, PromptTemplate input is not supported
|
||||
if tool_type == ToolType.PYTHON and prompt_template_count > 0:
|
||||
raise Exception(f"Input of type 'PromptTemplate' not supported in python tool '{f.__name__}'. ")
|
||||
|
||||
if tool_type == ToolType.CUSTOM_LLM and prompt_template_count == 0:
|
||||
raise Exception(f"No input of type 'PromptTemplate' was found in custom llm tool '{f.__name__}'. ")
|
||||
|
||||
if tool_type == ToolType.CUSTOM_LLM and prompt_template_count > 1:
|
||||
raise Exception(f"Multiple inputs of type 'PromptTemplate' were found in '{f.__name__}'. "
|
||||
"Only one input of this type is expected.")
|
||||
|
||||
outputs = {}
|
||||
# Note: We don't have output definition now
|
||||
# outputs = {"output": OutputDefinition("output", [ValueType.from_type(type(sign.return_annotation))], "", True)}
|
||||
# if is_dataclass(sign.return_annotation):
|
||||
# for f in fields(sign.return_annotation):
|
||||
# outputs[f.name] = OutputDefinition(f.name, [ValueType.from_type(
|
||||
# type(getattr(sign.return_annotation, f.name)))], "", False)
|
||||
return input_defs, outputs, connection_types
|
||||
Reference in New Issue
Block a user