From f7fa6364bf302cb8793c49aed4f968e1ca795e22 Mon Sep 17 00:00:00 2001 From: gavin-aguiar Date: Fri, 26 Apr 2024 20:29:04 -0500 Subject: [PATCH 1/9] Added openai decorators --- azure/functions/decorators/constants.py | 6 + azure/functions/decorators/function_app.py | 27 ++++ azure/functions/decorators/openai.py | 174 +++++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 azure/functions/decorators/openai.py diff --git a/azure/functions/decorators/constants.py b/azure/functions/decorators/constants.py index 12d8e5ef..da4f8915 100644 --- a/azure/functions/decorators/constants.py +++ b/azure/functions/decorators/constants.py @@ -30,3 +30,9 @@ DAPR_INVOKE = "daprInvoke" DAPR_PUBLISH = "daprPublish" DAPR_BINDING = "daprBinding" +ASSISTANT_SKILLS_TRIGGER = "assistantSkillsTrigger" +TEXT_COMPLETION = "TextCompletion" +ASSISTANT_QUERY = "assistantQuery" +EMBEDDINGS = "embeddings" +SEMANTIC_SEARCH = "semanticSearch" +ASSISTANT_CREATE = "assistantCreate" diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 6166ae65..2cfd5fa7 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -35,6 +35,7 @@ parse_iterable_param_to_enums, StringifyEnumJsonEncoder from azure.functions.http import HttpRequest from .generic import GenericInputBinding, GenericTrigger, GenericOutputBinding +from .openai import AssistantSkillTrigger from .retry_policy import RetryPolicy from .function_name import FunctionName from .warmup import WarmUpTrigger @@ -1346,6 +1347,32 @@ def decorator(): return wrap + def assistant_skill_trigger(self, + arg_name: str, + task_description: str, + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs: Any) -> Callable[..., Any]: + """ + PYDocs to be added + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_trigger( + trigger=AssistantSkillTrigger( + name=arg_name, + task_description=task_description, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + class BindingApi(DecoratorApi, ABC): """Interface to extend for using existing binding decorator functions.""" diff --git a/azure/functions/decorators/openai.py b/azure/functions/decorators/openai.py new file mode 100644 index 00000000..e732525d --- /dev/null +++ b/azure/functions/decorators/openai.py @@ -0,0 +1,174 @@ +from typing import Optional + +from azure.functions.decorators.constants import ASSISTANT_SKILLS_TRIGGER, TEXT_COMPLETION, ASSISTANT_QUERY, EMBEDDINGS, \ + SEMANTIC_SEARCH, ASSISTANT_CREATE +from azure.functions.decorators.core import Trigger, DataType, InputBinding, OutputBinding +from azure.functions.decorators.utils import StringifyEnum + + +class InputType(StringifyEnum): + + RawText = "raw_text", + FilePath = "file_path" + + +class OpenAIModels(StringifyEnum): + DefaultChatModel = "gpt-3.5-turbo" + DefaultEmbeddingsModel = "text-embedding-3-small" + + +class AssistantSkillTrigger(Trigger): + + @staticmethod + def get_binding_name() -> str: + return ASSISTANT_SKILLS_TRIGGER + + def __init__(self, + name: str, + task_description: str, + data_type: Optional[DataType] = None, + **kwargs): + self.task_description = task_description + super().__init__(name=name, data_type=data_type) + + +class TextCompletionInput(InputBinding): + + @staticmethod + def get_binding_name() -> str: + return TEXT_COMPLETION + + def __init__(self, + name: str, + prompt: str, + model: Optional[str] = OpenAIModels.DefaultChatModel, + temperature: Optional[str] = "0.5", + top_p: Optional[str] = None, + max_tokens: Optional[str] = "100", + data_type: Optional[DataType] = None, + **kwargs): + self.prompt = prompt + self.model = model + self.temperature = temperature + self.top_p = top_p + self.max_tokens = max_tokens + super().__init__(name=name, data_type=data_type) + + +class AssistantQueryInput(InputBinding): + + @staticmethod + def get_binding_name(): + return ASSISTANT_QUERY + + def __init__(self, + name: str, + id: str, + timestamp_utc: str, + data_type: Optional[DataType] = None, + **kwargs): + self.id = id + self.timestamp_utc = timestamp_utc + super().__init__(name=name, data_type=data_type) + + +class EmbeddingsInput(InputBinding): + + @staticmethod + def get_binding_name() -> str: + return EMBEDDINGS + + def __init__(self, + name: str, + input: str, + input_type: InputType, + model: Optional[str] = None, + max_chunk_length: Optional[int] = 8 * 1024, + max_overlap : Optional[int] = 128, + data_type: Optional[DataType] = None, + **kwargs): + self.name = name + self.input = input + self.input_type = input_type + self.model = model + self.max_chunk_length = max_chunk_length + self.max_overlap = max_overlap + super().__init__(name=name, data_type=data_type) + + +semantic_search_system_prompt = \ + """You are a helpful assistant. You are responding to requests + from a user about internal emails and documents. You can and + should refer to the internal documents to help respond to + requests. If a user makes a request that's not covered by the + internal emails and documents, explain that you don't know the + answer or that you don't have access to the information. + + The following is a list of documents that you can refer to when + answering questions. The documents are in the format + [filename]: [text] and are separated by newlines. If you answer + a question by referencing any of the documents, please cite the + document in your answer. For example, if you answer a question + by referencing info.txt, you should add "Reference: info.txt" + to the end of your answer on a separate line.""" + + +class SemanticSearchInput(InputBinding): + + @staticmethod + def get_binding_name() -> str: + return SEMANTIC_SEARCH + + def __init__(self, + name: str, + connnection_name: str, + collection: str, + query: Optional[str] = None, + embeddings_model: Optional[str] = OpenAIModels.DefaultEmbeddingsModel, + chat_model: Optional[str] = OpenAIModels.DefaultChatModel, + system_prompt: Optional[str] = semantic_search_system_prompt, + max_knowledge_count : Optional[int] = 1, + data_type: Optional[DataType] = None, + **kwargs): + self.name = name + self.connection_name = connnection_name + self.collection = collection + self.query = query + self.embeddings_model = embeddings_model + self.chat_model = chat_model + self.system_prompt = system_prompt + self.max_knowledge_count = max_knowledge_count + super().__init__(name=name, data_type=data_type) + + +class SemanticSearchOutput(OutputBinding): + + @staticmethod + def get_binding_name() -> str: + return SEMANTIC_SEARCH + + def __init__(self, + name: str, + connection_name: str, + collection: str, + embeddings_model: Optional[str] = OpenAIModels.DefaultEmbeddingsModel, + data_type: Optional[DataType] = None, + **kwargs): + self.name = name + self.connection_name = connection_name + self.collection = collection + self.embeddings_model = embeddings_model + super().__init__(name=name, data_type=data_type) + + +class AssistantCreateOutput(OutputBinding): + + @staticmethod + def get_binding_name(): + return ASSISTANT_CREATE + + def __init__(self, + name: str, + data_type: Optional[DataType] = None, + **kwargs): + super().__init__(name=name, data_type=data_type) From 5c68af74eb74d894ff3624ca0e3b794664add7df Mon Sep 17 00:00:00 2001 From: gavin-aguiar Date: Tue, 7 May 2024 09:17:44 -0500 Subject: [PATCH 2/9] Added openai decorators --- CODEOWNERS | 1 - azure/functions/decorators/constants.py | 11 +- azure/functions/decorators/function_app.py | 232 ++++++++++++++++++++- azure/functions/decorators/openai.py | 39 +++- 4 files changed, 272 insertions(+), 11 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0877a44f..978e1173 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -11,4 +11,3 @@ # * @vrdmr @gavin-aguiar @YunchuWang @pdthummar @hallvictoria - diff --git a/azure/functions/decorators/constants.py b/azure/functions/decorators/constants.py index 8eabacea..cd13904e 100644 --- a/azure/functions/decorators/constants.py +++ b/azure/functions/decorators/constants.py @@ -28,9 +28,16 @@ DAPR_SECRET = "daprSecret" DAPR_PUBLISH = "daprPublish" DAPR_INVOKE = "daprInvoke" -DAPR_PUBLISH = "daprPublish" DAPR_BINDING = "daprBinding" ORCHESTRATION_TRIGGER = "orchestrationTrigger" ACTIVITY_TRIGGER = "activityTrigger" ENTITY_TRIGGER = "entityTrigger" -DURABLE_CLIENT = "durableClient" \ No newline at end of file +DURABLE_CLIENT = "durableClient" +ASSISTANT_SKILLS_TRIGGER = "assistantSkillsTrigger" +TEXT_COMPLETION = "TextCompletion" +ASSISTANT_QUERY = "assistantQuery" +EMBEDDINGS = "embeddings" +EMBEDDINGS_STORE = "embeddingsStore" +ASSISTANT_CREATE = "assistantCreate" +ASSISTANT_POST = "assistantPost" +SEMANTIC_SEARCH = "semanticSearch" diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index ad32472a..d5659ffa 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -35,7 +35,9 @@ parse_iterable_param_to_enums, StringifyEnumJsonEncoder from azure.functions.http import HttpRequest from .generic import GenericInputBinding, GenericTrigger, GenericOutputBinding -from .openai import AssistantSkillTrigger +from .openai import AssistantSkillTrigger, OpenAIModels, TextCompletionInput, AssistantCreateOutput, \ + AssistantQueryInput, AssistantPostInput, InputType, EmbeddingsInput, semantic_search_system_prompt, \ + SemanticSearchInput from .retry_policy import RetryPolicy from .function_name import FunctionName from .warmup import WarmUpTrigger @@ -1443,7 +1445,7 @@ def assistant_skill_trigger(self, Union[DataType, str]] = None, **kwargs: Any) -> Callable[..., Any]: """ - PYDocs to be added + TODO: PYDocs """ @self._configure_function_builder @@ -2767,6 +2769,232 @@ def decorator(): return wrap + def text_completion_input(self, + arg_name: str, + prompt: str, + model: Optional[str] = OpenAIModels.DefaultChatModel, + temperature: Optional[str] = "0.5", + top_p: Optional[str] = None, + max_tokens: Optional[str] = "100", + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs) \ + -> Callable[..., Any]: + """ + TODO: pydocs + """ + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=TextCompletionInput( + name=arg_name, + prompt=prompt, + model=model, + temperature=temperature, + top_p=top_p, + max_tokens=max_tokens, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def assistant_create_output(self, arg_name: str, + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs) \ + -> Callable[..., Any]: + """ + TODO: pydocs + """ + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=AssistantCreateOutput( + name=arg_name, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def assistant_query_input(self, + arg_name: str, + id: str, + timestamp_utc: str, + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs) \ + -> Callable[..., Any]: + """ + TODO: pydocs + """ + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=AssistantQueryInput( + name=arg_name, + id=id, + timestamp_utc=timestamp_utc, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def assistant_post_input(self, arg_name: str, + id: str, + user_message: str, + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs) \ + -> Callable[..., Any]: + """ + TODO: pydocs + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=AssistantPostInput( + name=arg_name, + id=id, + user_message=user_message, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def embeddings_input(self, + arg_name: str, + input: str, + input_type: InputType, + model: Optional[str] = None, + max_chunk_length: Optional[int] = 8 * 1024, + max_overlap: Optional[int] = 128, + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs) \ + -> Callable[..., Any]: + """ + TODO: pydocs + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=EmbeddingsInput( + name=arg_name, + input=input, + input_type=input_type, + model=model, + max_chunk_length=max_chunk_length, + max_overlap=max_overlap, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def semantic_search_input(self, + arg_name: str, + connnection_name: str, + collection: str, + query: Optional[str] = None, + embeddings_model: Optional[str] = OpenAIModels.DefaultEmbeddingsModel, + chat_model: Optional[str] = OpenAIModels.DefaultChatModel, + system_prompt: Optional[str] = semantic_search_system_prompt, + max_knowledge_count: Optional[int] = 1, + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs) \ + -> Callable[..., Any]: + """ + TODO: pydocs + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=SemanticSearchInput( + name=arg_name, + connnection_name=connnection_name, + collection=collection, + query=query, + embeddings_model=embeddings_model, + chat_model=chat_model, + system_prompt=system_prompt, + max_knowledge_count=max_knowledge_count, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + + def embeddings_store_output(self, + arg_name: str, + input: str, + input_type: InputType, + connection_name: str, + collection: str, + model: Optional[str] = OpenAIModels.DefaultEmbeddingsModel, + max_chunk_length: Optional[int] = 8 * 1024, + max_overlap: Optional[int] = 128, + data_type: Optional[ + Union[DataType, str]] = None, + **kwargs) \ + -> Callable[..., Any]: + """ + TODO: pydocs + """ + + @self._configure_function_builder + def wrap(fb): + def decorator(): + fb.add_binding( + binding=SemanticSearchInput( + name=arg_name, + input=input, + input_type=input_type, + connnection_name=connection_name, + collection=collection, + model=model, + max_chunk_length=max_chunk_length, + max_overlap=max_overlap, + data_type=parse_singular_param_to_enum(data_type, + DataType), + **kwargs)) + return fb + + return decorator() + + return wrap + class SettingsApi(DecoratorApi, ABC): """Interface to extend for using existing settings decorator in diff --git a/azure/functions/decorators/openai.py b/azure/functions/decorators/openai.py index e732525d..77d57471 100644 --- a/azure/functions/decorators/openai.py +++ b/azure/functions/decorators/openai.py @@ -1,7 +1,8 @@ from typing import Optional -from azure.functions.decorators.constants import ASSISTANT_SKILLS_TRIGGER, TEXT_COMPLETION, ASSISTANT_QUERY, EMBEDDINGS, \ - SEMANTIC_SEARCH, ASSISTANT_CREATE +from azure.functions.decorators.constants import (ASSISTANT_SKILLS_TRIGGER, TEXT_COMPLETION, ASSISTANT_QUERY, + EMBEDDINGS, EMBEDDINGS_STORE, ASSISTANT_CREATE, ASSISTANT_POST, + SEMANTIC_SEARCH) from azure.functions.decorators.core import Trigger, DataType, InputBinding, OutputBinding from azure.functions.decorators.utils import StringifyEnum @@ -141,23 +142,49 @@ def __init__(self, super().__init__(name=name, data_type=data_type) -class SemanticSearchOutput(OutputBinding): +# TODO: Waiting on the PR to get merged +class AssistantPostInput(InputBinding): + + @staticmethod + def get_binding_name(): + return ASSISTANT_POST + + def __init__(self, name: str, + id: str, + user_message: str, + data_type: Optional[DataType] = None, + **kwargs): + self.name = name + self.id = id + self.user_message = user_message + super().__init__(name=name, data_type=data_type) + + +class EmbeddingsStoreOutput(OutputBinding): @staticmethod def get_binding_name() -> str: - return SEMANTIC_SEARCH + return EMBEDDINGS_STORE def __init__(self, name: str, + input: str, + input_type: InputType, connection_name: str, collection: str, - embeddings_model: Optional[str] = OpenAIModels.DefaultEmbeddingsModel, + model: Optional[str] = OpenAIModels.DefaultEmbeddingsModel, + max_chunk_length: Optional[int] = 8 * 1024, + max_overlap: Optional[int] = 128, data_type: Optional[DataType] = None, **kwargs): self.name = name + self.input = input + self.input_type = input_type self.connection_name = connection_name self.collection = collection - self.embeddings_model = embeddings_model + self.model = model + self.max_chunk_length = max_chunk_length + self.max_overlap = max_overlap super().__init__(name=name, data_type=data_type) From 4d552facb890939110a62580cbe212c388799f04 Mon Sep 17 00:00:00 2001 From: gavin-aguiar Date: Wed, 8 May 2024 13:45:24 -0500 Subject: [PATCH 3/9] Minor type fixes --- azure/functions/decorators/function_app.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index d5659ffa..8d6475ae 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -37,7 +37,7 @@ from .generic import GenericInputBinding, GenericTrigger, GenericOutputBinding from .openai import AssistantSkillTrigger, OpenAIModels, TextCompletionInput, AssistantCreateOutput, \ AssistantQueryInput, AssistantPostInput, InputType, EmbeddingsInput, semantic_search_system_prompt, \ - SemanticSearchInput + SemanticSearchInput, EmbeddingsStoreOutput from .retry_policy import RetryPolicy from .function_name import FunctionName from .warmup import WarmUpTrigger @@ -2919,7 +2919,7 @@ def decorator(): def semantic_search_input(self, arg_name: str, - connnection_name: str, + connection_name: str, collection: str, query: Optional[str] = None, embeddings_model: Optional[str] = OpenAIModels.DefaultEmbeddingsModel, @@ -2940,7 +2940,7 @@ def decorator(): fb.add_binding( binding=SemanticSearchInput( name=arg_name, - connnection_name=connnection_name, + connnection_name=connection_name, collection=collection, query=query, embeddings_model=embeddings_model, @@ -2977,7 +2977,7 @@ def embeddings_store_output(self, def wrap(fb): def decorator(): fb.add_binding( - binding=SemanticSearchInput( + binding=EmbeddingsStoreOutput( name=arg_name, input=input, input_type=input_type, From be4efa2ed546896f1d2fea223fc8a3bd252c5cb7 Mon Sep 17 00:00:00 2001 From: Victoria Hall Date: Wed, 8 May 2024 14:22:52 -0500 Subject: [PATCH 4/9] connection_name typos --- azure/functions/decorators/function_app.py | 4 ++-- azure/functions/decorators/openai.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 8d6475ae..d877487c 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -2940,7 +2940,7 @@ def decorator(): fb.add_binding( binding=SemanticSearchInput( name=arg_name, - connnection_name=connection_name, + connection_name=connection_name, collection=collection, query=query, embeddings_model=embeddings_model, @@ -2981,7 +2981,7 @@ def decorator(): name=arg_name, input=input, input_type=input_type, - connnection_name=connection_name, + connection_name=connection_name, collection=collection, model=model, max_chunk_length=max_chunk_length, diff --git a/azure/functions/decorators/openai.py b/azure/functions/decorators/openai.py index 77d57471..5144b633 100644 --- a/azure/functions/decorators/openai.py +++ b/azure/functions/decorators/openai.py @@ -122,7 +122,7 @@ def get_binding_name() -> str: def __init__(self, name: str, - connnection_name: str, + connection_name: str, collection: str, query: Optional[str] = None, embeddings_model: Optional[str] = OpenAIModels.DefaultEmbeddingsModel, @@ -132,7 +132,7 @@ def __init__(self, data_type: Optional[DataType] = None, **kwargs): self.name = name - self.connection_name = connnection_name + self.connection_name = connection_name self.collection = collection self.query = query self.embeddings_model = embeddings_model From 05242903df841cee9230b83467f9adc0e071d3e6 Mon Sep 17 00:00:00 2001 From: gavin-aguiar Date: Thu, 9 May 2024 07:53:34 -0500 Subject: [PATCH 5/9] Added tests --- azure/functions/decorators/constants.py | 2 +- azure/functions/decorators/function_app.py | 33 +++-- azure/functions/decorators/openai.py | 56 ++++---- tests/decorators/test_openai.py | 155 +++++++++++++++++++++ 4 files changed, 211 insertions(+), 35 deletions(-) create mode 100644 tests/decorators/test_openai.py diff --git a/azure/functions/decorators/constants.py b/azure/functions/decorators/constants.py index cd13904e..0948bc58 100644 --- a/azure/functions/decorators/constants.py +++ b/azure/functions/decorators/constants.py @@ -34,7 +34,7 @@ ENTITY_TRIGGER = "entityTrigger" DURABLE_CLIENT = "durableClient" ASSISTANT_SKILLS_TRIGGER = "assistantSkillsTrigger" -TEXT_COMPLETION = "TextCompletion" +TEXT_COMPLETION = "textCompletion" ASSISTANT_QUERY = "assistantQuery" EMBEDDINGS = "embeddings" EMBEDDINGS_STORE = "embeddingsStore" diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index d877487c..0af52c06 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -35,8 +35,10 @@ parse_iterable_param_to_enums, StringifyEnumJsonEncoder from azure.functions.http import HttpRequest from .generic import GenericInputBinding, GenericTrigger, GenericOutputBinding -from .openai import AssistantSkillTrigger, OpenAIModels, TextCompletionInput, AssistantCreateOutput, \ - AssistantQueryInput, AssistantPostInput, InputType, EmbeddingsInput, semantic_search_system_prompt, \ +from .openai import AssistantSkillTrigger, OpenAIModels, TextCompletionInput, \ + AssistantCreateOutput, \ + AssistantQueryInput, AssistantPostInput, InputType, EmbeddingsInput, \ + semantic_search_system_prompt, \ SemanticSearchInput, EmbeddingsStoreOutput from .retry_policy import RetryPolicy from .function_name import FunctionName @@ -297,7 +299,9 @@ def decorator(): self._function_builders.pop() self._function_builders.append(function_builder) return function_builder + return decorator() + return wrap def _get_durable_blueprint(self): @@ -310,9 +314,10 @@ def _get_durable_blueprint(self): df_bp = df.Blueprint() return df_bp except ImportError: - error_message = "Attempted to use a Durable Functions decorator, "\ - "but the `azure-functions-durable` SDK package could not be "\ - "found. Please install `azure-functions-durable` to use "\ + error_message = \ + "Attempted to use a Durable Functions decorator, " \ + "but the `azure-functions-durable` SDK package could not be " \ + "found. Please install `azure-functions-durable` to use " \ "Durable Functions." raise Exception(error_message) @@ -2772,7 +2777,8 @@ def decorator(): def text_completion_input(self, arg_name: str, prompt: str, - model: Optional[str] = OpenAIModels.DefaultChatModel, + model: Optional[ + OpenAIModels] = OpenAIModels.DefaultChatModel, # NoQA temperature: Optional[str] = "0.5", top_p: Optional[str] = None, max_tokens: Optional[str] = "100", @@ -2783,6 +2789,7 @@ def text_completion_input(self, """ TODO: pydocs """ + @self._configure_function_builder def wrap(fb): def decorator(): @@ -2811,6 +2818,7 @@ def assistant_create_output(self, arg_name: str, """ TODO: pydocs """ + @self._configure_function_builder def wrap(fb): def decorator(): @@ -2837,6 +2845,7 @@ def assistant_query_input(self, """ TODO: pydocs """ + @self._configure_function_builder def wrap(fb): def decorator(): @@ -2922,9 +2931,12 @@ def semantic_search_input(self, connection_name: str, collection: str, query: Optional[str] = None, - embeddings_model: Optional[str] = OpenAIModels.DefaultEmbeddingsModel, - chat_model: Optional[str] = OpenAIModels.DefaultChatModel, - system_prompt: Optional[str] = semantic_search_system_prompt, + embeddings_model: Optional[ + OpenAIModels] = OpenAIModels.DefaultEmbeddingsModel, # NoQA + chat_model: Optional[ + OpenAIModels] = OpenAIModels.DefaultChatModel, # NoQA + system_prompt: Optional[ + str] = semantic_search_system_prompt, max_knowledge_count: Optional[int] = 1, data_type: Optional[ Union[DataType, str]] = None, @@ -2962,7 +2974,8 @@ def embeddings_store_output(self, input_type: InputType, connection_name: str, collection: str, - model: Optional[str] = OpenAIModels.DefaultEmbeddingsModel, + model: Optional[ + OpenAIModels] = OpenAIModels.DefaultEmbeddingsModel, # NoQA max_chunk_length: Optional[int] = 8 * 1024, max_overlap: Optional[int] = 128, data_type: Optional[ diff --git a/azure/functions/decorators/openai.py b/azure/functions/decorators/openai.py index 5144b633..863328ef 100644 --- a/azure/functions/decorators/openai.py +++ b/azure/functions/decorators/openai.py @@ -1,9 +1,14 @@ from typing import Optional -from azure.functions.decorators.constants import (ASSISTANT_SKILLS_TRIGGER, TEXT_COMPLETION, ASSISTANT_QUERY, - EMBEDDINGS, EMBEDDINGS_STORE, ASSISTANT_CREATE, ASSISTANT_POST, +from azure.functions.decorators.constants import (ASSISTANT_SKILLS_TRIGGER, + TEXT_COMPLETION, + ASSISTANT_QUERY, + EMBEDDINGS, EMBEDDINGS_STORE, + ASSISTANT_CREATE, + ASSISTANT_POST, SEMANTIC_SEARCH) -from azure.functions.decorators.core import Trigger, DataType, InputBinding, OutputBinding +from azure.functions.decorators.core import Trigger, DataType, InputBinding, \ + OutputBinding from azure.functions.decorators.utils import StringifyEnum @@ -15,7 +20,7 @@ class InputType(StringifyEnum): class OpenAIModels(StringifyEnum): DefaultChatModel = "gpt-3.5-turbo" - DefaultEmbeddingsModel = "text-embedding-3-small" + DefaultEmbeddingsModel = "text-embedding-ada-002" class AssistantSkillTrigger(Trigger): @@ -42,7 +47,7 @@ def get_binding_name() -> str: def __init__(self, name: str, prompt: str, - model: Optional[str] = OpenAIModels.DefaultChatModel, + model: Optional[OpenAIModels] = OpenAIModels.DefaultChatModel, temperature: Optional[str] = "0.5", top_p: Optional[str] = None, max_tokens: Optional[str] = "100", @@ -85,7 +90,7 @@ def __init__(self, input_type: InputType, model: Optional[str] = None, max_chunk_length: Optional[int] = 8 * 1024, - max_overlap : Optional[int] = 128, + max_overlap: Optional[int] = 128, data_type: Optional[DataType] = None, **kwargs): self.name = name @@ -97,21 +102,21 @@ def __init__(self, super().__init__(name=name, data_type=data_type) -semantic_search_system_prompt = \ - """You are a helpful assistant. You are responding to requests - from a user about internal emails and documents. You can and - should refer to the internal documents to help respond to - requests. If a user makes a request that's not covered by the - internal emails and documents, explain that you don't know the - answer or that you don't have access to the information. +semantic_search_system_prompt = \ + """You are a helpful assistant. You are responding to requests + from a user about internal emails and documents. You can and + should refer to the internal documents to help respond to + requests. If a user makes a request that's not covered by the + internal emails and documents, explain that you don't know the + answer or that you don't have access to the information. - The following is a list of documents that you can refer to when - answering questions. The documents are in the format - [filename]: [text] and are separated by newlines. If you answer - a question by referencing any of the documents, please cite the - document in your answer. For example, if you answer a question - by referencing info.txt, you should add "Reference: info.txt" - to the end of your answer on a separate line.""" + The following is a list of documents that you can refer to when + answering questions. The documents are in the format + [filename]: [text] and are separated by newlines. If you answer + a question by referencing any of the documents, please cite the + document in your answer. For example, if you answer a question + by referencing info.txt, you should add "Reference: info.txt" + to the end of your answer on a separate line.""" class SemanticSearchInput(InputBinding): @@ -125,10 +130,12 @@ def __init__(self, connection_name: str, collection: str, query: Optional[str] = None, - embeddings_model: Optional[str] = OpenAIModels.DefaultEmbeddingsModel, - chat_model: Optional[str] = OpenAIModels.DefaultChatModel, + embeddings_model: Optional[ + OpenAIModels] = OpenAIModels.DefaultEmbeddingsModel, + chat_model: Optional[ + OpenAIModels] = OpenAIModels.DefaultChatModel, system_prompt: Optional[str] = semantic_search_system_prompt, - max_knowledge_count : Optional[int] = 1, + max_knowledge_count: Optional[int] = 1, data_type: Optional[DataType] = None, **kwargs): self.name = name @@ -172,7 +179,8 @@ def __init__(self, input_type: InputType, connection_name: str, collection: str, - model: Optional[str] = OpenAIModels.DefaultEmbeddingsModel, + model: Optional[ + OpenAIModels] = OpenAIModels.DefaultEmbeddingsModel, max_chunk_length: Optional[int] = 8 * 1024, max_overlap: Optional[int] = 128, data_type: Optional[DataType] = None, diff --git a/tests/decorators/test_openai.py b/tests/decorators/test_openai.py new file mode 100644 index 00000000..65367408 --- /dev/null +++ b/tests/decorators/test_openai.py @@ -0,0 +1,155 @@ +import unittest + +from azure.functions import DataType +from azure.functions.decorators.core import BindingDirection +from azure.functions.decorators.openai import AssistantSkillTrigger, \ + TextCompletionInput, OpenAIModels, AssistantQueryInput, EmbeddingsInput, \ + AssistantCreateOutput, SemanticSearchInput, EmbeddingsStoreOutput + + +class TestOpenAI(unittest.TestCase): + + def test_assistant_skills_trigger_valid_creation(self): + trigger = AssistantSkillTrigger(name="test", + task_description="test_description", + data_type=DataType.UNDEFINED, + dummy_field="dummy") + self.assertEqual(trigger.get_binding_name(), + "assistantSkillsTrigger") + self.assertEqual( + trigger.get_dict_repr(), {"name": "test", + "taskDescription": "test_description", + "dataType": DataType.UNDEFINED, + 'type': 'assistantSkillsTrigger', + 'dummyField': 'dummy', + "direction": BindingDirection.IN, + }) + + def test_text_completion_input_valid_creation(self): + input = TextCompletionInput(name="test", + prompt="test_prompt", + temperature="1", + max_tokens="1", + data_type=DataType.UNDEFINED, + model=OpenAIModels.DefaultChatModel, + dummy_field="dummy") + self.assertEqual(input.get_binding_name(), + "textCompletion") + self.assertEqual(input.get_dict_repr(), + {"name": "test", + "temperature": "1", + "maxTokens": "1", + 'type': 'textCompletion', + "dataType": DataType.UNDEFINED, + "dummyField": "dummy", + "prompt": "test_prompt", + "direction": BindingDirection.IN, + "model": OpenAIModels.DefaultChatModel + }) + + def test_assistant_query_input_valid_creation(self): + input = AssistantQueryInput(name="test", + timestamp_utc="timestamp_utc", + data_type=DataType.UNDEFINED, + id="test_id", + type="assistantQueryInput", + dummy_field="dummy") + self.assertEqual(input.get_binding_name(), + "assistantQuery") + self.assertEqual(input.get_dict_repr(), + {"name": "test", + "timestampUtc": "timestamp_utc", + "dataType": DataType.UNDEFINED, + "direction": BindingDirection.IN, + "type": "assistantQuery", + "id": "test_id", + "dummyField": "dummy" + }) + + def test_embeddings_input_valid_creation(self): + input = EmbeddingsInput(name="test", + data_type=DataType.UNDEFINED, + input="test_input", + input_type="test_input_type", + model="test_model", + max_overlap=1, + max_chunk_length=1, + dummy_field="dummy") + self.assertEqual(input.get_binding_name(), + "embeddings") + self.assertEqual(input.get_dict_repr(), + {"name": "test", + "type": "embeddings", + "dataType": DataType.UNDEFINED, + "input": "test_input", + "inputType": "test_input_type", + "model": "test_model", + "maxOverlap": 1, + "maxChunkLength": 1, + "direction": BindingDirection.IN, + "dummyField": "dummy"}) + + def test_assistant_create_output_valid_creation(self): + output = AssistantCreateOutput(name="test", + data_type=DataType.UNDEFINED) + self.assertEqual(output.get_binding_name(), + "assistantCreate") + self.assertEqual(output.get_dict_repr(), + {"name": "test", + "dataType": DataType.UNDEFINED, + "direction": BindingDirection.OUT, + "type": "assistantCreate"}) + + def test_semantic_search_input_valid_creation(self): + input = SemanticSearchInput(name="test", + data_type=DataType.UNDEFINED, + chat_model=OpenAIModels.DefaultChatModel, + embeddings_model=OpenAIModels.DefaultEmbeddingsModel, # NoQA + collection="test_collection", + connection_name="test_connection", + system_prompt="test_prompt", + query="test_query", + max_knowledge_count=1, + dummy_field="dummy_field") + self.assertEqual(input.get_binding_name(), + "semanticSearch") + self.assertEqual(input.get_dict_repr(), + {"name": "test", + "dataType": DataType.UNDEFINED, + "direction": BindingDirection.IN, + "dummyField": "dummy_field", + "chatModel": OpenAIModels.DefaultChatModel, + "embeddingsModel": OpenAIModels.DefaultEmbeddingsModel, # NoQA + "type": "semanticSearch", + "collection": "test_collection", + "connectionName": "test_connection", + "systemPrompt": "test_prompt", + "maxKnowledgeCount": 1, + "query": "test_query"}) + + def test_embeddings_store_output_valid_creation(self): + output = EmbeddingsStoreOutput(name="test", + data_type=DataType.UNDEFINED, + input="test_input", + input_type="test_input_type", + connection_name="test_connection", + max_overlap=1, + max_chunk_length=1, + collection="test_collection", + model=OpenAIModels.DefaultChatModel, + dummy_field="dummy_field") + self.assertEqual(output.get_binding_name(), + "embeddingsStore") + self.assertEqual(output.get_dict_repr(), + {"name": "test", + "dataType": DataType.UNDEFINED, + "direction": BindingDirection.OUT, + "dummyField": "dummy_field", + "input": "test_input", + "inputType": "test_input_type", + "collection": "test_collection", + "model": OpenAIModels.DefaultChatModel, + "connectionName": "test_connection", + "maxOverlap": 1, + "maxChunkLength": 1, + "type": "embeddingsStore"}) From ee2e8ba7c85663b07d1caf896be2b157f3de2134 Mon Sep 17 00:00:00 2001 From: gavin-aguiar Date: Fri, 10 May 2024 02:44:11 -0500 Subject: [PATCH 6/9] Added pydocs and minor fixes --- azure/functions/decorators/function_app.py | 215 ++++++++++++++++++--- azure/functions/decorators/openai.py | 6 +- tests/decorators/test_openai.py | 26 ++- 3 files changed, 212 insertions(+), 35 deletions(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 0af52c06..5f58f81f 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -1450,16 +1450,40 @@ def assistant_skill_trigger(self, Union[DataType, str]] = None, **kwargs: Any) -> Callable[..., Any]: """ - TODO: PYDocs - """ + Assistants build on top of the chat functionality to provide assistants + with custom skills defined as functions. This internally uses the + function calling feature OpenAIs GPT models to select which functions + to invoke and when. + Ref: https://platform.openai.com/docs/guides/function-calling + + You can define functions that can be triggered by assistants by using + + the `assistantSkillTrigger` trigger binding. These functions are + invoked by the extension when a assistant signals that it would like + to invoke a function in response to a user prompt. + + The name of the function, the description provided by the trigger, + and the parameter name are all hints that the underlying language model + use to determine when and how to invoke an assistant function. + + :param arg_name: The name of the variable that represents + :param function_description: The description of the assistant function, + which is provided to the model. + :param data_type: Defines how Functions runtime should treat the + parameter value. + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json. + + :return: Decorator function. + """ @self._configure_function_builder def wrap(fb): def decorator(): fb.add_trigger( trigger=AssistantSkillTrigger( name=arg_name, - task_description=task_description, + function_description=task_description, data_type=parse_singular_param_to_enum(data_type, DataType), **kwargs)) @@ -2576,8 +2600,6 @@ def dapr_state_output(self, :param arg_name: The name of the variable that represents DaprState output object in function code. - :param arg_name: The name of the variable that represents DaprState - input object in function code. :param state_store: State store containing the state for keys. :param key: The name of the key. :param dapr_address: Dapr address, it is optional field, by default @@ -2631,8 +2653,6 @@ def dapr_invoke_output(self, :param arg_name: The name of the variable that represents DaprState output object in function code. - :param arg_name: The name of the variable that represents DaprState - input object in function code. :param app_id: The dapr app name to invoke. :param method_name: The method name of the app to invoke. :param http_verb: The http verb of the app to invoke. @@ -2687,8 +2707,6 @@ def dapr_publish_output(self, :param arg_name: The name of the variable that represents DaprState output object in function code. - :param arg_name: The name of the variable that represents DaprState - input object in function code. :param pub_sub_name: The pub/sub name to publish to. :param topic: The name of the topic to publish to. :param dapr_address: Dapr address, it is optional field, by default @@ -2742,8 +2760,6 @@ def dapr_binding_output(self, :param arg_name: The name of the variable that represents DaprState output object in function code. - :param arg_name: The name of the variable that represents DaprState - input object in function code. :param binding_name: The configured name of the binding. :param operation: The configured operation. :param dapr_address: Dapr address, it is optional field, by default @@ -2777,17 +2793,49 @@ def decorator(): def text_completion_input(self, arg_name: str, prompt: str, - model: Optional[ - OpenAIModels] = OpenAIModels.DefaultChatModel, # NoQA + model: Optional[OpenAIModels] = OpenAIModels.DefaultChatModel, # NoQA temperature: Optional[str] = "0.5", top_p: Optional[str] = None, max_tokens: Optional[str] = "100", - data_type: Optional[ - Union[DataType, str]] = None, + data_type: Optional[Union[DataType, str]] = None, **kwargs) \ -> Callable[..., Any]: """ - TODO: pydocs + The textCompletion input binding can be used to invoke the + OpenAI Chat Completions API and return the results to the function. + + Ref: https://platform.openai.com/docs/guides/text-generation/chat-completions-vs-completions # NoQA + + The examples below define "who is" HTTP-triggered functions with a + hardcoded `"who is {name}?"` prompt, where `{name}` is the substituted + with the value in the HTTP request path. The OpenAI input binding + invokes the OpenAI GPT endpoint to surface the answer to the prompt to + the function, which then returns the result text as the response + content. + + :param arg_name: The name of the variable that represents DaprState + output object in function code. + :param prompt: The prompt to generate completions for, encoded as a + string. + :param model: the ID of the model to use. + :param temperature: The sampling temperature to use, between 0 and 2. + Higher values like 0.8 will make the output more random, while lower + values like 0.2 will make it more focused and deterministic. + :param top_p: An alternative to sampling with temperature, called + nucleus sampling, where the model considers the results of the tokens + with top_p probability mass. So 0.1 means only the tokens comprising + the top 10% probability mass are considered. It's generally recommend + to use this or temperature + :param max_tokens: The maximum number of tokens to generate in the + completion. The token count of your prompt plus max_tokens cannot + exceed the model's context length. Most models have a context length of + 2048 tokens (except for the newest models, which support 4096). + :param data_type: Defines how Functions runtime should treat the + parameter value + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. """ @self._configure_function_builder @@ -2816,7 +2864,17 @@ def assistant_create_output(self, arg_name: str, **kwargs) \ -> Callable[..., Any]: """ - TODO: pydocs + The assistantCreate output binding creates a new assistant with a + specified system prompt. + + :param arg_name: The name of the variable that represents DaprState + output object in function code. + :param data_type: Defines how Functions runtime should treat the + parameter value + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. """ @self._configure_function_builder @@ -2843,7 +2901,21 @@ def assistant_query_input(self, **kwargs) \ -> Callable[..., Any]: """ - TODO: pydocs + The assistantQuery input binding fetches the assistant history and + passes it to the function. + + :param arg_name: The name of the variable that represents DaprState + output object in function code. + :param timestamp_utc: the timestamp of the earliest message in the chat + history to fetch. The timestamp should be in ISO 8601 format - for + example, 2023-08-01T00:00:00Z. + :param id: The ID of the Assistant to query. + :param data_type: Defines how Functions runtime should treat the + parameter value + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. """ @self._configure_function_builder @@ -2866,12 +2938,27 @@ def decorator(): def assistant_post_input(self, arg_name: str, id: str, user_message: str, + model: Optional[str] = None, data_type: Optional[ Union[DataType, str]] = None, **kwargs) \ -> Callable[..., Any]: """ - TODO: pydocs + The assistantPost output binding sends a message to the assistant and + saves the response in its internal state. + + :param arg_name: The name of the variable that represents DaprState + output object in function code. + :param id: The ID of the assistant to update. + :param user_message: The user message that user has entered for + assistant to respond to. + :param model: The OpenAI chat model to use. + :param data_type: Defines how Functions runtime should treat the + parameter value + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. """ @self._configure_function_builder @@ -2882,6 +2969,7 @@ def decorator(): name=arg_name, id=id, user_message=user_message, + model=model, data_type=parse_singular_param_to_enum(data_type, DataType), **kwargs)) @@ -2903,7 +2991,27 @@ def embeddings_input(self, **kwargs) \ -> Callable[..., Any]: """ - TODO: pydocs + The embeddings input decorator creates embeddings which will be used to + measure the readiness of text strings. + + Ref: https://platform.openai.com/docs/guides/embeddings + + :param arg_name: The name of the variable that represents DaprState + output object in function code. + :param input: The input source containing the data to generate + embeddings for. + :param input_type: The type of the input. + :param model: The ID of the model to use. + :param max_chunk_length: The maximum number of characters to chunk the + input into. Default value: 8 * 1024 + :param max_overlap: The maximum number of characters to overlap + between chunks. Default value: 128 + :param data_type: Defines how Functions runtime should treat the + parameter value + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. """ @self._configure_function_builder @@ -2931,19 +3039,41 @@ def semantic_search_input(self, connection_name: str, collection: str, query: Optional[str] = None, - embeddings_model: Optional[ - OpenAIModels] = OpenAIModels.DefaultEmbeddingsModel, # NoQA - chat_model: Optional[ - OpenAIModels] = OpenAIModels.DefaultChatModel, # NoQA - system_prompt: Optional[ - str] = semantic_search_system_prompt, + embeddings_model: Optional[OpenAIModels] = OpenAIModels.DefaultEmbeddingsModel, # NoQA + chat_model: Optional[OpenAIModels] = OpenAIModels.DefaultChatModel, # NoQA + system_prompt: Optional[str] = semantic_search_system_prompt, # NoQA max_knowledge_count: Optional[int] = 1, data_type: Optional[ Union[DataType, str]] = None, **kwargs) \ -> Callable[..., Any]: """ - TODO: pydocs + The embeddings input decorator creates embeddings which will be used to + measure the readiness of text strings. + + Ref: https://platform.openai.com/docs/guides/embeddings + + :param arg_name: The name of the variable that represents DaprState + output object in function code. + :param connection_name: app setting or environment variable which + contains a connection string value. + :param collection: The name of the collection or table to search or + store. + :param query: The semantic query text to use for searching. + :param embeddings_model: The ID of the model to use for embeddings. + The default value is "text-embedding-ada-002". + :param chat_model: The name of the Large Language Model to invoke for + chat responses. The default value is "gpt-3.5-turbo". + :param system_prompt: Optional The system prompt to use for prompting + the large language model. + :param max_knowledge_count: Optional. The number of knowledge items to + inject into the SystemPrompt. Default value: 1 + :param data_type: Optional. Defines how Functions runtime should treat + the parameter value. Default value: None + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. """ @self._configure_function_builder @@ -2974,8 +3104,7 @@ def embeddings_store_output(self, input_type: InputType, connection_name: str, collection: str, - model: Optional[ - OpenAIModels] = OpenAIModels.DefaultEmbeddingsModel, # NoQA + model: Optional[OpenAIModels] = OpenAIModels.DefaultEmbeddingsModel, # NoQA max_chunk_length: Optional[int] = 8 * 1024, max_overlap: Optional[int] = 128, data_type: Optional[ @@ -2983,7 +3112,33 @@ def embeddings_store_output(self, **kwargs) \ -> Callable[..., Any]: """ - TODO: pydocs + Supported list of embeddings store is extensible, and more can be + added by authoring a specially crafted NuGet package. Visit the + currently supported vector specific folder for specific usage + information: + + - Azure AI Search + - Azure Data Explorer + - Azure Cosmos DB using MongoDB + + :param arg_name: The name of the variable that represents DaprState + output object in function code. + :param input: The input to generate embeddings for. + :param input_type: The type of the input. + :param connection_name: The name of an app setting or environment + variable which contains a connection string value + :param collection: The collection or table to search. + :param model: The ID of the model to use. + :param max_chunk_length: The maximum number of characters to chunk the + input into. + :param max_overlap: The maximum number of characters to overlap between + chunks. + :param data_type: Optional. Defines how Functions runtime should treat + the parameter value. Default value: None + :param kwargs: Keyword arguments for specifying additional binding + fields to include in the binding json + + :return: Decorator function. """ @self._configure_function_builder diff --git a/azure/functions/decorators/openai.py b/azure/functions/decorators/openai.py index 863328ef..2c213799 100644 --- a/azure/functions/decorators/openai.py +++ b/azure/functions/decorators/openai.py @@ -31,10 +31,10 @@ def get_binding_name() -> str: def __init__(self, name: str, - task_description: str, + function_description: str, data_type: Optional[DataType] = None, **kwargs): - self.task_description = task_description + self.function_description = function_description super().__init__(name=name, data_type=data_type) @@ -159,11 +159,13 @@ def get_binding_name(): def __init__(self, name: str, id: str, user_message: str, + model: Optional[str] = None, data_type: Optional[DataType] = None, **kwargs): self.name = name self.id = id self.user_message = user_message + self.model = model super().__init__(name=name, data_type=data_type) diff --git a/tests/decorators/test_openai.py b/tests/decorators/test_openai.py index 65367408..45fe8658 100644 --- a/tests/decorators/test_openai.py +++ b/tests/decorators/test_openai.py @@ -4,21 +4,22 @@ from azure.functions.decorators.core import BindingDirection from azure.functions.decorators.openai import AssistantSkillTrigger, \ TextCompletionInput, OpenAIModels, AssistantQueryInput, EmbeddingsInput, \ - AssistantCreateOutput, SemanticSearchInput, EmbeddingsStoreOutput + AssistantCreateOutput, SemanticSearchInput, EmbeddingsStoreOutput, \ + AssistantPostInput class TestOpenAI(unittest.TestCase): def test_assistant_skills_trigger_valid_creation(self): trigger = AssistantSkillTrigger(name="test", - task_description="test_description", + function_description="description", data_type=DataType.UNDEFINED, dummy_field="dummy") self.assertEqual(trigger.get_binding_name(), "assistantSkillsTrigger") self.assertEqual( trigger.get_dict_repr(), {"name": "test", - "taskDescription": "test_description", + "functionDescription": "description", "dataType": DataType.UNDEFINED, 'type': 'assistantSkillsTrigger', 'dummyField': 'dummy', @@ -100,6 +101,25 @@ def test_assistant_create_output_valid_creation(self): "direction": BindingDirection.OUT, "type": "assistantCreate"}) + def test_assistant_post_input_valid_creation(self): + input = AssistantPostInput(name="test", + id="test_id", + model="test_model", + user_message="test_message", + data_type=DataType.UNDEFINED, + dummy_field="dummy") + self.assertEqual(input.get_binding_name(), + "assistantPost") + self.assertEqual(input.get_dict_repr(), + {"name": "test", + "id": "test_id", + "model": "test_model", + "userMessage": "test_message", + "dataType": DataType.UNDEFINED, + "direction": BindingDirection.IN, + "dummyField": "dummy", + "type": "assistantPost"}) + def test_semantic_search_input_valid_creation(self): input = SemanticSearchInput(name="test", data_type=DataType.UNDEFINED, From 5354087ab6837799909091686999580eeb2204f6 Mon Sep 17 00:00:00 2001 From: gavin-aguiar Date: Fri, 10 May 2024 13:18:12 -0500 Subject: [PATCH 7/9] Addressed comments --- azure/functions/decorators/function_app.py | 51 ++++++++++++---------- azure/functions/decorators/openai.py | 6 +++ tests/decorators/test_openai.py | 6 +++ 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 5f58f81f..40ae0f4e 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -1445,7 +1445,10 @@ def decorator(): def assistant_skill_trigger(self, arg_name: str, - task_description: str, + function_description: str, + function_name : Optional[str] = None, + parameter_description_json: Optional[str] = None, # NoQA + model: Optional[OpenAIModels] = OpenAIModels.DefaultChatModel, # NoQA data_type: Optional[ Union[DataType, str]] = None, **kwargs: Any) -> Callable[..., Any]: @@ -1459,16 +1462,22 @@ def assistant_skill_trigger(self, You can define functions that can be triggered by assistants by using the `assistantSkillTrigger` trigger binding. These functions are - invoked by the extension when a assistant signals that it would like + invoked by the extension when an assistant signals that it would like to invoke a function in response to a user prompt. The name of the function, the description provided by the trigger, and the parameter name are all hints that the underlying language model use to determine when and how to invoke an assistant function. - :param arg_name: The name of the variable that represents + :param arg_name: The name of trigger parameter in the function code. :param function_description: The description of the assistant function, which is provided to the model. + :param function_name: The assistant function, which is provided to the + LLM. + :param parameter_description_json: A JSON description of the function + parameter, which is provided to the LLM. + If no description is provided, the description will be autogenerated. + :param model: The OpenAI chat model to use. :param data_type: Defines how Functions runtime should treat the parameter value. :param kwargs: Keyword arguments for specifying additional binding @@ -1483,7 +1492,7 @@ def decorator(): fb.add_trigger( trigger=AssistantSkillTrigger( name=arg_name, - function_description=task_description, + function_description=function_description, data_type=parse_singular_param_to_enum(data_type, DataType), **kwargs)) @@ -2813,8 +2822,7 @@ def text_completion_input(self, the function, which then returns the result text as the response content. - :param arg_name: The name of the variable that represents DaprState - output object in function code. + :param arg_name: The name of binding parameter in the function code. :param prompt: The prompt to generate completions for, encoded as a string. :param model: the ID of the model to use. @@ -2867,8 +2875,7 @@ def assistant_create_output(self, arg_name: str, The assistantCreate output binding creates a new assistant with a specified system prompt. - :param arg_name: The name of the variable that represents DaprState - output object in function code. + :param arg_name: The name of binding parameter in the function code. :param data_type: Defines how Functions runtime should treat the parameter value :param kwargs: Keyword arguments for specifying additional binding @@ -2904,8 +2911,7 @@ def assistant_query_input(self, The assistantQuery input binding fetches the assistant history and passes it to the function. - :param arg_name: The name of the variable that represents DaprState - output object in function code. + :param arg_name: The name of binding parameter in the function code. :param timestamp_utc: the timestamp of the earliest message in the chat history to fetch. The timestamp should be in ISO 8601 format - for example, 2023-08-01T00:00:00Z. @@ -2947,8 +2953,7 @@ def assistant_post_input(self, arg_name: str, The assistantPost output binding sends a message to the assistant and saves the response in its internal state. - :param arg_name: The name of the variable that represents DaprState - output object in function code. + :param arg_name: The name of binding parameter in the function code. :param id: The ID of the assistant to update. :param user_message: The user message that user has entered for assistant to respond to. @@ -2992,12 +2997,11 @@ def embeddings_input(self, -> Callable[..., Any]: """ The embeddings input decorator creates embeddings which will be used to - measure the readiness of text strings. + measure the relatedness of text strings. Ref: https://platform.openai.com/docs/guides/embeddings - :param arg_name: The name of the variable that represents DaprState - output object in function code. + :param arg_name: The name of binding parameter in the function code. :param input: The input source containing the data to generate embeddings for. :param input_type: The type of the input. @@ -3048,13 +3052,17 @@ def semantic_search_input(self, **kwargs) \ -> Callable[..., Any]: """ - The embeddings input decorator creates embeddings which will be used to - measure the readiness of text strings. + The semantic search feature allows you to import documents into a + vector database using an output binding and query the documents in that + database using an input binding. For example, you can have a function + that imports documents into a vector database and another function that + issues queries to OpenAI using content stored in the vector database as + context (also known as the Retrieval Augmented Generation, or RAG + technique). Ref: https://platform.openai.com/docs/guides/embeddings - :param arg_name: The name of the variable that represents DaprState - output object in function code. + :param arg_name: The name of binding parameter in the function code. :param connection_name: app setting or environment variable which contains a connection string value. :param collection: The name of the collection or table to search or @@ -3064,7 +3072,7 @@ def semantic_search_input(self, The default value is "text-embedding-ada-002". :param chat_model: The name of the Large Language Model to invoke for chat responses. The default value is "gpt-3.5-turbo". - :param system_prompt: Optional The system prompt to use for prompting + :param system_prompt: Optional. The system prompt to use for prompting the large language model. :param max_knowledge_count: Optional. The number of knowledge items to inject into the SystemPrompt. Default value: 1 @@ -3121,8 +3129,7 @@ def embeddings_store_output(self, - Azure Data Explorer - Azure Cosmos DB using MongoDB - :param arg_name: The name of the variable that represents DaprState - output object in function code. + :param arg_name: The name of binding parameter in the function code. :param input: The input to generate embeddings for. :param input_type: The type of the input. :param connection_name: The name of an app setting or environment diff --git a/azure/functions/decorators/openai.py b/azure/functions/decorators/openai.py index 2c213799..b88b37fc 100644 --- a/azure/functions/decorators/openai.py +++ b/azure/functions/decorators/openai.py @@ -32,9 +32,15 @@ def get_binding_name() -> str: def __init__(self, name: str, function_description: str, + function_name: Optional[str] = None, + parameter_description_json: Optional[str] = None, + model: Optional[OpenAIModels] = OpenAIModels.DefaultChatModel, data_type: Optional[DataType] = None, **kwargs): self.function_description = function_description + self.function_name = function_name + self.parameter_description_json = parameter_description_json + self.model = model super().__init__(name=name, data_type=data_type) diff --git a/tests/decorators/test_openai.py b/tests/decorators/test_openai.py index 45fe8658..9727890f 100644 --- a/tests/decorators/test_openai.py +++ b/tests/decorators/test_openai.py @@ -13,6 +13,9 @@ class TestOpenAI(unittest.TestCase): def test_assistant_skills_trigger_valid_creation(self): trigger = AssistantSkillTrigger(name="test", function_description="description", + function_name="test_function_name", + parameter_description_json="test_json", + model=OpenAIModels.DefaultChatModel, data_type=DataType.UNDEFINED, dummy_field="dummy") self.assertEqual(trigger.get_binding_name(), @@ -20,6 +23,9 @@ def test_assistant_skills_trigger_valid_creation(self): self.assertEqual( trigger.get_dict_repr(), {"name": "test", "functionDescription": "description", + "functionName": "test_function_name", + "parameterDescriptionJson": "test_json", + "model": OpenAIModels.DefaultChatModel, "dataType": DataType.UNDEFINED, 'type': 'assistantSkillsTrigger', 'dummyField': 'dummy', From bac2768723dc3945d79c7d2bde85277b966ade83 Mon Sep 17 00:00:00 2001 From: gavin-aguiar Date: Fri, 10 May 2024 14:30:43 -0500 Subject: [PATCH 8/9] Flake8 fixes --- azure/functions/decorators/function_app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/azure/functions/decorators/function_app.py b/azure/functions/decorators/function_app.py index 40ae0f4e..fe17ceb5 100644 --- a/azure/functions/decorators/function_app.py +++ b/azure/functions/decorators/function_app.py @@ -1446,7 +1446,7 @@ def decorator(): def assistant_skill_trigger(self, arg_name: str, function_description: str, - function_name : Optional[str] = None, + function_name: Optional[str] = None, parameter_description_json: Optional[str] = None, # NoQA model: Optional[OpenAIModels] = OpenAIModels.DefaultChatModel, # NoQA data_type: Optional[ @@ -1493,6 +1493,9 @@ def decorator(): trigger=AssistantSkillTrigger( name=arg_name, function_description=function_description, + function_name=function_name, + parameter_description_json=parameter_description_json, + model=model, data_type=parse_singular_param_to_enum(data_type, DataType), **kwargs)) From 7ec1a4b46f210e1b54334d6c60948fbf29f50508 Mon Sep 17 00:00:00 2001 From: gavin-aguiar Date: Fri, 10 May 2024 14:51:00 -0500 Subject: [PATCH 9/9] Removed todo comment --- azure/functions/decorators/openai.py | 1 - 1 file changed, 1 deletion(-) diff --git a/azure/functions/decorators/openai.py b/azure/functions/decorators/openai.py index b88b37fc..546a87e4 100644 --- a/azure/functions/decorators/openai.py +++ b/azure/functions/decorators/openai.py @@ -155,7 +155,6 @@ def __init__(self, super().__init__(name=name, data_type=data_type) -# TODO: Waiting on the PR to get merged class AssistantPostInput(InputBinding): @staticmethod