Skip to main content
Keenable provides web search and content access built for AI agents. In a Gradium voice agent, Keenable acts as the live web layer: the user asks a question out loud, the agent turns it into a search query, Keenable returns current web results in realtime mode, and Gradium speaks the answer back while the sources stream to the screen.

Keenable

Explore Keenable’s search API, CLI, and MCP server.

Voice web search demo

See a Gradbot demo that answers spoken questions with live web search.
A voice search agent built on Gradium and Keenable follows this loop:
  1. The user asks a spoken question, such as “What did the Fed announce today?”
  2. Gradium Speech-to-Text transcribes the request in real time.
  3. The agent calls a web_search tool with a focused query, silently and as its first action.
  4. Keenable returns the top results with titles, sources, and snippets.
  5. The agent answers in one to three short sentences, citing the source naturally.
  6. Gradium Text-to-Speech streams the answer back to the user.
mic → Gradium STT → LLM (tool-capable) → Keenable realtime search → Gradium TTS → speaker
Because Keenable’s realtime mode returns results in well under a second, the search fits inside a natural conversational pause. The caller hears an answer, not a progress report.

Core wiring

The demo exposes search as a single voice-agent tool. Gradium handles the live STT and TTS session; the agent decides when to call web_search.

Search tool definition

gradbot.ToolDef(
    name="web_search",
    description=(
        "Search the live web for current or factual information. Use this "
        "for any question about recent events, facts, prices, people, "
        "products, news, or anything you are unsure about. Returns the top "
        "results with title, source site, and a snippet."
    ),
    parameters_json=json.dumps({
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": (
                    "The search query as a clear natural-language "
                    "description of what to find."
                ),
            }
        },
        "required": ["query"],
    }),
)

Calling the Keenable Search API

When the model calls the tool, the handler runs a Keenable realtime search. The API takes a POST request with an X-API-Key header and returns a list of results.
import httpx

KEENABLE_API_URL = "https://api.keenable.ai/v1/search"

async def keenable_search(query: str, limit: int = 5) -> list[dict]:
    payload = {"query": query, "mode": "realtime"}
    headers = {
        "X-API-Key": os.environ["KEENABLE_API_KEY"],
        "Content-Type": "application/json",
    }

    async with httpx.AsyncClient() as client:
        r = await client.post(
            KEENABLE_API_URL, json=payload, headers=headers, timeout=30.0
        )
        r.raise_for_status()
        data = r.json()

    rows = []
    for item in data.get("results") or []:
        rows.append({
            "title": item.get("title") or "Untitled",
            "url": item.get("url"),
            "snippet": item.get("snippet") or item.get("description") or "",
        })
        if len(rows) >= limit:
            break
    return rows
Each result includes a title, url, and description, plus an extended snippet and published_at timestamp when available. The request body also accepts optional filters such as site to restrict results to one domain, and published_after or published_before to bound results by publication date.

Returning results to the agent

The tool handler sends a compact text summary back to the agent, along with instructions for how to speak the answer. Keeping the payload small keeps the LLM leg of the turn fast.
async def handle_tool_call(handle, input_handle, websocket):
    if handle.name != "web_search":
        await handle.send_error(f"Unknown tool: {handle.name}")
        return

    query = (handle.args.get("query") or "").strip()
    results = await keenable_search(query, limit=5)

    summary = "\n".join(
        f"- {r['title']} ({r['url']}): {r['snippet'][:300]}" for r in results
    )
    await handle.send(json.dumps({
        "success": True,
        "query": query,
        "results_summary": summary,
        "message": (
            "Answer the caller's question in 1-3 short spoken sentences "
            "using these results. Do NOT search again for this."
        ),
    }))

Agent session config

The session config wires the tool into a live Gradium voice session.
def make_config(msg: dict) -> gradbot.SessionConfig:
    return gradbot.SessionConfig(
        voice_id="_6Aslh2DxfmnRLmP",
        language=gradbot.LANGUAGES["en"],
        instructions=SYSTEM_PROMPT,
        tools=[WEB_SEARCH_TOOL],
        assistant_speaks_first=True,
        silence_timeout_s=0.0,
    )

await gradbot.websocket.handle_session(
    websocket,
    config=cfg,
    on_start=make_config,
    on_tool_call=handle_tool_call,
)

Keep the voice loop fast

Latency is what makes voice search feel magical or broken. The demo applies a few rules worth copying:
  • Search silently. Prompt the agent to call web_search immediately, without filler like “let me look that up”. The first spoken words should be the answer itself.
  • One search per question. Instruct the agent to trust its first results instead of reformulating the query to double-check. A second search doubles the wait.
  • Cap and truncate results. Five results with snippets trimmed to about 300 characters give the LLM enough to answer without inflating the prompt.
  • Warm up the search path. Fire a throwaway Keenable query when the user starts a session, a few seconds before the first real question, so DNS and the search origin are already warm.
  • Handle failures in the agent’s voice. If a search fails, return a tool message telling the agent to say it could not reach the web right now, rather than letting the model guess.

Authentication

Get an API key from the Keenable console. Keys are prefixed with keen_ and are passed in the X-API-Key header. Set it alongside your Gradium key:
VariablePurpose
KEENABLE_API_KEYKeenable search key (keen_...)
GRADIUM_API_KEYGradium voice (STT and TTS)

Why use Keenable with Gradium?

  • Answers inside a conversational pause: realtime mode is fast enough that the caller hears the answer without dead air or spoken filler.
  • Built for agents: results come back as clean structured JSON with titles, snippets, and timestamps, ready to summarize and speak.
  • Grounded spoken answers: the agent answers from live web results instead of stale model memory, and can cite the source naturally.
  • Focused follow-ups: callers can refine a question conversationally, and filters like site and published_after keep each new search sharp.

Demo

The voice web search demo shows the full pattern: a FastAPI app that runs a Gradbot session, exposes web_search as the agent’s only tool, streams source cards to the browser as results arrive, and reports per-turn latency across STT, LLM, search, and TTS. Use it as a reference for building your own instant voice search experience.