github deepset-ai/haystack v3.5.0-rc1

pre-release54 minutes ago

⭐️ Highlights

🧭 Classify and Route with the OpenAI Decisions API

Three new components use the OpenAI Decisions API to make choices about your data without writing a prompt. OpenAIDecisionsTextRouter sends text to a named output per label, OpenAIDecisionsImageRouter does the same for images and can send low-confidence decisions to a fallback branch, and OpenAIDecisionsDocumentClassifier answers choice, score, and yes/no questions about documents and stores the answers in their metadata.

from haystack.components.routers import OpenAIDecisionsTextRouter

router = OpenAIDecisionsTextRouter(
    labels={
        "billing": "Payments, invoices, refunds and charges",
        "technical": "Bugs, crashes and errors in the product",
    },
    instructions="Which team should handle this support request?",
    min_confidence=0.6,
)

print(router.run(text="I was charged twice for my subscription."))
# >> {'billing': 'I was charged twice for my subscription.'}

🧱 Structured Output for Agents

The new StructuredOutputHook validates the Agent's final reply against a Pydantic model or JSON schema and returns it as a dict in result["structured_output"]. If the reply doesn't validate, the hook sends the error back to the model and lets it try again, up to max_retries times.

from pydantic import BaseModel

from haystack.components.agents import Agent
from haystack.components.generators.chat import OpenAIResponsesChatGenerator
from haystack.dataclasses import ChatMessage
from haystack.hooks.structured_output import StructuredOutputHook


class CityInfo(BaseModel):
    city: str
    country: str


agent = Agent(
    chat_generator=OpenAIResponsesChatGenerator(generation_kwargs={"text_format": CityInfo}),
    state_schema={"structured_output": {"type": dict}},
    hooks={"on_exit": [StructuredOutputHook(schema=CityInfo)]},
)

result = agent.run(messages=[ChatMessage.from_user("Which city is the Eiffel Tower in?")])
print(result["structured_output"])
# >> {'city': 'Paris', 'country': 'France'}

Also new for Agents: the experimental StepBudgetReminderHook tells the model how many steps it has left as it approaches max_agent_steps, so it wraps up with an answer instead of being cut off.

🎯 Multivector Embeddings and MaxSimRanker

Document has a new multivector_embedding field for late-interaction models such as ColBERT, and the new MaxSimRanker reranks candidates by comparing each query vector to the document vectors and summing the best matches. DocumentJoiner(join_mode="merge_embeddings") can also combine dense, sparse, and multivector embeddings computed in parallel for the same documents.

