Files
Yao e908228d2d Support gen tool meta for package tool with custom strong type connection (#612)
# Description

Support generate tool meta yaml for custom package tool with custom
strong type connection.

Tool yaml is used to generate flow.tools.json for package tools. And
both portal and local uses the meta in flow.tools.json to display the
node interface. For portal, the node input type should be
CustomConnection. While for local extension, the node input type should
display as custom strong type connection. So we add a another field
"custom_type" for local extension to display (if no custom_type found,
read "type" field), and portal would still read the "type" field.

Test cases:
1. Tool uses custom strong type connection:
    tool:
    ```python
    from promptflow import ToolProvider, tool
    from my_tool_package.connections import MyFirstConnection
    
    
    class MyTool(ToolProvider):
        """
        Doc reference :
        """
    
        def __init__(self, connection: MyFirstConnection):
            super().__init__()
            self.connection = connection
    
        @tool
        def my_tool2(self, input_text: str) -> str:
            # Replace with your tool code.
            # Usually connection contains configs to connect to an API.
# Not all tools need a connection. You can remove it if you don't need
it.
            return input_text + self.connection.api_base

    ```
    yaml:
    ```
    my_tool_package.tools.my_tool_2.MyTool.my_tool:
      class_name: MyTool
      function: my_tool
      inputs:
        connection:
          custom_type:
          - MyFirstConnection
          type:
          - CustomConnection
        input_text:
          type:
          - string
      module: my_tool_package.tools.my_tool_2
      name: MyTool.my_tool
      type: python

    ```
2. Tool has custom strong type connection union as input:
    tool:
    ```python
    from promptflow import tool
from my_tool_package.connections import MyFirstConnection,
MySecondConnection
    from typing import Union
    
    
    @tool
def my_tool(connection: Union[MyFirstConnection, MySecondConnection],
input_text: str) -> str:
        # Replace with your tool code.
        # Usually connection contains configs to connect to an API.
# Not all tools need a connection. You can remove it if you don't need
it.
return f"connection_value is MyFirstConnection:
{str(isinstance(connection, MyFirstConnection))}"

    ```
    yaml:
    ```
    my_tool_package.tools.my_tool_1.my_tool:
      function: my_tool
      inputs:
        connection:
          custom_type:
          - MyFirstConnection
          - MySecondConnection
          type:
          - CustomConnection
        input_text:
          type:
          - string
      module: my_tool_package.tools.my_tool_1
      name: my_tool
      type: python

    ```
3. Tool uses orignal builtin CustomConnection:
    tool:
    ```python
    from promptflow import tool
    from promptflow.connections import CustomConnection
    
    
    @tool
    def my_tool3(connection: CustomConnection, input_text: str) -> str:
        # Replace with your tool code.
        # Usually connection contains configs to connect to an API.
# Not all tools need a connection. You can remove it if you don't need
it.
        return f"{input_text} {connection.api_base}"

    ```
    yaml:
    ```
    my_tool_package.tools.my_tool_3.my_tool3:
      function: my_tool3
      inputs:
        connection:
          type:
          - CustomConnection
        input_text:
          type:
          - string
      module: my_tool_package.tools.my_tool_3
      name: my_tool3
      type: python

    ```

---------

Co-authored-by: yalu4 <yalu4@microsoft.com>
2023-09-26 15:09:58 +08:00

124 lines
5.2 KiB
Python

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