Skip to content

Commit

Permalink
Merge branch 'main' into yigao/recording_draft
Browse files Browse the repository at this point in the history
  • Loading branch information
crazygao authored Oct 23, 2023
2 parents 8a6b456 + 8dfb81a commit f118d7b
Show file tree
Hide file tree
Showing 22 changed files with 326 additions and 22 deletions.
6 changes: 3 additions & 3 deletions docs/how-to-guides/set-global-configs.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pf config set <config_name>=<config_value>
```
For example:
```shell
pf config set connection.provider="azureml:/subscriptions/<your-subscription>/resourceGroups/<your-resourcegroup>/providers/Microsoft.MachineLearningServices/workspaces/<your-workspace>"
pf config set connection.provider="azureml://subscriptions/<your-subscription>/resourceGroups/<your-resourcegroup>/providers/Microsoft.MachineLearningServices/workspaces/<your-workspace>"
```

## Show config
Expand All @@ -25,7 +25,7 @@ After running the above config set command, show command will return the followi
```json
{
"connection": {
"provider": "azureml:/subscriptions/<your-subscription>/resourceGroups/<your-resourcegroup>/providers/Microsoft.MachineLearningServices/workspaces/<your-workspace>"
"provider": "azureml://subscriptions/<your-subscription>/resourceGroups/<your-resourcegroup>/providers/Microsoft.MachineLearningServices/workspaces/<your-workspace>"
}
}
```
Expand All @@ -40,7 +40,7 @@ Connections will be saved locally. `PFClient`(or `pf connection` commands) will
#### full azure machine learning workspace resource id
Set connection provider to a specific workspace with:
```
connection.provider=azureml:/subscriptions/<your-subscription>/resourceGroups/<your-resourcegroup>/providers/Microsoft.MachineLearningServices/workspaces/<your-workspace>
connection.provider=azureml://subscriptions/<your-subscription>/resourceGroups/<your-resourcegroup>/providers/Microsoft.MachineLearningServices/workspaces/<your-workspace>
```

When `get` or `list` connections, `PFClient`(or `pf connection` commands) will return workspace connections, and flow will be executed using these workspace connections.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from promptflow import tool
from typing import List, Union, Dict


def my_list_func(prefix: str = "", size: int = 10, **kwargs) -> List[Dict[str, Union[str, int, float, list, Dict]]]:
"""This is a dummy function to generate a list of items.
:param prefix: prefix to add to each item.
:param size: number of items to generate.
:param kwargs: other parameters.
:return: a list of items. Each item is a dict with the following keys:
- value: for backend use. Required.
- display_value: for UI display. Optional.
- hyperlink: external link. Optional.
- description: information icon tip. Optional.
"""
import random

words = ["apple", "banana", "cherry", "date", "elderberry", "fig", "grape", "honeydew", "kiwi", "lemon"]
result = []
for i in range(size):
random_word = f"{random.choice(words)}{i}"
cur_item = {
"value": random_word,
"display_value": f"{prefix}_{random_word}",
"hyperlink": f'https://www.google.com/search?q={random_word}',
"description": f"this is {i} item",
}
result.append(cur_item)

return result


@tool
def my_tool(input_text: list, input_prefix: str) -> str:
return f"Hello {input_prefix} {','.join(input_text)}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
my_tool_package.tools.tool_with_dynamic_list_input.my_tool:
function: my_tool
inputs:
input_text:
type:
- list
dynamic_list:
# UX send dynamic_list content to backend.
# specifies the function to generate dynamic list. format: <module>.<func>
func_path: my_tool_package.tools.tool_with_dynamic_list_input.my_list_func
func_kwargs:
- name: prefix # Argument name to be passed to the function
type:
- string
# if optional is not specified, default to false.
# this is for UX pre-validaton. If optional is false, but no input. UX can throw error in advanced.
optional: true
reference: ${inputs.input_prefix} # Dynamic reference to another input parameter
- name: size # Another argument name to be passed to the function
type:
- int
optional: true
default: 10
# enum and dynamic list may need below setting, default false.
# allow user to enter input `index_path` value manually.
allow_manual_entry: true
is_multi_select: true
# used to filter
input_prefix:
type:
- string
module: my_tool_package.tools.tool_with_dynamic_list_input
name: My Tool with Dynamic List Input
description: This is my tool with dynamic list input
type: python
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from my_tool_package.tools.tool_with_dynamic_list_input import my_tool, my_list_func


def test_my_tool():
result = my_tool(input_text=["apple", "banana"], input_prefix="My")
assert result == 'Hello My apple,banana'


def test_my_list_func():
result = my_list_func(prefix="My")
assert len(result) == 10
assert "value" in result[0]
3 changes: 3 additions & 0 deletions src/promptflow/promptflow/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from promptflow._core.tool import ToolProvider, tool

# control plane sdk functions
from promptflow._sdk._load_functions import load_flow

from ._sdk._pf_client import PFClient
from ._version import VERSION

Expand All @@ -19,6 +21,7 @@

__all__ = [
"PFClient",
"load_flow",
"log_metric",
"ToolProvider",
"tool",
Expand Down
6 changes: 3 additions & 3 deletions src/promptflow/promptflow/_cli/_pf/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ def add_config_set(subparsers):
""" # noqa: E501
activate_action(
name="set",
description="Set promptflow configs for current user.",
description="Set prompt flow configs for current user.",
epilog=epilog,
add_params=[add_param_set_positional] + logging_params,
subparsers=subparsers,
help_message="Set promptflow configs for current user, configs will be stored at ~/.promptflow/pf.yaml.",
help_message="Set prompt flow configs for current user, configs will be stored at ~/.promptflow/pf.yaml.",
action_param_name="sub_action",
)

Expand All @@ -48,7 +48,7 @@ def add_config_show(subparsers):

def add_config_parser(subparsers):
config_parser = subparsers.add_parser(
"config", description="A CLI tool to set promptflow configs for current user.", help="pf config"
"config", description="A CLI tool to set prompt flow configs for current user.", help="pf config"
)
subparsers = config_parser.add_subparsers()
add_config_set(subparsers)
Expand Down
46 changes: 45 additions & 1 deletion src/promptflow/promptflow/_core/tools_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import types
from functools import partial
from pathlib import Path
from typing import Callable, List, Mapping, Optional, Tuple, Union
from typing import Callable, List, Mapping, Optional, Tuple, Union, Dict

import pkg_resources
import yaml
Expand Down Expand Up @@ -170,6 +170,50 @@ def gen_tool_by_source(name, source: ToolSource, tool_type: ToolType, working_di
)


def append_workspace_triple_to_func_input_params(func_sig_params, func_input_params_dict, ws_triple_dict):
'''Append workspace triple to func input params.
:param func_sig_params: function signature parameters, full params.
:param func_input_params_dict: user input param key-values for dynamic list function.
:param ws_triple_dict: workspace triple dict, including subscription_id, resource_group_name, workspace_name.
:return: combined func input params.
'''
# append workspace triple to func input params if any below condition are met:
# 1. func signature has kwargs param.
# 2. func signature has param named 'subscription_id','resource_group_name','workspace_name'.
has_kwargs_param = any([param.kind == inspect.Parameter.VAR_KEYWORD for _, param in func_sig_params.items()])
if has_kwargs_param is False:
# keep only params that are in func signature. Or run into error when calling func.
avail_ws_info_dict = {k: v for k, v in ws_triple_dict.items() if k in set(func_sig_params.keys())}
else:
avail_ws_info_dict = ws_triple_dict

# if ws triple key is in func input params, it means user has provided value for it,
# do not expect implicit override.
combined_func_input_params = dict(avail_ws_info_dict, **func_input_params_dict)
return combined_func_input_params


def gen_dynamic_list(func_path: str, func_input_params_dict: Dict, ws_triple_dict: Dict[str, str] = None):
import importlib
import inspect

# TODO: validate func path.
module_name, func_name = func_path.rsplit('.', 1)
module = importlib.import_module(module_name)
func = getattr(module, func_name)
# get param names from func signature.
func_sig_params = inspect.signature(func).parameters
# TODO: validate if func input params are all in func signature params.
# TODO: add more tests to verify following appending logic.
combined_func_input_params = append_workspace_triple_to_func_input_params(
func_sig_params, func_input_params_dict, ws_triple_dict)
# TODO: error handling of func call.
result = func(**combined_func_input_params)
# TODO: validate response is of required format. Throw correct message if response is empty.
return result


class BuiltinsManager:
def __init__(self) -> None:
pass
Expand Down
2 changes: 2 additions & 0 deletions src/promptflow/promptflow/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
register_apis,
register_builtins,
register_connections,
gen_dynamic_list,
)
from promptflow._core.tracer import Tracer
from promptflow._sdk._constants import LOCAL_MGMT_DB_PATH
Expand Down Expand Up @@ -103,3 +104,4 @@
NotFoundException,
SqliteClient,
)
from promptflow.storage._run_storage import DefaultRunStorage
10 changes: 5 additions & 5 deletions src/promptflow/promptflow/_sdk/_serving/flow_invoker.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
update_environment_variables_with_connections,
)
from promptflow._sdk.entities._connection import _Connection
from promptflow._sdk.entities._flow import Flow
from promptflow._sdk.operations._flow_operations import FlowOperations
from promptflow._utils.multimedia_utils import convert_multimedia_data_to_base64, persist_multimedia_data
from promptflow.executor import FlowExecutor
Expand All @@ -30,8 +31,8 @@ class FlowInvoker:
"""
The invoker of a flow.
:param flow: The path of the flow.
:type flow: str
:param flow: The path of the flow, or the flow loaded by load_flow().
:type flow: [str, ~promptflow._sdk.entities._flow.Flow]
:param connection_provider: The connection provider, defaults to None
:type connection_provider: [str, Callable], optional
:param streaming: The function or bool to determine enable streaming or not, defaults to lambda: False
Expand All @@ -40,13 +41,12 @@ class FlowInvoker:

def __init__(
self,
flow: str,
flow: [str, Flow],
connection_provider: [str, Callable] = None,
streaming: Union[Callable[[], bool], bool] = False,
**kwargs,
):
self.flow_dir = flow
self.flow_entity = load_flow(self.flow_dir)
self.flow_entity = flow if isinstance(flow, Flow) else load_flow(source=flow)
self.streaming = streaming if isinstance(streaming, Callable) else lambda: streaming
# Pass dump_to path to dump flow result for extension.
self._dump_to = kwargs.get("dump_to", None)
Expand Down
23 changes: 23 additions & 0 deletions src/promptflow/promptflow/_sdk/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import promptflow
from promptflow._constants import EXTENSION_UA
from promptflow._core.tool_meta_generator import generate_tool_meta_dict_by_file
from promptflow._core.tools_manager import gen_dynamic_list
from promptflow._sdk._constants import (
DAG_FILE_NAME,
DEFAULT_ENCODING,
Expand Down Expand Up @@ -646,6 +647,28 @@ def _generate_tool_meta(
return res


def _gen_dynamic_list(function_config: Dict) -> List:
"""Generate dynamic list for a tool input.
:param function_config: function config in tool meta. Should contain'func_path' and 'func_kwargs'.
:return: a list of tool input dynamic enums.
"""
func_path = function_config.get("func_path", "")
func_kwargs = function_config.get("func_kwargs", {})
# May call azure control plane api in the custom function to list Azure resources.
# which may need Azure workspace triple.
# TODO: move this method to a common place.
from promptflow._cli._utils import get_workspace_triad_from_local

workspace_triad = get_workspace_triad_from_local()
if (workspace_triad.subscription_id and workspace_triad.resource_group_name
and workspace_triad.workspace_name):
return gen_dynamic_list(func_path, func_kwargs, workspace_triad._asdict())
# if no workspace triple available, just skip.
else:
return gen_dynamic_list(func_path, func_kwargs)


def _generate_package_tools(keys: Optional[List[str]] = None) -> dict:
import imp

Expand Down
18 changes: 18 additions & 0 deletions src/promptflow/promptflow/_sdk/entities/_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ def __init__(
super().__init__(code=code, **kwargs)

self._flow_dir, self._dag_file_name = self._get_flow_definition(self.code)
self._executable = None

@property
def flow_dag_path(self) -> Path:
Expand Down Expand Up @@ -172,3 +173,20 @@ def _dump_for_validation(self) -> Dict:
return yaml.safe_load(self.flow_dag_path.read_text(encoding=DEFAULT_ENCODING))

# endregion

# region MLFlow model requirements
@property
def inputs(self):
# This is used for build mlflow model signature.
if not self._executable:
self._executable = self._init_executable()
return {k: v.type.value for k, v in self._executable.inputs.items()}

@property
def outputs(self):
# This is used for build mlflow model signature.
if not self._executable:
self._executable = self._init_executable()
return {k: v.type.value for k, v in self._executable.outputs.items()}

# endregion
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ def node_test(
dependency_nodes_outputs=dependency_nodes_outputs,
connections=connections,
working_dir=self.flow.code,
output_sub_dir=".promptflow/intermediate",
)
record_node_run(result, self._origin_flow.code)
return result
Expand Down
10 changes: 10 additions & 0 deletions src/promptflow/promptflow/executor/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,16 @@ def __init__(self, line_number, timeout):
)


class EmptyLLMApiMapping(UserErrorException):
"""Exception raised when connection_type_to_api_mapping is empty and llm node provider can't be inferred"""

def __init__(self):
super().__init__(
message="LLM api mapping is empty, please ensure 'promptflow-tools' package has been installed.",
target=ErrorTarget.EXECUTOR,
)


class ResolveToolError(PromptflowException):
"""Exception raised when tool load failed.
Expand Down
3 changes: 3 additions & 0 deletions src/promptflow/promptflow/executor/_tool_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from promptflow.exceptions import ErrorTarget, PromptflowException, UserErrorException
from promptflow.executor._errors import (
ConnectionNotFound,
EmptyLLMApiMapping,
InvalidConnectionType,
InvalidCustomLLMTool,
InvalidSource,
Expand Down Expand Up @@ -219,6 +220,8 @@ def _get_node_connection(self, node: Node):
def _resolve_llm_node(self, node: Node, convert_input_types=False) -> ResolvedTool:
connection = self._get_node_connection(node)
if not node.provider:
if not connection_type_to_api_mapping:
raise EmptyLLMApiMapping()
# If provider is not specified, try to resolve it from connection type
node.provider = connection_type_to_api_mapping.get(type(connection).__name__)
tool: Tool = self._tool_loader.load_tool_for_llm_node(node)
Expand Down
Loading

0 comments on commit f118d7b

Please sign in to comment.