feat(retrievers): add Caesar web search retriever - #1850
Conversation
Adds Caesar as an opt-in web-search retriever (RETRIEVER=caesar),
alongside the existing providers (Tavily, Exa, Brave, fastCRW, etc.).
Caesar is a free agentic web-search API that works anonymously with no
API key. An optional CAESAR_API_KEY raises partner rate limits and is
sent as a Bearer token only when present. Mirrors the fastCRW retriever:
POSTs to /v1/search with requests (no new dependency) and normalizes
results to the standard {href, body} shape.
Docs: https://docs.trycaesar.com
Caesar began requiring an API key on every request on 2026-07-07. The retriever now raises when CAESAR_API_KEY is unset, mirroring the exa retriever, and always sends the Bearer header. Docs and tests updated.
|
Correction, and a pushed fix. Caesar began requiring an API key on every request on 2026-07-07. When I opened this PR, the API served anonymous traffic, and "keyless" was the whole reason I proposed it. That rationale is now gone, and I would rather say so plainly than leave a stale claim in the description. Pushed to this branch:
This is now just one more key-required provider rather than a keyless option. If you would rather not carry another one, say the word and I will close this. If you do want it, I will re-run a live query against a keyed account and post the output before it merges. Sorry for the churn. |
|
Re-verified against a keyed account, as promised. With With That closes the testing note in the description. The offer still stands: if you would rather not carry another key-required provider, say the word and I will close this. |
…ery_domains Caesar returns a `passages` array per result: the spans it selected for the query. The `snippet` field is the page's meta description, so it is the same text for every query that surfaces the document. Build the result body from the passages, falling back to the flat fields when a result has none. Also wire query_domains through to Caesar's source_policy.include_domains. The retriever accepted the argument and silently dropped it, so a domain-scoped report was searching the whole web, unlike the exa and tavily retrievers which both pass include_domains.
|
Update, and I would like to withdraw the offer to close this. Above, I said that once Caesar stopped being keyless this was "simply one more key-required provider." I had not actually read its search response carefully. Having done that, I no longer think that is true, and I found two things wrong with my own retriever. Both are fixed in The retriever was discarding the useful half of the response. Caesar returns a The
That was a real bug, and it would have shipped quietly. Tests are up to 9 and all pass, including passage preference, blank passages, and both If you still would rather not carry another keyed provider, that is a completely fair call and I will close it myself. But I did not want the decision made against the weaker version of the integration. |
|
Reviewed — the patch is well-formed (tests, docs, error handling), but it no longer applies to CI now runs on every PR, so a rebase gives you an immediate answer. Why this is not merged yet, and it isn't code quality.
That is a scope decision for @assafelovic, not a verdict on this patch. The sustainable answer is probably a documented plugin path — an entry-point group so a vendor ships and versions their own package — rather than accepting or rejecting each of these case by case, which is exactly why they have all been sitting. Not closing it. Flagging it so the decision gets made once instead of six times. |
What
Adds Caesar as a new web-search retriever, selectable via
RETRIEVER=caesar, alongside the existing providers (Tavily, Exa, Serper, Brave, fastCRW, and others).Caesar is an agentic web-search API. The retriever POSTs to its
/v1/searchendpoint and maps results into GPT Researcher's standard{href, body}shape.Disclosure: I work on GTM for Caesar, so this is an affiliated contribution.
Why
Caesar returns the passages it selected for your query inline with each search result, so the
bodyof each{href, body}carries the text that addresses the query rather than the page's marketing blurb.Its
snippetfield is the page's meta description, identical for every query that surfaces the document. Itspassagesare query-conditioned. Same document, two different queries, zero overlapping passages:So the retriever builds
bodyfrom the passages and falls back to the flat fields when a result has none. For a research agent that readsbodybefore deciding what to scrape, that is the difference between a meta description and roughly 1,200 characters of relevant text per source.An honest note on history: when I opened this PR the rationale was that Caesar was keyless. Caesar removed the anonymous tier on 2026-07-07, so that rationale is gone and a
CAESAR_API_KEYis required, created from an account at https://app.trycaesar.com. What replaced it is the argument above, which I think is the better one.Changes
CaesarSearchretriever undergpt_researcher/retrievers/caesar/.caesarin the retriever factory (actions/retriever.py) and inVALID_RETRIEVERS(retrievers/utils.py), and export it fromretrievers/__init__.py.RETRIEVER=caesarin the search-engines guide.tests/test_caesar_retriever.py.All changes are additive. No existing retriever is modified.
Implementation notes
get_api_keyraises a clear error namingCAESAR_API_KEYand pointing at where to obtain it. TheAuthorization: Bearerheader is always sent.bodypreferspassages, then falls back acrosssnippet,content, andpassage.hreffalls back acrossurl,canonical_url, andsource_url. Results with no URL are dropped, mirroring howbravenormalizes.query_domainsis now honored. It maps to Caesar'ssource_policy.include_domains. The earlier revision of this PR acceptedquery_domainsand silently ignored it, so a domain-scoped report was quietly searching the whole web.exaandtavilyboth passinclude_domains, and now so does this.CAESAR_API_URL(or acaesar_api_urlheader) can override the base URL, following the same override patterncrwuses.One tradeoff, stated plainly: preferring passages makes each
bodyroughly 4x larger than a meta description, which costs more tokens up front. For this project I think that is the right trade, since a betterbodyis exactly what improves source selection before scraping. It is one method (_body) if you would rather keep bodies short.Testing
tests/test_caesar_retriever.pycovers: a missingCAESAR_API_KEYraises, a present key produces theBearerheader, a header-supplied key is honored, passage preference over the meta description, blank-passage handling, the field-name fallbacks,query_domainsproducing asource_policy, its absence omitting one, and the empty-on-error path.Live, against a keyed account (the keyless transcript in the original description no longer reproduces):
And domain scoping, which now actually scopes:
Happy to adjust naming or anything else to fit your conventions.