Skip to content
Changelog

KeenableTools adds keyless web search and page reading to Agno agents

October 19, 20261 min read

Agno's KeenableTools registers web_search by default, and fetch_url_content joins it with enable_fetch=True or all=True. Search returns ranked results as markdown or JSON, each snippet folded onto one line and capped at max_snippet_chars (500 by default). Fetch returns a page's main content as markdown under its title.

With no KEENABLE_API_KEY set, the toolkit talks to Keenable's public endpoints. A key moves it to the authenticated endpoints, which lift rate limits. Requests go through httpx, already an Agno dependency, and agent.arun() picks up the async variants aweb_search and afetch_url_content.

from agno.agent import Agent
from agno.tools.keenable import KeenableTools
 
agent = Agent(tools=[KeenableTools(all=True)])

We called both tools against the live API with no key set. A search, trimmed to two results:

>>> tools = KeenableTools(max_results=2, max_snippet_chars=120)
>>> print(tools.web_search("RFC 6761 special-use domain names"))
# RFC 6761 special-use domain names
 
### [RFC 6761: Special-Use Domain Names](https://iana.org/go/rfc6761)
Special-Use Domain Names Abstract This document describes what it means to say that a Domain Name (DNS name) is reserved…
 
### [RFC 6761: Special-Use Domain Names | RFC Editor](https://rfc-editor.org/info/rfc6761)
Special-Use Domain Names Abstract This document describes what it means to say that a Domain Name (DNS name) is reserved…

A page fetch, trimmed after the first lines:

>>> print(tools.fetch_url_content("https://docs.agno.com/introduction"))
## Welcome to Agno | Agno
 
Welcome
 
# Welcome to Agno
 
Build your own agent platform.
...

A fetch carries the full URL to Keenable, so the toolkit resolves the hostname on your machine first. It refuses anything that is not http or https, special-use names such as localhost and .internal, hostnames that do not resolve, and any host whose addresses are loopback, private, link-local or otherwise not public. These calls return at once and send nothing:

http://127.0.0.1:8000/admin              -> Error: refusing to fetch a private/internal host: '127.0.0.1'
http://169.254.169.254/latest/meta-data/ -> Error: refusing to fetch a private/internal host: '169.254.169.254'
http://10.0.0.5/                         -> Error: refusing to fetch a private/internal host: '10.0.0.5'
http://localhost:7777                    -> Error: refusing to fetch a private/internal host: 'localhost'
file:///etc/passwd                       -> Error: refusing to fetch a non-http(s) URL: 'file:///etc/passwd'

API errors come back as text the agent can act on. A 401, 402 or 429 from Keenable becomes a labelled message carrying the backend's own detail.

See the cookbook, and learn more about toolkits in the documentation.

Frequently asked questions

No. With no KEENABLE_API_KEY set, KeenableTools calls Keenable's public endpoints. Set the key, or pass api_key, to switch to the authenticated endpoints, which lift rate limits.

Only web_search. Pass enable_fetch=True to add fetch_url_content, or all=True for both. Each has an async variant that agent.arun() uses.

fetch_url_content refuses URLs that are not http or https, localhost, names ending in .local, .internal, .localhost or .home.arpa, hostnames that do not resolve, and any host that resolves to a loopback, private, link-local, reserved, multicast or other non-public address. It returns an error string and sends no request.

No. The toolkit sends its requests through httpx, which Agno already depends on.

Shipped around the same time