Repository: deepset-ai/haystack
Pull request #9001: feat: time extension for ChatPromptBuilder
Author: login: mathislucka
Labels: name: topic:tests
Created: 2025-03-07T16:17:40Z | Merged: 2025-03-19T14:38:55Z

### Related Issues

- fixes [#8985
](https://github.com/deepset-ai/haystack/issues/8985)
### Proposed Changes:

 <!--- In case of a bug: Describe what caused the issue and how you solved it -->
 <!--- In case of a feature: Describe what did you add and how it works -->

### How did you test it?

<!-- unit tests, integration tests, manual verification, instructions for manual tests -->

### Notes for the reviewer

<!-- E.g. point out section where the reviewer  -->

### Checklist

- I have read the [contributors guidelines](https://github.com/deepset-ai/haystack/blob/main/CONTRIBUTING.md) and the [code of conduct](https://github.com/deepset-ai/haystack/blob/main/code_of_conduct.txt)
- I have updated the related issue with new insights and changes
- I added unit tests and updated the docstrings
- I've used one of the [conventional commit types](https://www.conventionalcommits.org/en/v1.0.0/) for my PR title: `fix:`, `feat:`, `build:`, `chore:`, `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:` and added `!` in case the PR includes breaking changes.
- I documented my code
- I ran [pre-commit hooks](https://github.com/deepset-ai/haystack/blob/main/CONTRIBUTING.md#installation) and fixed any issue



Diff:
diff --git a/haystack/components/builders/chat_prompt_builder.py b/haystack/components/builders/chat_prompt_builder.py
index a097c93194..21a90f785c 100644
--- a/haystack/components/builders/chat_prompt_builder.py
+++ b/haystack/components/builders/chat_prompt_builder.py
@@ -10,6 +10,7 @@
 
 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 +125,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 ChatPromptBuilder 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/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.
diff --git a/test/components/builders/test_chat_prompt_builder.py b/test/components/builders/test_chat_prompt_builder.py
index fcee86b173..838766918f 100644
--- a/test/components/builders/test_chat_prompt_builder.py
+++ b/test/components/builders/test_chat_prompt_builder.py
@@ -1,5 +1,6 @@
 from typing import Any, Dict, List, Optional
 from jinja2 import TemplateSyntaxError
+import arrow
 import logging
 import pytest
 
@@ -346,6 +347,87 @@ def test_warning_no_required_variables(self, caplog):
             )
             assert "ChatPromptBuilder has 2 prompt variables, but `required_variables` is not set. " in caplog.text
 
+    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):


Reviews:
user: login: sjrl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T08:05:36Z
user: login: sjrl
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T08:10:01Z
user: login: dfokina
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-10T08:53:52Z
user: login: sjrl
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T13:03:38Z

Review comments:
diff_hunk: @@ -124,7 +125,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.
path: haystack/components/builders/chat_prompt_builder.py
user: login: sjrl
body: Update doctoring to ChatPromptBuilder
created_at: 2025-03-10T08:05:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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.
path: releasenotes/notes/feat-time-extension-chatprompt-builder-fb159207088943d8.yaml
user: login: sjrl
body: @dfokina I noticed that I don’t think we have in depth docs on how to use this especially around the expected format. Could we add them to the PromptBuilder and ChatPromptBuilder? 
created_at: 2025-03-10T08:10:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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.
path: releasenotes/notes/feat-time-extension-chatprompt-builder-fb159207088943d8.yaml
user: login: dfokina
body: Yes, thanks for tagging, let me look into it!
created_at: 2025-03-10T08:53:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: coveralls
created_at: 2025-03-07T16:25:46Z
body: ## Pull Request Test Coverage Report for [Build 13947198075](https://coveralls.io/builds/72810564)


### Details

* **0** of **0**   changed or added relevant lines in **0** files are covered.
* **4** unchanged lines in **1** file lost coverage.
* Overall coverage decreased (**-0.01%**) to **90.054%**

---


|  Files with Coverage Reduction | New Missed Lines | % |
| :-----|--------------|--: |
| [components/builders/chat_prompt_builder.py](https://coveralls.io/builds/72810564/source?filename=components%2Fbuilders%2Fchat_prompt_builder.py#L132) | 4 | 93.67% |
<!-- | **Total:** | **4** |  | -->

|  Totals | [![Coverage Status](https://coveralls.io/builds/72810564/badge)](https://coveralls.io/builds/72810564) |
| :-- | --: |
| Change from base [Build 13945256266](https://coveralls.io/builds/72808684): |  -0.01% |
| Covered Lines: | 9742 |
| Relevant Lines: | 10818 |

---
##### 💛  - [Coveralls](https://coveralls.io)

author_association: COLLABORATOR
user: login: sjrl
created_at: 2025-03-10T08:13:00Z
body: @mathislucka thanks for your work on this! I do realize this is a similar implementation as in PromptBuilder but I did want to ask what happens if arrow is not installed and a user still tries to request a date? Will they get an informative error message that they are missing the install? 
author_association: CONTRIBUTOR
user: login: mathislucka
created_at: 2025-03-10T08:59:01Z
body: > @mathislucka thanks for your work on this! I do realize this is a similar implementation as in PromptBuilder but I did want to ask what happens if arrow is not installed and a user still tries to request a date? Will they get an informative error message that they are missing the install?


I believe they would not get an error or warning but I'm not sure how that could be implemented. At init time we could log a warning when the time extension is not installed (instead of just silently swallowing the ImportError) but I don't think it would be practical to try and parse the template itself to determine if a user attempts to use date rendering or not.

Should I add just the warning? Or do you have a better idea?
author_association: CONTRIBUTOR
user: login: sjrl
created_at: 2025-03-10T09:45:34Z
body: So this is the type of error they would get `jinja2.exceptions.TemplateSyntaxError: Encountered unknown tag 'now'.` at runtime. 

I'm not sure if a warning makes sense since it's not a true warning if they aren't using the time extension. 
author_association: CONTRIBUTOR
user: login: mathislucka
created_at: 2025-03-10T10:46:24Z
body: No, I mean we could add a warning at init time where we say something like:

`Install arrow if you want to use the time extension.` (ideally with a link to the docs for what the time extension is / does)
author_association: CONTRIBUTOR
user: login: sjrl
created_at: 2025-03-10T10:50:08Z
body: > No, I mean we could add a warning at init time where we say something like:
> 
> `Install arrow if you want to use the time extension.` (ideally with a link to the docs for what the time extension is / does)

Right I understand but I thought that might be an annoying warning to get as a user if they don’t plan on using the time extension. Although I think you’re right that it’s better to add that there so users are aware what to do to enable it. So I’d say go ahead and add it. 
author_association: CONTRIBUTOR
user: login: sjrl
created_at: 2025-03-19T13:03:27Z
body: I think it's fine to leave out the warning message for now and just document how to use the time extension in docs. 
author_association: CONTRIBUTOR