⭐️ Highlights
🛠️ Dynamic Tool Discovery with SearchableToolset
For applications with large tool catalogs, we’ve added the SearchableToolset. Instead of exposing all tools upfront, agents start with a single search_tools function and dynamically discover relevant tools using BM25-based keyword search.
This is particularly useful when connecting MCP servers via MCPToolset, where many tools may be available. By combining the two, agents can load only the tools they actually need at runtime, reducing context usage and improving tool selection.
from haystack.components.agents import Agent
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.dataclasses import ChatMessage
from haystack.tools import Tool, SearchableToolset
# Create a catalog of tools
catalog = [
Tool(name="get_weather", description="Get weather for a city", ...),
Tool(name="search_web", description="Search the web", ...),
# ... 100s more tools
]
toolset = SearchableToolset(catalog=catalog)
agent = Agent(chat_generator=OpenAIChatGenerator(), tools=toolset)
# The agent is initially provided only with the search_tools tool and will use it to find relevant tools.
result = agent.run(messages=[ChatMessage.from_user("What's the weather in Milan?")])📝 Reusable Prompt Templates for Agents
Agents now natively support Jinja2-templated user prompts. By defining a user_prompt and required_variables during initialization or at runtime, you can easily invoke the Agent with dynamic variables without having to manually build ChatMessage objects for every invocation. Plus, you can seamlessly append rendered prompts directly to prior conversation messages.
from haystack.components.agents import Agent
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.dataclasses import ChatMessage
agent = Agent(
chat_generator=OpenAIChatGenerator(),
tools=tools,
system_prompt="You are a helpful translation assistant.",
user_prompt="""{% message role="user"%}
Now summarize the conversation in {{ language }}.
{% endmessage %}""",
required_variables=["language"],
)
result = agent.run(
messages=[
ChatMessage.from_user("What are the main benefits of renewable energy?"),
ChatMessage.from_assistant("Renewable energy reduces greenhouse gas emissions, decreases dependence on fossil fuels, and can lower long-term energy costs."),
],
language="Spanish",
)⬆️ Upgrade Notes
-
Removed the deprecated
PipelineTemplateandPredefinedPipelineclasses, along with thePipeline.from_template()method. Users should migrate to Pipeline YAML files for similar functionality. See the [Serialization documentation](https://docs.haystack.deepset.ai/docs/serialization) for details on using YAML-based pipeline definitions. -
Default Hugging Face pipeline task updated to ``text-generation``
The default task used by
HuggingFaceLocalGeneratorhas been changed fromtext2text-generationtotext-generationand the default model has been changed from "google/flan-t5-base" to "Qwen/Qwen3-0.6B".In
transformersv5+,text2text-generationis no longer available as a valid pipeline task (see: huggingface/transformers#43256). While parts of the implementation still exist internally, it is no longer supported as a straightforward pipeline option.How to know if you are affected
- You are using
transformers>=5.0.0. - You explicitly set
task="text2text-generation"inHuggingFaceLocalGeneratororHuggingFaceLocalChatGenerator.
How to handle this change
- Replace
task="text2text-generation"withtask="text-generation". - Ensure that the selected model is compatible with the
text-generationpipeline (for example, causal language models). - If you rely on older behavior, pin
transformers<5. text2text-generationis now considered deprecated in Haystack and may be removed in a future release.
- You are using
🚀 New Features
-
Added
link_formatparameter toPPTXToDocumentandXLSXToDocumentconverters, allowing extraction of hyperlink addresses from PPTX and XLSX files.Supported formats:
"markdown":[text](url)"plain":text (url)"none"(default): Only text is extracted, link addresses are ignored.
This follows the same pattern already available in
DOCXToDocument. -
Added a new
LLMcomponent (haystack.components.generators.chat.LLM) that provides a simplified interface for text generation powered by a large language model. TheLLMcomponent is a streamlined version of theAgentthat focuses solely on single-turn text generation without tool usage. It supports system prompts, templated user prompts with required variables, streaming callbacks, and both synchronous (run) and asynchronous (run_async) execution.Usage example:
from haystack.components.generators.chat import LLM from haystack.components.generators.chat import OpenAIChatGenerator from haystack.dataclasses import ChatMessage llm = LLM( chat_generator=OpenAIChatGenerator(), system_prompt="You are a helpful translation assistant.", user_prompt="""{% message role="user"%} Summarize the following document: {{ document }} {% endmessage %}""", required_variables=["document"], ) result = llm.run(document="The weather is lovely today and the sun is shining. ") print(result["last_message"].text)
-
Added
SearchableToolsettohaystack.toolsmodule. This new toolset enables agents to dynamically discover tools from large catalogs using keyword-based (BM25) search. Instead of exposing all tools upfront (which can overwhelm LLMs with large tool definitions), agents start with a singlesearch_toolsfunction and progressively discover relevant tools as needed. For smaller catalogs, it operates in passthrough mode exposing all tools directly.Key features include configurable search threshold for automatic passthrough mode and top-k result limiting.
-
Added
user_promptandrequired_variablesparameters to theAgentcomponent. You can now define a reusable Jinja2-templated user prompt at initialization or at runtime, so the Agent can be invoked with different inputs without manually constructingChatMessageobjects each time.from haystack.components.agents import Agent from haystack.components.generators.chat import OpenAIChatGenerator agent = Agent( chat_generator=OpenAIChatGenerator(), tools=tools, system_prompt="You are a helpful translation assistant.", user_prompt="""{% message role="user"%} Translate the following document to {{ language }}: {{ document }} {% endmessage %}""", required_variables=["language", "document"], ) result = agent.run(language="French", document="The weather is lovely today.")
When you combine
messageswithuser_prompt, the rendered user prompt is appended to the provided messages. This is useful for passing prior conversation context alongside a new templated query. -
Added the
FileToFileContentcomponent, which converts local files intoFileContentobjects. These can be embedded intoChatMessageto pass to an LLM. -
Added
document_comparison_fieldparameter toDocumentMRREvaluator,DocumentMAPEvaluator, andDocumentRecallEvaluator.This allows users to compare documents using fields other than
content, such asidor metadata keys (viameta.<key>syntax).Previously, all three evaluators hardcoded
doc.contentfor comparison, which did not work well when documents were chunked or when ground truth was identified by custom metadata fields.
⚡️Enhancement Notes
-
The
LLMDocumentContentExtractornow extracts both content and metadata from image-based documents. When the LLM returns JSON,document_contentfills the document body and other keys are merged into metadata; plain text is still used as content. The fieldcontent_extraction_erroris no longer used and when an error occurs the fieldextraction_erroris added to metadata with the error message. -
Improved the deserialization error message for pipeline components to be more actionable and human-readable. The component data dictionary is now pretty-printed as formatted JSON, and the underlying error that caused the failure is explicitly surfaced, making it easier to quickly diagnose deserialization issues.
-
EmbeddingBasedDocumentSplitterandMultiQueryEmbeddingRetrievernow automatically invokewarm_up()whenrun()is called if they have not been warmed up yet. -
Improved
ComponentToolto correctly handle components whoserunmethod parameters are declared as top-levelOptionaltypes such aslist[ChatMessage] | None. The optional wrapper is now unwrapped before checking for afrom_dictmethod on the underlying type. As a result, when a parameter is typed aslist[ChatMessage] | Noneand receives a list of dictionaries,ComponentToolwill automatically coerce the input into a list ofChatMessageobjects usingChatMessage.from_dict. If the provided value isNone, the parameter is preserved asNone. -
Haystack now emits a
Warningwhen dataclass instances (e.g.Document,ChatMessage,StreamingChunk,ByteStream,SparseEmbedding) are mutated in place. Modifying shared instances can cause unexpected behavior in other parts of the pipeline. Usedataclasses.replaceto safely create updated copies instead.Instead of modifying attributes in place:
from haystack.dataclasses import Document doc = Document(content="old text", meta={"key": "value"}) # Not recommended: can affect other parts of the pipeline doc.content = "new text"
Use
dataclasses.replaceto create a new instance with the updated values:from dataclasses import replace from haystack.dataclasses import Document doc = Document(content="old text", meta={"key": "value"}) # Recommended: creates a new Document with updated content doc = replace(doc, content="new text")
🐛 Bug Fixes
-
Fixed an issue in
OpenAIChatGeneratorandOpenAIResponsesChatGeneratorwhere passing aFileContentobject without a filename would raise an error. A fallback filename is now automatically used instead. -
Ensure type display works correctly for parameterized generics when tracing is enabled. Previously, the
haystack.component.input_specandhaystack.component.output_spectag would strip the arguments present within a container type (e.g.list[str]would become"list"). Now we properly keep the arguments representation (e.g.list[str]becomes"list[str]"). -
Previously, flexible pipeline connections were not robust when the sender component returned a list containing a union of types. In flexible pipeline connections, if the receiver type is
strorChatMessage, the first element of the list sent by the sender component is extracted and converted to the receiver type.In the previous version,
list[str | int]was considered compatible withstr, which should not be the case. In fact, the sender component can legitimately return a list where the first element has typeint, which the receiver cannot handle.This is now fixed by ensuring that all possible element types of the sender list can be converted to the receiver type using the same conversion strategy.
-
Improved device handling when loading Hugging Face models in
TransformersSimilarityRankerandExtractiveReader.hf_device_mapis not always present anymore and is now only set when mixed-device loading is explicitly configured. The code has been updated to:- Check whether
hf_device_mapis available. - Fall back to the standard
deviceattribute when it is not.
This prevents attribute errors and ensures compatibility across different
transformersconfigurations. - Check whether
-
Updated failing unit tests to align with recent mocking and
transformersbehavior changes. -
PipelineRuntimeErrorraised byAgentnow provide clearer ownership by explicitly surfacing theAgentas the failing pipeline component.As a result:
component_namenow resolves to the name of theAgentin the pipeline, instead of the underlyingchat_generatorortool_invoker.component_typenow resolves tohaystack.components.agents.agent.Agentinstead of the concrete generator class such ashaystack.components.generators.chat.openai.OpenAIChatGenerator.- The error message now includes an additional outer section for the
Agentcomponent.
Example of new error message:
The following component failed to run: Component name: 'agent' Component type: 'Agent' Error: The following component failed to run: Component name: 'chat_generator' Component type: 'OpenAIChatGenerator' Error: Error code: 404 - {'error': {'message': 'The model ``gpt-4.2-mini`` does not exist or you do not have access to it.', 'type': 'invalid_request_error', 'param': None, 'code': 'model_not_found'}}
💙 Big thank you to everyone who contributed to this release!
@agnieszka-m, @Amanbig, @anakin87, @bilgeyucel, @bogdankostic, @davidsbatista, @edwiniac, @julian-risch, @kacperlukawski, @marc-mrt, @OGuggenbuehl, @OiPunk, @sjrl, @srini047, @vblagoje, @yaowubarbara