Repository: deepset-ai/haystack

Issue:
being able to add current date to prompt
**Is your feature request related to a problem? Please describe.**
When prompt engineering it can be useful to have the current date (date of search execution) inside the prompt.
This is especially useful when having outdated information in the corpus, which the model should ignore or tell that it is outdated. 

**Describe the solution you'd like**
It would be cool to use a variable, e.g. $current_date, inside the prompt which takes care of that. A fitting format would be, e.g. 6. June 2023.

cc @julian-risch @Timoeller 




Discussion:
Additional Info: having an explicit date variable will enable us to use it in dCs prompts. To me, this seems low effort and a huge impact. Please prioritize if possible.
What you can do in Haystack already is the following:
```python
from haystack.nodes import PromptNode, PromptTemplate
from datetime import date
prompt_node = PromptNode(
    "gpt-3.5-turbo", api_key=os.environ.get("OPENAI_API_KEY"), max_length=512, stop_words=["Observation:"]
)
prompt_node.default_prompt_template = PromptTemplate(f'''{{query}} today is {date.today()}''')
print(prompt_node.run(query="What day is today if"))
```
which results in: 'As an AI language model, I cannot predict the future, but assuming today is the current date, the answer would be June 9, 2023.'

So I think this is an issue in dC and it could be solved by allowing to run `date.today()` and doing `from datetime import date` for the user. What do you think @mathislucka @gosiapre ?
@julian-risch so this means you have non-serializable components in the PromptTemplate? Or why wouldn't it work in dC to put {date.today?} into the template?
How does the import work then? And you can't really run arbitrary functions in the prompt in dC, right? In Haystack you can.
You have no import in your example, that was why I was asking. Could you put that into a yaml, load it into a pipeline and run it without the import?
Ah wait, you are using a real f-string here. I think that's not ideal. The reason why we have these "preconfigured" functions for templates was to make them serializable and safe right?

I think this should be provided by haystack, there is nothing where a user could "import" anything in dC.
Okay, so in Haystack I could imagine this to be implemented in the fill method of the PromptTemplate here: https://github.com/deepset-ai/haystack/blob/main/haystack/nodes/prompt/prompt_template.py#LL517

If the prompt text contains sth like `$CURRENT_DATE`, Haystack could fill in the current timestamp in the standard format: `2023-06-12T23:42:29+00:00`. I am thinking language models can better work with 12. Juni 2023 but that would be language specific. So 12.06.2023 accessible through `$CURRENT_DATE` and 23:42:29 in 24h format accessible through `$CURRENT_TIME` is what I would suggest.
I'm not going to block this issue, but please consider all the implications of going in this direction. Adding "magic" variables like this is a slippery slope: today is just date and time in fixed format. Then what about timezone? What about the format of the date/time? What about the day of the week, the week number... what if the rest of the dates are all in MM-DD-YYYY format instead of DD-MM-YYYY and it confuses the language model? etc etc

My suggestion is to rather find a way to provide such variable to the prompt node as you do with any other, like the query, or the documents. For example, you could make a shaper that adds the current date to the promptnode context, so you have it available as any other variable, and "concentrate" the feature there, so all the datetime-related parameters are available there. Or let's find a way to read the date from the pipeline's `meta` dictionary and use that instead.

Just please let's not add even more unrelated features to the PromptTemplate. We have shapers precisely for these usecases. Let's use them.
The correct solution is to switch to a template language like Jinja that already support this kind of use cases. It's going to solve this and other problems that we already have we templates.

If we keep adding things like this we just create technical debt.
I like @ZanSara's suggestion of using a Shaper for this and I agree that it shouldn't be some magic variable. So like any other of the ready-made shaper functions we could have a timestamp function that is something like a very thin wrapper around `strftime` and allows the user to fully configure format and such in the pipeline yaml.

@silvanocerza I also agree with you here that a templating language would be a good choice but the question is, how can we go from our current solution to a solution with a templating language? It's a big project, we'd have to change it in haystack, think about how a template will work with the yaml configuration files, change the prompt hub, provide a migration path for haystack users and migrate in dC as well.
It might also be interesting to look at what msft is doing with guidance: https://github.com/microsoft/guidance

I also like how MiniChain implements it, they use jinja2 templates: https://github.com/srush/MiniChain/tree/main
> @silvanocerza I also agree with you here that a templating language would be a good choice but the question is, how can we go from our current solution to a solution with a templating language? It's a big project, we'd have to change it in haystack, think about how a template will work with the yaml configuration files, change the prompt hub, provide a migration path for haystack users and migrate in dC as well.

Just to be clear, I actually agree with @silvanocerza that a templating language would be a far superior solution. I also think most of us overestimates the amount of effort needed to migrate to this solution: in Haystack is all about deleting code, it's waaaay easier to deal with in YAML than our current solution, and the prompts just need to change the variables from `{question}` to `{{ question }}`. dC's migration effort should be minimal.

It's just about the difference between a 1hour effort (a new shaper) and a one-two days effort + the communication around it (Jinja2).


> It's just about the difference between a 1hour effort (a new shaper) and a one-two days effort + the communication around it (Jinja2).

