LangChain web search tool with Mux (Python and TypeScript)
Build a LangChain web search tool with the @tool decorator or tool() helper and create_agent, backed by Mux for Exa, Parallel and Keenable on one key.
A LangChain web search tool is a plain function wrapped with @tool in Python or tool() in TypeScript, with a schema that tells the model what arguments it takes. This guide builds one on top of Mux, so a single MUX_API_KEY gives the agent Exa, Parallel and Keenable with automatic fallback, then hands the tool to create_agent / createAgent. The code targets LangChain 1.x (Python langchain 1.4, TypeScript langchain 1.5).
Setup#
You need a Mux API key from the dashboard with some credit, plus a key for your chat model. The examples use Anthropic's Claude; any LangChain chat model with tool calling works the same way.
# Python
uv add langchain langchain-core langchain-anthropic getmux
# TypeScript
npm install langchain @langchain/core @langchain/anthropic zod getmux
export MUX_API_KEY="mux-..."
export ANTHROPIC_API_KEY="sk-ant-..."Versions used for this guide:
| Package | Python | TypeScript |
|---|---|---|
langchain |
1.4.6 | 1.5.18 |
langchain-core / @langchain/core |
1.6.12 | 1.2.18 |
getmux |
0.1.0 | 0.1.0 |
pydantic / zod |
2.14 | 4.6 |
In LangChain 1.x the agent constructor is create_agent (Python) and createAgent (TypeScript). Both replace the older create_react_agent from LangGraph prebuilts and the AgentExecutor from LangChain 0.x. The @tool decorator lives in langchain_core.tools and is re-exported from langchain.tools; in TypeScript tool() lives in @langchain/core/tools and is re-exported from langchain.
Define the tool#
The schema exposes query, an optional max_results and an optional include_domains. The body calls Mux with an ordered fallback list and returns only title, URL, snippet and publish date, so one search adds a few hundred tokens to the conversation instead of thousands.
# web_search.py
from pydantic import BaseModel, Field
from langchain_core.tools import tool
from getmux import Mux, MuxError
mux = Mux() # reads MUX_API_KEY
class WebSearchInput(BaseModel):
"""Arguments for the web_search tool."""
query: str = Field(description="The search query, phrased like a web search")
max_results: int = Field(default=5, ge=1, le=20, description="How many results to return")
include_domains: list[str] | None = Field(
default=None, description="Only return results from these domains, e.g. ['arxiv.org']"
)
@tool(args_schema=WebSearchInput)
def web_search(query: str, max_results: int = 5, include_domains: list[str] | None = None) -> dict:
"""Search the web for current information. Returns titles, URLs, short
snippets and publish dates. Use it for anything recent or that needs a source."""
try:
res = mux.search(
query,
models=["parallel/fast", "exa/auto"],
max_results=max_results,
include_domains=include_domains,
)
except MuxError as e:
# Hand the problem back as data so the agent can explain it instead of crashing.
return {"error": e.code, "message": e.message, "results": []}
return {
"model": res.model,
"results": [
{
"title": r.title,
"url": r.url,
"snippet": r.snippet,
"published_date": r.published_date,
}
for r in res.results
],
}The docstring (Python) or description (TypeScript) is what the model reads when deciding whether to call the tool, so say what the tool is for and when to use it. LangChain serializes a dict or object return value to JSON for the tool message.
models: ["parallel/fast", "exa/auto"] tries Parallel first at $0.001 per search and falls back to Exa at $0.007 if Parallel errors or is at capacity. Failed attempts are refunded, so you pay for the one that answered. The auto default would add keenable/pro as a third fallback; list the models yourself when you want to control the order.
Create the agent#
# agent.py
from langchain.agents import create_agent
from web_search import web_search
agent = create_agent(
model="anthropic:claude-opus-5-5",
tools=[web_search],
system_prompt="You are a research assistant. Use web_search for current facts and cite the URL of every source you use.",
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "What changed in the EU AI Act implementation timeline this month?"}]}
)
print(result["messages"][-1].content)create_agent runs the full loop: the model decides to call web_search, LangChain executes it, appends a tool message, and calls the model again until it answers without a tool call. result["messages"] holds the whole transcript, including each tool call's arguments and the JSON your tool returned, which is a convenient place to collect the URLs the agent actually consulted. Pass a model instance instead of the string if you need to set temperature or other options.
Variations#
- Recency filter. Add
published_after: str | Noneto the schema and pass it tomux.search. Dates areYYYY-MM-DDand normalized across providers.published_beforeis not applied by Parallel models. - Full page text.
content="full"returns page text on Exa and joined excerpts on Parallel. It is large, so put it in a separateread_pagetool rather than the main search tool. - Different quality or latency.
["exa/auto", "parallel/advanced"]favors quality;["parallel/turbo", "exa/instant"]favors speed. Every option is listed on the models page.
| Model | Per 1,000 requests | Included results | Extra result | Latency |
|---|---|---|---|---|
| exa/instant Exa instant, lowest-latency neural search | $4.00 | 10 | +$1.00 / 1k | ~300 ms |
| exa/fast Exa fast | $7.00 | 10 | +$1.00 / 1k | ~500 ms |
| exa/auto Exa auto (default, best general quality) | $7.00 | 10 | +$1.00 / 1k | ~1 s |
| exa/deep Exa deep multi-query search | $12.00 | 10 | +$1.00 / 1k | ~5 s |
| parallel/turbo Parallel turbo, ~200ms | $1.00 | 10 | +$1.00 / 1k | ~200 ms |
| parallel/fast Parallel fast, <1s | $1.00 | 10 | +$1.00 / 1k | ~800 ms |
| parallel/basic Parallel basic | $5.00 | 10 | +$1.00 / 1k | ~1 s |
| parallel/advanced Parallel advanced, highest quality excerpts | $5.00 | 10 | +$1.00 / 1k | ~3 s |
| keenable/pro Keenable pro index | $4.00 | 10 | none | ~700 ms |
| keenable/realtime Keenable realtime index | $4.00 | 10 | none | ~700 ms |
Cost control#
Agents search more than you expect. Give each agent its own Mux key with a credit limit so a bad loop stops at a number you chose. This needs a management key (mux-mgmt-...), created once in the dashboard:
import os
from getmux import Mux
admin = Mux(api_key=os.environ["MUX_MGMT_KEY"])
secret, key = admin.keys.create("langchain-agent", limit=10, limit_reset="daily")
# Put `secret` in MUX_API_KEY for the agent. It is shown only once.Past the limit, Mux returns 402 key_limit_exceeded and charges nothing. The limit is checked atomically with the debit, so concurrent agents cannot overshoot it. The dashboard breaks usage down per key. See managing keys.
Handling 402 and 429#
Both getmux clients retry 429 and most 5xx responses twice with backoff and honor Retry-After. The ones left for you:
| Error | Meaning | In a LangChain tool |
|---|---|---|
402 insufficient_credits |
Account balance is empty | Return an error object; alert someone to add credits. |
402 key_limit_exceeded |
This key hit its limit for the period | Return an error object; raise the limit or wait for the reset. |
429 rate_limited |
Over 20 requests per second on one key | Retried by the SDK. If it persists, lower parallelism. |
429 too_many_concurrent_requests |
Over 50 searches in flight on the account | Retried by the SDK. Queue tool calls across agents. |
502 all_providers_failed |
Every model in models failed |
Not retried. Safe to retry once; nothing was charged. |
The example returns MuxError as data rather than raising. In LangChain 1.x an uncaught exception inside a tool stops the run unless you add the tool error middleware, and even then the model only sees a generic message. Returning {"error": ..., "results": []} lets the model tell the user that search is unavailable and answer from what it knows. Branch on e.code if you want different handling for billing versus transient errors. See the errors page.
FAQ#
How is this different from LangChain's built-in search tool integrations?#
LangChain ships community wrappers for individual search APIs, each with its own argument names, result shape and account. The tool here is a few dozen lines you own, with one response shape and one bill across Exa, Parallel and Keenable, and fallback between them. If you only ever want one provider and already have a contract with them, their native integration is a fine choice.
Can I use this with LangGraph directly?#
Yes. create_agent returns a compiled LangGraph graph, and the tool itself is a standard BaseTool, so you can pass it to a ToolNode in your own graph or bind it to a model with bind_tools.
Does create_agent limit how many times the tool is called?#
Not by default. Use the recursion_limit in the invoke config, or LangChain's tool call limit middleware, to cap steps. For a hard ceiling on money rather than steps, use the per-key credit limit described above.
Which model should the agent use?#
Any chat model with tool calling. The examples use Claude via langchain-anthropic / @langchain/anthropic; swap the provider:model string and install the matching package for OpenAI, Google or others. The tool code does not change.
Get a key and run your first search in the quickstart.
Also available as markdown for your coding assistant.
More guides
Add a web search tool to the Vercel AI SDK with Mux
Build a web search tool for the Vercel AI SDK with the tool() helper and multi-step generateText, using Mux for Exa and Parallel results on one key.
Exa pricing explained: what each Exa search tier costs
Exa pricing through Mux: per-request and extra-result math for exa/instant, fast, auto and deep, worked examples, and when your own Exa contract is cheaper.
Keenable search API through Mux: models and pricing
What the Keenable search API offers through Mux: two models, a flat $4 per 1,000 requests for up to 50 results, native date filters, and no page text.