Release Notes
v3.2.0-rc1
Upgrade Notes
-
Serialized
OutputAdapterandConditionalRoutercomponents containing Jinjacustom_filtersmust now be loaded withPipeline.load(..., unsafe=True)(or the equivalentPipeline.loads/Pipeline.from_dictoption). -
The
tool_result_offloadedmeta 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.writeandreadare typedstr | bytes. Nothing changes at runtime for a text-only store, but one that subclasses the protocol and annotatescontent: strmay 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_ToolsetWrapperclass): 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 withPipeline.add_component()orPipeline.add_components()is now a no-op.pipeline.add_components( { "retriever": retriever, "prompt_builder": prompt_builder, "llm": llm, } )
-
Added the experimental
SummarizationCompactor(use withCompactionHook), 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:
historical_turns: Starting with the oldest, summarize as few complete historical turns as needed.historical_summaries: When no complete historical turns remain, combine as few of the oldest historical summaries as needed.current_task_steps: Summarize the fewest oldest steps needed to reach the target while preserving themin_keep_stepsnewest steps.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 inexit_reason. -
Added
TokenBudgetHook(experimental), a ready-made hook that caps how many tokens an Agent run may spend. The run ends withexit_reasonset 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()andPipeline.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_DESERIALIZATIONenvironment variable as a process-wide equivalent of loading withunsafe=True. When set to a truthy value (1ortrue), everyPipeline.load/Pipeline.loads/Pipeline.from_dictcall 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 ownunsafe: trueflag. This is intended for deployments that only ever load fully trusted pipelines and cannot passunsafe=Trueat 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
unsafeargument of their own —Tool.from_dict,State.from_dict(agent snapshot resume), and theConditionalRouterandOutputAdapterJinja 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'sskills_dirnow also accepts aSecret, 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. -
AnswerJoinernow logs an informational message whensort_by_scoreis enabled and some answers have no score, matching the existing behaviour ofDocumentJoiner. Such answers are still sorted as if their score were-infinity, but the demotion is no longer silent. This is common when joiningGeneratedAnswerobjects, which carry no score at all. -
Add the
min_content_lengthparameter toDocumentCleaner. 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"toDocumentSplitterandDocumentPreprocessor. Splits text by LLM token count using tiktoken. The encoding defaults to"o200k_base"(current OpenAI models) and can be changed via the newtokenizer_encodingparameter.split_length,split_overlap, andsplit_thresholdall work as usual. Requirespip install tiktoken. -
ToolResultOffloadHooknow offloads tool results carryingImageContentorFileContent, 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
filenamewhen it has one and of itsmime_typeotherwise (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_contentattribute on theToolResultStoreprotocol, whichFileSystemToolResultStoresets toTrue. A store leaving it at itsFalsedefault 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.
-
FallbackChatGeneratornow wraps every internalChatGeneratorattempt in ahaystack.chat_generator.runtracing span for both synchronous and asynchronous execution. With content tracing enabled, each internal span records the forwarded inputs. The successful output remains on the enclosingFallbackChatGeneratorcomponent span so tracing backends do not count its token usage twice. -
TopPSamplernow rejects negative, boolean, and non-integermin_top_kvalues during initialization with a clearValueError.
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
Pipelinecomponent outputs typed aslist[T]to connect to inputs typed asIterable[T]. Lists already satisfy the iterable input contract and are passed through without conversion. - Fixed the
AnswerJoiner.rundocstring: it did not mention that thesort_by_scoreinit parameter affects the order of the returned answers. The docstring now documents that whensort_by_score=Truethe merged answers are sorted by score in descending order (answers without a score are handled as if their score were-infinity) beforetop_kis applied, and that the input order is preserved otherwise. - Fix
Agent.clone()andAgent.to_dict()/from_dict()raisingTypeErrorfor anAgentbuilt on a chat generator that does not accept atoolsparameter.toolsis 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 inAgent.run(). Passingtools=[]explicitly to such anAgentis accepted for the same reason. A non-emptytoolsvalue still raises, and aToolsetis still never tested for truthiness at init. - Prevent
Agentfrom retrying an empty response when a chat generator reportsfinish_reason="length". This can happen when reasoning tokens exhaust the maximum output token budget.OpenAIResponsesChatGeneratornow maps terminal Responses API events to HaystackFinishReasonvalues and attachesfinish_reasonto the returnedChatMessage. Agentnow reportsexit_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 anon_exithook.AgentToolalso warns the calling model that such results may be incomplete.JsonSchemaValidatornow accepts an empty JSON Schema ({}) and honors it when passed torun(). Empty schemas are valid JSON Schema and match every JSON value.- Fix
AutoMergingRetrieverrejecting documents whose hierarchy metadata uses__level=0or__block_size=0.HierarchicalDocumentSplitterstores 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
strinstead ofrepr, because some carry their whole input in theirrepr-UnicodeDecodeErrorkeeps 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. LLMMetadataExtractornow removes themetadata_extraction_errorandmetadata_extraction_responsekeys from a document's metadata on every successful extraction, in bothrunandrun_async. Previously, when a document fromfailed_documentswas re-run and the LLM returned an empty JSON object{}, the document ended up indocumentsstill carrying both keys from the earlier failed attempt.TextFileToDocument,CSVToDocument,MarkdownToDocumentandMultiFileConverternow default toutf-8-siginstead ofutf-8, so a UTF-8 byte order mark (BOM) is stripped rather than leaking intoDocument.contentas a zero-width character.utf-8-sigdecodes plain UTF-8 identically, so files without a BOM are unaffected.JSONConverterno longer silently skips UTF-8 files that start with a byte order mark. The file content is now decoded withutf-8-sig. Previously theUnicodeErrorwas 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.DocumentJoinernow raises aValueErrorwhen any value inweightsis negative. Previously negative weights were normalized by their -- possibly negative -- sum, which flipped their sign and could produce negative document scores inmergemode. For exampleweights=[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
DocumentJoinerdistribution-based rank fusion when an input list contains a single document or has zero score variance. DocumentSplitternow addssplit_id,split_idx_startandpage_numberto the metadata of the chunks it creates whensplit_by="function", like it already did for all other split modes. Without this metadata, components that rely on it, such asSentenceWindowRetriever, could not be used with chunks produced by a custom splitting function. Empty chunks returned by the splitting function are now also skipped, unlessskip_empty_documents=False.- Fix
DocumentSplitterwithsplit_by="token"generating redundant trailing chunks containing only overlap. The splitter now stops creating chunks after reaching the end of the document. DocumentToImageContentno longer raises aValueErrorfor the whole batch when one document is missing the file path orpage_numbermetadata, has an invalid file path, or has an unsupported MIME type. That document now getsNoneinimage_contentsand a warning with the reason is logged, while the other documents are still converted. This letsLLMDocumentContentExtractorreturn such documents infailed_documentsinstead of failing the entire run.- Fixed
ToolResultOffloadHookoffloading empty tool results. An empty string, or a result made up of nothing but empty text blocks, was written to theToolResultStoreas a zero-byte entry and replaced in the conversation by a pointer ending in a danglingPreview:- 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
FilterRetrieverso that an empty runtimefiltersdictionary clears filters provided at initialization during synchronous and asynchronous execution. - Fixed
document_matches_filterraisingAttributeErrorfor dotted filter fields whose root is not aDocumentattribute (for example{"field": "typo.x", ...}). Such fields are now treated as missing, consistent with non-dotted unknown fields and the documented behavior. This affectedInMemoryDocumentStore.filter_documentsandMetadataRouter. - Fixed
CompactionHookmis-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()whenresponse_formatis passed as a dictionary. - Fixed
ComponentDevice.first_deviceraising aValueErrorwhen 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 withdevice_map='auto'), sofirst_devicenow skips disk entries and returns the first usable device instead of crashing. If the device map is empty or contains only disk devices, aValueErroris raised. - Fix
deserialize_secrets_inplacewithrecursive=True: serialized secrets were left as plain dicts instead of being converted back toSecret, and nested dictionaries deeper than one level were never visited. - Fix
FileTypeRouter.run()writing themetapassed to it into themetadict of the inputByteStreamobjects. The metadata is now added to a copy, soByteStreamsources are left untouched, matching the behaviour already in place for file path sources. - Prevent
FilterPolicy.MERGEfrom 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_filterandcount_unique_metadata_by_filter_asyncraisingTypeErrorwhen metadata values are JSON-serializable lists or dictionaries, and make composite metadata deduplication consistent withget_metadata_field_unique_values. JsonSchemaValidatorno longer raisesValueErrorwhen the message content is a top-level JSON scalar like"hello",42,trueornull. The same crash happened for JSON arrays of scalars. Those values now reach the schema validator, which either accepts them or returns the usualvalidation_erroroutput.- Fixed
LinkContentFetcher.run_async()omitting default headers configured throughclient_kwargs. Asynchronous and synchronous fetches now apply the same client-default headers. - Fix token-based splitting in
DocumentSplitterandRecursiveDocumentSplitterfailing 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_asyncconverting documents to images on the event loop.DocumentToImageContentreads every file and renders the requested PDF pages, and it has norun_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_headersmetadata inMarkdownHeaderSplitterwhenkeep_headers=Falseand secondary splitting is enabled. Editing one chunk's parent headers no longer changes its siblings' metadata. MetaFieldGroupingRankerno longer partially reorders a group before falling back on aTypeError. Whensort_docs_byvalues 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
AssertionErrorcrash when comparing aPipelineto any non-Pipeline object (such aspipeline == object()orpipeline in [object(), ...]) caused by an invertedisinstancecheck inPipelineBase.__eq__. QueryExpandernow returns an empty query list whenqueryisNoneor not a string, instead of raisingAttributeErroronstr.strip.- Fixed an issue where
RecursiveDocumentSplitterapplied 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
Toolsubclasses such asComponentToolinOpenAIResponsesChatGenerator.from_dict()andAzureOpenAIResponsesChatGenerator.from_dict(). - Fixed
FileSystemSkillStore.load_skillincluding symlinks that resolve outside the skill directory in its bundled-file manifest. Escaping symlinks are now excluded, matchingread_skill_filetraversal protection. - Correct the
split_overlapvalidation message inRecursiveDocumentSplitter(0 is the default and only negative values are rejected) and clarify that overlap is measured insplit_units. Also fix stale docstrings inAnswerJoiner(documented parameters the methods do not take, and claimed sorting that only happens for answers) and themeta_fieldranker (referenced a nonexistentscoremode). - Fixed
create_tool_from_function, the@tooldecorator, andComponentToolstrippingtitlekeys inside OpenAPI 3.0 singularexamplevalues when building a tool's JSON schema.exampleis now treated as instance data alongsidedefault,const,enumandexamples, so atitlekey carried via Pydanticjson_schema_extrastays part of the tool contract. - Fixed
MetaFieldRankerraising aTypeErrorwhenmeta_value_typeis 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
XLSXToDocumentskipping 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. LinkContentFetchernow performs the configured number of retries in synchronous runs. Previously,retry_attemptscounted the initial request as an attempt, so synchronous runs performed one fewer retry than configured and behaved differently from asynchronous runs.- Fixed
LLMDocumentContentExtractorincorrectly treating a chat generator output that carries its ownerrorfield as a failed LLM call. Such documents are now processed normally. LLMMessagesRouternow raises aValueErrorwhenoutput_namescontains"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 tolist[ChatMessage]and lost the LLM reply at run time, while"unmatched"made a matched decision indistinguishable from an unmatched one.- Fixed
LLMMetadataExtractorincorrectly treating an extractederrormetadata field as an extraction failure.errorcan now be used inexpected_keyslike any other metadata key. LLMRankerno longer crashes withAttributeErrorwhenqueryisNone(or another non-string) coming from an upstream pipeline component. Bothrunandrun_asyncnow return the documents unranked, matching the existing empty-query fallback.- Fixed
MarkdownHeaderSplittersilently dropping a document's leading header line whensecondary_splitis used withkeep_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 fromheader_split_levels, or input metadata that already contained aheaderkey. The header line is now only stripped from chunks that truly came from a header split. MarkdownHeaderSplitterno 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 whenheader_split_levelsexcluded 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 emptyheaderandparent_headersmetadata, 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_numbermetadata fromMarkdownHeaderSplitterwhen secondary splitting uses overlap. Page breaks in overlapping content are no longer counted multiple times, and custompage_break_charactervalues are respected. - Fixed
EmbeddingBasedDocumentSplitteremitting a final split shorter thanmin_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 reachmax_length, the same limit that already governs forward merges. MultiQueryTextRetriever,MultiQueryEmbeddingRetrieverandMultiRetrievernow honourmax_workersin theirrun_asyncmethods. Previously only the synchronousrunbounded fan-out (via aThreadPoolExecutor);run_asynclaunched every sub-query / sub-retriever call at once, ignoringmax_workersand risking rate limits or connection exhaustion against the underlying retriever or embedder.run_asyncnow bounds concurrency with anasyncio.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_formataccepts 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(andAzureOpenAIDocumentEmbedder) now requestencoding_format="float"from the embeddings endpoint, matching the synchronousrun. The async batch path was missed when this was added in #9655, sorun_asynclet the OpenAI SDK negotiate thebase64wire format, which some OpenAI-compatible endpoints do not support.- Fixed
OpenAIChatGeneratorandOpenAIResponsesChatGeneratorso that an empty runtimetoolslist overrides tools configured at initialization. - Add the init parameters
timeoutandmax_retriesto theto_dictmethod ofOpenAIImageGenerator. They were dropped on serialization, so a pipeline saved and reloaded silently fell back to theOPENAI_TIMEOUT/OPENAI_MAX_RETRIESdefaults instead of the configured values. This matchesOpenAIChatGenerator,OpenAITextEmbedderandOpenAIDocumentEmbedder, which already serialize both. OutputAdapterandConditionalRouterno longer coerce string outputs to other Python types whenoutput_type=str. Previously the rendered template was always passed throughast.literal_evalin 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 integer42), producing a value that did not match the declaredstroutput type. Literal evaluation is now skipped whenoutput_typeisstr, 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.
PythonCodeSplitternow 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 undermeta["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.RecursiveDocumentSplitterandTextCleanernow keep their init parameters when serialized, for example in a pipeline saved withPipeline.dumps(). Before,RecursiveDocumentSplitterwrotesplit_unit: wordwhatever unit it was created with, so a reloaded splitter configured for characters or tokens counted words instead. A reloadedTextCleanerlost 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
DocumentSplitterwithsplit_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 thesplit_by="token"fix. - Fix an
IndexError: list index out of rangewhen a chat generator's stream completes without emitting any chunks._convert_streaming_chunks_to_chat_messagenow returns an assistant message with empty content andNonemetadata instead. SuperComponent.run_asyncnow returns outputs mapped from components that are not leaves of the wrapped pipeline, likerundoes. Before, an output that was also consumed inside the pipeline, such asretriever.documentsfeeding a prompt builder, was silently missing from the async result. This also affected aPipelineToolrun byAgent.run_async, whoseoutputs_to_stategot nothing for such outputs.create_tool_from_functionand the@tooldecorator now work with functions defined in a module that usesfrom __future__ import annotations. Before, the parameter annotations were plain strings, and creating the tool raised aSchemaGenerationErrorfor any parameter typed with something other than a builtin, such asAnnotated,Literal,Optional,DocumentorState. The annotations are now resolved first, andAnnotateddescriptions are kept.MetadataRouternow raises a clearValueErrorwhen a rule uses the reserved output nameunmatched, instead of failing with an internal duplicate-keywordTypeErrorduring output registration.FileTypeRouterandDocumentTypeRouternow raise a clearValueErrorwhen a MIME type uses a reserved output name (unclassified, orfailedonFileTypeRouter), instead of failing with an internal duplicate-keywordTypeErrorduring output registration.- Fixed
XLSXToDocumentwriting the stringnaninto an empty cell whentable_format="markdown". The same cell is written as an empty field bytable_format="csv", so an empty cell now reads as empty in both formats. A different placeholder can be set withtable_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