Bundled tools¶
kavalai[common] ships a handful of ready-made tools. They are ordinary
@pythontool functions, so you register them like any other Python tool:
from kavalai import FunctionKernel
from kavalai.tools.webtools.http_client import http_request
kernel = FunctionKernel()
kernel.register_python_tool("http.request", http_request)
response = await kernel.call_tool(
"python://http.request", {"method": "GET", "url": "https://example.com"}
)
In a workflow, declare them under python_functions instead — see
Workflow YAML reference:
python_functions:
- {name: web.search, path: kavalai.tools.webtools.crawl4ai.web_search}
- {name: web.crawl, path: kavalai.tools.webtools.crawl4ai.crawl_url}
Tool |
Import path |
Needs |
|---|---|---|
|
|
a headless browser |
|
|
a headless browser |
|
|
— |
Web crawling¶
crawl_url¶
Renders a page in a headless browser and returns clean Markdown — which is what you want to feed a model, rather than raw HTML full of navigation and scripts.
crawl_url(url: str, include_html: bool = False,
bypass_cache: bool = False, timeout: float = 60.0) -> Crawl4aiResponse
Argument |
Description |
|---|---|
|
The page to fetch. |
|
Also return the cleaned HTML. Default |
|
Refetch instead of using the crawler cache. Default |
|
Page load timeout in seconds. Default |
Returns a Crawl4aiResponse with url, success, markdown,
html, status_code, metadata and error_message. Failures come
back as success=False with an error_message rather than raising, so an
agent can read the error and try something else.
crawl_url refuses a URL whose host is, or resolves to, a private,
loopback, link-local or cloud metadata address; the check is
kavalai.net.ensure_public_url(). A refusal is a failure like any other:
success=False and an error_message beginning Refused:. When
crawl4ai reports that the page ended on a different URL, that URL is checked in
the same way and the content is withheld if it fails.
from kavalai.tools.webtools.crawl4ai import crawl_url
response = await crawl_url(
"http://metadata.google.internal/computeMetadata/v1/"
)
print(response.success, response.error_message)
False Refused: metadata.google.internal is an internal host name
The browser runs in a separate process and resolves names itself, so the SDK
sees only the requested URL and the final one. A redirect through an internal
address in between, or a DNS answer that changes after the check, is not
observed. Full protection for browser-based crawling therefore requires
network egress control — a firewall rule or network policy that gives the
browser, or the crawl4ai container, no route to internal addresses (see
Deployment).
An agent meant to read an intranet is given a tool built with the check switched off. The switch is an argument of the factory, not of the tool, so the model cannot set it:
from kavalai.tools.webtools.crawl4ai import make_crawl_url
kernel.register_python_tool(
"intranet.crawl", make_crawl_url(allow_private_networks=True)
)
web_search¶
A web search that needs no API key: it scrapes the DuckDuckGo HTML endpoint through the same browser.
web_search(query: str, count: int = 10,
timeout: float = 60.0) -> WebSearchResponse
Returns query, success, error_message and results — a list of
WebSearchResult (title, url, snippet).
Note
Both tools drive a real browser, so they are slow (seconds, not
milliseconds) relative to an HTTP call, and scraped search results depend on
a page layout that DuckDuckGo may change. For production search volume,
register a keyed search API as a rest:// server instead.
docker-compose.yml includes a crawl4ai service if you would rather
run the crawler as a container.
The pair is combined in examples/business_info_agent/business_info.py: a
search node finds candidate pages, an agent node restricted to the crawl tool
reads the promising ones, and an LLM node writes the summary. The case file
beside it grades what that agent reports about a company it cannot answer from
memory.
HTTP¶
http_request¶
Any HTTP request, with optional basic auth and an optional Tor proxy. This is
the general-purpose escape hatch when an endpoint does not deserve a full
rest:// server registration.
async http_request(method: str, url: str, params=None, headers=None,
json_body=None, data_body=None, auth_user=None,
auth_password=None, timeout: float = 30.0,
use_proxy: bool = False) -> HttpResponse
Returns status_code, headers, text and json_data (parsed when
the response is JSON, otherwise None). The tool is a coroutine function, so
a direct call is awaited. Redirects are not followed: a 3xx response is
returned as it is.
Private addresses¶
By default the tool refuses a target that is, or resolves to, a private,
loopback, link-local or cloud metadata address, and raises
kavalai.net.UnsafeUrlError; through the kernel the same message
arrives in a FunctionKernelException. The request goes through
kavalai.net.PublicOnlyTransport, which checks every address the host
resolves to and then connects to the address it checked. A second DNS lookup,
which could return a different answer, never takes place, while the Host
header, TLS SNI and certificate verification still use the host name.
from kavalai.net import UnsafeUrlError
from kavalai.tools.webtools.http_client import http_request
try:
await http_request("GET", "http://169.254.169.254/latest/meta-data/")
except UnsafeUrlError as error:
print(error)
169.254.169.254 is a non-public address
An agent meant to call an intranet API is given a tool built with
allow_private_networks=True. The switch is an argument of the factory, not
of the tool, so the model cannot set it:
from kavalai import FunctionKernel
from kavalai.tools.webtools.http_client import make_http_request
kernel = FunctionKernel()
kernel.register_python_tool(
"intranet.request", make_http_request(allow_private_networks=True)
)
A workflow names an importable attribute under python_functions, so the
tool is built once in a module of your own and declared by that path:
# myapp/tools.py
from kavalai.tools.webtools.http_client import make_http_request
intranet_request = make_http_request(allow_private_networks=True)
python_functions:
- {name: intranet.request, path: myapp.tools.intranet_request}
The guarded client connects directly and does not use the HTTP_PROXY /
HTTPS_PROXY variables, since a proxy would resolve the name itself.
Proxy¶
use_proxy=True routes the request through the Tor proxy configured by
KAVALAI_TOR_PROXY_HOST / KAVALAI_TOR_PROXY_PORT; docker-compose.yml
provides a torproxy service. The proxy is allowed whatever its own
address. Because the proxy resolves the target name, the connection cannot be
pinned, and only the pre-check kavalai.net.ensure_public_url() applies:
the name is resolved locally once, the proxy may receive a different answer,
and the local resolver sees the name as well as Tor.
Warning
The guard decides where a request may go, not what it does there. A model
with a general HTTP tool can still call any public endpoint with any method
and body. Prefer registering specific rest:// tools, or restrict the
node with allowed_tools, when the model chooses the target.
Writing your own¶
These are unremarkable functions — a decorator, type hints and a docstring. Yours will look the same:
from pydantic import BaseModel
from kavalai import pythontool
class PondReading(BaseModel):
depth_m: float
water: str
@pythontool
def measure_pond(name: str) -> PondReading:
"""Measure a Green Village pond by name."""
...
The docstring and the type hints are the interface the model sees, so write them for a reader who knows nothing about your codebase. See Agents & tools and Tools.