Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: time extension for ChatPromptBuilder #9001

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion haystack/components/builders/chat_prompt_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update doctoring to ChatPromptBuilder

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):
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thanks for tagging, let me look into it!

83 changes: 83 additions & 0 deletions test/components/builders/test_chat_prompt_builder.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
Expand Down
Loading