Skip to content

Commit bd7c9df

Browse files
16oeahryalu4
andauthored
Move custom tool template scripts from Azure/promptflow to microsoft/promptflow (#74)
Co-authored-by: yalu4 <[email protected]>
1 parent 8fbc69f commit bd7c9df

File tree

10 files changed

+290
-0
lines changed

10 files changed

+290
-0
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import argparse
2+
import os
3+
import re
4+
from jinja2 import Environment, FileSystemLoader
5+
6+
7+
def make_pythonic_variable_name(input_string):
8+
variable_name = input_string.strip()
9+
variable_name = re.sub(r'\W|^(?=\d)', '_', variable_name)
10+
if not variable_name[0].isalpha() and variable_name[0] != '_':
11+
variable_name = f'_{variable_name}'
12+
13+
return variable_name
14+
15+
16+
def convert_tool_name_to_class_name(tool_name):
17+
return ''.join(word.title() for word in tool_name.split('_'))
18+
19+
20+
def create_file(path):
21+
with open(path, 'w'):
22+
pass
23+
24+
25+
def create_folder(path):
26+
os.makedirs(path, exist_ok=True)
27+
28+
29+
def create_tool_project_structure(destination: str, package_name: str, tool_name: str,
30+
function_name: str, is_class_way=False):
31+
if is_class_way:
32+
class_name = convert_tool_name_to_class_name(tool_name)
33+
34+
# Load templates
35+
file_loader = FileSystemLoader('scripts\\templates')
36+
env = Environment(loader=file_loader)
37+
38+
# Create new directory
39+
if os.path.exists(destination):
40+
print("Destination already exists. Please choose another one.")
41+
return
42+
43+
os.makedirs(destination, exist_ok=True)
44+
45+
# Generate setup.py
46+
template = env.get_template('setup.py.j2')
47+
output = template.render(package_name=package_name, tool_name=tool_name)
48+
with open(os.path.join(destination, 'setup.py'), 'w') as f:
49+
f.write(output)
50+
51+
# Generate MANIFEST.in
52+
template = env.get_template('MANIFEST.in.j2')
53+
output = template.render(package_name=package_name)
54+
with open(os.path.join(destination, 'MANIFEST.in'), 'w') as f:
55+
f.write(output)
56+
57+
# Create tools folder and __init__.py, tool.py inside it
58+
tools_dir = os.path.join(destination, package_name, 'tools')
59+
create_folder(tools_dir)
60+
create_file(os.path.join(tools_dir, '__init__.py'))
61+
with open(os.path.join(tools_dir, '__init__.py'), 'w') as f:
62+
f.write('__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore\n')
63+
64+
# Generate tool.py
65+
if is_class_way:
66+
template = env.get_template('tool2.py.j2')
67+
output = template.render(class_name=class_name, function_name=function_name)
68+
else:
69+
template = env.get_template('tool.py.j2')
70+
output = template.render(function_name=function_name)
71+
with open(os.path.join(tools_dir, f'{tool_name}.py'), 'w') as f:
72+
f.write(output)
73+
74+
# Generate utils.py
75+
template = env.get_template('utils.py.j2')
76+
output = template.render()
77+
with open(os.path.join(tools_dir, 'utils.py'), 'w') as f:
78+
f.write(output)
79+
80+
create_file(os.path.join(destination, package_name, '__init__.py'))
81+
with open(os.path.join(destination, package_name, '__init__.py'), 'w') as f:
82+
f.write('__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore\n')
83+
84+
# Create yamls folder and __init__.py inside it
85+
yamls_dir = os.path.join(destination, package_name, 'yamls')
86+
create_folder(yamls_dir)
87+
88+
# Create tool yaml
89+
if is_class_way:
90+
template = env.get_template('tool2.yaml.j2')
91+
output = template.render(package_name=package_name, tool_name=tool_name, class_name=class_name,
92+
function_name=function_name)
93+
else:
94+
template = env.get_template('tool.yaml.j2')
95+
output = template.render(package_name=package_name, tool_name=tool_name, function_name=function_name)
96+
with open(os.path.join(yamls_dir, f'{tool_name}.yaml'), 'w') as f:
97+
f.write(output)
98+
99+
# Create test folder and __init__.py inside it
100+
tests_dir = os.path.join(destination, 'tests')
101+
create_folder(tests_dir)
102+
create_file(os.path.join(tests_dir, '__init__.py'))
103+
104+
# Create test_tool.py
105+
if is_class_way:
106+
template = env.get_template('test_tool2.py.j2')
107+
output = template.render(package_name=package_name, tool_name=tool_name, class_name=class_name,
108+
function_name=function_name)
109+
else:
110+
template = env.get_template('test_tool.py.j2')
111+
output = template.render(package_name=package_name, tool_name=tool_name, function_name=function_name)
112+
with open(os.path.join(tests_dir, f'test_{tool_name}.py'), 'w') as f:
113+
f.write(output)
114+
115+
print(f'Generated tool package template for {package_name} at {destination}')
116+
117+
118+
if __name__ == "__main__":
119+
parser = argparse.ArgumentParser(description="promptflow tool template generation arguments.")
120+
121+
parser.add_argument("--package-name", "-p", type=str, help="your tool package's name", required=True)
122+
parser.add_argument("--destination", "-d", type=str,
123+
help="target folder you want to place the generated template", required=True)
124+
parser.add_argument("--tool-name", "-t", type=str,
125+
help="your tool's name, by default is hello_world_tool", required=False)
126+
parser.add_argument("--function-name", "-f", type=str,
127+
help="your tool's function name, by default is your tool's name", required=False)
128+
parser.add_argument("--use-class", action='store_true', help="Specify whether to use a class implementation way.")
129+
130+
args = parser.parse_args()
131+
132+
destination = args.destination
133+
134+
package_name = make_pythonic_variable_name(args.package_name)
135+
package_name = package_name.lower()
136+
137+
if args.tool_name:
138+
tool_name = make_pythonic_variable_name(args.tool_name)
139+
else:
140+
tool_name = 'hello_world_tool'
141+
tool_name = tool_name.lower()
142+
143+
if args.function_name:
144+
function_name = make_pythonic_variable_name(args.function_name)
145+
else:
146+
function_name = tool_name
147+
function_name = function_name.lower()
148+
149+
create_tool_project_structure(destination, package_name, tool_name, function_name, args.use_class)

scripts/templates/MANIFEST.in.j2

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
include {{ package_name }}/yamls/*.yaml

scripts/templates/setup.py.j2

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from setuptools import find_packages, setup
2+
3+
PACKAGE_NAME = "{{ package_name }}"
4+
5+
setup(
6+
name=PACKAGE_NAME,
7+
version="0.0.1",
8+
description="This is my tools package",
9+
packages=find_packages(),
10+
entry_points={
11+
"package_tools": ["{{ tool_name }} = {{ package_name }}.tools.utils:list_package_tools"],
12+
},
13+
include_package_data=True, # This line tells setuptools to include files from MANIFEST.in
14+
)

scripts/templates/test_tool.py.j2

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import pytest
2+
import unittest
3+
4+
from promptflow.connections import CustomConnection
5+
from {{ package_name }}.tools.{{ tool_name }} import {{ function_name }}
6+
7+
8+
@pytest.fixture
9+
def my_custom_connection() -> CustomConnection:
10+
my_custom_connection = CustomConnection(
11+
{
12+
"api-key" : "my-api-key",
13+
"api-secret" : "my-api-secret",
14+
"api-url" : "my-api-url"
15+
}
16+
)
17+
return my_custom_connection
18+
19+
20+
class TestTool:
21+
def test_{{ function_name }}(self, my_custom_connection):
22+
result = {{ function_name }}(my_custom_connection, input_text="Microsoft")
23+
assert result == "Hello Microsoft"
24+
25+
26+
# Run the unit tests
27+
if __name__ == "__main__":
28+
unittest.main()

scripts/templates/test_tool2.py.j2

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pytest
2+
import unittest
3+
4+
from {{ package_name }}.tools.{{ tool_name }} import {{ class_name }}
5+
6+
7+
@pytest.fixture
8+
def my_url() -> str:
9+
my_url = "https://www.bing.com"
10+
return my_url
11+
12+
13+
@pytest.fixture
14+
def my_tool_provider(my_url) -> {{ class_name }}:
15+
my_tool_provider = {{ class_name }}(my_url)
16+
return my_tool_provider
17+
18+
19+
class TestTool:
20+
def test_{{ tool_name }}(self, my_tool_provider):
21+
result = my_tool_provider.{{ function_name }}(query="Microsoft")
22+
assert result == "Hello Microsoft"
23+
24+
25+
# Run the unit tests
26+
if __name__ == "__main__":
27+
unittest.main()

scripts/templates/tool.py.j2

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from promptflow import tool
2+
from promptflow.connections import CustomConnection
3+
4+
5+
@tool
6+
def {{ function_name }}(connection: CustomConnection, input_text: str) -> str:
7+
# Replace with your tool code.
8+
# Usually connection contains configs to connect to an API.
9+
# Use CustomConnection is a dict. You can use it like: connection.api_key, connection.api_base
10+
# Not all tools need a connection. You can remove it if you don't need it.
11+
return "Hello " + input_text

scripts/templates/tool.yaml.j2

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{{ package_name }}.tools.{{ tool_name }}.{{ function_name }}:
2+
function: {{ function_name }}
3+
inputs:
4+
connection:
5+
type:
6+
- CustomConnection
7+
input_text:
8+
type:
9+
- string
10+
module: {{ package_name }}.tools.{{ tool_name }}
11+
name: Hello World Tool
12+
description: This is hello world tool
13+
type: python

scripts/templates/tool2.py.j2

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from promptflow import ToolProvider, tool
2+
import urllib.request
3+
4+
5+
class {{ class_name }}(ToolProvider):
6+
7+
def __init__(self, url: str):
8+
super().__init__()
9+
# Load content from url might be slow, so we do it in __init__ method to make sure it is loaded only once.
10+
self.content = urllib.request.urlopen(url).read()
11+
12+
@tool
13+
def {{ function_name }}(self, query: str) -> str:
14+
# Replace with your tool code.
15+
return "Hello " + query

scripts/templates/tool2.yaml.j2

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{{ package_name }}.tools.{{ tool_name }}.{{ class_name }}.{{ function_name }}:
2+
class_name: {{ class_name }}
3+
function: {{ function_name }}
4+
inputs:
5+
url:
6+
type:
7+
- string
8+
query:
9+
type:
10+
- string
11+
module: {{ package_name }}.tools.{{ tool_name }}
12+
name: Hello World Tool
13+
description: This is hello world tool
14+
type: python

scripts/templates/utils.py.j2

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import yaml
2+
from pathlib import Path
3+
4+
5+
def collect_tools_from_directory(base_dir) -> dict:
6+
tools = {}
7+
for f in Path(base_dir).glob("**/*.yaml"):
8+
with open(f, "r") as f:
9+
tools_in_file = yaml.safe_load(f)
10+
for identifier, tool in tools_in_file.items():
11+
tools[identifier] = tool
12+
return tools
13+
14+
15+
def list_package_tools():
16+
"""List package tools"""
17+
yaml_dir = Path(__file__).parents[1] / "yamls"
18+
return collect_tools_from_directory(yaml_dir)

0 commit comments

Comments
 (0)