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
View File
@@ -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
+96
View File
@@ -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)
+24
View File
@@ -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]
+123
View File
@@ -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