From e980c14d6a22f41b31d5d7153a3a598148a120de Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Thu, 21 Mar 2024 15:09:24 -0700 Subject: [PATCH] core[patch]: allow "placeholder" type in from_messages tuples (#19152) Co-authored-by: Erick Friis --- libs/core/langchain_core/prompts/chat.py | 74 ++++++++++++++++++- .../tests/unit_tests/prompts/test_chat.py | 19 +++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/libs/core/langchain_core/prompts/chat.py b/libs/core/langchain_core/prompts/chat.py index 1c74bdd9c49..e4be7adf872 100644 --- a/libs/core/langchain_core/prompts/chat.py +++ b/libs/core/langchain_core/prompts/chat.py @@ -551,7 +551,10 @@ MessageLike = Union[BaseMessagePromptTemplate, BaseMessage, BaseChatPromptTempla MessageLikeRepresentation = Union[ MessageLike, - Tuple[Union[str, Type], Union[str, List[dict], List[object]]], + Tuple[ + Union[str, Type], + Union[str, List[dict], List[object]], + ], str, ] @@ -590,6 +593,45 @@ class ChatPromptTemplate(BaseChatPromptTemplate): # ] #) + Messages Placeholder: + + .. code-block:: python + + # In addition to Human/AI/Tool/Function messages, + # you can initialize the template with a MessagesPlaceholder + # either using the class directly or with the shorthand tuple syntax: + + template = ChatPromptTemplate.from_messages([ + ("system", "You are a helpful AI bot."), + # Means the template will receive an optional list of messages under + # the "conversation" key + ("placeholder", "{conversation}") + # Equivalently: + # MessagesPlaceholder(variable_name="conversation", optional=True) + ]) + + prompt_value = template.invoke( + { + "conversation": [ + ("human", "Hi!"), + ("ai", "How can I assist you today?"), + ("human", "Can you make me an ice cream sundae?"), + ("ai", "No.") + ] + } + ) + + # Output: + # ChatPromptValue( + # messages=[ + # SystemMessage(content='You are a helpful AI bot.'), + # HumanMessage(content='Hi!'), + # AIMessage(content='How can I assist you today?'), + # HumanMessage(content='Can you make me an ice cream sundae?'), + # AIMessage(content='No.'), + # ] + #) + Single-variable template: If your prompt has only a single input variable (i.e., 1 instance of "{variable_nams}"), @@ -949,6 +991,36 @@ def _create_template_from_message_type( message = AIMessagePromptTemplate.from_template(cast(str, template)) elif message_type == "system": message = SystemMessagePromptTemplate.from_template(cast(str, template)) + elif message_type == "placeholder": + if isinstance(template, str): + if template[0] != "{" or template[-1] != "}": + raise ValueError( + f"Invalid placeholder template: {template}." + " Expected a variable name surrounded by curly braces." + ) + var_name = template[1:-1] + message = MessagesPlaceholder(variable_name=var_name, optional=True) + elif len(template) == 2 and isinstance(template[1], bool): + var_name_wrapped, is_optional = template + if not isinstance(var_name_wrapped, str): + raise ValueError( + "Expected variable name to be a string." f" Got: {var_name_wrapped}" + ) + if var_name_wrapped[0] != "{" or var_name_wrapped[-1] != "}": + raise ValueError( + f"Invalid placeholder template: {var_name_wrapped}." + " Expected a variable name surrounded by curly braces." + ) + var_name = var_name_wrapped[1:-1] + + message = MessagesPlaceholder(variable_name=var_name, optional=is_optional) + else: + raise ValueError( + "Unexpected arguments for placeholder message type." + " Expected either a single string variable name" + " or a list of [variable_name: str, is_optional: bool]." + f" Got: {template}" + ) else: raise ValueError( f"Unexpected message type: {message_type}. Use one of 'human'," diff --git a/libs/core/tests/unit_tests/prompts/test_chat.py b/libs/core/tests/unit_tests/prompts/test_chat.py index bcc1633cc61..305e981dfe9 100644 --- a/libs/core/tests/unit_tests/prompts/test_chat.py +++ b/libs/core/tests/unit_tests/prompts/test_chat.py @@ -535,6 +535,25 @@ def test_chat_prompt_message_placeholder_partial() -> None: assert prompt.format_messages() == [SystemMessage(content="foo")] +def test_chat_prompt_message_placeholder_tuple() -> None: + prompt = ChatPromptTemplate.from_messages([("placeholder", "{convo}")]) + assert prompt.format_messages(convo=[("user", "foo")]) == [ + HumanMessage(content="foo") + ] + + assert prompt.format_messages() == [] + + # Is optional = True + optional_prompt = ChatPromptTemplate.from_messages( + [("placeholder", ["{convo}", False])] + ) + assert optional_prompt.format_messages(convo=[("user", "foo")]) == [ + HumanMessage(content="foo") + ] + with pytest.raises(KeyError): + assert optional_prompt.format_messages() == [] + + def test_messages_prompt_accepts_list() -> None: prompt = ChatPromptTemplate.from_messages([MessagesPlaceholder("history")]) value = prompt.invoke([("user", "Hi there")]) # type: ignore