Release Notes
🚀 New Features
-
Tools and components can now declare a
State(orState | None) parameter in their signature to receive the live agentStateobject at invocation time — no extra wiring needed.For function-based tools created with
@toolorcreate_tool_from_function, add astateparameter annotated asState:from haystack.components.agents import State from haystack.tools import tool @tool def my_tool(query: str, state: State) -> str: """Search using context from agent state.""" history = state.get("history") ...
For component-based tools created with
ComponentTool, declare aStateinput socket on the component'srunmethod:from haystack import component from haystack.components.agents import State from haystack.tools import ComponentTool @component class MyComponent: @component.output_types(result=str) def run(self, query: str, state: State) -> dict: history = state.get("history") ... tool = ComponentTool(component=MyComponent())
In both cases,
ToolInvokerautomatically injects the runtimeStateobject before calling the tool, andState/Optional[State]parameters are excluded from the LLM-facing schema so the model is not asked to supply them.This is an alternative to the existing
inputs_from_stateandoutputs_to_stateoptions onToolandComponentTool, which map individual state keys to specific tool parameters and outputs in a declarative way. Injecting the fullStateobject is more flexible and useful when a tool needs to read from or write to multiple keys, but it couples the tool implementation directly toState.
⚡️ Enhancement Notes
-
Clarify in the Markdown-producing converter documentation that
DocumentCleanerwith its default settings can flatten Markdown output, and update the example pipelines forPaddleOCRVLDocumentConverter,MistralOCRDocumentConverter,AzureDocumentIntelligenceConverter, andMarkItDownConverterto avoid routing Markdown content through the default cleaner configuration. -
Made
_create_agent_snapshotrobust towards serialization errors. If serializing agent component inputs fails, a warning is logged and an empty dictionary is used as a fallback, preventing the serialization error from masking the real pipeline runtime error. -
Standardize HTTP request handling in Haystack by adopting
httpxfor both synchronous and asynchronous requests, replacingrequests. Error reporting for failed requests has also been improved: exceptions now include additional details alongside the reason field. -
Add
run_asyncmethod toLLMMetadataExtractor.ChatGeneratorrequests now run concurrently using the existingmax_workersinit parameter. -
MarkdownHeaderSplitternow accepts aheader_split_levelsparameter (list of integers 1–6, default all levels) to control which header depths create split boundaries. For example,header_split_levels=[1, 2]splits only on#and##headers, merging content under deeper headers into the preceding chunk. -
MarkdownHeaderSplitternow ignores#lines that appear inside fenced code blocks (triple-backtick or triple-tilde), preventing Python comments and other hash-prefixed lines in code from being misidentified as Markdown headers. -
Expand the
PaddleOCRVLDocumentConverterdocumentation with more detailed guidance on advanced parameters, common usage scenarios, and a more realistic configuration example for layout-heavy documents.
⬆️ Upgrade Notes
-
As part of the migration from
requeststohttpx,request_with_retryandasync_request_with_retry(inhaystack.utils.requests_utils) no longer raiserequests.exceptions.RequestExceptionon failure; they now raisehttpx.HTTPErrorinstead. This also affectsHuggingFaceTEIRanker, which relies on these utilities. Users catchingrequests.exceptions.RequestExceptionshould update their code to catchhttpx.HTTPError. -
Agent.run()andAgent.run_async()now requiremessagesas an explicit argument (no longer optional). If you were relying on the defaultNonevalue in Haystack version 2.26 or 2.27, pass an empty list instead:agent.run(messages=[], ...)
LLM.run()andLLM.run_async()are unaffected — they still acceptNoneand default to an empty list internally. -
The
LLMcomponent now requiresuser_promptto be provided at initialization and it must contain at least one Jinja2 template variable (e.g.{{ variable_name }}). This ensures the component always exposes at least one required input socket, which is necessary for correct pipeline scheduling.required_variablesnow defaults to"*"(all variables inuser_promptare required), and passing an empty list raises aValueError.If you are affected: update any code that instantiates
LLMwithout auser_prompt, or with auser_promptthat has no template variables, to include at least one variable.Before:
llm = LLM(chat_generator=OpenAIChatGenerator(), system_prompt="You are helpful.")
After:
llm = LLM( chat_generator=OpenAIChatGenerator(), system_prompt="You are helpful.", user_prompt='{% message role="user" %}{{ query }}{% endmessage %}', )
🐛 Bug Fixes
-
When using the MarkdownHeaderSplitter, child headers in the split chunks previously lost their direct parent header in the metadata. Previously if one executed the code below:
from haystack.components.preprocessors import MarkdownHeaderSplitter from haystack import Document text = """ # header 1 intro text ## header 1.1 text 1 ## header 1.2 text 2 ### header 1.2.1 text 3 ### header 1.2.2 text 4 """ document = Document(content=text) splitter = MarkdownHeaderSplitter( keep_headers=True, secondary_split="word" ) result = splitter.run(documents=[document])["documents"] for doc in result: print(f"Header: {doc.meta['header']}, parent headers: {doc.meta['parent_headers']}")
We would have expected this output:
Header: header 1, parent headers: [] Header: header 1.1, parent headers: ['header 1'] Header: header 1.2, parent headers: ['header 1'] Header: header 1.2.1, parent headers: ['header 1', 'header 1.2'] Header: header 1.2.2, parent headers: ['header 1', 'header 1.2']But we actually got:
Header: header 1, parent headers: [] Header: header 1.1, parent headers: [] Header: header 1.2, parent headers: ['header 1'] Header: header 1.2.1, parent headers: ['header 1'] Header: header 1.2.2, parent headers: ['header 1', 'header 1.2']The occurred when a parent header had its own content chunk before the first child header.
This has been fixed, so even when a parent header has its own content chunk before the first child header, all content is preserved.
-
Reverts the change that made
Agentmessages optional, as it caused issues with pipeline execution. As a consequence, theLLMcomponent now defaults to an empty messages list unless provided at runtime.
💙 Big thank you to everyone who contributed to this release!
@Aftabbs, @Amanbig, @anakin87, @bilgeyucel, @bogdankostic, @davidsbatista, @dina-deifallah, @jimmyzhuu, @julian-risch, @kacperlukawski, @maxdswain, @MechaCritter, @ritikraj2425, @sarahkiener, @sjrl, @soheinze, @srini047, @tholor
Release Notes
v2.28.0-rc2
Upgrade Notes
-
The
LLMcomponent now requiresuser_promptto be provided at initialization and it must contain at least one Jinja2 template variable (e.g.{{ variable_name }}). This ensures the component always exposes at least one required input socket, which is necessary for correct pipeline scheduling.required_variablesnow defaults to"*"(all variables inuser_promptare required), and passing an empty list raises aValueError.If you are affected: update any code that instantiates
LLMwithout auser_prompt, or with auser_promptthat has no template variables, to include at least one variable.Before:
llm = LLM(chat_generator=OpenAIChatGenerator(), system_prompt="You are helpful.")
After:
llm = LLM( chat_generator=OpenAIChatGenerator(), system_prompt="You are helpful.", user_prompt='{% message role="user" %}{{ query }}{% endmessage %}', )
v2.28.0-rc1
Upgrade Notes
-
As part of the migration from
requeststohttpx,request_with_retryandasync_request_with_retry(inhaystack.utils.requests_utils) no longer raiserequests.exceptions.RequestExceptionon failure; they now raisehttpx.HTTPErrorinstead. This also affectsHuggingFaceTEIRanker, which relies on these utilities. Users catchingrequests.exceptions.RequestExceptionshould update their code to catchhttpx.HTTPError. -
Agent.run()andAgent.run_async()now requiremessagesas an explicit argument (no longer optional). If you were relying on the defaultNonevalue in Haystack version 2.26 or 2.27, pass an empty list instead:agent.run(messages=[], ...)
LLM.run()andLLM.run_async()are unaffected — they still acceptNoneand default to an empty list internally.
New Features
-
Tools and components can now declare a
State(orState | None) parameter in their signature to receive the live agentStateobject at invocation time — no extra wiring needed.For function-based tools created with
@toolorcreate_tool_from_function, add astateparameter annotated asState:from haystack.components.agents import State from haystack.tools import tool @tool def my_tool(query: str, state: State) -> str: """Search using context from agent state.""" history = state.get("history") ...
For component-based tools created with
ComponentTool, declare aStateinput socket on the component'srunmethod:from haystack import component from haystack.components.agents import State from haystack.tools import ComponentTool @component class MyComponent: @component.output_types(result=str) def run(self, query: str, state: State) -> dict: history = state.get("history") ... tool = ComponentTool(component=MyComponent())
In both cases
ToolInvokerautomatically injects the runtimeStateobject before calling the tool, andState/Optional[State]parameters are excluded from the LLM-facing schema so the model is not asked to supply them.This is an alternative to the existing
inputs_from_stateandoutputs_to_stateoptions onToolandComponentTool, which map individual state keys to specific tool parameters and outputs declaratively. Injecting the fullStateobject is more flexible and useful when a tool needs to read from or write to multiple keys, but it couples the tool implementation directly toState.
Enhancement Notes
- Clarify in the Markdown-producing converter documentation that
DocumentCleanerwith its default settings can flatten Markdown output, and update the example pipelines forPaddleOCRVLDocumentConverter,MistralOCRDocumentConverter,AzureDocumentIntelligenceConverter, andMarkItDownConverterto avoid routing Markdown content through the default cleaner configuration. - Made
_create_agent_snapshotrobust towards serialization errors. If serializing agent component inputs fails, a warning is logged and an empty dictionary is used as a fallback, preventing the serialization error from masking the real pipeline runtime error. - Standardize HTTP request handling in Haystack by adopting
httpxfor both synchronous and asynchronous requests, replacingrequests. Error reporting for failed requests has also been improved: exceptions now include additional details alongside the reason field. - Add
run_asyncmethod toLLMMetadataExtractor.ChatGeneratorrequests now run concurrently using the existingmax_workersinit parameter. MarkdownHeaderSplitternow accepts aheader_split_levelsparameter (list of integers 1–6, default all levels) to control which header depths create split boundaries. For example,header_split_levels=[1, 2]splits only on#and##headers, merging content under deeper headers into the preceding chunk.MarkdownHeaderSplitternow ignores#lines that appear inside fenced code blocks (triple-backtick or triple-tilde), preventing Python comments and other hash-prefixed lines in code from being misidentified as Markdown headers.- Expand the
PaddleOCRVLDocumentConverterdocumentation with more detailed guidance on advanced parameters, common usage scenarios, and a more realistic configuration example for layout-heavy documents.
Bug Fixes
-
When using the MarkdownHeaderSplitter, in the split chunks, the child header previously lost its direct parent header in the metadata. Previously if one executed the code below:
from haystack.components.preprocessors import MarkdownHeaderSplitter from haystack import Document text = """ # header 1 intro text ## header 1.1 text 1 ## header 1.2 text 2 ### header 1.2.1 text 3 ### header 1.2.2 text 4 """ document = Document(content=text) splitter = MarkdownHeaderSplitter( keep_headers=True, secondary_split="word" ) result = splitter.run(documents=[document])["documents"] for doc in result: print(f"Header: {doc.meta['header']}, parent headers: {doc.meta['parent_headers']}")
We would have expected this output:
Header: header 1, parent headers: [] Header: header 1.1, parent headers: ['header 1'] Header: header 1.2, parent headers: ['header 1'] Header: header 1.2.1, parent headers: ['header 1', 'header 1.2'] Header: header 1.2.2, parent headers: ['header 1', 'header 1.2']But instead we actually got:
Header: header 1, parent headers: [] Header: header 1.1, parent headers: [] Header: header 1.2, parent headers: ['header 1'] Header: header 1.2.1, parent headers: ['header 1'] Header: header 1.2.2, parent headers: ['header 1', 'header 1.2']The error happened when a parent header had its own content chunk before the first child header.
This has been fixed so even when a parent header has its own content chunk before the first child header all content is preserved.
-
Reverts the change that made
Agentmessages optional as it caused issues with pipeline execution. As a consequence, theLLMcomponent now defaults to an empty messages list unless provided at runtime.
💙 Big thank you to everyone who contributed to this release!
@Aftabbs, @Amanbig, @anakin87, @bilgeyucel, @bogdankostic, @davidsbatista, @dina-deifallah, @jimmyzhuu, @julian-risch, @kacperlukawski, @maxdswain, @MechaCritter, @ritikraj2425, @sarahkiener, @sjrl, @soheinze, @srini047, @tholor