github deepset-ai/haystack v3.2.0-rc1

pre-release5 hours ago

Release Notes

v3.2.0-rc1

Upgrade Notes

  • Serialized OutputAdapter and ConditionalRouter components containing Jinja custom_filters must now be loaded with Pipeline.load(..., unsafe=True) (or the equivalent Pipeline.loads / Pipeline.from_dict option).

  • The tool_result_offloaded meta key that the hook sets on an offloaded message now always holds a list of store references rather than a single reference string, since a result can span several entries. Code reading that key (for example to re-read an offloaded result) should index into the list.

    ToolResultStore.write and read are typed str | bytes. Nothing changes at runtime for a text-only store, but one that subclasses the protocol and annotates content: str may need a type-checker fix.

  • The + operator for combining Toolsets has been removed. Pass Toolsets as a list wherever tools are accepted instead: Agent(..., tools=[toolset_a, toolset_b]). Pipelines serialized with a +-combined Toolset cannot be loaded anymore (their YAML references the removed internal _ToolsetWrapper class): recreate them with the list form and serialize them again.

  • Toolset.add() now only accepts a single Tool, not another Toolset. To combine Toolsets, pass them as a list: Agent(..., tools=[toolset_a, toolset_b]).

New Features

  • Add Pipeline.add_components() to add a mapping of component names to component instances in one call. Re-adding the same component instance under the same name with Pipeline.add_component() or Pipeline.add_components() is now a no-op.

    pipeline.add_components(
        {
            "retriever": retriever,
            "prompt_builder": prompt_builder,
            "llm": llm,
        }
    )
  • Added the experimental SummarizationCompactor (use with CompactionHook), which progressively summarizes a conversation until it fits a target token budget, preserving useful context from long-running Agents instead of dropping older messages.

    It uses four summarization tiers in order until the target token budget is reached:

    1. historical_turns: Starting with the oldest, summarize as few complete historical turns as needed.
    2. historical_summaries: When no complete historical turns remain, combine as few of the oldest historical summaries as needed.
    3. current_task_steps: Summarize the fewest oldest steps needed to reach the target while preserving the min_keep_steps newest steps.
    4. current_task_summaries: When no more steps can be summarized, combine as few of the current task's oldest summaries as needed to reach the target.
    from typing import Annotated
    from haystack.components.agents import Agent
    from haystack.components.generators.chat import OpenAIResponsesChatGenerator
    from haystack.hooks.compaction import CompactionHook, SummarizationCompactor
    from haystack.tools import tool
    
    @tool
    def web_search(query: Annotated[str, "The search query"]) -> str:
        """Search the web for current information."""
        return f"Search results for: {query}"
    
    agent_generator = OpenAIResponsesChatGenerator(model="gpt-5.4")
    summary_generator = OpenAIResponsesChatGenerator(model="gpt-5.4-nano")
    
    compaction_hook = CompactionHook(
        compactor=SummarizationCompactor(
            chat_generator=summary_generator,
            min_keep_steps=2,
            approximate_summary_tokens=1_024,
        ),
        context_window=400_000,
        compact_at=0.7,
        compact_to=0.4,
    )
    
    agent = Agent(
        chat_generator=agent_generator,
        tools=[web_search],
        hooks={"before_llm": [compaction_hook]},
    )
  • Added stop_run, a control flag that lets an Agent hook end a run cleanly instead of raising an error. The hook sets it to a reason of your choice (state.set("stop_run", "my_reason")), and the Agent stops without another model call and reports that reason in exit_reason.

  • Added TokenBudgetHook (experimental), a ready-made hook that caps how many tokens an Agent run may spend. The run ends with exit_reason set to "token_budget_exceeded", keeping the messages collected so far.

    from haystack.hooks.budget import TokenBudgetHook
    
    agent = Agent(
        chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"),
        tools=[search],
        hooks={"before_llm": [TokenBudgetHook(max_total_tokens=100_000)]},
    )
  • Add Pipeline.connect_many() to connect multiple (sender, receiver) pairs in one call. Pipeline.add_component() and Pipeline.add_components() now return the pipeline, allowing pipeline-building methods to be chained.

    pipeline = (
        Pipeline()
        .add_components(
            {
                "retriever": retriever,
                "prompt_builder": prompt_builder,
                "llm": llm,
            }
        )
        .connect_many(
            [
                ("retriever", "prompt_builder.documents"),
                ("prompt_builder", "llm"),
            ]
        )
    )
  • Added the HAYSTACK_UNSAFE_DESERIALIZATION environment variable as a process-wide equivalent of loading with unsafe=True. When set to a truthy value (1 or true), every Pipeline.load / Pipeline.loads / Pipeline.from_dict call skips all deserialization safety checks — the module allowlist, the builtin/import-primitive and control-plane denylists, the object-internals traversal guard, and the refusal to honor a component's own unsafe: true flag. This is intended for deployments that only ever load fully trusted pipelines and cannot pass unsafe=True at every call site. A warning is logged the first time it takes effect. Only enable it when every pipeline the process loads is trusted: a single untrusted pipeline then leads to arbitrary code execution.

    The switch is not limited to pipeline loading: it disables the same checks for every deserialization path in the process, including ones that take no unsafe argument of their own — Tool.from_dict, State.from_dict (agent snapshot resume), and the ConditionalRouter and OutputAdapter Jinja sandbox flags. A deployment that loads trusted pipelines at startup but accepts serialized tools or agent state at request time is therefore exposed on the request path as well, not only at load time.

    The variable is read once, on the first deserialization in the process, and the result is then frozen for the process lifetime: writes to it afterwards are ignored, in either direction. Set it before the first pipeline is loaded. Freezing keeps the safety mode of a process from changing under a caller's feet and, because the first read happens before any deserialized data can run, stops a hostile pipeline from switching the checks off while it is being loaded.