Provided that:
- I advocated for using a template language instead of shapers since the beginning, but my idea was pushed back (twice)
- I made [my own template library](https://github.com/masci/banks) to validate the idea, and I know it works

I think one-two days is ok for putting some duct tape on top of the existing implementation, but Jinja is extremely powerful and if we don't have a clear design in mind, we'll keep facing problems as we stumble upon them. To get this right we need more time, and while I strongly advocate (again) we switch to a template language eventually, for now if the shaper works I'd just go with that.

Resolving patch:
diff --git a/haystack/nodes/other/shaper.py b/haystack/nodes/other/shaper.py
--- a/haystack/nodes/other/shaper.py
+++ b/haystack/nodes/other/shaper.py
@@ -1,6 +1,7 @@
 from functools import reduce
 import inspect
 import re
+from datetime import datetime
 from string import Template
 from typing import Literal, Optional, List, Dict, Any, Tuple, Union, Callable
 
@@ -26,6 +27,19 @@ def rename(value: Any) -> Any:
     return value
 
 
+def current_datetime(format: str = "%H:%M:%S %d/%m/%y") -> str:
+    """
+    Function that outputs the current time and/or date formatted according to the parameters.
+
+    Example:
+
+    ```python
+    assert current_datetime("%d.%m.%y %H:%M:%S") == 01.01.2023 12:30:10
+    ```
+    """
+    return datetime.now().strftime(format)
+
+
 def value_to_list(value: Any, target_list: List[Any]) -> List[Any]:
     """
     Transforms a value into a list containing this value as many times as the length of the target list.
@@ -544,6 +558,7 @@ def documents_to_strings(
 
 REGISTERED_FUNCTIONS: Dict[str, Callable[..., Any]] = {
     "rename": rename,
+    "current_datetime": current_datetime,
     "value_to_list": value_to_list,
     "join_lists": join_lists,
     "join_strings": join_strings,
diff --git a/haystack/nodes/prompt/prompt_template.py b/haystack/nodes/prompt/prompt_template.py
--- a/haystack/nodes/prompt/prompt_template.py
+++ b/haystack/nodes/prompt/prompt_template.py
@@ -22,6 +22,7 @@
     BaseOutputParser,
     AnswerParser,
     to_strings,
+    current_datetime,
     join,  # used as shaping function
     format_document,
     format_answer,
@@ -37,7 +38,10 @@
 logger = logging.getLogger(__name__)
 
 PROMPT_TEMPLATE_ALLOWED_FUNCTIONS = json.loads(
-    os.environ.get(HAYSTACK_PROMPT_TEMPLATE_ALLOWED_FUNCTIONS, '["join", "to_strings", "replace", "enumerate", "str"]')
+    os.environ.get(
+        HAYSTACK_PROMPT_TEMPLATE_ALLOWED_FUNCTIONS,
+        '["join", "to_strings", "replace", "enumerate", "str", "current_datetime"]',
+    )
 )
 PROMPT_TEMPLATE_SPECIAL_CHAR_ALIAS = {"new_line": "\n", "tab": "\t", "double_quote": '"', "carriage_return": "\r"}
 PROMPT_TEMPLATE_STRIPS = ["'", '"']
@@ -289,7 +293,8 @@ def __init__(self):
     def visit_FormattedValue(self, node: ast.FormattedValue) -> Optional[ast.AST]:
         """
         Replaces the f-string expression with a unique ID and stores the corresponding expression in a dictionary.
-        If the expression is the raw `documents` variable, it is encapsulated into a call to `documents_to_strings` to ensure that the documents get rendered correctly.
+        If the expression is the raw `documents` variable, it is encapsulated into a call to `documents_to_strings`
+        to ensure that the documents get rendered correctly.
         """
         super().generic_visit(node)
 
diff --git a/haystack/nodes/prompt/shapers.py b/haystack/nodes/prompt/shapers.py
--- a/haystack/nodes/prompt/shapers.py
+++ b/haystack/nodes/prompt/shapers.py
@@ -4,6 +4,7 @@
 
 from haystack.nodes.other.shaper import (  # pylint: disable=unused-import
     Shaper,
+    current_datetime,  # used as shaping function
     join_documents_to_string as join,  # used as shaping function
     format_document,
     format_answer,


Test patch:
diff --git a/test/nodes/test_shaper.py b/test/nodes/test_shaper.py
--- a/test/nodes/test_shaper.py
+++ b/test/nodes/test_shaper.py
@@ -1,6 +1,8 @@
-import pytest
+from datetime import datetime
 import logging
 
+import pytest
+
 import haystack
 from haystack import Pipeline, Document, Answer
 from haystack.document_stores.memory import InMemoryDocumentStore
@@ -191,6 +193,46 @@ def test_rename_yaml(tmp_path):
     assert result["invocation_context"]["questions"] == "test query"
 
 
+#
+# current_datetime
+#
+
+
+@pytest.mark.unit
+def test_current_datetime():
+    shaper = Shaper(func="current_datetime", inputs={}, outputs=["date_time"], params={"format": "%y-%m-%d"})
+    results, _ = shaper.run()
+    assert results["invocation_context"]["date_time"] == datetime.now().strftime("%y-%m-%d")
+
+
+@pytest.mark.unit
+def test_current_datetime_yaml(tmp_path):
+    with open(tmp_path / "tmp_config.yml", "w") as tmp_file:
+        tmp_file.write(
+            f"""
+            version: ignore
+            components:
+            - name: shaper
+              type: Shaper
+              params:
+                func: current_datetime
+                params:
+                  format: "%y-%m-%d"
+                outputs:
+                  - date_time
+            pipelines:
+              - name: query
+                nodes:
+                  - name: shaper
+                    inputs:
+                      - Query
+        """
+        )
+    pipeline = Pipeline.load_from_yaml(path=tmp_path / "tmp_config.yml")
+    result = pipeline.run()
+    assert result["invocation_context"]["date_time"] == datetime.now().strftime("%y-%m-%d")
+
+
 #
 # value_to_list
 #