Repository navigation
v3.0.0-rc2
Pre-releaseRelease Notes
v3.0.0-rc2
Bug Fixes
- BUGFIX THAT GOES INTO RC2
v3.0.0-rc1
Upgrade Notes
-
AsyncPipelinehas been removed. Its asynchronous capabilities are now part of the singlePipelineclass, which provides both a synchronousrunmethod and the asynchronousrun_async,run_async_generator, andstreammethods (mirroring how components exposerunandrun_async).To migrate, replace
AsyncPipelinewithPipeline:# Before from haystack import AsyncPipeline pipeline = AsyncPipeline() result = await pipeline.run_async(data) # After from haystack import Pipeline pipeline = Pipeline() result = await pipeline.run_async(data)
Serialized pipelines,
SuperComponents, andPipelineTools are loaded asPipelineinstances.Pipeline.runruns synchronously and blocks until completion; in an async context useawait pipeline.run_async(...). The formerAsyncPipeline.runwas a synchronous wrapper around the concurrent engine, so to preserve that behavior useasyncio.run(pipeline.run_async(...)). Note thatPipeline.runexecutes components sequentially and does not acceptconcurrency_limit.Both synchronous and asynchronous runs are traced under a single
haystack.pipeline.runoperation name, distinguished by ahaystack.pipeline.execution_modetag (syncorasync). Previously, asynchronous runs used thehaystack.async_pipeline.runoperation name. -
continue_runis now a reserved key inAgent.state_schema(set by anon_exithook to keep the Agent running). Passing it via thestate_schemaargument now raisesValueError. Rename any conflicting state key (e.g.my_continue_run) to migrate. -
DocumentNDCGEvaluatornow matches documents by theircontentfield by default instead of their auto-generatedid. Previously, ground truth and retrieved documents were matched only if they had identicalidvalues, which rarely happened in practice since IDs are generated independently for each Document instance. As a result, NDCG scores computed with this evaluator may change for existing pipelines. To keep the previousid-based matching behavior, passdocument_comparison_field="id"when constructing the evaluator. -
Agentno longer validatesexit_conditionsagainst tool names at initialization. Exit conditions are now evaluated at runtime, so a condition naming a tool that is loaded later — e.g. a tool passed at runtime or one discovered bySearchableToolset— works as expected. Consequently, an unknown exit-condition name no longer raises aValueErrorat construction. -
Tool support is now validated against the tools resolved for each run, not only the tools provided at initialization. Passing tools at runtime via
run(tools=...)to a chat generator that does not support tools now raises aTypeErrorinstead of silently ignoring them. -
Components in Haystack that use external resources now consistently create those resources during warm-up rather than in
__init__. As a result, some components that previously raised errors during initialization (for example, because of a missing API key) now raise them duringwarm_upinstead. Similar changes will be rolled out to components in Haystack Core Integrations in the future. -
step_count,token_usage, andtool_call_countsare now reserved keys inAgent.state_schema. Passing any of them via thestate_schemaargument now raisesValueError. Rename the conflicting state key (e.g.my_token_usage) to migrate. -
Confirmation strategies now receive the model-requested tool arguments in
tool_paramsrather than the fully-prepared arguments. Values injected fromStatevia a tool'sinputs_from_statemapping (and theStateobject injected intoState-typed parameters) are no longer included in what is presented for confirmation — that injection now happens only at tool execution time. If yourConfirmationUIorConfirmationPolicydisplayed or inspected those state-injected values, it will now see only the arguments the model provided. -
The hash used to auto-generate
Document.idis now computed from a canonical (key-sorted) JSON serialization ofmeta. Documents with emptymetaare unaffected, but most other documents will get different IDs than they did before:- documents with non-empty
meta(the serialization changes fromdict's repr to JSON); - documents whose
metacontains non-JSON-serializable values such asdatetimeor custom classes (these are now serialized viastr(...)rather thanrepr(...), e.g."2024-01-01 00:00:00"instead of"datetime.datetime(2024, 1, 1, 0, 0)").
If you rely on auto-generated IDs to match documents already persisted in a
DocumentStore, you will need to re-ingest the affected documents (or pass the previousidexplicitly when constructing theDocument). - documents with non-empty
-
Pipeline deserialization (
Pipeline.load/Pipeline.loads/Pipeline.from_dict) now refuses to import classes from modules outside a trusted-module allowlist. By default the allowlist containshaystack,haystack_integrations,haystack_experimental,builtins,typing, andcollections. Pipelines that reference custom components or callables in other packages will fail to load with aDeserializationErroruntil the additional modules are added to the allowlist.To restore loading of such pipelines, extend the allowlist via one of:
- per-call kwarg:
Pipeline.load(fp, allowed_modules=["mypkg.*"]); - process-wide programmatic API:
from haystack.core.serialization import allow_deserialization_module; - environment variable:
HAYSTACK_DESERIALIZATION_ALLOWLIST="mypkg.*,otherpkg.*"; - or, if the source of the serialized data is fully trusted, bypass the allowlist with
Pipeline.load(fp, unsafe=True).
- per-call kwarg:
-
During pipeline deserialization,
default_from_dictnow validates the keys ofinit_parametersagainst the parent class's__init__signature before recursing into any nested{"type": "...", "init_parameters": {...}}dictionary. A nested dict whose key is not an accepted parameter name on the parent class is rejected with aDeserializationErrorbefore the nested type is imported. Classes whose constructor takes**kwargsare exempt, since their accepted parameter set cannot be statically determined.This may surface pre-existing YAML bugs — e.g. typos, leftovers from renamed or removed parameters, or stale snapshots from older Haystack versions. The fix is to update the YAML so each nested-component key matches a real
__init__parameter on the parent class. -
PromptBuilderandChatPromptBuildernow treat every Jinja2 template variable as required by default. Previously, variables were optional by default unless explicitly listed inrequired_variables.To check if you're affected: look for
PromptBuilder(...)orChatPromptBuilder(...)calls in your code that do not passrequired_variables. If those components relied on missing variables being silently rendered as empty strings, the behavior depends on how the missing variable was reaching the builder:- Calling
builder.run()directly without providing a required variable now raisesValueError. - In a pipeline, if a required variable is neither provided as a user input nor connected from another component,
Pipeline.runraisesValueError("Missing mandatory input '<var>' for component '<name>'."). - In a pipeline, if a required variable is connected to a sender that never produces output on a given run (for example, a loop-fed input on the first iteration or a conditional branch that isn't taken), the pipeline cannot make progress and raises
PipelineComponentsBlockedError.
Migration options:
- To require only a subset of variables, pass
required_variables=["var1", "var2"]. - To preserve the previous "all optional" behavior, explicitly pass
required_variables=None. - To require everything (the new default), no change is needed.
- Calling
-
Human-in-the-Loop tool confirmation on
Agentis now expressed as abefore_toolhook instead of a dedicated concept. Theconfirmation_strategiesandconfirmation_strategy_contextparameters have been removed fromAgent.__init__,Agent.run, andAgent.run_async. To migrate, wrap your confirmation strategies in the newConfirmationHook, register it under thebefore_toolhook point, and pass any request-scoped resources through the generichook_contextrun argument instead ofconfirmation_strategy_context. The Human-in-the-Loop module has also moved fromhaystack.human_in_the_looptohaystack.hooks.human_in_the_loop, so that it lives alongside the other built-in hooks (such as tool result offloading); update your imports to the new location:from haystack.components.agents import Agent from haystack.hooks.human_in_the_loop import ( AlwaysAskPolicy, BlockingConfirmationStrategy, ConfirmationHook, RichConsoleUI, ) confirmation_hook = ConfirmationHook( confirmation_strategies={ "my_tool": BlockingConfirmationStrategy( confirmation_policy=AlwaysAskPolicy(), confirmation_ui=RichConsoleUI() ) } ) agent = Agent(chat_generator=..., tools=[...], hooks={"before_tool": [confirmation_hook]}) agent.run(messages=[...], hook_context={"websocket": ws})
-
The agent-specific breakpoint API has been removed. The
AgentBreakpoint,ToolBreakpoint, andAgentSnapshotdataclasses are no longer exported fromhaystack.dataclasses, and thebreak_point,snapshot, andsnapshot_callbackparameters have been removed fromAgent.runandAgent.run_async.Pipeline.runno longer acceptsAgentBreakpointfor itsbreak_pointargument, and theagent_snapshotfield has been removed fromPipelineSnapshot. Pipeline-level breakpoints viaBreakpointandPipelineSnapshotcontinue to work as before, but pausing and resuming execution inside an Agent (at the chat generator or tool invoker) is no longer supported. -
AzureOCRDocumentConverterhas been moved out of Haystack into theazure-form-recognizer-haystackintegration package. Install the new package withpip install azure-form-recognizer-haystackand update your imports.Before:
from haystack.components.converters import AzureOCRDocumentConverter
After:
from haystack_integrations.components.converters.azure_form_recognizer import AzureOCRDocumentConverter
-
The
DatadogTracerhas been moved out of Haystack into thedatadog-haystackintegration package. It is no longer auto-enabled whenddtraceis installed; instead, use the newDatadogConnectorcomponent to enable Datadog tracing in your pipeline. Install the new package withpip install datadog-haystack.Before:
import ddtrace from haystack import tracing from haystack.tracing.datadog import DatadogTracer tracing.enable_tracing(DatadogTracer(ddtrace.tracer))
After:
from haystack import Pipeline from haystack_integrations.components.connectors.datadog import DatadogConnector pipe = Pipeline() pipe.add_component("tracer", DatadogConnector())
Alternatively, you can still enable the tracer manually:
import ddtrace from haystack import tracing from haystack_integrations.tracing.datadog import DatadogTracer tracing.enable_tracing(DatadogTracer(ddtrace.tracer))
-
OpenAIGenerator,AzureOpenAIGenerator,HuggingFaceAPIGenerator, andHuggingFaceLocalGeneratorhave been removed. Use the Chat Generators instead:OpenAIChatGenerator,AzureOpenAIChatGenerator,HuggingFaceAPIChatGenerator, andHuggingFaceLocalChatGenerator. As of v3.0, all ChatGenerators accept a plainstras input and normalise it to a user message.Before:
from haystack.components.generators import OpenAIGenerator gen = OpenAIGenerator() text = gen.run("What is NLP?")["replies"][0] # str
After:
from haystack.components.generators.chat import OpenAIChatGenerator gen = OpenAIChatGenerator() reply = gen.run("What is NLP?")["replies"][0] # ChatMessage text = reply.text # str; metadata is in reply.meta
-
DALLEImageGeneratorhas been renamed toOpenAIImageGeneratorand moved tohaystack.components.generators.openai_image_generator. The rename reflects that OpenAI retired the DALL-E model family but the component works with newer image generation models.Before:
from haystack.components.generators import DALLEImageGenerator generator = DALLEImageGenerator()
After:
from haystack.components.generators import OpenAIImageGenerator generator = OpenAIImageGenerator()
-
haystack-experimentalis no longer a core dependency of Haystack and is no longer installed automatically withhaystack-ai. If your code imports fromhaystack_experimental(directly or through an integration that relies on it), you must now install it explicitly, for example withpip install haystack-experimental. Installations that do not usehaystack_experimentalare unaffected. -
HuggingFaceAPIChatGenerator,HuggingFaceAPITextEmbedder,HuggingFaceAPIDocumentEmbedder, andHuggingFaceTEIRankerhave been moved out of Haystack into thehuggingface-api-haystackintegration package. Install the new package withpip install huggingface-api-haystackand update your imports.Before:
from haystack.components.generators.chat import HuggingFaceAPIChatGenerator from haystack.components.embedders import HuggingFaceAPITextEmbedder, HuggingFaceAPIDocumentEmbedder from haystack.components.rankers import HuggingFaceTEIRanker
After:
from haystack_integrations.components.generators.huggingface_api import HuggingFaceAPIChatGenerator from haystack_integrations.components.embedders.huggingface_api import ( HuggingFaceAPITextEmbedder, HuggingFaceAPIDocumentEmbedder, ) from haystack_integrations.components.rankers.huggingface_api import HuggingFaceTEIRanker
-
DocumentLanguageClassifierandTextLanguageRouterhave been moved out of Haystack into thelangdetect-haystackintegration package. Install the new package withpip install langdetect-haystackand update your imports.Before:
from haystack.components.classifiers import DocumentLanguageClassifier from haystack.components.routers import TextLanguageRouter
After:
from haystack_integrations.components.classifiers.langdetect import DocumentLanguageClassifier from haystack_integrations.components.routers.langdetect import TextLanguageRouter
-
The
NamedEntityExtractorcomponent (along with theNamedEntityAnnotationandNamedEntityExtractorBackendclasses) has been removed from Haystack and moved into dedicated integration packages, one per backend.For the Hugging Face backend, install
transformers-haystackand update your imports:Before:
from haystack.components.extractors import NamedEntityExtractor extractor = NamedEntityExtractor(backend="hugging_face", model="dslim/bert-base-NER")
After:
from haystack_integrations.components.extractors.transformers import TransformersNamedEntityExtractor extractor = TransformersNamedEntityExtractor(model="dslim/bert-base-NER")
For the spaCy backend, install
spacy-haystackand update your imports:Before:
from haystack.components.extractors import NamedEntityExtractor extractor = NamedEntityExtractor(backend="spacy", model="en_core_web_trf")
After:
from haystack_integrations.components.extractors.spacy import SpacyNamedEntityExtractor extractor = SpacyNamedEntityExtractor(model="en_core_web_trf")
-
The
OpenAPIConnector,OpenAPIServiceConnectorandOpenAPIServiceToFunctionscomponents have been removed from Haystack and moved into the dedicatedopenapi-haystackintegration package.Install the package and update your imports:
Before:
from haystack.components.connectors import OpenAPIConnector, OpenAPIServiceConnector from haystack.components.converters import OpenAPIServiceToFunctions
After:
from haystack_integrations.components.connectors.openapi import OpenAPIConnector, OpenAPIServiceConnector from haystack_integrations.components.converters.openapi import OpenAPIServiceToFunctions
These components are a legacy way to connect Haystack to external APIs. For most use cases, consider using the
MCPToolinstead, which is the modern, standardized way to give pipelines and agents access to external tools and services. -
The
OpenTelemetryTracerhas been moved out of Haystack into theopentelemetry-haystackintegration package, and theopentelemetry-sdkdependency is no longer installed with Haystack. OpenTelemetry tracing is no longer auto-enabled whenopentelemetry-sdkis installed; instead, use the newOpenTelemetryConnectorcomponent to enable OpenTelemetry tracing in your pipeline. Install the new package withpip install opentelemetry-haystack.Before:
from opentelemetry import trace from haystack import tracing from haystack.tracing import OpenTelemetryTracer tracing.enable_tracing(OpenTelemetryTracer(trace.get_tracer("my_application")))
After:
from opentelemetry import trace from haystack import tracing from haystack_integrations.tracing.opentelemetry import OpenTelemetryTracer tracing.enable_tracing(OpenTelemetryTracer(trace.get_tracer("my_application")))
Alternatively, add the
OpenTelemetryConnectorto your pipeline to enable tracing:from haystack import Pipeline from haystack_integrations.components.connectors.opentelemetry import OpenTelemetryConnector pipe = Pipeline() pipe.add_component("tracer", OpenTelemetryConnector())
-
Breaking change:
haystack.tracing.auto_enable_tracinghas been removed (and is no longer called onimport haystack). Since Haystack no longer ships a built-in tracing backend, there is nothing to auto-enable. Enable tracing explicitly instead, for example by adding theOpenTelemetryConnectorcomponent to your pipeline, or by callinghaystack.tracing.enable_tracing(...)with your tracer. TheHAYSTACK_AUTO_TRACE_ENABLEDenvironment variable no longer has any effect. -
SearchApiWebSearchhas been moved out of Haystack into thesearchapi-haystackintegration package. Install the new package withpip install searchapi-haystackand update your imports.Before:
from haystack.components.websearch import SearchApiWebSearch
After:
from haystack_integrations.components.websearch.searchapi import SearchApiWebSearch
-
The Sentence Transformers components have been moved out of Haystack into the
sentence-transformers-haystackintegration package. This includesSentenceTransformersTextEmbedder,SentenceTransformersDocumentEmbedder,SentenceTransformersSparseTextEmbedder,SentenceTransformersSparseDocumentEmbedder,SentenceTransformersDocumentImageEmbedder,SentenceTransformersSimilarityRanker, andSentenceTransformersDiversityRanker. Install the new package withpip install sentence-transformers-haystackand update your imports.Before:
from haystack.components.embedders import ( SentenceTransformersTextEmbedder, SentenceTransformersDocumentEmbedder, SentenceTransformersSparseTextEmbedder, SentenceTransformersSparseDocumentEmbedder, ) from haystack.components.embedders.image import SentenceTransformersDocumentImageEmbedder from haystack.components.rankers import SentenceTransformersSimilarityRanker, SentenceTransformersDiversityRanker
After:
from haystack_integrations.components.embedders.sentence_transformers import ( SentenceTransformersTextEmbedder, SentenceTransformersDocumentEmbedder, SentenceTransformersSparseTextEmbedder, SentenceTransformersSparseDocumentEmbedder, SentenceTransformersDocumentImageEmbedder, ) from haystack_integrations.components.rankers.sentence_transformers import ( SentenceTransformersSimilarityRanker, SentenceTransformersDiversityRanker, )
-
Removed the unused
serialize_class_instanceanddeserialize_class_instancefunctions fromhaystack.utils.base_serialization. These were internal helpers not used anywhere in Haystack. If you were importing them directly, inline their logic: use{"type": generate_qualified_class_name(type(obj)), "data": obj.to_dict()}to serialize, andobj_class.from_dict(data["data"])(after resolvingobj_classwithimport_class_by_name(data["type"])) to deserialize. -
SerperDevWebSearchhas been moved out of Haystack into theserperdev-haystackintegration package. Install the new package withpip install serperdev-haystackand update your imports.Before:
from haystack.components.websearch import SerperDevWebSearch
After:
from haystack_integrations.components.websearch.serperdev import SerperDevWebSearch
-
TikaDocumentConverterhas been moved out of Haystack into thetika-haystackintegration package. Install the new package withpip install tika-haystackand update your imports.Before:
from haystack.components.converters import TikaDocumentConverter
After:
from haystack_integrations.components.converters.tika import TikaDocumentConverter
-
Removed the
ToolInvokercomponent. Code that importsToolInvokerfromhaystack.components.toolsor uses it as a standalone Pipeline component must be updated.To execute tools in an agentic loop, pass the tools directly to
haystack.components.agents.Agent. The Agent now owns tool execution, including tool/toolset warm-up, state injection, streaming callback passthrough, and sync/async tool invocation. The previoustool_invoker_kwargsAgent parameter has been removed; usetool_concurrency_limitandtool_streaming_callback_passthroughinstead. -
Tool results are now always serialized to a string using
json.dumps(withensure_ascii=False, falling back tostronly when the result is not JSON-serializable). TheToolInvoker'sconvert_result_to_json_stringparameter, which defaulted toFalseand selectedstr-based serialization, has been removed along with the component. As a result, non-string tool results now appear as JSON rather than Pythonreproutput: for example, a tool returning{"message": "hi"}now produces the string{"message": "hi"}instead of{'message': 'hi'}. Update any code or tests that assert on the previousstr-based format. -
HuggingFaceLocalChatGenerator,ExtractiveReader,TransformersZeroShotDocumentClassifier,TransformersTextRouter, andTransformersZeroShotTextRouterhave been moved out of Haystack into thetransformers-haystackintegration package. Install the new package withpip install transformers-haystackand update your imports.Before:
from haystack.components.classifiers import TransformersZeroShotDocumentClassifier from haystack.components.generators.chat import HuggingFaceLocalChatGenerator from haystack.components.readers import ExtractiveReader from haystack.components.routers import TransformersTextRouter, TransformersZeroShotTextRouter
After:
from haystack_integrations.components.classifiers.transformers import TransformersZeroShotDocumentClassifier from haystack_integrations.components.generators.transformers import TransformersChatGenerator from haystack_integrations.components.readers.transformers import TransformersExtractiveReader from haystack_integrations.components.routers.transformers import ( TransformersTextRouter, TransformersZeroShotTextRouter, )
-
The legacy
TransformersSimilarityRankercomponent has been removed. UseSentenceTransformersSimilarityRankerinstead, which provides the same functionality along with async support.Before:
from haystack.components.rankers import TransformersSimilarityRanker ranker = TransformersSimilarityRanker(model="cross-encoder/ms-marco-MiniLM-L-6-v2")
After:
from haystack.components.rankers import SentenceTransformersSimilarityRanker ranker = SentenceTransformersSimilarityRanker(model="cross-encoder/ms-marco-MiniLM-L-6-v2")
-
The
user_promptandsystem_promptparameters have been removed fromAgent.run/Agent.run_asyncandLLM.run/LLM.run_async. Both prompts must now be set at initialization time on the component; they can no longer be overridden per-run, including when the component is used inside a pipeline (Pipeline.run(data={"agent": {"user_prompt": ...}})orPipeline.run(data={"llm": {"user_prompt": ...}})is no longer supported).system_promptanduser_promptboth accept either a plain string template or an explicit Jinja2 message template. Plain string templates are rendered as a single message with the expected role (systemforsystem_prompt,userforuser_prompt). Explicit Jinja2 message templates must contain a single message block and render to exactly one message with the matching role.Before:
agent = Agent(chat_generator=..., tools=[...], system_prompt="Default system prompt.") agent.run(messages=[...], system_prompt="Per-run override.")
After:
# Construct a new Agent or LLM with the desired prompt. agent = Agent(chat_generator=..., tools=[...], system_prompt="Default system prompt.") agent.run(messages=[...]) # If you need to build prompts at runtime, construct an Agent without # init prompts and pass the prompt messages through the messages input. agent = Agent(chat_generator=..., tools=[...]) agent.run(messages=[ChatMessage.from_system("Runtime system prompt."), ChatMessage.from_user("...")])
-
LocalWhisperTranscriberandRemoteWhisperTranscriberhave been moved out of Haystack into thewhisper-haystackintegration package. Install the new package withpip install whisper-haystackand update your imports.Before:
from haystack.components.audio import LocalWhisperTranscriber from haystack.components.audio import RemoteWhisperTranscriber
After:
from haystack_integrations.components.audio.whisper import LocalWhisperTranscriber from haystack_integrations.components.audio.whisper import RemoteWhisperTranscriber
LocalWhisperTranscriberadditionally requires theopenai-whisperpackage (andffmpeg), which you can install withpip install "openai-whisper>=20231106". -
Tools now read a value from the
Agent'sStateby name only when they declare an explicitinputs_from_statemapping. Previously, when a tool had noinputs_from_state, theAgentwould implicitly inject aStatevalue into any tool parameter whose name happened to match aStatekey, on any call where the model did not already supply that argument (LLM-provided arguments always took precedence). This implicit name-matching has been removed. The change applies to every tool type, sinceComponentTool,PipelineTool,MCPTool, and others all derive from the baseToolclass.This is a breaking change. If you relied on a tool parameter being filled from a same-named
Statekey, add an explicit mapping, for exampleinputs_from_state={"documents": "documents"}to feed thedocumentsstate key into the tool'sdocumentsparameter.Auto-injection of the full
Stateobject into a parameter annotated asStateis unchanged and continues to work without any mapping. -
The
raise_on_invalid_filter_syntaxutility function has been removed. Its implementation was incorrect and never used across Haystack and Haystack Core Integrations. -
haystack.logging.configure_loggingnow attaches its formatting handler to Haystack's own logger namespaces (haystack,haystack_integrationsandhaystack_experimental) instead of the root logger. This makes Haystack a better-behaved library when it runs next to other services in the same process: the log records of the host application and other libraries are no longer reformatted or duplicated by Haystack. To restore the previous behavior of formatting every log record in the process, callconfigure_logging(logger_name=""). -
Importing Haystack no longer configures
structlogprocess-wide. The import-time setup now only installs Haystack's own scoped logging handler (so Haystack's logs are still formatted) and leaves the globalstructlogconfiguration untouched, so the host application's own nativestructlogloggers are no longer reformatted by merely importing Haystack.structlogis configured only when you callconfigure_logging(configure_structlog=True). -
The
Agentcomponent now emits a different shape of tracing spans. Previously each iteration of the agent loop produced two child spans viaPipeline._run_component— one for the chat generator and one for the tool invoker — each carryinghaystack.component.nameandhaystack.component.typetags. The agent now opens a singlehaystack.agent.stepspan per loop iteration with two nested children:haystack.agent.step.llm(the chat generator call) andhaystack.agent.step.tool(the tool invocation, only emitted when tool calls happen). These new spans do NOT carryhaystack.component.*tags; instead they expose new content tagshaystack.agent.step.llm.input/.outputandhaystack.agent.step.tool.input/.output.Custom tracer backends or
SpanHandlerimplementations that dispatched oncomponent_type == "ToolInvoker"orcomponent_type.endswith("ChatGenerator")for Agent-internal spans will need to be updated to dispatch on the new operation names (haystack.agent.step.llm/haystack.agent.step.tool) instead. The Langfuse integration'sDefaultSpanHandleris affected — seeMIGRATION.mdfor a concrete example of a customSpanHandlerthat maps the new operations to Langfusechain/generation/toolobservations.
New Features
-
Added an
after_toolhook point to theAgent. Hooks registered underafter_toolrun after tools execute, once their result messages are instate.data["messages"], and before the exit check and the next LLM call. Use it to rewrite the freshly produced tool-result messages, for example to offload, redact, truncate, or summarize results before the next LLM call sees them. It does not run on the plain-text exit step, where no tools run. Register it like any other hook:hooks={"after_tool": [my_hook]}. -
Added hooks to the
Agent. Passhooksas a dictionary mapping a hook point to a list of hooks the Agent runs at that point. Each hook receives the liveStateand influences the run by mutating it in place; hooks for a hook point run in list order, and the same hook can be registered under multiple hook points. Valid hook points are:before_llm: Runs before each chat-generator call.before_tool: Runs after the model requests tool calls, before any tools run. After these hooks run, the Agent re-reads the current last message fromstate["messages"]. If that message contains tool calls, those calls are executed. If it does not, no tools run for that step, no tool-based exit condition is triggered, and the Agent loops back to the next LLM call unlessmax_agent_stepshas been reached.on_exit: Runs when the Agent is about to stop on an exit condition. Anon_exithook can keep the Agent running by setting thecontinue_runcontrol flag (state.set("continue_run", True)), usually alongside a message telling the model what to do next.on_exithooks run when the Agent stops on an exit condition, but not when it stops becausemax_agent_stepsis reached.
A hook is any object with a
run(state)method (optionallyrun_async(state)); use the@hookdecorator to build one from a function. This enables patterns such as building run-time system context, retrieving memories, and requiring a tool to be called before finishing.The example below registers one hook at each placement to show what each can do:
from datetime import datetime, timezone from typing import Annotated from haystack.components.agents import Agent from haystack.components.agents.state import State, replace_values from haystack.components.generators.chat import OpenAIChatGenerator from haystack.dataclasses import ChatMessage from haystack.hooks import hook from haystack.tools import tool @tool def search(query: Annotated[str, "The search query"]) -> str: """Search the web.""" # Placeholder: would call a real search API return "Fusion startups reported net-energy-gain milestones this year." @hook def build_context(state: State) -> None: # before_llm: build run-time system context once, before the first model call. if state.get("step_count") == 0: now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") system = ChatMessage.from_system(f"You are a research assistant. The current time is {now}.") state.set("messages", [system, *state.data["messages"]], handler_override=replace_values) @hook def audit_tool_calls(state: State) -> None: # before_tool: see which tools the model is about to run. pending = state.data["messages"][-1].tool_calls print(f"about to run: {[tc.tool_name for tc in pending]}") @hook def require_search(state: State) -> None: # on_exit: keep going until the agent has actually searched. if state.get("tool_call_counts", {}).get("search", 0) == 0: state.set("messages", [ChatMessage.from_system("Search before answering.")]) state.set("continue_run", True) agent = Agent( chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"), tools=[search], hooks={ "before_llm": [build_context], "before_tool": [audit_tool_calls], "on_exit": [require_search], }, ) result = agent.run(messages=[ChatMessage.from_user("What are the latest developments in fusion energy?")]) print(result["last_message"].text)
Class-based hooks may also implement the optional lifecycle methods
warm_up/warm_up_asyncandclose/close_async. The Agent calls them from its ownwarm_up/warm_up_asyncandclose/close_async, so a hook can defer opening clients or reading credentials until warm-up and release them on close. When a class-based hook should be serializable, store serializable constructor arguments on the hook and rebuild runtime clients from those values. For example, anon_exithook can grade the Agent's answer with its own LLM and ask it to improve a weak answer before finishing, warming the judge's client during the Agent's warm-up:from typing import Any from haystack.core.serialization import default_from_dict, default_to_dict class GradeFinalAnswer: """Grade the Agent's answer with an LLM and ask it to improve a weak answer before finishing.""" def __init__(self, model: str = "gpt-5.4-nano"): self.model = model self._judge = OpenAIChatGenerator(model=self.model) def warm_up(self) -> None: # Warm up the judge's own client during the Agent's warm-up. self._judge.warm_up() def run(self, state: State) -> None: answer = state.data["messages"][-1].text or "" verdict = self._judge.run( messages=[ChatMessage.from_user(f"Reply with only PASS or FAIL. Is this answer complete?\n\n{answer}")] )["replies"][0].text or "" if "FAIL" in verdict.upper(): state.set("messages", [ChatMessage.from_user("Your answer was incomplete. Please improve it.")]) state.set("continue_run", True) def to_dict(self) -> dict[str, Any]: return default_to_dict(self, model=self.model) @classmethod def from_dict(cls, data: dict[str, Any]) -> "GradeFinalAnswer": return default_from_dict(cls, data) agent = Agent(chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"), hooks={"on_exit": [GradeFinalAnswer()]}) result = agent.run(messages=[ChatMessage.from_user("Explain how vaccines work.")]) print(result["last_message"].text)
-
Add native async tool invocation.
Toolnow accepts an optionalasync_function(a coroutine function) alongside the existingfunctionfield. WhenAgent.run_asyncinvokes a tool, it now awaits theasync_functionof Tool if available.@tool/create_tool_from_functionautomatically routesasync defcallables toasync_function, so simply decorating anasync defproduces an async tool:from typing import Annotated from haystack.tools import tool @tool async def weather(city: Annotated[str, "The name of the city"]) -> str: """Get the weather for a city.""" ...
ComponentToolautomatically wires an async invoker for components that definerun_async(__haystack_supports_async__isTrue).PipelineToolenables the async path when wrapping anAsyncPipelineand falls back to the threaded sync path when wrapping a regularPipeline. -
Added
before_runandafter_runhook points to theAgent. Hooks registered underbefore_runrun exactly once per run, after the state is initialized and before the first LLM call — use them to rewrite the initial messages or seed state without re-running on every step likebefore_llmdoes. Hooks registered underafter_runrun exactly once per run, after the step loop has ended and before the Agent builds its return value, regardless of whether the run stopped on an exit condition or becausemax_agent_stepswas reached (unlikeon_exit); mutations to the state are reflected in the returnedmessages,last_message, andstate_schemaoutputs. Register them like any other hook:hooks={"before_run": [my_hook], "after_run": [my_other_hook]}. -
Added a new
{% insert %}tag to the Jinja2 string templates used byChatPromptBuilder. The tag is a placeholder that evaluates an expression to aChatMessageor a list ofChatMessageobjects and expands it into the prompt, so messages provided at runtime can be interleaved with literal{% message %}blocks. For example, you can wrap the runtime messages with a system message above and a templated user message below, then pass the messages (and any template variables) at run time:from haystack.components.builders import ChatPromptBuilder from haystack.dataclasses import ChatMessage template = """ {% message role="system" %}You are a helpful assistant.{% endmessage %} {% insert messages %} {% message role="user" %}{{ query }}{% endmessage %} """ builder = ChatPromptBuilder(template=template) result = builder.run( messages=[ChatMessage.from_user("Hi"), ChatMessage.from_assistant("Hello!")], query="What's the weather?", ) # result["prompt"] -> [system, user "Hi", assistant "Hello!", user "What's the weather?"]
All content types (tool calls, tool call results, images, reasoning,
nameandmeta) round trip without loss. A missing or empty value expands to nothing.The expression can be a plain variable (
{% insert messages %}), a slice or index ({% insert messages[-1:] %},{% insert messages[-1] %}), or a combination of variables ({% insert previous + current %}). Multiple{% insert %}tags can be used in a single template, so the runtime messages can be split, reordered, or repeated across different positions. -
Added
MockChatGenerator, a Chat Generator that returns predefined responses without calling any API. It is a deterministic, zero-cost drop-in replacement for real Chat Generators in tests, smoke tests, and quick prototypes. It supports a fixed response, a list of responses cycled across calls (useful to drive Agent-like loops), aresponse_fncallable for input-dependent replies, and an echo mode (the default) that returns the last user message. It implements the full Chat Generator interface, includingrun,run_async, streaming callbacks, and serialization. -
Added
MockTextEmbedderandMockDocumentEmbedder, embedders that return deterministic embeddings without calling any API. They are zero-cost drop-in replacements for real embedders in tests, smoke tests, and quick prototypes. By default each embedding is derived from a stable hash of the (prepared) text, so the same text always yields the same unit-length embedding and different texts yield different embeddings, making the mocks usable in retrieval pipelines and reproducible across runs and processes. A fixedembeddingvector or a customembedding_fncallable can be provided instead. Both implement the full embedder interface, includingrun,run_async, and serialization. -
Introduced the
PythonCodeSplittercomponent, a syntax-aware splitter for Python source files:- Parses sources with the
astmodule and merges units (module docstring, import blocks, top-level functions, class headers, methods, nested classes, and remaining statements) greedily into chunks of roughlymax_effective_lines. - Keeps whole functions and methods together; falls back to a line-based secondary split (using
DocumentSplitter) with overlap only for functions whose effective length exceedsoversized_factor * max_effective_lines. - Optionally strips docstrings into chunk metadata via
strip_docstrings=True, and prepends the enclosing class signature to chunks whose members live in a later chunk viapreserve_class_definition=True. - Emits per-chunk metadata including
start_line,end_line,unit_kinds,include_classes,decorators,docstrings,source_id, andsplit_id.
- Parses sources with the
-
Added a
reasoning_modeflattened generation kwarg toOpenAIResponsesChatGenerator, which is merged into thereasoningdictionary sent to the OpenAI Responses API. This lets you set the reasoning mode (for examplestandardorpro, supported since GPT-5.6) without having to construct the nestedreasoningdict yourself. -
Added an
include_reasoning_encrypted_contentflattened generation kwarg toOpenAIResponsesChatGenerator. Setting it toTrueappendsreasoning.encrypted_contentto theincludelist sent to the OpenAI Responses API, so reasoning items can be reused across multi-turn conversations when the Responses API is used statelessly (for example whenstoreisFalseor zero data retention is enabled). -
Added a
run_asyncmethod toLLMRanker,QueryExpander,LLMMessagesRouterandLLMDocumentContentExtractorso they can run natively in anAsyncPipeline. The method delegates to the chat generator'srun_asyncmethod when available. If the chat generator only implements a synchronousrunmethod, it is executed in a thread to avoid blocking the event loop.LLMDocumentContentExtractorprocesses documents concurrently withasyncio, bounded bymax_workers. -
Added a
run_asyncmethod toRemoteWhisperTranscriberandOpenAIImageGeneratorso they can run natively in anAsyncPipeline. Both components now hold anAsyncOpenAIclient alongside the existing synchronous client and call the OpenAI API without blocking the event loop. -
Added
SkillToolset, aToolsetthat lets anAgentdiscover and read "skills" through progressive disclosure, similar to how Claude Code and Codex expose skills. A skill is a directory containing aSKILL.mdfile with YAML frontmatter (nameanddescription) and a markdown body of instructions, optionally bundling additional reference files.SkillToolsetis backed by aSkillStore; useFileSystemSkillStoreto load skills from a local directory and add the toolset to anAgent:from haystack.components.agents import Agent from haystack.components.generators.chat import OpenAIChatGenerator from haystack.tools import SkillToolset from haystack.skill_stores.file_system import FileSystemSkillStore skills_toolset = SkillToolset(FileSystemSkillStore("skills/")) agent = Agent(chat_generator=OpenAIChatGenerator(), tools=skills_toolset)
Skills are discovered when the toolset is warmed up (the
Agentdoes this automatically before a run). The names and descriptions of all discovered skills are baked into theload_skilltool description, so the model can see which skills exist without any system prompt injection. The toolset exposes two tools:load_skillreturns a skill's full instructions on demand, andread_skill_filereads a file bundled with a skill (with path-traversal protection).read_skill_filereturns text files as strings, images asImageContent, and PDFs asFileContent, so anAgentbacked by a multimodal chat generator can read a skill's visual assets — for example a reference screenshot or a showcase PDF — directly.Custom stores can back
SkillToolsetwith any storage system (a database, a remote API, ...) by implementing theSkillStoreprotocol. -
Added tool result offloading for the
Agentvia the newhaystack.hooks.tool_result_offloadingmodule.ToolResultOffloadHookis anafter_toolhook that writes selected tool results to a store and replaces them in the conversation with a compact pointer (a reference plus a short preview). The next LLM call sees the reference instead of the full result, which helps manage the context window and is a step towards letting an Agent operate on offloaded results with follow-up tools (e.g. a bash tool that reads the referenced files).Configure what gets offloaded, per tool, via
offload_strategies:- Map a tool name, a tuple of tool names, or the wildcard
"*"to anOffloadPolicy. More specific keys win over"*"; a tool with no matching key is not offloaded. - Built-in policies:
AlwaysOffload,NeverOffload, andOffloadOverChars(offload when the result exceeds a character threshold). - For custom conditions, implement the
OffloadPolicyprotocol (it provides defaultto_dict/from_dict, so a stateless policy only needs ashould_offloadmethod).
Choose where results are stored:
FileSystemToolResultStorewrites results to the local file system.- Implement the
ToolResultStoreprotocol to target other backends. - The hook is stateless and can be shared across concurrent runs. In a multi-user server, pass an isolated store per run via the Agent's
hook_context(hook_context={RESULT_STORE_CONTEXT_KEY: per_request_store}) so concurrent users write to separate locations and never read each other's results.
What is offloaded:
- Only successful, text results are offloaded. Error results (including
before_toolhuman-in-the-loop rejections) are left in context. - Non-text results (image or file content) are left in context; supporting only text is a deliberate choice for now. A warning is logged when a non-text result has a matching offload policy.
- Each result is offloaded at most once.
from typing import Annotated from haystack.components.agents import Agent from haystack.components.generators.chat import OpenAIChatGenerator from haystack.dataclasses import ChatMessage from haystack.hooks.tool_result_offloading import ( FileSystemToolResultStore, OffloadOverChars, ToolResultOffloadHook, ) from haystack.tools import tool @tool def search(query: Annotated[str, "The search query"]) -> str: """Search the web and return the (potentially large) results.""" return f"... large result for {query} ..." offload_hook = ToolResultOffloadHook( store=FileSystemToolResultStore(root="tool_results"), offload_strategies={"*": OffloadOverChars(4000)}, ) agent = Agent( chat_generator=OpenAIChatGenerator(model="gpt-5.4-nano"), tools=[search], hooks={"after_tool": [offload_hook]}, ) agent.run(messages=[ChatMessage.from_user("Summarize today's tech news")])
- Map a tool name, a tuple of tool names, or the wildcard
-
Added
AsyncPipeline.stream(), a convenient way to stream LLM chunks from an async application, such as an API endpoint. It runs the pipeline and returns a handle that can be iterated withasync forto receiveStreamingChunkobjects from streaming-capable components as they are produced. After iteration ends,handle.resultholds the final pipeline output (the same dict returned byrun_async). By default every streaming-capable component is forwarded; passstreaming_componentsto stream only specific components. If the consumer abandons iteration, the underlying pipeline run is cancelled automatically; passcancel_on_abandon=Falseto let it run to completion.import asyncio from haystack import AsyncPipeline from haystack.components.builders import ChatPromptBuilder from haystack.components.generators.chat import OpenAIChatGenerator from haystack.dataclasses import ChatMessage pipe = AsyncPipeline() pipe.add_component( "prompt_builder", ChatPromptBuilder(template=[ChatMessage.from_user("Tell me about {{topic}}")]), ) pipe.add_component("llm", OpenAIChatGenerator()) pipe.connect("prompt_builder.prompt", "llm.messages") async def main(): handle = pipe.stream(data={"prompt_builder": {"topic": "Italy"}}) async for chunk in handle: print(chunk.content, end="", flush=True) return handle.result result = asyncio.run(main())
-
All Haystack
ChatGeneratorcomponents now also accept a plain string for themessagesparameter in addition to a list ofChatMessageobjects. The string is automatically converted into a list containing aChatMessagewith theuserrole. This is done to simplify switching from Generators to ChatGenerators; Generators might be removed in Haystack 3.0.This applies to
AzureOpenAIChatGenerator,AzureOpenAIResponsesChatGenerator,FallbackChatGenerator,HuggingFaceAPIChatGenerator,HuggingFaceLocalChatGenerator,OpenAIChatGenerator, andOpenAIResponsesChatGenerator.The same change will be soon applied to ChatGenerators available in Haystack Core Integrations.
Example:
from haystack.components.generators.chat import OpenAIChatGenerator generator = OpenAIChatGenerator() # passing a string is equivalent to passing [ChatMessage.from_user("...")] response = generator.run("What's Natural Language Processing?") print(response["replies"][0].text)
-
Components and pipelines now have a consistent resource lifecycle for acquiring and releasing resources such as HTTP clients and connections. In addition to the existing
warm_up, components can now implementwarm_up_asyncto acquire asynchronous resources, andclose/close_asyncto release them.Pipelinenow implements the same lifecycle methods. In addition to the existingwarm_up,warm_up_async()runs async warm-up on the serving event loop (falling back to the synchronouswarm_up), whileclose()/close_async()release synchronous and asynchronous resources respectively. Closing is always explicit; pipelines are never closed automatically after a run. -
Added a
sharedparameter toInMemoryDocumentStore. WhenTrue(the default, matching previous behavior), documents live in process-global storage shared across instances that use the sameindex. WhenFalse, documents are kept instance-local and freed when the instance is garbage collected. Useshared=Falsefor stores that are created frequently (for example, one per request in a served application) to avoid unbounded growth of the process-global storage. -
Added optional YAML frontmatter extraction to
MarkdownToDocument. When initialized withextract_frontmatter=True, YAML frontmatter at the beginning of a Markdown file is removed from the converted content and added toDocument.meta. -
Pipeline.load/Pipeline.loads/Pipeline.from_dictnow accept two new keyword arguments:allowed_modules: list[str] | None— additional module patterns that may be imported during this deserialization call. Patterns are matched as prefixes ("mypkg"matchesmypkgand any submodule) or, if they contain*/?/[, usingfnmatchrules.unsafe: bool = False— when set toTrue, the deserialization allowlist is bypassed entirely. Only use this when you fully trust the source of the serialized data.
A new public helper,
haystack.core.serialization.allow_deserialization_module(pattern), extends the process-wide allowlist. TheHAYSTACK_DESERIALIZATION_ALLOWLISTenvironment variable (comma-separated patterns) is read at runtime on every deserialization call. -
Added
ConfirmationHook, abefore_toolAgent hook that applies Human-in-the-Loop confirmation strategies to pending tool calls. It confirms, modifies, or rejects the tool calls the model requested before they run by rewriting the conversation in the Agent'sState. Itsconfirmation_strategiesmapping accepts a single tool name, a tuple of tool names, or the wildcard"*"that applies to any tool without a more specific entry, so you can set a default strategy for all tools and override individual ones. -
Agent hooks may now declare an
allowed_hook_pointsattribute listing the hook points they support. TheAgentvalidates it at construction and raises aValueErrorif such a hook is registered under an unsupported hook point. For example,ConfirmationHooksetsallowed_hook_points = ("before_tool",), so registering it underbefore_llmoron_exitnow fails fast instead of silently doing nothing. Hooks that do not declare the attribute can still be registered under any hook point. -
haystack.logging.configure_logginggained three parameters:logger_nameto choose which logger(s) the formatting handler is attached to,propagateto control whether Haystack's loggers propagate their records to ancestor loggers, andconfigure_structlogto control whether the call configures the process-globalstructlog(taking over any existing configuration). Setpropagate=Falseto let Haystack fully own the output of its own logs and avoid duplicate log lines when the host application also configures the root logger.
Enhancement Notes
-
Added
document_comparison_fieldparameter toDocumentNDCGEvaluator, consistent withDocumentMAPEvaluator,DocumentMRREvaluator, andDocumentRecallEvaluator. Users can now match documents by"content","id", or any"meta.<key>"field when calculating NDCG scores. -
Add
output_passthroughoption toConditionalRouter. Whenoutput_passthrough: Trueis set in a route, theoutputfield is treated as a plain variable name instead of a Jinja2 template, and the value is passed directly from the pipeline inputs to the route output. This allows routing of complex non-basic types such as dataclasses and Pydantic models without unwanted Jinja2 template processing.Without
output_passthrough, the router rendersoutputas a Jinja2 template, which converts the value to its string representation. Custom types cannot survive that round-trip:# Without output_passthrough — the object is silently converted to a string routes = [ { "condition": "{{True}}", "output": "{{query}}", "output_name": "out", "output_type": ParsedQuery, } ] router = ConditionalRouter(routes) result = router.run(query=ParsedQuery(text="hello", intent="search", entities=[])) # result["out"] == "ParsedQuery(text='hello', intent='search', entities=[])" # ^^^ str, not ParsedQuery — the object was destroyed
Set
output_passthrough: Trueto skip Jinja2 entirely and pass the value directly from kwargs:from haystack.components.routers import ConditionalRouter from dataclasses import dataclass, field @dataclass class ParsedQuery: text: str intent: str # "search" | "chat" entities: list[str] = field(default_factory=list) routes = [ { "condition": "{{query.intent == 'search'}}", "output": "query", # variable name, not a Jinja2 template "output_name": "search_query", "output_type": ParsedQuery, "output_passthrough": True, }, { "condition": "{{query.intent == 'chat'}}", "output": "query", "output_name": "chat_query", "output_type": ParsedQuery, "output_passthrough": True, }, ] router = ConditionalRouter(routes) query = ParsedQuery(text="What is Haystack?", intent="search", entities=["Haystack"]) result = router.run(query=query) assert isinstance(result["search_query"], ParsedQuery) # type preserved assert result["search_query"] is query # same object, no copying
-
Added
run_asynctoTextEmbeddingRetriever,MultiQueryEmbeddingRetriever, andMultiQueryTextRetriever. These components now execute natively as coroutines inAsyncPipeline, delegating to each wrapped component'srun_asyncwhen available and falling back to a thread executor otherwise. -
Components that delegate to sub-components in their
run_asyncmethod (LLMMetadataExtractor,EmbeddingBasedDocumentSplitter,FallbackChatGenerator,TextEmbeddingRetriever,MultiRetriever,MultiQueryTextRetrieverandMultiQueryEmbeddingRetriever) now use a shared utility to run the sub-component. If the sub-component only implements a synchronousrunmethod, it is executed in a thread to avoid blocking the event loop. Notably,LLMMetadataExtractorandEmbeddingBasedDocumentSplitterpreviously fell back to their fully synchronousrunmethod in this case, which blocked the event loop for the duration of the call. -
Duplicate tool names are now detected at the start of each
Agentstep (before the chat generator call) instead of only at tool execution, so the error surfaces earlier and the LLM is never sent duplicate tool schemas. -
Pipeline.runnow acceptsbreak_pointandpipeline_snapshotat the same time. This enables a stepped debugging experience: run until a breakpoint, then resume from the resulting snapshot with a new breakpoint on the next component, pausing again for inspection at each step. The only rejected combination is abreak_pointtargeting the same component and visit count as the snapshot's break point, since it would trigger again before the resumed component runs and the pipeline could not make any progress. -
AzureOpenAIChatGeneratornow accepts aSecretfor theazure_endpointandapi_versionparameters in addition to a plain string. This makes it possible to resolve these values from environment variables at runtime, for example withSecret.from_env_var("AZURE_OPENAI_ENDPOINT"), so the same serialized pipeline can switch between environments (e.g. dev and prod) by changing environment variables instead of the pipeline definition. -
ChatMessage.from_dictnow also accepts the format Pydantic produces when it auto-serializesChatMessageas a plain dataclass (e.g. viamodel_dumpon a Pydantic model withChatMessagefields). In this format, content parts appear without their wrapping key (for example{"tool_name": "search", "arguments": {}}instead of{"tool_call": {"tool_name": "search", "arguments": {}}}) and are identified by their required field names. This makes it possible to round-tripChatMessageobjects that were implicitly serialized as part of larger Pydantic models. -
Removed duplicated class-import logic between
core/serialization.pyandutils/type_serialization.py, including the deserialization-allowlist and builtin-type-safety checks, by sharing a single internal implementation. No change to public behavior or to the security checks themselves. -
Agentnow exposes three new outputs that are populated automatically during a run and made available alongsidemessagesandlast_messagein the result dict:step_count(int): the number of steps the agent ran.token_usage(dict[str, Any]): aggregated token usage summed across every LLM call in the runtool_call_counts(dict[str, int]): number of times each tool was invoked, keyed by tool name.
These fields are added to
Agent.state_schemaautomatically so that tools registered viainputs_from_statecan read them mid-run. They are exposed only as Agent outputs so cannot be passed in as inputs toAgent.run/Agent.run_async. -
LLMnow exposes atoken_usageoutput alongsidemessagesandlast_message. BecauseLLMnever invokes tools and always runs exactly one step,step_countandtool_call_countsinherited fromAgentare not exposed onLLM. -
Fix grammar in the
AzureOpenAIGeneratorandAzureOpenAIChatGeneratordocstring code examples ("<this a model name..."→"<this is a model name...") so that copy-pasted snippets read correctly. -
The
haystack.toolspackage now imports its heavier modules lazily. Importing the package no longer eagerly pulls inComponentTool,PipelineTool, andSearchableToolset(and their transitive dependencies such asPipelineandInMemoryDocumentStore); these are resolved on first access instead. This reduces import time when those tools are not used. All public names remain importable fromhaystack.toolsexactly as before. -
The
Agentnow emits onehaystack.agent.step.tooltracing span per tool call instead of a single span grouping every tool call in a step. Each span carries the tool's name and description as tags, plus the call arguments and result when content tracing is enabled. This matches the OpenTelemetry and Langfuse convention of a single tool call per span, so traces render one observation per tool invocation. -
LoggingTracernow serializes the log records it emits when a span closes, so a span's operation name and its tags stay contiguous even when spans are opened and closed from parallel threads (for example, theAgentrunning tool calls concurrently). -
Streaming-capable components (
OpenAIChatGenerator,OpenAIResponsesGenerator,HuggingFaceAPIChatGenerator,HuggingFaceLocalChatGenerator,Agent) now accept a synchronous streaming callback in asynchronous contexts (run_async). A warning is logged because the callback runs synchronously on the event loop and may block it, but the run proceeds. Async callbacks are still preferred for performance.import asyncio from haystack.components.generators.chat import OpenAIChatGenerator from haystack.dataclasses import ChatMessage, StreamingChunk def print_chunk(chunk: StreamingChunk) -> None: print(chunk.content, end="", flush=True) async def main(): llm = OpenAIChatGenerator() # previously this raised; now it logs a warning and streams as expected await llm.run_async( [ChatMessage.from_user("Tell me about Italy")], streaming_callback=print_chunk, ) asyncio.run(main())
-
Simplified the pipeline scheduler by removing the
DEFER_LASTcomponent priority. Components that previously gotDEFER_LAST(those with unresolved lazy-variadic sockets) now share theDEFERpriority and are tie-broken via topological order, matching the behavior of all other deferred components. This should not materially change how components are scheduled in a pipeline. -
ToolCallResultcan now carryFileContentparts, in addition toTextContentandImageContent. AToolcan return a file (such as a PDF) as its result so a multimodal chat generator that supports file inputs (for exampleOpenAIResponsesChatGenerator) passes it straight to the model. Combine this with a tool'soutputs_to_string={"raw_result": True}to keep the content parts intact instead of converting them to a string. -
Toolsetgained two methods:get_selectable_tools()returns every tool available for name-based selection (ignoring any active selection restriction, e.g. the one used bySearchableToolset), andspawn()returns an isolated, run-scoped copy of the Toolset. Subclasses with additional run-scoped state can overridespawn(). -
SearchableToolsetnow exposes its full catalog of tools viaSearchableToolset.get_selectable_tools(). -
Toolset.warm_up()is now idempotent: warming up a toolset more than once no longer re-warms its tools. CallingToolset.add()after the toolset has been warmed up now warms up the newly added tool (or toolset) immediately, so it is ready to use without an extrawarm_up()call. -
Removed the use of
Pipeline._run_componentinsideAgent.runandAgent.run_async. The chat generator and tool invoker are now invoked directly via theirrun/run_asyncmethods, decoupling the Agent from the Pipeline internals. -
Pipeline.draw()andPipeline.show()now validate the Mermaid server response before writing it to disk. The response body is checked against the expected output format (PNG, JPEG, WebP, SVG, or PDF) via its magic-byte signature, and theContent-Typeheader is checked as well. If the response is empty or does not match the requested format, aPipelineDrawingErroris raised and no file is written. This prevents a misconfigured or untrustedserver_urlfrom causing arbitrary content (for example an HTML error page) to be saved verbatim to the output path.
Deprecation Notes
-
OpenAIGenerator,AzureOpenAIGenerator,HuggingFaceAPIGenerator, andHuggingFaceLocalGeneratorhave been deprecated and will be removed in Haystack 3.0. Generators living in Haystack Core Integrations will also be removed in Haystack 3.0.Their chat counterparts (
OpenAIChatGenerator,AzureOpenAIChatGenerator,HuggingFaceAPIChatGenerator,HuggingFaceLocalChatGenerator) are the replacement. Starting from Haystack 2.30.0, all ChatGenerators also accept a plainstras input to make the transition easier.How to migrate:
Direct usage (running a generator from Python code)
Before:
from haystack.components.generators import OpenAIGenerator gen = OpenAIGenerator() result = gen.run("What is NLP?") text = result["replies"][0] # str meta = result["meta"][0] # dict with model metadata
After:
from haystack.components.generators.chat import OpenAIChatGenerator gen = OpenAIChatGenerator() result = gen.run("What is NLP?") # str input accepted directly reply = result["replies"][0] # ChatMessage text = reply.text # str meta = reply.meta # dict with model metadata (now on the message)
System prompt
Before:
from haystack.components.generators import OpenAIGenerator gen = OpenAIGenerator(system_prompt="You are concise.") result = gen.run("What is NLP?")
After:
from haystack.components.generators.chat import OpenAIChatGenerator from haystack.dataclasses import ChatMessage gen = OpenAIChatGenerator() result = gen.run([ ChatMessage.from_system("You are concise."), ChatMessage.from_user("What is NLP?"), ])
Pipeline usage
Pipelines that connected
PromptBuilder(output:str) to a legacy Generator continue to work unchanged when you swap in a ChatGenerator. The Haystack pipeline type system automatically convertsstrtolist[ChatMessage]at the connection edge.Before:
from haystack.components.generators import OpenAIGenerator from haystack.components.builders import PromptBuilder pipeline.add_component("prompt_builder", PromptBuilder(template=prompt_template)) pipeline.add_component("llm", OpenAIGenerator()) pipeline.connect("prompt_builder", "llm") # str -> str
After, minimal change (smart connection still works):
from haystack.components.generators.chat import OpenAIChatGenerator from haystack.components.builders import PromptBuilder pipeline.add_component("prompt_builder", PromptBuilder(template=prompt_template)) pipeline.add_component("llm", OpenAIChatGenerator()) pipeline.connect("prompt_builder", "llm") # str -> list[ChatMessage], auto-converted
Alternatively, use
ChatPromptBuilder:from haystack.components.generators.chat import OpenAIChatGenerator from haystack.components.builders import ChatPromptBuilder from haystack.dataclasses import ChatMessage template = [ChatMessage.from_user(prompt_template)] pipeline.add_component("prompt_builder", ChatPromptBuilder(template=template)) pipeline.add_component("llm", OpenAIChatGenerator()) pipeline.connect("prompt_builder.prompt", "llm.messages")
-
HuggingFaceAPIChatGenerator,HuggingFaceAPITextEmbedder,HuggingFaceAPIDocumentEmbedder, andHuggingFaceTEIRankerare deprecated and will be removed from Haystack in version 3.0. They are moving to thehuggingface-api-haystackpackage. To continue using them, install the package withpip install huggingface-api-haystackand update your imports as follows:from haystack_integrations.components.generators.huggingface_api import HuggingFaceAPIChatGenerator from haystack_integrations.components.embedders.huggingface_api import HuggingFaceAPIDocumentEmbedder from haystack_integrations.components.embedders.huggingface_api import HuggingFaceAPITextEmbedder from haystack_integrations.components.rankers.huggingface_api import HuggingFaceTEIRanker
-
DocumentLanguageClassifierandTextLanguageRouterare deprecated and will be removed from Haystack in version 3.0. They are moving to thelangdetect-haystackpackage. To continue using them, install the package withpip install langdetect-haystackand update your imports as follows:from haystack_integrations.components.classifiers.langdetect import DocumentLanguageClassifier from haystack_integrations.components.routers.langdetect import TextLanguageRouter
-
TikaDocumentConverteris deprecated and will be removed from Haystack in version 3.0. It is moving to thetika-haystackpackage. To continue using it, install the package withpip install tika-haystackand update your import as follows:from haystack_integrations.components.converters.tika import TikaDocumentConverter
-
TransformersZeroShotDocumentClassifier,TransformersTextRouter,TransformersZeroShotTextRouter,HuggingFaceLocalChatGenerator,NamedEntityExtractor, andExtractiveReaderare deprecated and will be removed from Haystack in version 3.0. They are moving to thetransformers-haystackpackage. Some of them are also being renamed. To continue using them, install the package withpip install transformers-haystackand update your imports as follows:from haystack_integrations.components.classifiers.transformers import TransformersZeroShotDocumentClassifier from haystack_integrations.components.routers.transformers import TransformersTextRouter from haystack_integrations.components.routers.transformers import TransformersZeroShotTextRouter # HuggingFaceLocalChatGenerator was renamed to TransformersChatGenerator from haystack_integrations.components.generators.transformers import TransformersChatGenerator # NamedEntityExtractor (Hugging Face backend) was renamed to TransformersNamedEntityExtractor from haystack_integrations.components.extractors.transformers import TransformersNamedEntityExtractor # ExtractiveReader was renamed to TransformersExtractiveReader from haystack_integrations.components.readers.transformers import TransformersExtractiveReader
-
LocalWhisperTranscriberandRemoteWhisperTranscriberare deprecated and will be removed from Haystack in version 3.0. They are moving to thewhisper-haystackpackage. To continue using them, install the package withpip install whisper-haystackand update your imports as follows:from haystack_integrations.components.audio.whisper import LocalWhisperTranscriber from haystack_integrations.components.audio.whisper import RemoteWhisperTranscriber
Security Notes
- Loading a pipeline from YAML used to dynamically import any class referenced in the file, which made a crafted YAML capable of causing arbitrary classes to be imported and instantiated by
Pipeline.from_dict. Deserialization is now gated by an allowlist of trusted module namespaces (see the upgrade note for how to extend it). In addition,default_from_dictnow refuses to recurse into nested{"type": "..."}dictionaries keyed by parameter names that the parent class does not actually accept — blocking attempts to smuggle untrusted classes into unused parameter slots. - Dangerous builtins such as
eval,exec,compile,__import__,open, andgetattrcan no longer be resolved during deserialization, even thoughbuiltinsis on the default allowlist. Attempting to deserialize one as a callable, a type annotation, or a nested{"type": ...}class reference raises aDeserializationError. The block can be bypassed withunsafe=Truewhen the source of the serialized data is fully trusted.
Bug Fixes
-
Six retriever components (CogneeRetriever, FAISSEmbeddingRetriever, FalkorDBCypherRetriever, FalkorDBEmbeddingRetriever, MongoDBAtlasFullTextRetriever, ValkeyEmbeddingRetriever) were missing from the retriever index table in both current and versioned docs.
-
Agentnow correctly executes tools supplied only at runtime viarun(tools=...)/run_async(tools=...)when no tools were provided at initialization. Previously the run stopped after the first LLM call because the step guard checked the init-time tools instead of the tools resolved for the run. -
Initializing an
Agentno longer warms up aSearchableToolset. Previously construction could iterate the tools, andSearchableToolsetwarms itself up when iterated, so it was warmed up too early. Its warm-up now happens only viaAgent.warm_up()or at run time. -
The
haystack.agent.runtracing span'shaystack.agent.toolstag now reflects the tools actually used for the run. Previously it always reported the init-time tools, so it was inconsistent with the run when tools were overridden at runtime viarun(tools=...)/run_async(tools=...). -
Fixed a bug in
Agent(and the underlying tool invocation) where parallel tool calls in a single step did not respect read/write dependencies onState. Tool arguments were prepared once, up front, from the initial state, so a tool reading aStatekey viainputs_from_statenever observed a write made by another tool in the same step viaoutputs_to_state. For example, two calls to a retrieval tool that both reads and writes thedocumentskey would each see the original documents instead of the second call seeing the first call's results.Tool calls within a step are now scheduled into batches based on their
inputs_from_stateandoutputs_to_statemappings. Calls that only read, or that touch disjoint keys, still run in parallel; a call that reads a key another call writes is run in a later batch, after that write has been merged intoState, with its arguments re-prepared so it sees the updated value. Tools that receive the liveStateobject are treated as reading and writing every key and run on their own. The order the LLM requested the calls in no longer affects correctness. -
Fixed the
Agentexiting prematurely under the defaultexit_conditions=["text"]. The agent now only stops when the last message is an assistant message with non-empty text (or when no tool invoker is configured). Previously, if the LLM produced an invalid tool call that was discarded, the resulting assistant message with empty text and no tool calls would trigger an exit, preventing the agent from recovering. The agent now continues looping so the model can recover on the next iteration. -
Fixed a task leak in
AsyncPipelinewhen running components concurrently. Previously, if one component raised an error while sibling components were still running, those in-flight tasks were neither awaited nor cancelled and kept running in the background until the event loop was torn down. They are now cancelled and drained before the original error is re-raised. The same cleanup now also applies when iteration ofrun_async_generatoris stopped early (e.g. the consumer breaks out of the loop and closes the generator) or the run is cancelled: any tasks still in flight are cancelled instead of leaking. Note that cancellation only interrupts components that run natively async. Sync components are offloaded to a worker thread, which cannot be interrupted and runs to completion in the background; its outputs are discarded, so pipeline state stays consistent, but the component's side effects still complete. -
ComponentToolno longer adds a top-leveldescriptionkey to the generatedparametersJSON schema. The wrapped component's docstring was emitted inside the parameters schema, where LLM providers (OpenAI, Anthropic, Google) do not use it, and for tools wrapping aSuperComponentit produced a confusing auto-generated summary. The parameters schema now only carries per-parameter descriptions, matching plainTooland function-based tools. The tool-leveldescriptionis unchanged. -
Fixed
CSVToDocumentinconversion_mode="row"crashing the entire batch when a CSV row has more fields than the header (ragged rows, e.g. an unquoted comma inside a value). Surplus values are now stored under an explicitextra_columnsmetadata key instead of theNonekey, which previously broke Document id generation and discarded results from all sources. -
Prevent
Document.from_dict()from mutating the input dictionary during deserialization. -
DocumentJoinernow raises a clearValueErrorwhen the providedweightssum to zero, instead of failing with an opaqueZeroDivisionErrorwhile normalizing them. -
Prevent DocumentLanguageClassifier from crashing when
Document.content=Noneby marking them as unmatched and logging a warning. -
Improved the validation and compatibility layer of
Document:- since Haystack 2.x,
embeddingis a list of floats, while it was a NumPy array in 1.x, so a NumPyembeddingis converted to a list for backward compatibility. The conversion only happened whenembeddingwas passed as a keyword argument: passed positionally, the array was silently left as is, potentially causing downstream issues. contenttype was validated only when passed as a keyword argument.contenttype validation was also skipped when the value was falsy, such as0or[], and failed later with an unrelated error.
- since Haystack 2.x,
-
Fixed a
TypeErrorraised when filtering documents with the ordering operators (>,>=,<,<=) where both the filter value and the document meta value aredatetimeobjects but one is timezone-aware and the other is naive. The naive/aware reconciliation is now applied to any pair ofdatetimeoperands, matching the behavior already used for ISO 8601 string dates. -
Fixed a bug where
Agentwould not exit when the model emitted multiple tool calls in a single turn and the configured exit-condition tool was not the first one in the list. Previously, only the first tool call in each assistant message was checked againstexit_conditions, so a reply like[search, finish](withexit_conditions=["finish"]) would silently fail to stop the loop and keep iterating untilmax_agent_stepswas reached. Since parallel tool calls are now the norm for frontier models, this could quietly turn a single successful turn into dozens of wasted LLM calls. TheAgentnow inspects every tool call in the message, so the exit condition is honored regardless of ordering. -
Fix
AnswerBuilder.run()mutating themetadict of inputDocumentobjects.source_index(andreferencedwhenreference_patternis set) are now only added to the document copies insideGeneratedAnswer.documents, not to the originals. -
Fixed
GeneratedAnswer.from_dictandExtractedAnswer.from_dictmutating their input dictionary in place. They replaced nested values (documents,document, offsets andmeta["all_messages"]) directly inside the passed dictionary, so deserializing the same dictionary a second time received already-parsed objects and raised anAttributeError. Both methods now copy their input before deserializing and are side-effect free. -
Fixed
component_to_dictserialization validation not detecting non-string keys in nested dictionaries. The check only inspected the top-level dictionary, so a non-string key inside a nestedinit_parametersvalue passed validation and could later fail during marshalling; nested dictionaries are now validated too. -
Fixed a crash in
DocumentJoinerwhen usingjoin_mode="distribution_based_rank_fusion"with documents that havescore=None. The score normalization now treats a missing score as0(consistent with how the mean/standard-deviation are computed), instead of raisingTypeError: unsupported operand type(s) for -: 'NoneType' and 'float'. -
Fixed
default_from_dict(and thereforecomponent_from_dict) mutating the caller'sdatadictionary in place when deserializing nested objects such asSecretorComponentDevice. The serialized sub-dictionaries were being replaced by their deserialized instances inside the input dict; the input is now left unchanged, so the samedatacan be safely deserialized more than once. -
Fixed
deserialize_callablefailing for functions decorated with@toolwhen the decorated function is asynchronous. Such tools store the callable inasync_functionwithfunctionset toNone, which previously caused deserialization to raise aDeserializationError. The underlying coroutine function is now resolved correctly. -
Fixed
DocumentCleanerwiping the middle page of a 3-page document whenremove_repeated_substrings=True. Header/footer detection compares the pages between the first and last, which is a single page for a 3-page document; its own longest ngram was then treated as a repeated header/footer and removed. Detection now requires at least two pages to compare, so unique content is preserved while genuine repeated headers/footers are still removed. -
Fixed
DocumentJoinerinconcatenatemode so that documents with a score of exactly0.0are no longer treated as unscored during deduplication. Previously a truthiness check coercedscore=0.0to-inf, which could cause a worse, negatively-scored duplicate to be kept instead of the0.0-scored document. Themergemode was updated to the same explicitis not Nonecheck for consistency; its observable behavior is unchanged. -
Fixed
DocumentTypeRoutermisrouting documents whose MIME type contains a regex metacharacter. A declared type such as the standard IANAimage/svg+xmlwas compiled as a regex, so the+was treated as a quantifier and the document fell intounclassifiedinstead of its own bucket. Declared MIME types are now matched by exact equality first, falling back to regex matching so patterns likeaudio/.*keep working. -
Fixed
FaithfulnessEvaluatorandContextRelevanceEvaluatorto exclude failed queries (scorenan) from the aggregate mean score instead of silently returningnan. A warning is now logged when one or more queries are skipped. -
Fixed
ExtractiveReaderraisingValueErrorwhen the number of valid answer spans for a sequence was smaller thananswers_per_seq(for example with short documents or whenanswers_per_seqexceeded the number of upper-triangular, non-masked (start, end) token pairs)._postprocessnow filters the per-sequence probabilities by the same validity mask it already applied to the start/end token indices, so the three structures always have matching lengths. -
Fixed
FileToFileContentsharing a singleextradictionary across all producedFileContentobjects whenextrais passed as one dict (or left asNone). EachFileContentnow gets its own copy, so mutating one file'sextrano longer leaks into the others. -
Fixed
GeneratedAnswer.from_dictraising anIndexErrorwhen the serializedmetacontained an emptyall_messageslist. Deserialization now guards against the empty list (consistent withto_dict) and round-trips correctly. -
Fixes
HuggingFaceLocalGeneratorso that when multiplestop_wordsare configured the generator returns exactly N replies instead of N×M replies. Previously the list comprehension created a cross-product, duplicating replies and leaving some stop words unstripped. -
HierarchicalDocumentSplitterno longer mutates the metadata of the inputDocument._add_meta_datanow returns a newDocumentwith a copiedmetadict viadataclasses.replaceinstead of writing__block_size,__parent_id,__children_idsand__levelonto the caller'sDocument. -
Fixed a bug where configuring an
Agentconfirmation strategy could make a tool run with stale arguments. HITL confirmation prepared and baked each tool's final arguments up front (injectinginputs_from_statevalues from the state at the start of the tool calling step), which defeated the per-batch argument preparation in tool execution: a tool that reads a state key written by another tool in the same step would run with the pre-step value instead of the freshly produced one. HITL confirmation now operates on the model-requested arguments and leaves state injection to tool execution, so dependent tools again receive the correct values. -
Fixed a
KeyErrorraised by the human-in-the-loop confirmation strategies when a chat model hallucinated a tool call whose name is not in the agent's tools. Such tool calls now bypass confirmation and pass through unchanged, so the tool-calling code reports the unknown tool uniformly viaToolNotFoundException(respectingraise_on_failure) instead of crashing. -
HTMLToDocumentnow skipsByteStreamobjects with empty content instead of passing them to trafilatura. This prevents noisy lxml parse error logs (such as "Document is empty") when a fetcher emits an empty stream. -
Fixed
LLMEvaluator.to_dictnot serializing theraise_on_failureparameter, causing it to silently reset toTrueafter a pipeline round-trip. -
Fixed
InMemoryDocumentStore.load_from_diskreconstructing documents with the plainDocumentconstructor instead ofDocument.from_dict. Documents containing ablob(ByteStream) or asparse_embedding(SparseEmbedding) were loaded with those fields as raw dictionaries, which later crashedrepr(),to_dict(), equality comparison,save_to_diskof the reloaded store, and any component accessingdocument.blob.data. -
Fixed
MarkdownHeaderSplitterdropping content whenkeep_headers=False. The secondary split re-stripped a leading header with a non-anchored search, which matched the first#line anywhere in the body (an embedded lower-level header, or a#comment inside a fenced code block) and discarded everything before it. The match is now anchored to the start of the content, so only a genuine leading header is stripped. -
Fix the
max_runs_per_componentlimit onPipelineandAsyncPipelineso that a component is now stopped when its number of runs reachesmax_runs_per_componentrather than when it strictly exceeds it. Previously, a component could run one more time than the configured limit beforePipelineMaxComponentRunswas raised. This only affected pipelines with loops. The check is also skipped forBLOCKEDcomponents, since they cannot run anyway and should not trigger the error. -
Add
EmbeddingBasedDocumentSplitterto the PreProcessors index page, and addMarkdownHeaderSplitterandSpacyNamedEntityExtractorto the sidebar navigation. These components had documentation pages but were missing from navigation, making them undiscoverable by users browsing the docs. -
Fixed a
TypeErrorwhen filtering documents on a nested metadata field (e.g.meta.user.age) where an intermediate value is not a dictionary (for exampleNoneor a scalar). Such intermediate values are now treated as a missing field (None) instead of raising, which previously crashedfilter_documentsfor stores likeInMemoryDocumentStore. -
Fixed
ChatMessage.from_openai_dict_formatcrashing on zero-argument tool calls. OpenAI-compatible servers (vLLM, llama.cpp, Ollama, LM Studio, ...) may send an empty string or omit theargumentsfield for a tool call that takes no arguments; this previously raisedjson.JSONDecodeErrororKeyError. Such tool calls are now parsed into aToolCallwith emptyarguments. -
expand_page_range()now raises aValueError: too many values to unpackwhen a page range string contained more than one hyphen (e.g."10-20-30"). The parser now validates the format and raises a clearValueErrorwith an explanatory message for invalid inputs. -
Fixed
RecursiveDocumentSplitterproducing incorrectsplit_idx_startmetadata values whensplit_unit="word"(or"token") is used together withsplit_overlap > 0. Previously,self.split_overlap(a word/token count) was subtracted directly from a character-offset counter, causing every chunk after the first to report a wrong start position in the original document. The fix uses the actual character length of the overlap suffix, obtained from_get_overlap(), so the offset is always correct regardless ofsplit_unit. -
Fix
InputSocket.__post_init__crashing withIndexErrorwhen a Variadic input uses a bareIterablewithout a type argument (e.g.Annotated[Iterable, HAYSTACK_VARIADIC_ANNOTATION]). Now raisesComponentErrorwith a clear message instead of an unhelpfulIndexError. -
HierarchicalDocumentSplitternow validates itsblock_sizesandsplit_overlapparameters at construction time. Previously, an emptyblock_sizesset silently produced no splits, and asplit_overlaptoo large relative to the smallest block size raised a confusing error referencingsplit_length, a parameter that doesn't exist on this component.HierarchicalDocumentSplitternow raises a clearValueErrorin both cases, using its own parameter names and identifying the offending value. -
SearchableToolsetnow builds its internal BM25InMemoryDocumentStorewithshared=False, so its search index is freed together with the toolset instead of accumulating inInMemoryDocumentStore's process-global storage. Previously, repeatedly constructing aSearchableToolset(for example, building anAgentper request in a served application) leaked one storage partition per instance for the lifetime of the process. -
LLMMetadataExtractornow raises a clearValueErrorwhen thepromptcontains no template variables. Previously this case raised an unhelpfulIndexError: list index out of range. The error message now consistently explains that the prompt must contain exactly one variable calleddocument. -
Document.idis now deterministic regardless of the insertion order of keys inmeta. Previously the hash was built fromdict's repr, which reflects insertion order, so two documents with the same content and the samemetacould get different IDs depending on how themetadict was constructed. This silently brokeDuplicatePolicy.SKIP/FAILand any cache or dedup table keyed on the document ID whenever upstream code producedmetain different orders. -
Fixed
MetaFieldGroupingRankercrashing with an uncaughtTypeErrorwhen a group containedsort_docs_byvalues of mutually non-comparable types (for example anintand astr). The sort is now wrapped in atry/except, so in that case the group's original insertion order is kept and a warning is logged, mirroring the behavior ofMetaFieldRanker. -
Preserve falsy metadata values such as
0,False, and empty strings whenmeta_fields_to_embedis used by sentence-transformer embedders and rankers. -
Remove zero-frequency vocabulary entries after deleting documents from
InMemoryDocumentStoreso previous document history no longer changes BM25Okapi scores for the active corpus. -
haystack.logging.configure_loggingno longer freezes the log level at import time. Previously the level was captured once when the function ran (atimport haystack), so a log level set later by the application was ignored forstructlog-native loggers. The effective level is now read on every log call. -
Fixed the logger in
haystack.utils.requests_utilsbeing named after the module's file path instead ofhaystack.utils.requests_utils, which kept its records outside thehaystacklogger namespace. -
haystack.logging.getLoggeris now idempotent. Previously, calling it more than once for the same logger name wrapped the already-wrapped logger methods again, which caused the log message to be run throughstr.formatonce per call. As a result a field value containing{...}could be re-interpolated and pull in the value of another field. Each logger is now patched only once. -
The patched
findCallerused to determine the source of a log entry no longer prints to stdout and no longer masks errors with a misleadingNameError, matching structlog's ownfindCallerimplementation. -
SearchableToolsetnow raises aValueErrorduringwarm_up()when its catalog contains tools with duplicate names. Previously duplicates were silently collapsed, which could cause a search hit to resolve to the wrong tool. -
SearchableToolsetserialization now preserves the catalog shape: a catalog passed as a singleToolsetround-trips back to aToolset(previously it was always rebuilt as a list). Serialization now reusesserialize_tools_or_toolset/deserialize_tools_or_toolset_inplace. -
Fixed
HuggingFaceAPIDocumentEmbedderserialization to preserve the configuredconcurrency_limit. -
Fix
TransformersZeroShotDocumentClassifier.to_dictto include theclassification_fieldandmulti_labelinit parameters, which were previously dropped on serialization and reset to defaults afterfrom_dict. -
Runtime tool-name selection via
Agent.run(tools=["tool_a", "tool_b"])now resolves correctly when aSearchableToolsetis configured. Previously theSearchableToolsetwas flattened into a single-item list (just its search tool), which broke its search and lazy-loading behavior. -
A
Toolsetis no longer mutated in place during anAgentrun. Each run now operates on an isolated per-run copy of the configuredToolset(via the newToolset.spawn()method). This makes concurrent runs that share the sameToolsetinstance safe: per-run state such as the active tool-name selection, and aSearchableToolset's discovered tools, can no longer leak or collide across runs. -
Agentnow warms up tools and toolsets passed at runtime viaAgent.run(tools=...)/Agent.run_async(tools=...), not only the tools provided at initialization. -
Combining two toolsets with
+produces an internal_ToolsetWrapperthat can now be serialized and deserialized correctly.to_dict()/from_dict()preserve each wrapped toolset (via its ownto_dict()), and the wrapper'swarm_up()is now idempotent.