Enhancement Notes

  • FileSystemSkillStore's skills_dir now also accepts a Secret, so the skills directory path can be sourced from an environment variable (e.g. Secret.from_env_var("SKILLS_DIR")) instead of being hard-coded in pipeline configuration.

  • AnswerJoiner now logs an informational message when sort_by_score is enabled and some answers have no score, matching the existing behaviour of DocumentJoiner. Such answers are still sorted as if their score were -infinity, but the demotion is no longer silent. This is common when joining GeneratedAnswer objects, which carry no score at all.

  • Add the min_content_length parameter to DocumentCleaner. After the configured cleaning steps run, text documents shorter than the threshold (ignoring leading and trailing whitespace) are omitted from the output.

  • Add split_by="token" to DocumentSplitter and DocumentPreprocessor. Splits text by LLM token count using tiktoken. The encoding defaults to "o200k_base" (current OpenAI models) and can be changed via the new tokenizer_encoding parameter. split_length, split_overlap, and split_threshold all work as usual. Requires pip install tiktoken.

  • ToolResultOffloadHook now offloads tool results carrying ImageContent or FileContent, not just text.

    Each part of a result gets its own store entry, with images and files decoded from base64 to raw bytes, under the extension of its filename when it has one and of its mime_type otherwise (falling back to .bin). For example:

    Tool result offloaded to 3 files:
    1. text (412 characters) at '/abs/path/tool_results/2_fetch_call-123_0.txt'. Preview: Quarterly report...
    2. image/png (48210 bytes) at '/abs/path/tool_results/2_fetch_call-123_1.png'
    3. application/pdf named 'q3.pdf' (1048576 bytes) at '/abs/path/tool_results/2_fetch_call-123_2.pdf'
    

    Binary content is opt-in, via a new supports_binary_content attribute on the ToolResultStore protocol, which FileSystemToolResultStore sets to True. A store leaving it at its False default never receives bytes: images and files stay in context with a warning, as before, so existing custom stores keep working unchanged.

    An offload policy receives the result as a single string: the text and base64 payloads of all its parts concatenated.

  • FallbackChatGenerator now wraps every internal ChatGenerator attempt in a haystack.chat_generator.run tracing span for both synchronous and asynchronous execution. With content tracing enabled, each internal span records the forwarded inputs. The successful output remains on the enclosing FallbackChatGenerator component span so tracing backends do not count its token usage twice.

  • TopPSampler now rejects negative, boolean, and non-integer min_top_k values during initialization with a clear ValueError.

