A provider-neutral web-search component for Haystack with bounded retries #12294
auxiliar-ag
started this conversation in
Show and tell
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
A provider-neutral web-search component for Haystack with bounded retries
A Haystack component's
run()method hands its return value straight to whatever is connected downstream in aPipeline. If a web-search step treats any 200 response as a finished job, aChatPromptBuilderor a retriever further down the graph ends up working from an empty or malformed result set as if it had real search results to reason over. The failure stays invisible until someone inspects what actually flowed through the connection.A component with a retry budget, not a retry loop
Below is
BoundedRetryWebSearch, a class decorated with@componentthat takes an ordered list of injected provider clients and a per-provider attempt limit. It never opens a live HTTP connection itself. Each provider client is a small object with asearch(query)method that either returns aProviderResponse(status_code, payload)or raisesTransportError, so the retry and fallback logic runs the same way under test as it would in production, against aFakeProviderClientthat plays back a scripted list of responses instead of a real request. A retry is spent only when the call itself did not complete: a caughtTransportError, or a non-2xx status. A 2xx response that turns out to carry an emptyresultslist, or a body that is not shaped like the expected contract, stops the retry loop for that provider immediately, because asking the same provider the same query again will not turn an unusable result into a useful one. That case, and only that case, moves straight to the next provider in the list. This is the exact source under test, not a shortened version written for this post:Wiring it into a Pipeline
run()returns two keys.resultsis a list of plain{"title", "url"}dicts for whatever component consumes them next.receiptis a small dict meant for a log line or a metrics sink, not for the pipeline graph itself. The two are separate outputs on purpose:resultsis operational data a downstream component needs to do its job, so it legitimately carries the URL forward, whilereceiptnever does.receipthas a fixed set of seven keys (outcome,task_success,transport_success,provider_used,fallback_used,attempts_total,result_count), andprovider_usedis a positional label likeprovider_1rather than a provider's own name, so nothing in it can leak the query text, a credential, a raw response body, or a raw URL even if a caller logs the whole dict. The test below builds a two-componentPipeline, connectssearch.resultsto a small counting component'sresultsinput, and runs it end to end with twoFakeProviderClientinstances standing in for a primary and a fallback provider:Why the retry budget only covers transport failures
Spending every retry on transport errors and none on a content failure is a deliberate choice, not an oversight: a
TransportErroror a 503 says nothing about the query, so trying again can plausibly help, while an emptyresultsarray from a 200 response is the provider's actual answer, and repeating it changes nothing. I drew that same split, a provider's error rate scored apart from whether its response was actually useful, from NativePort's public page on how it measures providers, which records a non-2xx response and an unsatisfying 2xx as two different numbers instead of folding both into one pass or fail count.This is a sample component written against the current
haystack-aiAPI, not a packaged NativePort-Haystack integration. NativePort has no involvement in the retry, fallback, or validation logic above; the link is to the page that shaped how I think about separating those two kinds of failure, nothing more.Has anyone settled on a fixed attempt limit per provider before falling back, or does the right number vary enough across providers that you end up tuning it per deployment?
Disclosure: NativePort is where I work. This post was drafted with AI assistance; the component and the Pipeline test above ran exactly as shown before publication.
All reactions