From c0b678c12def760eef2b7e38c8a3d89815285f52 Mon Sep 17 00:00:00 2001 From: mathislucka <mathis.lucka@gmail.com> Date: Fri, 7 Mar 2025 17:16:52 +0100 Subject: [PATCH 1/3] feat: time extension for ChatPromptBuilder --- .../builders/chat_prompt_builder.py | 10 ++- .../builders/test_chat_prompt_builder.py | 83 +++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/haystack/components/builders/chat_prompt_builder.py b/haystack/components/builders/chat_prompt_builder.py index 33e2feda2d..2730314aaf 100644 --- a/haystack/components/builders/chat_prompt_builder.py +++ b/haystack/components/builders/chat_prompt_builder.py @@ -10,6 +10,8 @@ from haystack import component, default_from_dict, default_to_dict, logging from haystack.dataclasses.chat_message import ChatMessage, ChatRole, TextContent +from haystack.utils import Jinja2TimeExtension + logger = logging.getLogger(__name__) @@ -124,7 +126,13 @@ def __init__( self.required_variables = required_variables or [] self.template = template variables = variables or [] - self._env = SandboxedEnvironment() + try: + # The Jinja2TimeExtension needs an optional dependency to be installed. + # If it's not available we can do without it and use the PromptBuilder as is. + self._env = SandboxedEnvironment(extensions=[Jinja2TimeExtension]) + except ImportError: + self._env = SandboxedEnvironment() + if template and not variables: for message in template: if message.is_from(ChatRole.USER) or message.is_from(ChatRole.SYSTEM): diff --git a/test/components/builders/test_chat_prompt_builder.py b/test/components/builders/test_chat_prompt_builder.py index 1c39922df4..9fe5991fd2 100644 --- a/test/components/builders/test_chat_prompt_builder.py +++ b/test/components/builders/test_chat_prompt_builder.py @@ -1,5 +1,7 @@ from typing import Any, Dict, List, Optional from jinja2 import TemplateSyntaxError + +import arrow import pytest from haystack.components.builders.chat_prompt_builder import ChatPromptBuilder @@ -335,6 +337,87 @@ def test_example_in_pipeline_simple(self): } assert result == expected_dynamic + def test_with_custom_dateformat(self) -> None: + template = [ChatMessage.from_user("Formatted date: {% now 'UTC', '%Y-%m-%d' %}")] + builder = ChatPromptBuilder(template=template) + + result = builder.run()["prompt"] + + now_formatted = f"Formatted date: {arrow.now('UTC').strftime('%Y-%m-%d')}" + assert len(result) == 1 + assert result[0].role == "user" + assert result[0].text == now_formatted + + def test_with_different_timezone(self) -> None: + template = [ChatMessage.from_user("Current time in New York is: {% now 'America/New_York' %}")] + builder = ChatPromptBuilder(template=template) + + result = builder.run()["prompt"] + + now_ny = f"Current time in New York is: {arrow.now('America/New_York').strftime('%Y-%m-%d %H:%M:%S')}" + assert len(result) == 1 + assert result[0].role == "user" + assert result[0].text == now_ny + + def test_date_with_addition_offset(self) -> None: + template = [ChatMessage.from_user("Time after 2 hours is: {% now 'UTC' + 'hours=2' %}")] + builder = ChatPromptBuilder(template=template) + + result = builder.run()["prompt"] + + now_plus_2 = f"Time after 2 hours is: {(arrow.now('UTC').shift(hours=+2)).strftime('%Y-%m-%d %H:%M:%S')}" + assert len(result) == 1 + assert result[0].role == "user" + assert result[0].text == now_plus_2 + + def test_date_with_subtraction_offset(self) -> None: + template = [ChatMessage.from_user("Time after 12 days is: {% now 'UTC' - 'days=12' %}")] + builder = ChatPromptBuilder(template=template) + + result = builder.run()["prompt"] + + now_minus_12 = f"Time after 12 days is: {(arrow.now('UTC').shift(days=-12)).strftime('%Y-%m-%d %H:%M:%S')}" + assert len(result) == 1 + assert result[0].role == "user" + assert result[0].text == now_minus_12 + + def test_invalid_timezone(self) -> None: + template = [ChatMessage.from_user("Current time is: {% now 'Invalid/Timezone' %}")] + builder = ChatPromptBuilder(template=template) + + # Expect ValueError for invalid timezone + with pytest.raises(ValueError, match="Invalid timezone"): + builder.run() + + def test_invalid_offset(self) -> None: + template = [ChatMessage.from_user("Time after invalid offset is: {% now 'UTC' + 'invalid_offset' %}")] + builder = ChatPromptBuilder(template=template) + + # Expect ValueError for invalid offset + with pytest.raises(ValueError, match="Invalid offset or operator"): + builder.run() + + def test_multiple_messages_with_date_template(self) -> None: + template = [ + ChatMessage.from_user("Current date is: {% now 'UTC' %}"), + ChatMessage.from_assistant("Thank you for providing the date"), + ChatMessage.from_user("Yesterday was: {% now 'UTC' - 'days=1' %}"), + ] + builder = ChatPromptBuilder(template=template) + + result = builder.run()["prompt"] + + now = f"Current date is: {arrow.now('UTC').strftime('%Y-%m-%d %H:%M:%S')}" + yesterday = f"Yesterday was: {(arrow.now('UTC').shift(days=-1)).strftime('%Y-%m-%d %H:%M:%S')}" + + assert len(result) == 3 + assert result[0].role == "user" + assert result[0].text == now + assert result[1].role == "assistant" + assert result[1].text == "Thank you for providing the date" + assert result[2].role == "user" + assert result[2].text == yesterday + class TestChatPromptBuilderDynamic: def test_multiple_templated_chat_messages(self): From be018c36da0c3836dfa53ddc8c5f31011802aea1 Mon Sep 17 00:00:00 2001 From: mathislucka <mathis.lucka@gmail.com> Date: Fri, 7 Mar 2025 17:20:29 +0100 Subject: [PATCH 2/3] chore: release notes --- haystack/components/builders/chat_prompt_builder.py | 1 - ...t-time-extension-chatprompt-builder-fb159207088943d8.yaml | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/feat-time-extension-chatprompt-builder-fb159207088943d8.yaml diff --git a/haystack/components/builders/chat_prompt_builder.py b/haystack/components/builders/chat_prompt_builder.py index 2730314aaf..b8d4acdfac 100644 --- a/haystack/components/builders/chat_prompt_builder.py +++ b/haystack/components/builders/chat_prompt_builder.py @@ -12,7 +12,6 @@ from haystack.dataclasses.chat_message import ChatMessage, ChatRole, TextContent from haystack.utils import Jinja2TimeExtension - logger = logging.getLogger(__name__) diff --git a/releasenotes/notes/feat-time-extension-chatprompt-builder-fb159207088943d8.yaml b/releasenotes/notes/feat-time-extension-chatprompt-builder-fb159207088943d8.yaml new file mode 100644 index 0000000000..f1c707c0dc --- /dev/null +++ b/releasenotes/notes/feat-time-extension-chatprompt-builder-fb159207088943d8.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Users can now work with date and time in the ChatPromptBuilder. + In the same way as the PromptBuilder, the ChatPromptBuilder now supports arrow to work with datetime. From 3f4ffc377902ef74ef34ace36ed28cc3d7d719be Mon Sep 17 00:00:00 2001 From: Sebastian Husch Lee <sjrl423@gmail.com> Date: Wed, 19 Mar 2025 13:57:32 +0100 Subject: [PATCH 3/3] Fix comment --- haystack/components/builders/chat_prompt_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/haystack/components/builders/chat_prompt_builder.py b/haystack/components/builders/chat_prompt_builder.py index b8d4acdfac..eadc68209a 100644 --- a/haystack/components/builders/chat_prompt_builder.py +++ b/haystack/components/builders/chat_prompt_builder.py @@ -127,7 +127,7 @@ def __init__( variables = variables or [] try: # The Jinja2TimeExtension needs an optional dependency to be installed. - # If it's not available we can do without it and use the PromptBuilder as is. + # If it's not available we can do without it and use the ChatPromptBuilder as is. self._env = SandboxedEnvironment(extensions=[Jinja2TimeExtension]) except ImportError: self._env = SandboxedEnvironment()