Security Notes

  • Prevent arbitrary callables registered as serialized Jinja custom filters from executing while a pipeline is loaded in safe mode. Jinja can invoke filters with constant arguments during template compilation, and its sandbox does not apply callable-safety checks to filters.
  • Harden callable deserialization by checking the real module of every object traversed in a dotted callable path. This prevents an allowlisted module from exposing an object defined in an unallowlisted module that leads back to an otherwise allowlisted final callable.

Bug Fixes

  • Allow Pipeline component outputs typed as list[T] to connect to inputs typed as Iterable[T]. Lists already satisfy the iterable input contract and are passed through without conversion.
  • Fixed the AnswerJoiner.run docstring: it did not mention that the sort_by_score init parameter affects the order of the returned answers. The docstring now documents that when sort_by_score=True the merged answers are sorted by score in descending order (answers without a score are handled as if their score were -infinity) before top_k is applied, and that the input order is preserved otherwise.
  • Fix Agent.clone() and Agent.to_dict()/from_dict() raising TypeError for an Agent built on a chat generator that does not accept a tools parameter. tools is normalized to an empty list at init, and both round-trip paths passed that empty list back to the constructor, where it was treated as "tools were provided". An empty list now carries no tools, matching the equivalent check already applied in Agent.run(). Passing tools=[] explicitly to such an Agent is accepted for the same reason. A non-empty tools value still raises, and a Toolset is still never tested for truthiness at init.
  • Prevent Agent from retrying an empty response when a chat generator reports finish_reason="length". This can happen when reasoning tokens exhaust the maximum output token budget. OpenAIResponsesChatGenerator now maps terminal Responses API events to Haystack FinishReason values and attaches finish_reason to the returned ChatMessage.
  • Agent now reports exit_reason="length" or "content_filter" when a tool-call-free model reply ends for either reason, including replies containing partial text. These incomplete generations stop by default but can be recovered with an on_exit hook. AgentTool also warns the calling model that such results may be incomplete.
  • JsonSchemaValidator now accepts an empty JSON Schema ({}) and honors it when passed to run(). Empty schemas are valid JSON Schema and match every JSON value.
  • Fix AutoMergingRetriever rejecting documents whose hierarchy metadata uses __level=0 or __block_size=0. HierarchicalDocumentSplitter stores those values on the root node, so validation now checks for key presence instead of truthiness.
  • Log values are now bounded in size. Exceptions passed to a logger are rendered with str instead of repr, because some carry their whole input in their repr - UnicodeDecodeError keeps the entire buffer it failed to decode, so logging one raised while decoding a large file would emit that whole file as a single log line. Any remaining value longer than 4096 characters is truncated.
  • LLMMetadataExtractor now removes the metadata_extraction_error and metadata_extraction_response keys from a document's metadata on every successful extraction, in both run and run_async. Previously, when a document from failed_documents was re-run and the LLM returned an empty JSON object {}, the document ended up in documents still carrying both keys from the earlier failed attempt.
  • TextFileToDocument, CSVToDocument, MarkdownToDocument and MultiFileConverter now default to utf-8-sig instead of utf-8, so a UTF-8 byte order mark (BOM) is stripped rather than leaking into Document.content as a zero-width character. utf-8-sig decodes plain UTF-8 identically, so files without a BOM are unaffected.
  • JSONConverter no longer silently skips UTF-8 files that start with a byte order mark. The file content is now decoded with utf-8-sig. Previously the UnicodeError was caught and logged as a warning, so a BOM file was dropped from the pipeline without raising - three input files could produce two documents with no error.
  • DocumentJoiner now raises a ValueError when any value in weights is negative. Previously negative weights were normalized by their -- possibly negative -- sum, which flipped their sign and could produce negative document scores in merge mode. For example weights=[1, -2] was normalized to [-1.0, 2.0]. The existing error for weights that sum to zero is unchanged.
  • Preserve original document scores in DocumentJoiner distribution-based rank fusion when an input list contains a single document or has zero score variance.
  • DocumentSplitter now adds split_id, split_idx_start and page_number to the metadata of the chunks it creates when split_by="function", like it already did for all other split modes. Without this metadata, components that rely on it, such as SentenceWindowRetriever, could not be used with chunks produced by a custom splitting function. Empty chunks returned by the splitting function are now also skipped, unless skip_empty_documents=False.
  • Fix DocumentSplitter with split_by="token" generating redundant trailing chunks containing only overlap. The splitter now stops creating chunks after reaching the end of the document.
  • DocumentToImageContent no longer raises a ValueError for the whole batch when one document is missing the file path or page_number metadata, has an invalid file path, or has an unsupported MIME type. That document now gets None in image_contents and a warning with the reason is logged, while the other documents are still converted. This lets LLMDocumentContentExtractor return such documents in failed_documents instead of failing the entire run.
  • Fixed ToolResultOffloadHook offloading empty tool results. An empty string, or a result made up of nothing but empty text blocks, was written to the ToolResultStore as a zero-byte entry and replaced in the conversation by a pointer ending in a dangling Preview: - growing the context the hook exists to shrink, and inviting the model to read back an empty file. Such results are now left in context, as empty-list results already were.
  • Fixed FilterRetriever so that an empty runtime filters dictionary clears filters provided at initialization during synchronous and asynchronous execution.
  • Fixed document_matches_filter raising AttributeError for dotted filter fields whose root is not a Document attribute (for example {"field": "typo.x", ...}). Such fields are now treated as missing, consistent with non-dotted unknown fields and the documented behavior. This affected InMemoryDocumentStore.filter_documents and MetadataRouter.
  • Fixed CompactionHook mis-estimating the context size when compaction leaves a conversation with no assistant message, for example when a compactor summarizes every Agent step away. The hook now records the whole compacted conversation as accounted for, and the estimate reads that count back unchanged, so a second compaction hook running right after sees the true size instead of counting the conversation twice.
  • Fix crash in AzureOpenAIChatGenerator.to_dict() when response_format is passed as a dictionary.
  • Fixed ComponentDevice.first_device raising a ValueError when the first entry of a device map is a disk device. Disk devices are only valid as part of a device map (for example when HuggingFace offloads weights with device_map='auto'), so first_device now skips disk entries and returns the first usable device instead of crashing. If the device map is empty or contains only disk devices, a ValueError is raised.
  • Fix deserialize_secrets_inplace with recursive=True: serialized secrets were left as plain dicts instead of being converted back to Secret, and nested dictionaries deeper than one level were never visited.
  • Fix FileTypeRouter.run() writing the meta passed to it into the meta dict of the input ByteStream objects. The metadata is now added to a copy, so ByteStream sources are left untouched, matching the behaviour already in place for file path sources.
  • Prevent FilterPolicy.MERGE from mutating initialization or runtime filters when combining logical filters. Reusing a retriever for multiple runs now applies each runtime filter independently.
  • Fix InMemoryDocumentStore.count_unique_metadata_by_filter and count_unique_metadata_by_filter_async raising TypeError when metadata values are JSON-serializable lists or dictionaries, and make composite metadata deduplication consistent with get_metadata_field_unique_values.
  • JsonSchemaValidator no longer raises ValueError when the message content is a top-level JSON scalar like "hello", 42, true or null. The same crash happened for JSON arrays of scalars. Those values now reach the schema validator, which either accepts them or returns the usual validation_error output.
  • Fixed LinkContentFetcher.run_async() omitting default headers configured through client_kwargs. Asynchronous and synchronous fetches now apply the same client-default headers.
  • Fix token-based splitting in DocumentSplitter and RecursiveDocumentSplitter failing on documents containing strings such as <|endoftext|>. These strings are now preserved and counted as ordinary text, including when splitting with overlap.
  • Fixed LLMDocumentContentExtractor.run_async converting documents to images on the event loop. DocumentToImageContent reads every file and renders the requested PDF pages, and it has no run_async, so calling it directly blocked the loop for the whole batch before any LLM call started. It now runs through _execute_component_async, which hands the synchronous work to a thread.
  • Fix shared parent_headers metadata in MarkdownHeaderSplitter when keep_headers=False and secondary splitting is enabled. Editing one chunk's parent headers no longer changes its siblings' metadata.
  • MetaFieldGroupingRanker no longer partially reorders a group before falling back on a TypeError. When sort_docs_by values are mutually non-comparable (e.g. int and str), the group's original insertion order is now guaranteed, as the sorting happens on a copy instead of in place.
  • Fix unhandled AssertionError crash when comparing a Pipeline to any non-Pipeline object (such as pipeline == object() or pipeline in [object(), ...]) caused by an inverted isinstance check in PipelineBase.__eq__.
  • QueryExpander now returns an empty query list when query is None or not a string, instead of raising AttributeError on str.strip.
  • Fixed an issue where RecursiveDocumentSplitter applied the overlap at every recursion level when chunking text with multiple separators, producing chunks that were not substrings of the source text. The overlap is now applied exactly once, on the final chunk list.
  • Fixed deserialization of Tool subclasses such as ComponentTool in OpenAIResponsesChatGenerator.from_dict() and AzureOpenAIResponsesChatGenerator.from_dict().
  • Fixed FileSystemSkillStore.load_skill including symlinks that resolve outside the skill directory in its bundled-file manifest. Escaping symlinks are now excluded, matching read_skill_file traversal protection.
  • Correct the split_overlap validation message in RecursiveDocumentSplitter (0 is the default and only negative values are rejected) and clarify that overlap is measured in split_units. Also fix stale docstrings in AnswerJoiner (documented parameters the methods do not take, and claimed sorting that only happens for answers) and the meta_field ranker (referenced a nonexistent score mode).
  • Fixed create_tool_from_function, the @tool decorator, and ComponentTool stripping title keys inside OpenAPI 3.0 singular example values when building a tool's JSON schema. example is now treated as instance data alongside default, const, enum and examples, so a title key carried via Pydantic json_schema_extra stays part of the tool contract.
  • Fixed MetaFieldRanker raising a TypeError when meta_value_type is set and a document contains an unhashable metadata value, such as a list or dictionary. These values now follow the existing warning and original-document fallback behavior.
  • Fix XLSXToDocument skipping workbooks with hyperlinks in numeric or boolean columns when using pandas 3. Hyperlink text is now inserted without a dtype error, while columns without hyperlinks retain their original types and formatting.
  • LinkContentFetcher now performs the configured number of retries in synchronous runs. Previously, retry_attempts counted the initial request as an attempt, so synchronous runs performed one fewer retry than configured and behaved differently from asynchronous runs.
  • Fixed LLMDocumentContentExtractor incorrectly treating a chat generator output that carries its own error field as a failed LLM call. Such documents are now processed normally.
  • LLMMessagesRouter now raises a ValueError when output_names contains "chat_generator_text" or "unmatched". Those two outputs are always created by the router, so reusing either name used to be accepted and then silently broke the component: "chat_generator_text" had its socket retyped to list[ChatMessage] and lost the LLM reply at run time, while "unmatched" made a matched decision indistinguishable from an unmatched one.
  • Fixed LLMMetadataExtractor incorrectly treating an extracted error metadata field as an extraction failure. error can now be used in expected_keys like any other metadata key.
  • LLMRanker no longer crashes with AttributeError when query is None (or another non-string) coming from an upstream pipeline component. Both run and run_async now return the documents unranked, matching the existing empty-query fallback.
  • Fixed MarkdownHeaderSplitter silently dropping a document's leading header line when secondary_split is used with keep_headers=False. This happened for any chunk that wasn't the result of an actual header split, for example when a document has no headers, only headers without body content, a header at a level excluded from header_split_levels, or input metadata that already contained a header key. The header line is now only stripped from chunks that truly came from a header split.
  • MarkdownHeaderSplitter no longer drops the text that precedes the first header it splits on. A document's opening paragraph, title block, or front matter was silently lost, and so was any content above the first matching header when header_split_levels excluded the levels above it. A document whose headers all had empty bodies lost that text too, because it was treated as header-only. That text is now emitted as a leading chunk with empty header and parent_headers metadata, so every chunk still carries the documented metadata fields. Leading text that is only whitespace joins the first chunk instead of becoming a chunk of its own, which keeps the chunks a byte-exact partition of the input without adding an empty chunk.
  • Fixed incorrect page_number metadata from MarkdownHeaderSplitter when secondary splitting uses overlap. Page breaks in overlapping content are no longer counted multiple times, and custom page_break_character values are respected.
  • Fixed EmbeddingBasedDocumentSplitter emitting a final split shorter than min_length. Small splits were only merged forward, so the last one had nothing left to absorb and was returned as its own document. It is now merged into the preceding split, unless doing so would reach max_length, the same limit that already governs forward merges.
  • MultiQueryTextRetriever, MultiQueryEmbeddingRetriever and MultiRetriever now honour max_workers in their run_async methods. Previously only the synchronous run bounded fan-out (via a ThreadPoolExecutor); run_async launched every sub-query / sub-retriever call at once, ignoring max_workers and risking rate limits or connection exhaustion against the underlying retriever or embedder. run_async now bounds concurrency with an asyncio.Semaphore, matching the behaviour of the LLM extractors.
  • The OpenAI Chat Completions and Responses converters no longer raise on an assistant message with no content parts, which a Chat Generator returns when it discards a malformed tool call. It is sent with empty content, which the APIs accept, so the next LLM call goes through. ChatMessage.from_openai_dict_format accepts the same empty content, so such a message round-trips.
  • The Hugging Face message converter no longer raises on an assistant message with no content parts, and sends it with empty content instead.
  • OpenAIDocumentEmbedder.run_async (and AzureOpenAIDocumentEmbedder) now request encoding_format="float" from the embeddings endpoint, matching the synchronous run. The async batch path was missed when this was added in #9655, so run_async let the OpenAI SDK negotiate the base64 wire format, which some OpenAI-compatible endpoints do not support.
  • Fixed OpenAIChatGenerator and OpenAIResponsesChatGenerator so that an empty runtime tools list overrides tools configured at initialization.
  • Add the init parameters timeout and max_retries to the to_dict method of OpenAIImageGenerator. They were dropped on serialization, so a pipeline saved and reloaded silently fell back to the OPENAI_TIMEOUT/OPENAI_MAX_RETRIES defaults instead of the configured values. This matches OpenAIChatGenerator, OpenAITextEmbedder and OpenAIDocumentEmbedder, which already serialize both.
  • OutputAdapter and ConditionalRouter no longer coerce string outputs to other Python types when output_type=str. Previously the rendered template was always passed through ast.literal_eval in safe mode, so a string that happened to be a valid Python literal was silently converted (for example "1,000" became the tuple (1, 0) and "42" became the integer 42), producing a value that did not match the declared str output type. Literal evaluation is now skipped when output_type is str, so string outputs are returned unchanged. Other output types still reconstruct structured literals as before.
  • Preserve finish reasons and usage metadata when an OpenAI Responses API stream emits events after the completed response event.
  • PythonCodeSplitter now deep-copies the metadata of the document it splits, matching the other splitters. Previously it copied it shallowly, so nested values such as a list under meta["tags"] were shared between every chunk and with the input document, and editing one chunk's metadata changed all the others. The secondary line-based split of oversized units had the same problem.
  • RecursiveDocumentSplitter and TextCleaner now keep their init parameters when serialized, for example in a pipeline saved with Pipeline.dumps(). Before, RecursiveDocumentSplitter wrote split_unit: word whatever unit it was created with, so a reloaded splitter configured for characters or tokens counted words instead. A reloaded TextCleaner lost all of its options and returned the texts unchanged.
  • Pipeline snapshot resumption now rejects snapshots whose component set differs from the current pipeline. This prevents newly added components from being silently skipped during a resumed run, while preserving the existing validation for components removed since the snapshot was created.
  • Fix DocumentSplitter with split_by="word", "period", "page", "passage", "line" or "sentence" generating redundant trailing chunks containing only overlap. Segments that add no new text beyond the already covered units are now skipped, mirroring the split_by="token" fix.
  • Fix an IndexError: list index out of range when a chat generator's stream completes without emitting any chunks. _convert_streaming_chunks_to_chat_message now returns an assistant message with empty content and None metadata instead.
  • SuperComponent.run_async now returns outputs mapped from components that are not leaves of the wrapped pipeline, like run does. Before, an output that was also consumed inside the pipeline, such as retriever.documents feeding a prompt builder, was silently missing from the async result. This also affected a PipelineTool run by Agent.run_async, whose outputs_to_state got nothing for such outputs.
  • create_tool_from_function and the @tool decorator now work with functions defined in a module that uses from __future__ import annotations. Before, the parameter annotations were plain strings, and creating the tool raised a SchemaGenerationError for any parameter typed with something other than a builtin, such as Annotated, Literal, Optional, Document or State. The annotations are now resolved first, and Annotated descriptions are kept.
  • MetadataRouter now raises a clear ValueError when a rule uses the reserved output name unmatched, instead of failing with an internal duplicate-keyword TypeError during output registration.
  • FileTypeRouter and DocumentTypeRouter now raise a clear ValueError when a MIME type uses a reserved output name (unclassified, or failed on FileTypeRouter), instead of failing with an internal duplicate-keyword TypeError during output registration.
  • Fixed XLSXToDocument writing the string nan into an empty cell when table_format="markdown". The same cell is written as an empty field by table_format="csv", so an empty cell now reads as empty in both formats. A different placeholder can be set with table_format_kwargs={"missingval": "N/A"}.

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

