core[patch]: fix loss of partially initialized variables during prompt composition (#30096)

**Description:**
This PR addresses the loss of partially initialised variables when
composing different prompts. I.e. it allows the following snippet to
run:

```python
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([('system', 'Prompt {x} {y}')]).partial(x='1')
appendix = ChatPromptTemplate.from_messages([('system', 'Appendix {z}')])

(prompt + appendix).invoke({'y': '2', 'z': '3'})
```

Previously, this would have raised a `KeyError`, stating that variable
`x` remains undefined.

**Issue**
References issue #30049

**Todo**
- [x] **Add tests and docs**: If you're adding a new integration, please
include
1. a test for the integration, preferably unit tests that do not rely on
network access,
2. an example notebook showing its use. It lives in
`docs/docs/integrations` directory.


- [x] **Lint and test**: Run `make format`, `make lint` and `make test`
from the root of the package(s) you've modified. See contribution
guidelines for more: https://python.langchain.com/docs/contributing/

---------

Co-authored-by: Eugene Yurtsev <eyurtsev@gmail.com>
This commit is contained in:
Thommy257 2025-03-28 21:41:57 +01:00 committed by GitHub
parent e7883d5b9f
commit 372dc7f991
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 36 additions and 4 deletions

View File

@ -1040,19 +1040,34 @@ class ChatPromptTemplate(BaseChatPromptTemplate):
Returns:
Combined prompt template.
"""
partials = {**self.partial_variables}
# Need to check that other has partial variables since it may not be
# a ChatPromptTemplate.
if hasattr(other, "partial_variables") and other.partial_variables:
partials.update(other.partial_variables)
# Allow for easy combining
if isinstance(other, ChatPromptTemplate):
return ChatPromptTemplate(messages=self.messages + other.messages) # type: ignore[call-arg]
return ChatPromptTemplate(messages=self.messages + other.messages).partial(
**partials
) # type: ignore[call-arg]
elif isinstance(
other, (BaseMessagePromptTemplate, BaseMessage, BaseChatPromptTemplate)
):
return ChatPromptTemplate(messages=self.messages + [other]) # type: ignore[call-arg]
return ChatPromptTemplate(messages=self.messages + [other]).partial(
**partials
) # type: ignore[call-arg]
elif isinstance(other, (list, tuple)):
_other = ChatPromptTemplate.from_messages(other)
return ChatPromptTemplate(messages=self.messages + _other.messages) # type: ignore[call-arg]
return ChatPromptTemplate(messages=self.messages + _other.messages).partial(
**partials
) # type: ignore[call-arg]
elif isinstance(other, str):
prompt = HumanMessagePromptTemplate.from_template(other)
return ChatPromptTemplate(messages=self.messages + [prompt]) # type: ignore[call-arg]
return ChatPromptTemplate(messages=self.messages + [prompt]).partial(
**partials
) # type: ignore[call-arg]
else:
msg = f"Unsupported operand type for +: {type(other)}"
raise NotImplementedError(msg)

View File

@ -582,6 +582,23 @@ def test_chat_message_partial() -> None:
assert template2.format(input="hello") == get_buffer_string(expected)
def test_chat_message_partial_composition() -> None:
"""Test composition of partially initialized messages."""
prompt = ChatPromptTemplate.from_messages([("system", "Prompt {x} {y}")]).partial(
x="1"
)
appendix = ChatPromptTemplate.from_messages([("system", "Appendix {z}")])
res = (prompt + appendix).format_messages(y="2", z="3")
expected = [
SystemMessage(content="Prompt 1 2"),
SystemMessage(content="Appendix 3"),
]
assert res == expected
async def test_chat_tmpl_from_messages_multipart_text() -> None:
template = ChatPromptTemplate.from_messages(
[