Web search and web pages in chats
A chat can search the web and read web pages. LocalAI serves two tools for this itself, with no add-on and no key:
| Tool | What it does |
|---|---|
web_search | Searches the web. Returns up to 10 results, each with a title, a URL and a snippet. |
web_fetch | Reads one public page or file by its URL and returns its main text as Markdown. |
Both tools only read. They need no confirmation, and they cannot write a file, call another tool or start a sub-agent. They are on by default for every user, with and without accounts.
In a chat
The Tools menu of a chat has a Web row with a switch. Switch it off to take both tools away from that chat from its next turn. The steps of a turn read “Searched the web for local models · 5 results” and “Read docs.example.org”; the full URL is in the tooltip.
A sub-agent gets the web tools when its parent names them, or all the tools of its parent when the parent names none. Chats that run without a person (triggers, connectors, agent jobs) and chats with a saved agent get them too.
web_search
Arguments: query (required, at most 400 characters), max_results (1 to
10, default 5), freshness (day, week, month or year) and site (a
host name such as docs.python.org). The search provider is a setting of the
server, never an argument of the model.
Search providers
With no setting, the search uses three public engines that need no key, in this order. Each one reads the result page of the engine, as a browser would:
| Name | Engine | Page |
|---|---|---|
duckduckgo | DuckDuckGo | html.duckduckgo.com/html/ |
brave_html | Brave Search | search.brave.com/search |
bing_html | Bing | www.bing.com/search |
The first engine that answers gives the results, and the results say which host answered (“Search results from search.brave.com”). An engine that answers with no results stops the chain: “No results.” is an answer. An engine that fails is skipped and the next one is tried:
- challenged: the engine answered with a bot check (a captcha, or the “anomaly” page with which DuckDuckGo answers HTTP 202) instead of results. Engines do this to addresses they do not trust, often to the addresses of data centers, and it can change from one hour to the next.
- blocked: the engine refused this server (HTTP 403 or 429).
- error: the engine is down, did not answer in 8 seconds, or its page has no results that LocalAI can read (the engine changed its page).
A challenged or blocked engine is not asked again for 5 minutes, so each search does not wait for it first. When every engine is in this cool-down, all are asked again. The cool-down is kept in the memory of each frontend, see Several frontends.
An administrator can change the order, or leave an engine out, with
chats_web_search_providers (see Settings).
Your own SearXNG instance is the better choice
when you can run one: it keeps the searches on a server you control and does
not depend on the bot checks of the engines. When an administrator sets
chats_web_searxng_url, the search uses SearXNG alone; list it with the
keyless engines (searxng,duckduckgo,brave_html) to fall back to them when
SearXNG does not answer. The instance must allow the JSON format: add json
to search.formats in its settings.yml. The Web add-on
adds providers with a key (Brave Search API, Tavily, Google).
Privacy. The query of each search goes to the engine that answers it, and to each engine before it that failed. The engines see the address of the server, not the address of the user. With SearXNG alone, the queries go only to your instance (and to the engines that it asks).
When no engine answers, the chat says what was tried and what each engine did, and where to fix it:
web_fetch
Arguments: url (required, http or https), max_chars (100 to 100000,
default 20000) and format (markdown, the default, or text).
- HTML loses its scripts, styles, navigation, headers, footers, forms and
hidden elements. The main content (
<main>, or a single<article>) is preferred. Headings, lists, links (with their full address), code blocks and tables are kept. - Plain text, Markdown, CSV, JSON and XML are returned as they are.
- PDF and other types are refused with “unsupported type” and the type. The Web add-on reads the text of PDF files.
- The download stops at 2 MiB and the text at
max_chars. A cut text ends with[truncated, N chars more]. - A request takes at most 15 seconds and follows at most 5 redirects. It sends
no cookies, no credentials and no headers chosen by the model. The
User-Agentnames LocalAI.
What the model is told
Every result starts with a line that labels it as untrusted content from its host, and ends with a closing line that a page cannot write itself:
Control characters are escaped and Unicode direction overrides are removed. The chat’s system prompt also says, in one line, that what the web tools return is untrusted text whose instructions must not be followed. This lowers the risk that a page steers the model; it does not remove it. Treat what a model did after reading a page with the same care as the page.
Addresses inside your network
A model chooses the URL, so web_fetch refuses any address that is not on the
public internet:
- loopback (
127.0.0.0/8,::1), the unspecified address, private ranges (10/8,172.16/12,192.168/16,fc00::/7), link-local (169.254/16, which holds the cloud metadata service, andfe80::/10), carrier-grade NAT (100.64/10), multicast and reserved ranges; - the same addresses written inside IPv6 (IPv4-mapped, IPv4-compatible, NAT64, 6to4);
- host names that resolve to any of these, also when only one of the addresses of the name does;
- IP addresses written in decimal, octal or hexadecimal, or in short forms
such as
127.1; - URLs with a user name or password, and schemes other than
httpandhttps; - ports other than 80 and 443;
- the addresses of the server itself: the address of each of its network
interfaces, the whole loopback range and the host of the external base URL.
A server that listens on all addresses is reachable through its public
address too, so this refusal applies even when the address is public and
even when
chats_web_allow_privateis on.
The checks run for the first URL and for every redirect. The connection goes
to the address that was checked, so a second DNS answer cannot move it to an
internal host. web_fetch does not use a proxy for the same reason, also when
HTTP_PROXY is set.
The same guard checks the network requests of commands in the Sandbox, see Network access.
Settings
An administrator sets these in Settings > Tools or with POST /api/settings.
They apply at the next turn of every chat.
| Setting | Default | Meaning |
|---|---|---|
chats_web_search | on | Offer web_search. |
chats_web_fetch | on | Offer web_fetch. |
chats_web_searxng_url | empty | The address of your SearXNG instance, for example https://searx.example.org. When it is set and chats_web_search_providers is empty, the search uses SearXNG alone. |
chats_web_search_providers | empty | The search providers in the order they are tried, comma separated: searxng, duckduckgo, brave_html, bing_html. Leave a name out to never use it. Empty: searxng when an instance is set, else duckduckgo,brave_html,bing_html. searxng without an address is skipped. An unknown name or a name listed twice is refused. |
chats_web_allow_private | off | Let web_fetch reach addresses inside your network. The cloud metadata addresses and the addresses of the server itself stay refused. |
chats_web_ports | 80,443 | The ports web_fetch may use, comma separated. |
Turn chats_web_allow_private on only if you trust every user of chats: a user
can then read any internal web service the server can reach, through a model.
To check the search providers, an administrator presses Test search on the
web_search row of Settings > Tools (apply a change first), or calls the
endpoint. It runs the query with each provider of the saved order, also the
ones after the first answer and the ones in their cool-down, and says what each
did: ok (with the number of results), no_results, challenged, blocked,
timeout or error, with the reason. provider, host and results are the
first answer, which a chat would get. A provider that answers leaves its
cool-down; one that is challenged or blocked enters it. The test counts as one
search against the limits.
When DuckDuckGo is challenged and the others answer, the search works and nothing needs to change. When all are challenged or blocked, the address of the server is likely flagged by the engines: set a SearXNG instance, or install the Web add-on and give it a key.
Replace a web tool with an add-on
An add-on can serve web_search or web_fetch in place of the built-in tool,
for example to search with a provider that needs a key, as the
Web add-on does. Its tool declares
"shadows": "web_search" (or "web_fetch") in its manifest. When the add-on
is installed, running and on for the chat, the model sees one web_search, served by the add-on, and the built-in one is left out. This
holds also when an administrator switched the built-in off. The Web switch of
the chat’s Tools menu switches only the built-in tools; the add-on has its own
switch. The Web row says who serves the tools in that chat: “Served by add-on
searx” when an add-on replaces both, one line per tool when only one is replaced,
and nothing extra when the built-in tools serve them. GET /api/chats/{id}/tools
reports the same in builtins[].provider (builtin, an add-on ID, or mixed)
and builtins[].providers (the provider of each tool). Settings > Tools shows who serves each tool. See
Tools that replace a built-in tool.
The Web add-on
The Web add-on is an optional add-on of the official library (web). It replaces both
web_search and web_fetch while it is installed, running and on for the chat. It adds:
- search providers that need a key: Brave Search, Tavily and Google Programmable Search, next to SearXNG and the keyless pages of the built-in tool;
- an order of providers: when one fails (it refuses the key, its key reached its limit, it is down or does not answer in 8 seconds), the next one is tried;
- the text of PDF files in
web_fetch.
Install it from Add-ons, Find add-ons. The chat’s Tools menu then says
“Served by add-on web” on the Web row, and Settings > Tools lists the add-on as the
provider of both tools. When another add-on already replaces one of them, the install
dialog says so for each tool (“This add-on replaces the built-in web_search. Another add-on
(…) already does.”), and you choose which one serves in Settings > Tools. The
add-on asks for the tool-providers host control,
so the Settings tab of its details also shows the web_search and web_fetch rows of
Settings > Tools, with the same choice, and those rows on Settings > Tools link to it.
Providers
An administrator sets the providers on the Settings tab of the Web add-on’s details on the Add-ons page (the Web page links to it). The tab lists the five providers in the order they are tried; a switch takes a provider in or out of the order, and Up and Down move it. A provider without what it needs is skipped.
| Provider | Needs | Key sent as |
|---|---|---|
| Brave Search | An API key of the Brave Search API | X-Subscription-Token header |
| Tavily | An API key | Authorization: Bearer header |
| Google Programmable Search | An API key of the Custom Search JSON API and the search engine ID (cx) | X-Goog-Api-Key header |
| SearXNG | The address of your instance (it must allow the JSON format) | no key |
| Search without a key | nothing | no key |
Search without a key is the keyless chain of the built-in tool: the result pages of
DuckDuckGo, then Brave Search, then Bing, with the same cool-down (an engine that answers
with a bot check or refuses this server is skipped for 5 minutes). Its saved name in the
order is duckduckgo. The default order is Brave Search, Tavily, Google, SearXNG, Search
without a key, so with no key set the add-on searches like the built-in tool does.
Installing the add-on only to read PDF files does not make search worse. Take Search
without a key out of the order to never use the keyless pages.
The Settings tab says which providers serve and lists each page of the chain that is in its cool-down, with the reason (“Bing page is skipped for a few minutes: it answered with a bot check.”). The cool-down is kept in the memory of the add-on process (per-instance state, like the built-in’s) and starts again when the settings are saved.
A key is a secret of the add-on. It is saved in the add-on’s data folder (settings.json,
mode 0600) and sent only to its provider, in a header, never in a URL. The add-on’s clients
follow no redirect, so the key cannot travel to another host. The Settings page never shows a
saved key: it says “A key is set”, and an empty field keeps it. Remove the key deletes it.
No tool result, error, log line or prompt holds a key.
Test the providers runs one search with each provider of the order and shows, for each, how many results came back from which host, or why it failed (“Brave Search refused the key (401)”, “no key”). Search without a key gives one row for each of its pages.
When no provider answers, the chat reads “no search provider works”, followed by each provider and its reason, and that an administrator can set the providers in Settings.
Reading pages and PDF files
web_fetch of the add-on reads what the built-in reads, and PDF files. A PDF comes back as
its text, page by page, each page under a Page N line. A scanned PDF has no text. A PDF
with a password, or one that is damaged, is refused with that reason.
The add-on uses the same SSRF guard as the built-in tool, in its own process: public
addresses only, ports 80 and 443, every redirect checked, at most 5 redirects, 15 seconds,
no proxy, no cookies. The runtime settings chats_web_allow_private and chats_web_ports
do not apply to the add-on: it never reaches an address inside your network. Its download
stops at 10 MiB instead of 2 MiB so that common PDF files fit; a PDF that is cut there is
refused (“the PDF is larger than 10 MiB”), because the rest of the file is missing. The text
returned has the built-in limits (maxChars, 20000 by default, 100000 at most) and the same
untrusted-content label.
Arguments
The add-on’s tools have the arguments of the built-in tools, with the names in camel case,
because the add-on contract allows no underscore in an argument name: maxResults for
max_results and maxChars for max_chars. The model reads the add-on’s schema.
The add-on in a cluster
The add-on runs on the add-on owner, one process for the whole cluster. Its settings are in its data folder there, and its search cache and rate limits (the same values as the built-in: 20 searches per user and 60 for everyone each minute, 5 minutes of cache) are in the memory of that process, so every frontend sees the same. Saving the settings starts the cache again.
Limits
| Limit | Value |
|---|---|
| Searches per user | 20 per minute |
| Searches for everyone | 60 per minute |
| Cache of identical searches | 5 minutes, 512 searches |
| Download | 2 MiB |
| Text returned | 20000 characters by default, 100000 at most |
| Time | 15 seconds for web_fetch, 10 seconds for a SearXNG search, 8 seconds for each provider of the chain |
| Cool-down of a challenged or blocked provider | 5 minutes |
| Redirects | 5 |
A search answered from the cache does not count against the limits.
Several frontends
The cache, the rate limits and the cool-down of the search providers are kept in the memory of each frontend (per-instance state). With several frontends, each has its own cache, and the limit for everyone applies to each frontend, so N frontends allow N times 60 searches a minute. Each frontend finds out on its own that an engine challenges it: the engines judge each address, and frontends on different addresses can get different answers, so a shared cool-down could skip an engine that works for this frontend. The cost is at most one wasted request per engine every 5 minutes on each frontend. A change of the settings starts the cache and the cool-down again. The settings are runtime settings like the others.
