Repository: deepset-ai/haystack

Issue:
Unable to load HF model in offline mode
**Describe the bug**
When i tried to follow this [page](https://haystack.deepset.ai/tutorials/22_pipeline_with_promptnode) for building a pipeline with a prompt node, using a hf model that is residing locally. But Instantiating a PromptNode fails while using a local HF model.

```
from haystack.nodes import PromptNode, PromptTemplate, PromptModelInvocationLayer, PromptModel
from haystack.nodes.prompt.providers import HFLocalInvocationLayer

lfqa_prompt = PromptTemplate(
    name="lfqa",
    prompt_text="""Synthesize a comprehensive answer from the following text for the given question. 
                             Provide a clear and concise response that summarizes the key points and information presented in the text. 
                             Your answer should be in your own words and be no longer than 50 words. 
                             \n\n Related text: {join(documents)} \n\n Question: {query} \n\n Answer:""",
)

prompt_node = PromptNode(model_name_or_path='./hf_models/google/flan-t5-base/', default_prompt_template=lfqa_prompt,)

```

**Error message**
```
ValueError: Model ./hf_models/google/flan-t5-base/ is not supported - no matching invocation layer found. Currently
supported invocation layers are: [<class 'haystack.nodes.prompt.providers.HFLocalInvocationLayer'>, <class 
'haystack.nodes.prompt.providers.OpenAIInvocationLayer'>, <class 
'haystack.nodes.prompt.providers.AzureOpenAIInvocationLayer'>, <class 
'haystack.nodes.prompt.providers.ChatGPTInvocationLayer'>] You can implement and provide custom invocation layer 
for ./hf_models/google/flan-t5-base/ by subclassing PromptModelInvocationLayer
```
**Expected behavior**
PromptNode Should be created when a local HF model is provided as an input.

**Additional context**
To Avoid the Error Message we could create a PromptModel (with correct PromptModelInvocationLayer) ourselves and pass it as an input to PromptNode. But that raises another Error, as it tries to get the task type from HFApi.

```
from haystack.nodes import PromptNode, PromptTemplate, PromptModelInvocationLayer, PromptModel
from haystack.nodes.prompt.providers import HFLocalInvocationLayer

lfqa_prompt = PromptTemplate(
    name="lfqa",
    prompt_text="""Synthesize a comprehensive answer from the following text for the given question. 
                             Provide a clear and concise response that summarizes the key points and information presented in the text. 
                             Your answer should be in your own words and be no longer than 50 words. 
                             \n\n Related text: {join(documents)} \n\n Question: {query} \n\n Answer:""",
)

google_flant5_prompt_model = PromptModel(
    model_name_or_path="./hf_models/google/flan-t5-xl",
    invocation_layer_class=HFLocalInvocationLayer
    )

prompt_node = PromptNode(model_name_or_path=google_flant5_prompt_model , default_prompt_template=lfqa_prompt,)
```
But This gives another Error: 
```
RuntimeError: Instantiating a pipeline without a task set raised an error: Repo id must be in the form 'repo_name' 
or 'namespace/repo_name': './hf_models/google/flan-t5-xl'. Use `repo_type` argument if needed.
```


**To Reproduce**

```
from haystack.nodes import PromptNode, PromptTemplate, PromptModelInvocationLayer, PromptModel
from haystack.nodes.prompt.providers import HFLocalInvocationLayer

lfqa_prompt = PromptTemplate(
    name="lfqa",
    prompt_text="""Synthesize a comprehensive answer from the following text for the given question. 
                             Provide a clear and concise response that summarizes the key points and information presented in the text. 
                             Your answer should be in your own words and be no longer than 50 words. 
                             \n\n Related text: {join(documents)} \n\n Question: {query} \n\n Answer:""",
)

prompt_node = PromptNode(model_name_or_path='./hf_models/google/flan-t5-base/', default_prompt_template=lfqa_prompt,)


```

**FAQ Check**
- [x] Have you had a look at [our new FAQ page](https://docs.haystack.deepset.ai/docs/faq)?

**System:**
 - OS: CentOS
 - GPU/CPU: 
 - Haystack version (commit or version number): 1.15.1
 - DocumentStore: InMemoryDocumentStore
 - Retriever: BM25Retriever



Discussion:

Resolving patch:
diff --git a/haystack/nodes/prompt/invocation_layer/hugging_face.py b/haystack/nodes/prompt/invocation_layer/hugging_face.py
--- a/haystack/nodes/prompt/invocation_layer/hugging_face.py
+++ b/haystack/nodes/prompt/invocation_layer/hugging_face.py
@@ -47,7 +47,7 @@ def __init__(
         :param kwargs: Additional keyword arguments passed to the underlying model. Due to reflective construction of
         all PromptModelInvocationLayer instances, this instance of HFLocalInvocationLayer might receive some unrelated
         kwargs. Only kwargs relevant to the HFLocalInvocationLayer are considered. The list of supported kwargs
-        includes: trust_remote_code, revision, feature_extractor, tokenizer, config, use_fast, torch_dtype, device_map.
+        includes: task_name, trust_remote_code, revision, feature_extractor, tokenizer, config, use_fast, torch_dtype, device_map.
         For more details about pipeline kwargs in general, see
         Hugging Face [documentation](https://huggingface.co/docs/transformers/en/main_classes/pipelines#transformers.pipeline).
 
@@ -119,8 +119,15 @@ def __init__(
 
         if len(model_input_kwargs) > 0:
             logger.info("Using model input kwargs %s in %s", model_input_kwargs, self.__class__.__name__)
-        self.task_name = get_task(model_name_or_path, use_auth_token=use_auth_token)
+
+        # If task_name is not provided, get the task name from the model name or path (uses HFApi)
+        if "task_name" in kwargs:
+            self.task_name = kwargs.get("task_name")
+        else:
+            self.task_name = get_task(model_name_or_path, use_auth_token=use_auth_token)
+
         self.pipe = pipeline(
+            task=self.task_name,  # task_name is used to determine the pipeline type
             model=model_name_or_path,
             device=self.devices[0] if "device_map" not in model_input_kwargs else None,
             use_auth_token=self.use_auth_token,


Test patch:
diff --git a/test/prompt/test_prompt_model.py b/test/prompt/test_prompt_model.py
--- a/test/prompt/test_prompt_model.py
+++ b/test/prompt/test_prompt_model.py
@@ -3,7 +3,7 @@
 import pytest
 
 from haystack.nodes.prompt.prompt_model import PromptModel
-from haystack.nodes.prompt.invocation_layer import PromptModelInvocationLayer
+from haystack.nodes.prompt.invocation_layer import PromptModelInvocationLayer, HFLocalInvocationLayer
 
 from .conftest import create_mock_layer_that_supports
 
@@ -36,3 +36,28 @@ def test_construtor_with_custom_model():
 def test_constructor_with_no_supported_model():
     with pytest.raises(ValueError, match="Model some-random-model is not supported"):
         PromptModel("some-random-model")
+
+
+def create_mock_pipeline(model_name_or_path=None, max_length=100):
+    return Mock(
+        **{"model_name_or_path": model_name_or_path},
+        return_value=Mock(**{"model_name_or_path": model_name_or_path, "tokenizer.model_max_length": max_length}),
+    )
+
+
+@pytest.mark.unit
+def test_hf_local_invocation_layer_with_task_name():
+    mock_pipeline = create_mock_pipeline()
+    mock_get_task = Mock(return_value="dummy_task")
+
+    with patch("haystack.nodes.prompt.invocation_layer.hugging_face.get_task", mock_get_task):
+        with patch("haystack.nodes.prompt.invocation_layer.hugging_face.pipeline", mock_pipeline):
+            PromptModel(
+                model_name_or_path="local_model",
+                max_length=100,
+                model_kwargs={"task_name": "dummy_task"},
+                invocation_layer_class=HFLocalInvocationLayer,
+            )
+            # checking if get_task is called when task_name is passed to HFLocalInvocationLayer constructor
+            mock_get_task.assert_not_called()
+            mock_pipeline.assert_called_once()