@abhati27, @abo3losh1, @AjayShivran, @alanhuangyoo, @anakin87, @Anurag-M1, @Arman-Beykmohammadi, @ArzelaAscoIi, @Awshesh12, @bilgeyucel, @bogdankostic, @businessarshgoyal, @coder058, @CoralGarden52, @davidsbatista, @dfedoryshchev, @dingpuyu, @Diwak4r, @Dxfory, @eatLaoJun, @ege-arhan, @feiiiiii5, @fng713, @Goodnight77, @gyanu2507, @Harsh23Kashyap, @iridescentWen, @JbravoI, @JimmyWang0417, @jliounis, @julian-risch, @kacperlukawski, @Koushik890, @Kuang-xianxin, @L4XB, @Lesereingrape, @lets-order-some-fries, @linhongyu510, @mfurkanakinci, @mikemikimike, @MohammadHijjawi97, @mrchtr, @Nikhi00718, @otiscuilei, @PattonBrown, @pcbeingused333, @PeterSmith0127-lcm, @Rainmemery, @rautaditya2606, @Ricky-7-Yan, @sainikhiljuluri, @samrusani, @seanxuu, @ShousenZHANG, @simpleqt, @sjrl, @spacesheepinternet, @teachershuang, @tstadel, @Vedant-Agarwal, @vercel[bot], @winter-street, @xblwh, @yavuz-yilmaz

Don't miss a new haystack release

NewReleases is sending notifications on new releases.