github deepset-ai/haystack v2.28.0-rc2

latest release: v2.28.0
pre-release10 hours ago

Release Notes

🚀 New Features

  • Tools and components can now declare a State (or State | None) parameter in their signature to receive the live agent State object at invocation time — no extra wiring needed.

    For function-based tools created with @tool or create_tool_from_function, add a state parameter annotated as State:

    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 a State input socket on the component's run method:

    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, ToolInvoker automatically injects the runtime State object before calling the tool, and State/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_state and outputs_to_state options on Tool and ComponentTool, which map individual state keys to specific tool parameters and outputs in a declarative way. Injecting the full State object is more flexible and useful when a tool needs to read from or write to multiple keys, but it couples the tool implementation directly to State.

⚡️ Enhancement Notes

  • Clarify in the Markdown-producing converter documentation that DocumentCleaner with its default settings can flatten Markdown output, and update the example pipelines for PaddleOCRVLDocumentConverter, MistralOCRDocumentConverter, AzureDocumentIntelligenceConverter, and MarkItDownConverter to avoid routing Markdown content through the default cleaner configuration.

  • Made _create_agent_snapshot robust 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 httpx for both synchronous and asynchronous requests, replacing requests. Error reporting for failed requests has also been improved: exceptions now include additional details alongside the reason field.

  • Add run_async method to LLMMetadataExtractor. ChatGenerator requests now run concurrently using the existing max_workers init parameter.

  • MarkdownHeaderSplitter now accepts a header_split_levels parameter (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.

  • MarkdownHeaderSplitter now 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 PaddleOCRVLDocumentConverter documentation 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 requests to httpx, request_with_retry and async_request_with_retry (in haystack.utils.requests_utils) no longer raise requests.exceptions.RequestException on failure; they now raise httpx.HTTPError instead. This also affects HuggingFaceTEIRanker, which relies on these utilities. Users catching requests.exceptions.RequestException should update their code to catch httpx.HTTPError.

  • Agent.run() and Agent.run_async() now require messages as an explicit argument (no longer optional). If you were relying on the default None value in Haystack version 2.26 or 2.27, pass an empty list instead:

    agent.run(messages=[], ...)

    LLM.run() and LLM.run_async() are unaffected — they still accept None and default to an empty list internally.

  • The LLM component now requires user_prompt to 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_variables now defaults to "*" (all variables in user_prompt are required), and passing an empty list raises a ValueError.

    If you are affected: update any code that instantiates LLM without a user_prompt, or with a user_prompt that 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 Agent messages optional, as it caused issues with pipeline execution. As a consequence, the LLM component 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 LLM component now requires user_prompt to 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_variables now defaults to "*" (all variables in user_prompt are required), and passing an empty list raises a ValueError.

    If you are affected: update any code that instantiates LLM without a user_prompt, or with a user_prompt that 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 requests to httpx, request_with_retry and async_request_with_retry (in haystack.utils.requests_utils) no longer raise requests.exceptions.RequestException on failure; they now raise httpx.HTTPError instead. This also affects HuggingFaceTEIRanker, which relies on these utilities. Users catching requests.exceptions.RequestException should update their code to catch httpx.HTTPError.

  • Agent.run() and Agent.run_async() now require messages as an explicit argument (no longer optional). If you were relying on the default None value in Haystack version 2.26 or 2.27, pass an empty list instead:

    agent.run(messages=[], ...)

    LLM.run() and LLM.run_async() are unaffected — they still accept None and default to an empty list internally.

New Features

  • Tools and components can now declare a State (or State | None) parameter in their signature to receive the live agent State object at invocation time — no extra wiring needed.

    For function-based tools created with @tool or create_tool_from_function, add a state parameter annotated as State:

    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 a State input socket on the component's run method:

    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 ToolInvoker automatically injects the runtime State object before calling the tool, and State/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_state and outputs_to_state options on Tool and ComponentTool, which map individual state keys to specific tool parameters and outputs declaratively. Injecting the full State object is more flexible and useful when a tool needs to read from or write to multiple keys, but it couples the tool implementation directly to State.

Enhancement Notes

  • Clarify in the Markdown-producing converter documentation that DocumentCleaner with its default settings can flatten Markdown output, and update the example pipelines for PaddleOCRVLDocumentConverter, MistralOCRDocumentConverter, AzureDocumentIntelligenceConverter, and MarkItDownConverter to avoid routing Markdown content through the default cleaner configuration.
  • Made _create_agent_snapshot robust 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 httpx for both synchronous and asynchronous requests, replacing requests. Error reporting for failed requests has also been improved: exceptions now include additional details alongside the reason field.
  • Add run_async method to LLMMetadataExtractor. ChatGenerator requests now run concurrently using the existing max_workers init parameter.
  • MarkdownHeaderSplitter now accepts a header_split_levels parameter (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.
  • MarkdownHeaderSplitter now 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 PaddleOCRVLDocumentConverter documentation 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 Agent messages optional as it caused issues with pipeline execution. As a consequence, the LLM component 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

Don't miss a new haystack release

NewReleases is sending notifications on new releases.