This is the first step toward multivector retrieval in Haystack (#13257). Embedders that produce multivector embeddings are in progress in Haystack Core Integrations: Sentence Transformers and vLLM.

from haystack import Document
from haystack.components.rankers import MaxSimRanker

# Representative multivector embedding for "How do solar panels work?"
query_multivector_embedding = [[1.0, 0.0], [0.0, 1.0]]

documents = [
    Document(content="Wind turbines generate electricity.", multivector_embedding=[[1.0, 0.0], [0.0, 0.2]]),
    Document(content="Solar panels turn sunlight into power.", multivector_embedding=[[0.95, 0.05], [0.15, 0.9]]),
    Document(content="Solar cells absorb light.", multivector_embedding=[[0.9, 0.1], [0.1, 0.9]]),
]

ranker = MaxSimRanker(top_k=2)
result = ranker.run(query_multivector_embedding=query_multivector_embedding, documents=documents)
for document in result["documents"]:
    print(f"{document.score:.2f}: {document.content}")
# >> 1.85: Solar panels turn sunlight into power.
# >> 1.80: Solar cells absorb light.

⚠️ New Default OpenAI Model

OpenAIChatGenerator, OpenAIResponsesChatGenerator, and the LLM-based evaluators now default to gpt-5.6-terra, the replacement OpenAI recommends for gpt-5-mini. OpenAI shuts down gpt-5-mini on December 11, 2026. Pipelines saved with model: gpt-5-mini keep using it, so update their YAML before then.

⬆️ Upgrade Notes

  • Raise the minimum supported openai SDK version from 2.6.0 to 3.26.0 to support the Decisions API.

  • Declared pydantic>=2.12 (>=2.14 on Python 3.15) as the minimum supported version. pydantic releases before 2.11 generate different JSON schemas for tool parameters in create_tool_from_function/@tool, ComponentTool, and PipelineTool.

  • If you don't set keep_default_na or na_values in read_excel_kwargs, XLSXToDocument now keeps cell text such as "NA", "N/A", "NULL", and "None" instead of treating it as missing. A numeric column that contains such text is therefore no longer converted to floats, so in CSV output its whole numbers render as 1 instead of 1.0. To restore the previous behavior, pass read_excel_kwargs={"keep_default_na": True}.

  • Components with a Variadic input, or a list input connected to several senders, now receive the values in the order the senders were connected with Pipeline.connect(). Before, Pipeline.run() passed them in the order the senders ran, so the results of order-dependent components such as DocumentJoiner with weights can change: connect the senders in the order you want the values to arrive.

    If you load pipelines from YAML, check the order of your connections entries. Pipelines saved before this release list connections grouped by sender, not in the order they were made, and the order of the entries now decides the order in which a socket receives its values. For example, if embedding_retriever is listed before bm25_retriever in connections, DocumentJoiner now applies weights[0] to the embedding retriever, where before it could apply it to the BM25 retriever. Reorder the entries that feed a Variadic or multi-sender list socket so the senders come in the order you expect.

  • The default model of OpenAIChatGenerator and OpenAIResponsesChatGenerator changed from gpt-5-mini to gpt-5.6-terra, the replacement OpenAI recommends. OpenAI shuts down gpt-5-mini on December 11, 2026. LLMEvaluator, ContextRelevanceEvaluator, and FaithfulnessEvaluator use the new default too.

    Pipelines serialized before this change store model: gpt-5-mini and keep using it. Update the model value in their YAML before December 11, 2026. To keep gpt-5-mini until then, pass it explicitly: OpenAIChatGenerator(model="gpt-5-mini").

  • If you read id, encrypted_content, or status directly from reply.reasoning.extra in OpenAIResponsesChatGenerator replies, read them from entries in reply.reasoning.extra["reasoning_items"] instead. For example, replace reply.reasoning.extra["id"] with reply.reasoning.extra["reasoning_items"][0]["id"] for the first item, or iterate over the list to handle all reasoning items.

  • OpenAIChatGenerator and AzureOpenAIChatGenerator now request token usage metadata by default when streaming. To restore the previous behavior, pass generation_kwargs={"stream_options": {"include_usage": False}}.

  • max_agent_steps is now a reserved state key on Agent. If you defined a custom state_schema key named max_agent_steps, rename it (e.g. my_max_agent_steps): the Agent now raises a ValueError at initialization when a reserved key is redefined.

🚀 New Features

  • Add the optional multivector_embedding field to Document to store multi-vector representation such as those generated by late-interaction models like ColBERT. This is a first step toward supporting multivector embedders, retrievers, and rankers.

  • Add MaxSimRanker to rank documents by their similarity to a query using precomputed multivector embeddings. In two-stage retrieval, an embedding retriever can quickly select candidates using dense similarity, then MaxSimRanker performs a more fine-grained comparison of the query and each candidate's content to return the most relevant documents.

  • Add OpenAIDecisionsDocumentClassifier to answer choice, score, and predicate questions about documents using the OpenAI Decisions API. Answers are stored in document metadata.

  • Add OpenAIDecisionsImageRouter to route images to different pipeline branches based on their content, with optional text context. Images with low-confidence decisions can be sent to a fallback branch.

  • Add OpenAIDecisionsTextRouter to route text to named categories using the OpenAI Decisions API.

  • Added StructuredOutputHook to haystack.hooks.structured_output. Registered under on_exit, it parses the Agent's final reply as JSON, validates it against a Pydantic model or a JSON schema, and stores the result as a dict under the structured_output key by default. The key must be declared in the Agent's state_schema, otherwise the hook raises an error at runtime.

    When a reply can't be parsed, the hook sends the validation error back to the model and keeps the Agent running so it can correct its reply, up to max_retries times (2 by default). When the reply still can't be parsed, or the Agent stops on another exit condition, None is stored.

    from pydantic import BaseModel
    
    from haystack.components.agents import Agent
    from haystack.components.generators.chat import OpenAIResponsesChatGenerator
    from haystack.dataclasses import ChatMessage
    from haystack.hooks.structured_output import StructuredOutputHook
    
    
    class CityInfo(BaseModel):
        city: str
        country: str
    
    
    agent = Agent(
        chat_generator=OpenAIResponsesChatGenerator(generation_kwargs={"text_format": CityInfo}),
        state_schema={"structured_output": {"type": dict}},
        hooks={"on_exit": [StructuredOutputHook(schema=CityInfo)]},
    )
    
    result = agent.run(messages=[ChatMessage.from_user("Which city is the Eiffel Tower in?")])
    print(result["structured_output"])
    # {'city': 'Paris', 'country': 'France'}
  • DocumentJoiner now supports join_mode="merge_embeddings" to combine the embedding and sparse_embedding fields of documents with the same ID. This allows dense and sparse document embedders to run in parallel and their outputs to be combined before passing them to DocumentWriter.

  • Added StepBudgetReminderHook (experimental), a before_llm hook that tells the model how many steps it has left once an Agent run gets close to max_agent_steps. Before each of the last remind_within_steps LLM calls, it adds a user message wrapped in <system-reminder> tags, such as "You have 2 of 10 steps remaining", and on the last step asks for the final answer. This lets the Agent wrap up instead of being cut off mid-investigation.

    from haystack.hooks.budget import StepBudgetReminderHook
    
    agent = Agent(
        chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
        tools=[search],
        max_agent_steps=20,
        hooks={"before_llm": [StepBudgetReminderHook(remind_within_steps=3)]},
    )

    Reminder messages are marked with meta={"system_reminder": True}. Context compaction recognizes the marker and never treats a reminder as the user's task.

⚡️ Enhancement Notes

  • Added the missing delete_by_filter_async() method to InMemoryDocumentStore. Previously, only the sync delete_by_filter() was available, unlike the other methods of the store, which all have an async counterpart. The new method runs delete_by_filter() in the store's thread-pool executor and returns the number of deleted documents.
  • The default modify_template of BlockingConfirmationStrategy now tells the LLM that the user wants the result of the modified tool call used and that it should not call the tool again with the original arguments. With the previous wording, models often repeated the original tool call after a user modified its parameters. Pass modify_template explicitly to keep the previous message.
  • DocumentJoiner(join_mode="merge_embeddings") now supports multivector embeddings in addition to dense and sparse embeddings when combining documents with the same ID.
  • OpenAIChatGenerator now includes several methods to make it easier for subclasses to customize the component's behavior while reducing code duplication.
  • Deep copies of Document objects and pipeline inputs are now faster, with the same results as before. Document now implements __deepcopy__, which copies the embedding list in one step instead of element by element. Pipeline also returns immutable values such as strings and numbers unchanged when copying component inputs, without running the more expensive checks meant for other objects. Pipelines that pass embeddings or many embedded documents between components benefit most.
  • EvaluationRunResult.comparative_detailed_report has a new align_on parameter to match the rows of the two runs by an input column, such as a question ID or the question itself, instead of by position. This compares runs correctly even if they evaluated the same questions in a different order, or not all of the same questions. Rows without a match are kept with None for the other run.
  • HierarchicalDocumentSplitter now supports split_by="token" (requires pip install tiktoken), with the encoding set by the new tokenizer_encoding parameter (default "o200k_base"). A new warm_up() method loads the tokenizer up front instead of on the first run().
  • PPTXToDocument now extracts text from grouped shapes and tables, which were previously skipped. Tables are included in the document content as CSV.
  • OpenAIChatGenerator and OpenAIResponsesChatGenerator now preserve OpenAI API refusals in ChatMessage.meta["refusal"]. Applications can inspect this metadata to detect and handle refusals.
  • OpenAIResponsesChatGenerator now preserves text annotations in reply.meta["annotations"], with aligned annotation_offsets for locating citations in the concatenated reply text.
  • Add a tokenizer_encoding parameter to RecursiveDocumentSplitter to choose the tiktoken encoding used when split_unit="token", as in DocumentSplitter. It defaults to "o200k_base", the encoding used until now. Set it to match your model, for example "cl100k_base" for OpenAI's text-embedding-3 models, so that chunks fit the model's token limit.
  • RecursiveDocumentSplitter now validates split_unit at initialization, raising a ValueError for values other than "word", "char", or "token" instead of failing later during run().
  • Simplify ChatMessage serialization and OpenAI format conversion for improved readability and maintainability.
  • Hooks can now read the Agent's step limit from the internal max_agent_steps state key, for example with state.get("max_agent_steps"). It is not returned as an Agent output.

🐛 Bug Fixes

  • Fixed Agent losing token counts in token_usage when a later LLM call reports a usage field as null, for example prompt_tokens_details reported on one call and null on the next. A null value now counts as not reported, so the totals no longer depend on the order of the calls.

  • FilterRetriever, AutoMergingRetriever, SentenceWindowRetriever, CacheChecker and DocumentWriter now work with document stores that implement only the synchronous API. Their run_async methods use the store's async method when it exists and otherwise run the synchronous one in a worker thread, instead of failing on the missing async method.

  • Fix AutoMergingRetriever.run() and run_async() when retrieved leaves from HierarchicalDocumentSplitter have different depths. Shallower leaves now merge together with siblings promoted from deeper branches, preventing missed parent merges and duplicate parent documents in the retrieved context.

  • ComponentTool now skips unknown invocation arguments with a warning instead of raising KeyError, including extra properties in custom schemas and arguments added by the LLM. Custom schemas with unknown properties also produce a warning during construction. A ValueError is raised when a custom schema omits mandatory component inputs not provided through inputs_from_state. Automatically injected State inputs are excluded. This also applies to AgentTool when a custom parameters schema omits messages and it is not provided through inputs_from_state. Previously, construction succeeded but invocation failed because the required input was missing.

  • ImageContent.from_dict, FileContent.from_dict and TextContent.from_dict now build the calling class instead of the base class, so a subclass survives a to_dict / from_dict round trip. This matches the behaviour of ByteStream.from_dict.

  • Fixed DocumentCleaner removing indentation and newlines at the start and end of every page when remove_regex was set, even on pages where the regex matched nothing. Pages without a match are now left unchanged, which preserves Markdown and code content when remove_extra_whitespaces, remove_empty_lines and strip_whitespaces are disabled. Pages where the regex matches are still stripped as before.

  • DocumentJoiner now raises a ValueError when weights contains NaN or infinite values. Previously, these values were accepted and turned the document scores into NaN in the merge and reciprocal_rank_fusion join modes.

  • DOCXToDocument now reads text that a paragraph keeps inside a tracked insertion, moved text, a smart tag, custom XML, a simple field or a filled-in inline content control. Previously only a paragraph's direct runs and hyperlinks were read, so a document with unaccepted tracked changes lost every inserted word, and a filled form field lost its value. Deleted text, the old place of moved text and content controls that still show their placeholder prompt are left out, as the document does not show them.

  • DocumentRecallEvaluator and DocumentNDCGEvaluator now support list and dictionary metadata values in document_comparison_field instead of raising TypeError. Nested values are supported, and duplicate documents retain the existing recall and relevance scoring behavior.

  • FileTypeRouter no longer loses the detected MIME type when meta is passed alongside file paths.

  • Fix AutoMergingRetriever incorrectly counting duplicate child documents when evaluating merge thresholds. Documents are now deduplicated by ID before merging, retaining the highest-scoring copy in both synchronous and asynchronous retrieval.

  • EvaluationRunResult.comparative_detailed_report no longer drops the first run's scores when both runs have the same run_name. Their columns are named with _first and _second again, as they were before the report stopped using pandas. When align_on isn't set and the two runs have a different number of rows, it logs a warning and pads the shorter run with None instead of returning columns of different lengths.

  • DocumentToImageContent now skips an image file that cannot be decoded instead of failing the whole run() call. The skipped document gets None in image_contents and a warning is logged.

  • ImageFileToImageContent now skips a source that is not a valid image instead of failing the whole run() call. A non-image file in a mixed batch, such as a .txt file, is logged and omitted from image_contents, while the other sources are still converted.

  • Fixed ListJoiner.from_dict mutating the caller's data in place: the serialized list_type_ string was replaced with the deserialized type, so a second from_dict call on the same dictionary failed and the dictionary could no longer be dumped to JSON or YAML. from_dict now works on a copy and the caller's data is left untouched.

  • TiktokenCounter no longer raises ValueError when message or tool content contains literal special-token strings such as <|endoftext|>. They are now encoded as ordinary text and counted, consistent with DocumentSplitter and RecursiveDocumentSplitter.

  • Fix LLMEvaluator input validation to reject malformed tuples and non-list types with ValueError.

  • LLMRanker now skips response indices that do not represent valid integers.

  • Fixed MetadataRouter.from_dict mutating the caller's serialized dictionary in place, which made a second from_dict call on the same dictionary fail.

  • Fix MSGToDocument incorrectly including Cc and Bcc recipients in the To line. To, Cc and Bcc now use transport headers when available, falling back to Outlook recipients when headers are missing or blank.

  • ChatMessage.from_openai_dict_format now accepts an empty string as content for user, system, developer, and tool messages instead of raising ValueError. Messages such as ChatMessage.from_tool(tool_result="", ...) or ChatMessage.from_user(text="") now round-trip through to_openai_dict_format.

  • Ensures PythonCodeSplitter emits syntactically valid chunks when strip_docstrings=True is used on a function or class whose docstring is its only statement.

  • Fixed an issue in PythonCodeSplitter where form feed characters (\f) and certain Unicode separators could cause functions to be split incorrectly across chunks. Chunk boundaries and the start_line / end_line metadata now remain correct when these characters appear in the source.

  • DocumentMRREvaluator, DocumentMAPEvaluator and DocumentNDCGEvaluator no longer count an empty comparison value as a match. Previously, when the document_comparison_field was an empty string on both a ground truth and a retrieved Document, for example meta.file_id="", unrelated Documents were scored as relevant and could get a perfect score. DocumentRecallEvaluator already ignored empty values. With the default content field, this also means a ground truth Document with content="" is now excluded. DocumentMRREvaluator and DocumentMAPEvaluator now log a warning when none of the ground truth Documents for a question has a valid comparison value, like DocumentRecallEvaluator does.

  • RecursiveDocumentSplitter with split_unit="word" no longer counts a whitespace-only piece, such as a page break (\f) or a tab between two spaces, as a word when measuring chunks against split_length. The word-mode fixed-size fallback already skipped them, so it produced chunks that the rest of the splitter measured as longer than split_length, and chunks split at separators held fewer real words than split_length whenever they contained a page break. With split_overlap, such a piece could also be taken as the overlapping word.

    This changes the chunk boundaries for text containing page breaks or other whitespace surrounded by spaces. Re-split and re-index documents if you rely on stable chunk boundaries.

  • OpenAIResponsesChatGenerator and AzureOpenAIResponsesChatGenerator now accept chat histories with assistant messages produced by other providers, for example when FallbackChatGenerator switches from AmazonBedrockChatGenerator. Reasoning without a Responses API reasoning item id is dropped instead of being sent as an invalid input item, and tool calls without a call_id in extra use the tool call id as call_id. Previously, the API rejected these requests with a 400 error.

  • SASEvaluator now normalizes Cross Encoder scores based on the model instead of the scores in the batch. Previously, raw logits were only passed through a sigmoid if at least one score in the batch was above 1, so the same answer could get a different score depending on the other answers in the batch, and batches of only wrong answers returned negative scores. This affected models that return raw logits, such as cross-encoder/ms-marco-MiniLM-L-6-v2.

  • Fixed DocumentSplitter sentence-boundary overlap keeping one extra sentence when a sentence already contained exactly split_overlap words. The next chunk now stops at that word count instead of pulling in the sentence before it.

  • SlidingWindowCompactor no longer always removes the summary a SummarizationCompactor leaves in the current task. The summary is now kept or removed together with the Agent step after it. A summary at the start of history is now treated as the oldest historical turn.

  • Fixed StructuredOutputHook failing to parse JSON wrapped in Markdown code fences using CRLF line endings. Fenced replies using either LF or CRLF line endings are now stripped before structured-output parsing.

  • Fixed an issue where XLSXToDocument dropped literal text values such as "NA" and "N/A", converting them into empty cells. When neither keep_default_na nor na_values is supplied in read_excel_kwargs, the converter uses keep_default_na=False and na_values=[""] to preserve literal text while still recognizing empty cells as missing.

  • Fixed XLSXToDocument attaching hyperlinks to the wrong cells when read_excel_kwargs includes skiprows, usecols, or names.

  • HierarchicalDocumentSplitter now sets page_number and split_idx_start relative to the original document for documents below the first level. Previously they were counted from the start of the parent block, so deeper leaves reported the wrong page.

  • InMemoryDocumentStore.bm25_retrieval() and embedding_retrieval(), and their async variants, now raise a ValueError for a negative top_k. Previously, a negative value silently dropped documents from the end of the results, for example top_k=-1 returned 9 of 10 matches.

  • In JSONConverter, reject an extra_meta_fields value that is a string other than the literal '*' at component creation. Previously such a string was silently iterated character by character, producing meaningless metadata keys and dropping the intended fields from the converted documents.

  • PythonCodeSplitter with strip_docstrings=True no longer drops code that follows a docstring on the same line, such as """Docs."""; return 5. Previously the whole line was removed, silently deleting the statement and possibly leaving a chunk that fails to parse. In this case the docstring is now kept in the chunk content.

  • Pipeline.run_async() passed the values of a Variadic input, or a list input connected to several senders, in the order the senders finished, which could change between runs, so DocumentJoiner could apply its weights to the wrong retriever. Values now arrive in connection order, as in Pipeline.run(), with pipeline inputs passed directly to such a socket first. Pipeline.to_dict() now saves connections in the order they were made, so a pipeline loaded with Pipeline.from_dict() keeps the same order.

  • LinkContentFetcher no longer adds a text/plain entry to its handlers dictionary when it fetches content whose type matches no registered pattern (for example font/woff or multipart/form-data). Such content is still handled as text, as before.

  • LLM now declares exit_reason as an output, so it can be connected to other components in a Pipeline. It is one of "text" (a complete reply), "length" (the reply was cut off by the token limit) or "content_filter" (the reply was cut off by the provider's content filter), taken from the chat generator's finish_reason. For example, you can route a truncated reply to a retry with a higher max_tokens.

  • ConfirmationHook now marks the user message it adds when the user modifies a tool call's arguments with meta={"system_reminder": True}. SlidingWindowCompactor and SummarizationCompactor took that message for the user's task, so compaction could remove the real request. They now skip it when finding the task. StructuredOutputHook marks its correction messages the same way.

  • MarkdownHeaderSplitter no longer includes the optional closing sequence of an ATX header in the header and parent_headers metadata. For example, ## Install ## now produces the header Install instead of Install ##. The chunk content is unchanged.

  • MarkdownHeaderSplitter now follows the CommonMark rules for fenced code blocks, so # lines inside them are no longer split off as headers. A block can now close with a longer fence than the one that opened it, fences may be indented by up to three spaces, and a block that is never closed runs to the end of the document. A line that starts with inline code, where more backticks follow the opening ones on the same line, is not treated as a fence.

  • MarkdownHeaderSplitter now recognizes ATX headers with up to three leading spaces, preserving section boundaries and header hierarchy metadata for indented Markdown headers. Lines indented by four or more spaces remain ordinary content.

  • MarkdownToDocument with table_to_single_line=True now separates table cells with spaces and tables from surrounding content with newlines. Previously, cell contents and consecutive tables were concatenated without separators, losing word boundaries.

  • MetaFieldRanker now rejects a NaN weight in its constructor and in run() with a ValueError instead of returning documents with NaN scores.

  • MultiRetriever now raises a ValueError at initialization if join_mode is not "concatenate" or "reciprocal_rank_fusion". Previously, unsupported values such as "rrf" were silently treated as "concatenate" when top_k was not set.

  • OpenAIChatGenerator now works with tools on gpt-5.6 models, gpt-6-sol, and gpt-6-luna. Over the Chat Completions API, these models accept tools only with reasoning_effort="none" and returned HTTP 400 when tools were sent without it. When a request to one of these models includes tools and generation_kwargs doesn't set reasoning_effort, OpenAIChatGenerator now sends reasoning_effort="none", so these requests run without reasoning. To combine reasoning with tools, use OpenAIResponsesChatGenerator.

  • OpenAIResponsesChatGenerator and AzureOpenAIResponsesChatGenerator now raise a RuntimeError when a streamed response fails, that is when the stream ends with a response.failed event or contains an error event. Previously these events were ignored and the generator returned an empty reply without a finish reason, so the failure went unnoticed.

  • ByteStream.from_dict() now preserves subclasses when deserializing a dictionary, instead of always returning a ByteStream instance.

  • MockChatGenerator.from_dict() now preserves the supplied serialized configuration, so the same dictionary can be loaded more than once when using predefined responses, a response function, or a streaming callback.

  • Pipeline.run() no longer changes the visit counts stored in a PipelineSnapshot when resuming it. Replaying the same snapshot or retrying after a downstream failure now preserves the saved state and does not incorrectly exhaust max_runs_per_component.

  • Preserve field text, such as slide numbers, in PPTXToDocument when link_format is "markdown" or "plain". Field values in text boxes and table cells are kept in their original position alongside text and links.

  • Preserve all reasoning items returned by OpenAIResponsesChatGenerator in both streaming and non-streaming responses, including each item's ID and encrypted content. Individual items are stored in a single ReasoningContent.extra["reasoning_items"] list. Reasoning items and function calls retain their original relative order when replayed in a multi-turn conversation. Previously serialized single-item reasoning metadata remains supported.

  • PythonCodeSplitter now keeps decorators with the methods or classes they belong to, without copying them to other chunks. This also works with multiline decorators.

  • MetaFieldRanker now treats a NaN relevance score as out of range in linear_score mode. It logs the existing warning and defaults the score to 0 instead of sorting the document by a NaN score.

  • create_tool_from_function and the @tool decorator now raise ValueError when a required parameter name starts with an underscore, such as _tenant, and that parameter is not listed in inputs_from_state. Those names are not exposed to the LLM, so a tool created without the value could not be called. Provide the parameter through inputs_from_state or give it a default. Underscore-prefixed parameters that already have a default stay omitted from the schema.

  • Fix MultiQueryEmbeddingRetriever, MultiQueryTextRetriever, and TextEmbeddingRetriever sorting so documents without a score are placed after all scored documents, including documents with negative scores.

  • Runtime streaming callbacks now correctly override initialization callbacks, avoiding errors and warnings caused by unused callbacks. Callback selection also correctly handles callable objects that evaluate to false.

  • SearchableToolset now rejects catalog tools that share the bootstrap search tool name when the catalog has at least search_threshold tools. Previously, discovering such a tool produced duplicate names and failed the Agent run. Set search_tool_name to a different name to use that catalog.

  • create_tool_from_function and ComponentTool now exclude callback parameters wrapped in typing.Annotated from their tool schemas, including optional callbacks. Annotating a callback no longer causes tool construction to fail with SchemaGenerationError; the callback's default remains available when the tool is invoked.

  • ImageContent.from_file_path() now raises a ValueError naming the file when it cannot be converted to an image, for example because it does not exist or is not an image. Previously it raised an unhelpful IndexError: list index out of range.

  • SlidingWindowCompactor no longer removes system reminders from the current task, such as the ones StepBudgetReminderHook adds. Before, every compaction removed them, including a reminder added for the LLM call about to happen, which the model then never saw. Now a reminder is kept or removed together with the next assistant message and its tool results, and reminders after the last assistant message are always kept.

  • Fix StructuredOutputHook for list-typed state keys, such as {"type": list[str]}. The hook now replaces the state value with the validated result, or None on failure, instead of appending to it. Previously, a failed attempt followed by a successful retry returned [None, "Paris"] instead of ["Paris"].

  • StructuredOutputHook now honors max_retries when StepBudgetReminderHook (or another hook adding system_reminder messages) is registered. The hook counts its corrections since the last user message, and it treated each reminder as a new user turn, so the count reset every step: the Agent kept asking for corrections until max_agent_steps and stopped with exit_reason="max_agent_steps". Reminders are now skipped when counting.

  • create_tool_from_function and the @tool decorator now leave *args and **kwargs out of the Tool's parameters schema. Before, they were added as required parameters under their own names, such as args and kwargs, and the LLM was asked to fill them. A value the LLM sent for *args made the tool call fail with ToolInvocationError, and a value for **kwargs reached the function nested under the parameter's name. An untyped *args or **kwargs no longer raises a missing type hint ValueError.

  • Tool now rejects non-empty inputs_from_state mappings for tools that take no inputs. The error is raised when the tool is created instead of when it is invoked. If the callable's signature cannot be inspected, parameter names in inputs_from_state are not checked at construction time.

  • Fix FileSystemToolResultStore altering line endings in text results on Windows.

  • Pipeline.connect now accepts a Union output connected to a Union input when each member of the output fits a member of the input, in any order. For example, str | None connects to str | int | None, and list[Path | ByteStream] connects to the list[str | Path | ByteStream] input of converters such as TextFileToDocument. Before, the members were compared by position, and these connections raised PipelineConnectError. Literal types are matched the same way: Literal["b", "c"] now connects to Literal["a", "b", "c"].

  • OpenAIDocumentEmbedder and AzureOpenAIDocumentEmbedder now check batch_size when the component is created and raise a ValueError if it is not a positive integer, such as 0, -1, or 0.5 (for example ValueError: batch_size must be a positive integer, but got 0). Before this fix, an invalid batch_size was accepted without any error. The problem only appeared later, when run or run_async was called, with errors such as ValueError: n must be at least one that do not mention batch_size.

  • MultiRetriever, MultiQueryTextRetriever, MultiQueryEmbeddingRetriever, LLMMetadataExtractor and LLMDocumentContentExtractor now raise a ValueError at initialization when max_workers is smaller than 1. Previously the invalid value was accepted, run failed later inside ThreadPoolExecutor, and run_async silently treated it as 1.

💙 Big thank you to everyone who contributed to this release!

@Aditya17-bot, @Afloat16, @amarthya-14, @anakin87, @anh-nguyenn, @aryamangodara, @Aurantium-47, @bogdankostic, @BugBuster18, @chrikrah, @D1822787566, @davidsbatista, @desty, @Dhruv-Patil-1720, @Elias-Sun, @Elonon901001, @emme1t, @erdogan-deniz, @Eryx-Z, @gauravch-code, @geyazheng, @Guo-Yixin, @Harbor404, @HareeshDaxton, @HarshitR2004, @hazelxue, @hermanrous, @hessamhzDS, @its-amann, @JackyHsu-X, @jaeminbird, @jeevan1098, @JimmyWang0417, @julian-risch, @kerthans, @kun654, @L4XB, @lalith0407, @lester076, @linhongyu510, @MALLADISARMA, @mengchen-atx, @mittalpk, @Naeemeh146, @nik13, @NotZhu, @pcbeingused333, @pei711, @porkedchop, @priy-anshugupta, @Quantum-Enigma-dot, @Revinand, @senthurapandi2000, @shivsin25, @shuaiyafei, @simin-yuan, @sjrl, @Smilewithoutfalling, @solitarybear, @Starchaser1024, @SuhrudhC, @sunhouy, @Tanishq-Choudhary, @TemitopeKadri, @Vedant-Agarwal, @Winoren, @wozhenbang2004, @Ytq1115, @yuv009

Don't miss a new haystack release

NewReleases is sending notifications on new releases.