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 | None to the schema and pass it to mux.search. Dates are YYYY-MM-DD and normalized across providers. published_before is 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 separate read_page tool 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.
ModelPer 1,000 requestsIncluded resultsExtra resultLatency
exa/instant
Exa instant, lowest-latency neural search
$4.0010+$1.00 / 1k~300 ms
exa/fast
Exa fast
$7.0010+$1.00 / 1k~500 ms
exa/auto
Exa auto (default, best general quality)
$7.0010+$1.00 / 1k~1 s
exa/deep
Exa deep multi-query search
$12.0010+$1.00 / 1k~5 s
parallel/turbo
Parallel turbo, ~200ms
$1.0010+$1.00 / 1k~200 ms
parallel/fast
Parallel fast, <1s
$1.0010+$1.00 / 1k~800 ms
parallel/basic
Parallel basic
$5.0010+$1.00 / 1k~1 s
parallel/advanced
Parallel advanced, highest quality excerpts
$5.0010+$1.00 / 1k~3 s
keenable/pro
Keenable pro index
$4.0010none~700 ms
keenable/realtime
Keenable realtime index
$4.0010none~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