Skip to content

Commit

Permalink
Automated Pull Master Merge Tue 09/26/202311:46:35.80
Browse files Browse the repository at this point in the history
  • Loading branch information
gjwoods committed Sep 26, 2023
2 parents 7678f6e + 664abdb commit 165dc9e
Show file tree
Hide file tree
Showing 53 changed files with 1,348 additions and 254 deletions.
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"tcsetattr",
"pysqlite",
"AADSTS700082",
"levelno",
"Mobius"
],
"allowCompoundWords": true
Expand Down
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Prompt flow

[![Python package](https://img.shields.io/pypi/v/promptflow)](https://pypi.org/project/promptflow/)
[![Python](https://img.shields.io/pypi/pyversions/promptflow.svg?maxAge=2592000)](https://pypi.python.org/pypi/promptflow/)
[![Python](https://img.shields.io/pypi/pyversions/promptflow.svg?maxAge=2592000)](https://pypi.python.org/pypi/promptflow/)
[![PyPI - Downloads](https://img.shields.io/pypi/dm/promptflow)](https://pypi.org/project/promptflow/)
[![CLI](https://img.shields.io/badge/CLI-reference-blue)](https://microsoft.github.io/promptflow/reference/pf-command-reference.html)
[![vsc extension](https://img.shields.io/visual-studio-marketplace/i/prompt-flow.prompt-flow?logo=Visual%20Studio&label=Extension%20)](https://marketplace.visualstudio.com/items?itemName=prompt-flow.prompt-flow)
Expand Down Expand Up @@ -85,9 +85,9 @@ Prompt Flow is a tool designed to **build high quality LLM apps**, the developme

### Develop your own LLM apps

#### VS Code Extension<img src="examples/tutorials/quick-start/media/logo_pf.png" alt="logo" width="25"/>
#### VS Code Extension<img src="examples/tutorials/quick-start/media/logo_pf.png" alt="logo" width="25"/>

We also offer a VS Code extension (a flow designer) for an interactive flow development experience with UI.
We also offer a VS Code extension (a flow designer) for an interactive flow development experience with UI.

<img src="examples/tutorials/quick-start/media/vsc.png" alt="vsc" width="1000"/>

Expand Down Expand Up @@ -139,6 +139,25 @@ For more information see the
or contact [[email protected]](mailto:[email protected])
with any additional questions or comments.

## Data Collection

The software may collect information about you and your use of the software and
send it to Microsoft if configured to enable telemetry.
Microsoft may use this information to provide services and improve our products and services.
You may turn on the telemetry as described in the repository.
There are also some features in the software that may enable you and Microsoft
to collect data from users of your applications. If you use these features, you
must comply with applicable law, including providing appropriate notices to
users of your applications together with a copy of Microsoft's privacy
statement. Our privacy statement is located at
https://go.microsoft.com/fwlink/?LinkID=824704. You can learn more about data
collection and use in the help documentation and our privacy statement. Your
use of the software operates as your consent to these practices.

### Telemetry Configuration

Telemetry collection is off by default. To opt in, please run `pf config set cli.telemetry_enabled=true` to turn it on.

## License

Copyright (c) Microsoft Corporation. All rights reserved.
Expand Down
22 changes: 19 additions & 3 deletions scripts/tool/utils/tool_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def resolve_annotation(anno) -> Union[str, list]:
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)
Expand All @@ -39,17 +40,32 @@ def param_to_definition(param, value_type) -> (InputDefinition, bool):
value_type = str
is_connection = False
if ConnectionType.is_connection_value(value_type):
typ = [value_type.__name__]
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:
typ = [t.__name__ for t in value_type]
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), is_connection
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:
Expand Down
3 changes: 3 additions & 0 deletions src/promptflow/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@
- **pf flow validate**: support validate flow
- **pf config set**: support set user-level promptflow config.
- Support workspace connection provider, usage: `pf config set connection.provider=azureml:/subscriptions/<subscription_id>/resourceGroups/<resource_group>/providers/Microsoft.MachineLearningServices/workspaces/<workspace_name>`
- **Telemetry**: enable telemetry and won't collect by default, use `pf config set cli.telemetry_enabled=true` to opt in.

### Bugs Fixed
- [Flow build] Fix flow build file name and environment variable name when connection name contains space.
- Reserve `.promptflow` folder when dump run snapshot.
- Read/write log file with encoding specified.
- Avoid inconsistent error message when executor exits abnormally.
- Align inputs & outputs row number in case partial completed run will break `pfazure run show-details`.
- Fix bug that failed to parse portal url for run data when the form is an asset id.

### Improvements
- [Executor][Internal] Improve error message with more details and actionable information.
- [SDK/CLI] `pf/pfazure run show-details`:
- Add `--max-results` option to control the number of results to display.
- Add `--all-results` option to display all results.
- Add validation for azure `PFClient` constructor in case wrong parameter is passed.

## 0.1.0b6 (2023.09.15)

Expand Down
1 change: 1 addition & 0 deletions src/promptflow/promptflow/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@
SERPAPI_API_KEY = "serpapi-api-key"
CONTENT_SAFETY_API_KEY = "content-safety-api-key"
ERROR_RESPONSE_COMPONENT_NAME = "promptflow"
EXTENSION_UA = "prompt-flow-extension"
9 changes: 6 additions & 3 deletions src/promptflow/promptflow/_core/_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ class PackageToolNotFoundError(ValidationException):
pass


class LoadToolError(ValidationException):
class MissingRequiredInputs(ValidationException):
pass


class MissingRequiredInputs(LoadToolError):
pass
class ToolLoadError(UserErrorException):
"""Exception raised when tool load failed."""

def __init__(self, module: str = None, **kwargs):
super().__init__(target=ErrorTarget.TOOL, module=module, **kwargs)


class ToolExecutionError(UserErrorException):
Expand Down
14 changes: 12 additions & 2 deletions src/promptflow/promptflow/_core/tools_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
import pkg_resources
import yaml

from promptflow._core._errors import MissingRequiredInputs, NotSupported, PackageToolNotFoundError
from promptflow._core._errors import MissingRequiredInputs, NotSupported, PackageToolNotFoundError, ToolLoadError
from promptflow._core.tool_meta_generator import (
_parse_tool_from_function,
collect_tool_function_in_module,
Expand Down Expand Up @@ -213,8 +213,18 @@ def _load_package_tool(tool_name, module_name, class_name, method_name, node_inp
message=f"Required inputs {list(missing_inputs)} are not provided for tool '{tool_name}'.",
target=ErrorTarget.EXECUTOR,
)
try:
api = getattr(provider_class(**init_inputs_values), method_name)
except Exception as ex:
error_type_and_message = f"({ex.__class__.__name__}) {ex}"
raise ToolLoadError(
module=module_name,
message_format="Failed to load package tool '{tool_name}': {error_type_and_message}",
tool_name=tool_name,
error_type_and_message=error_type_and_message,
) from ex
# Return the init_inputs to update node inputs in the afterward steps
return getattr(provider_class(**init_inputs_values), method_name), init_inputs
return api, init_inputs

@staticmethod
def load_tool_by_api_name(api_name: str) -> Tool:
Expand Down
11 changes: 9 additions & 2 deletions src/promptflow/promptflow/_sdk/_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

from promptflow._sdk._constants import LOGGER_NAME, ConnectionProvider
from promptflow._sdk._logger_factory import LoggerFactory
from promptflow._sdk._utils import dump_yaml, load_yaml
from promptflow._sdk._utils import call_from_extension, dump_yaml, load_yaml
from promptflow.exceptions import ErrorTarget, ValidationException

logger = LoggerFactory.get_logger(name=LOGGER_NAME, verbosity=logging.WARNING)
Expand All @@ -26,7 +26,8 @@ class ConfigFileNotFound(ValidationException):
class Configuration(object):

CONFIG_PATH = Path.home() / ".promptflow" / "pf.yaml"
COLLECT_TELEMETRY = "cli.collect_telemetry"
COLLECT_TELEMETRY = "cli.telemetry_enabled"
EXTENSION_COLLECT_TELEMETRY = "extension.telemetry_enabled"
INSTALLATION_ID = "cli.installation_id"
CONNECTION_PROVIDER = "connection.provider"
_instance = None
Expand All @@ -41,6 +42,10 @@ def __init__(self):
if not self._config:
self._config = {}

@property
def config(self):
return self._config

@classmethod
def get_instance(cls):
"""Use this to get instance to avoid multiple copies of same global config."""
Expand Down Expand Up @@ -142,6 +147,8 @@ def get_connection_provider(self) -> Optional[str]:

def get_telemetry_consent(self) -> Optional[bool]:
"""Get the current telemetry consent value. Return None if not configured."""
if call_from_extension():
return self.get_config(key=self.EXTENSION_COLLECT_TELEMETRY)
return self.get_config(key=self.COLLECT_TELEMETRY)

def set_telemetry_consent(self, value):
Expand Down
10 changes: 10 additions & 0 deletions src/promptflow/promptflow/_sdk/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,20 @@ class CustomStrongTypeConnectionConfigs:
PREFIX = "promptflow.connection."
TYPE = "custom_type"
MODULE = "module"
PACKAGE = "package"
PACKAGE_VERSION = "package_version"
PROMPTFLOW_TYPE_KEY = PREFIX + TYPE
PROMPTFLOW_MODULE_KEY = PREFIX + MODULE
PROMPTFLOW_PACKAGE_KEY = PREFIX + PACKAGE
PROMPTFLOW_PACKAGE_VERSION_KEY = PREFIX + PACKAGE_VERSION

@staticmethod
def is_custom_key(key):
return key not in [
CustomStrongTypeConnectionConfigs.PROMPTFLOW_TYPE_KEY,
CustomStrongTypeConnectionConfigs.PROMPTFLOW_MODULE_KEY,
CustomStrongTypeConnectionConfigs.PROMPTFLOW_PACKAGE_KEY,
CustomStrongTypeConnectionConfigs.PROMPTFLOW_PACKAGE_VERSION_KEY,
]


Expand Down Expand Up @@ -291,3 +297,7 @@ class ConnectionProvider(str, Enum):
LOCAL_SERVICE_PORT = 5000

BULK_RUN_LINE_ERRORS = "BulkRunLineErrors"

RUN_MACRO = "${run}"
VARIANT_ID_MACRO = "${variant_id}"
TIMESTAMP_MACRO = "${timestamp}"
60 changes: 58 additions & 2 deletions src/promptflow/promptflow/_sdk/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
# ---------------------------------------------------------

import collections
import hashlib
import json
import logging
import multiprocessing
import os
import re
import shutil
import sys
import tempfile
import zipfile
from contextlib import contextmanager
Expand All @@ -27,12 +29,14 @@
from marshmallow import ValidationError

import promptflow
from promptflow._constants import EXTENSION_UA
from promptflow._core.tool_meta_generator import generate_tool_meta_dict_by_file
from promptflow._sdk._constants import (
DAG_FILE_NAME,
DEFAULT_ENCODING,
FLOW_TOOLS_JSON,
FLOW_TOOLS_JSON_GEN_TIMEOUT,
HOME_PROMPT_FLOW_DIR,
KEYRING_ENCRYPTION_KEY_NAME,
KEYRING_ENCRYPTION_LOCK_PATH,
KEYRING_SYSTEM,
Expand Down Expand Up @@ -695,14 +699,33 @@ def process_node(_node, _node_path):
return flow_tools


def setup_user_agent_to_operation_context(user_agent):
def update_user_agent_from_env_var():
"""Update user agent from env var to OperationContext"""
from promptflow._core.operation_context import OperationContext

if "USER_AGENT" in os.environ:
# Append vscode or other user agent from env
OperationContext.get_instance().append_user_agent(os.environ["USER_AGENT"])


def setup_user_agent_to_operation_context(user_agent):
"""Setup user agent to OperationContext"""
from promptflow._core.operation_context import OperationContext

update_user_agent_from_env_var()
# Append user agent
OperationContext.get_instance().append_user_agent(user_agent)
context = OperationContext.get_instance()
context.append_user_agent(user_agent)
return context.get_user_agent()


def call_from_extension() -> bool:
"""Return true if current request is from extension."""
from promptflow._core.operation_context import OperationContext

update_user_agent_from_env_var()
context = OperationContext().get_instance()
return EXTENSION_UA in context.get_user_agent()


def generate_random_string(length: int = 6) -> str:
Expand Down Expand Up @@ -750,3 +773,36 @@ def get_local_connections_from_executable(executable):
# ignore when connection not found since it can be configured with env var.
raise Exception(f"Connection {n!r} required for flow {executable.name!r} is not found.")
return result


def _generate_connections_dir():
# Get Python executable path
python_path = sys.executable

# Hash the Python executable path
hash_object = hashlib.sha1(python_path.encode())
hex_dig = hash_object.hexdigest()

# Generate the connections system path using the hash
connections_dir = (HOME_PROMPT_FLOW_DIR / "envs" / hex_dig / "connections").resolve()
return connections_dir


# This function is used by extension to generate the connection files every time collect tools.
def refresh_connections_dir(connection_spec_files, connection_template_yamls):
connections_dir = _generate_connections_dir()
if os.path.isdir(connections_dir):
shutil.rmtree(connections_dir)
os.makedirs(connections_dir)

if connection_spec_files and connection_template_yamls:
for connection_name, content in connection_spec_files.items():
file_name = connection_name + ".spec.json"
with open(connections_dir / file_name, "w") as f:
json.dump(content, f, indent=2)

for connection_name, content in connection_template_yamls.items():
yaml_data = yaml.safe_load(content)
file_name = connection_name + ".template.yaml"
with open(connections_dir / file_name, "w") as f:
yaml.dump(yaml_data, f, sort_keys=False)
32 changes: 32 additions & 0 deletions src/promptflow/promptflow/_sdk/entities/_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,33 @@ def __init__(
super().__init__(configs=configs, secrets=secrets, **kwargs)
self.module = kwargs.get("module", self.__class__.__module__)
self.custom_type = custom_type or self.__class__.__name__
self.package = kwargs.get(CustomStrongTypeConnectionConfigs.PACKAGE, None)
self.package_version = kwargs.get(CustomStrongTypeConnectionConfigs.PACKAGE_VERSION, None)

def __getattribute__(self, item):
# Note: The reason to overwrite __getattribute__ instead of __getattr__ is as follows:
# Custom strong type connection is written this way:
# class MyCustomConnection(CustomStrongTypeConnection):
# api_key: Secret
# api_base: str = "This is a default value"
# api_base has a default value, my_custom_connection_instance.api_base would not trigger __getattr__.
# The default value will be returned directly instead of the real value in configs.
annotations = getattr(super().__getattribute__("__class__"), "__annotations__", {})
if item in annotations:
if annotations[item] == Secret:
return self.secrets[item]
else:
return self.configs[item]
return super().__getattribute__(item)

def __setattr__(self, key, value):
annotations = getattr(super().__getattribute__("__class__"), "__annotations__", {})
if key in annotations:
if annotations[key] == Secret:
self.secrets[key] = value
else:
self.configs[key] = value
return super().__setattr__(key, value)

def _to_orm_object(self) -> ORMConnection:
custom_connection = self._convert_to_custom()
Expand All @@ -678,6 +705,11 @@ def _convert_to_custom(self):
# update configs
self.configs.update({CustomStrongTypeConnectionConfigs.PROMPTFLOW_TYPE_KEY: self.custom_type})
self.configs.update({CustomStrongTypeConnectionConfigs.PROMPTFLOW_MODULE_KEY: self.module})
if self.package and self.package_version:
self.configs.update({CustomStrongTypeConnectionConfigs.PROMPTFLOW_PACKAGE_KEY: self.package})
self.configs.update(
{CustomStrongTypeConnectionConfigs.PROMPTFLOW_PACKAGE_VERSION_KEY: self.package_version}
)

custom_connection = CustomConnection(configs=self.configs, secrets=self.secrets, **self.kwargs)
return custom_connection
Expand Down
Loading

0 comments on commit 165dc9e

Please sign in to comment.