Add degoog as the search provider, and make it the default
The operator already runs it, so this is the one provider that costs nothing to adopt: no key, no card, no third party, and no new container. It aggregates several engines and returns them merged, and its /api/search reply is already the shape everything downstream expects — title, url, snippet, with content as a fallback for the roughly one result in six that has no snippet. It is also now the only provider whose response has been read from a live service rather than from documentation, which is why it is the default.
This commit is contained in:
@@ -585,6 +585,7 @@
|
|||||||
# Per-host because one is an address on this network and the other is a credential. Only the
|
# Per-host because one is an address on this network and the other is a credential. Only the
|
||||||
# General Chat profile can use these — it is the profile that cannot change anything, which is
|
# General Chat profile can use these — it is the profile that cannot change anything, which is
|
||||||
# why it is the one allowed to look outside. Off until AI_WEB_SEARCH_ENABLED says otherwise.
|
# why it is the one allowed to look outside. Off until AI_WEB_SEARCH_ENABLED says otherwise.
|
||||||
|
HOSTN_DEGOOG_URL="" # e.g. http://localhost:4444 — self-hosted, no key, /api/search
|
||||||
HOSTN_SEARXNG_URL="" # e.g. http://localhost:8888 — needs format: [json] in its settings.yml
|
HOSTN_SEARXNG_URL="" # e.g. http://localhost:8888 — needs format: [json] in its settings.yml
|
||||||
HOSTN_WEB_SEARCH_API_KEY="" # brave or tavily; unused when the provider is searxng
|
HOSTN_WEB_SEARCH_API_KEY="" # brave or tavily; unused when the provider is searxng
|
||||||
HOSTN_OLLAMA_EMBED_MODEL="nomic-embed-text" # embeddings — the generation model cannot embed
|
HOSTN_OLLAMA_EMBED_MODEL="nomic-embed-text" # embeddings — the generation model cannot embed
|
||||||
|
|||||||
@@ -1837,13 +1837,15 @@
|
|||||||
# when it is ticked. A question about this machine hands off to the assistant before the search
|
# when it is ticked. A question about this machine hands off to the assistant before the search
|
||||||
# would run, so it never reaches the internet even with the box ticked.
|
# would run, so it never reaches the internet even with the box ticked.
|
||||||
#
|
#
|
||||||
# Provider: searxng | brave | tavily
|
# Provider: degoog | searxng | brave | tavily
|
||||||
|
# degoog self-hosted, no key — set HOST*_DEGOOG_URL. Aggregates several engines and returns
|
||||||
|
# them merged. The default, and the only one verified against a live service here
|
||||||
# searxng self-hosted, no key, no third party — set HOST*_SEARXNG_URL, and enable format: [json]
|
# searxng self-hosted, no key, no third party — set HOST*_SEARXNG_URL, and enable format: [json]
|
||||||
# in its own settings.yml, which the default image ships with off
|
# in its own settings.yml, which the default image ships with off
|
||||||
# brave HOST*_WEB_SEARCH_API_KEY, free tier available
|
# brave HOST*_WEB_SEARCH_API_KEY, free tier available
|
||||||
# tavily HOST*_WEB_SEARCH_API_KEY
|
# tavily HOST*_WEB_SEARCH_API_KEY
|
||||||
AI_WEB_SEARCH_ENABLED=false
|
AI_WEB_SEARCH_ENABLED=false
|
||||||
AI_WEB_SEARCH_PROVIDER=searxng
|
AI_WEB_SEARCH_PROVIDER=degoog
|
||||||
AI_WEB_SEARCH_RESULTS=4
|
AI_WEB_SEARCH_RESULTS=4
|
||||||
AI_WEB_SEARCH_TIMEOUT=6
|
AI_WEB_SEARCH_TIMEOUT=6
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,16 @@
|
|||||||
// their URLs, for the worker to put in front of the model as context it can cite.
|
// their URLs, for the worker to put in front of the model as context it can cite.
|
||||||
//
|
//
|
||||||
// OPERATIONAL MODEL
|
// OPERATIONAL MODEL
|
||||||
// One provider at a time, named by AI_WEB_SEARCH_PROVIDER. Three are supported because the
|
// One provider at a time, named by AI_WEB_SEARCH_PROVIDER. Four are supported because the right
|
||||||
// right answer depends on what the operator is willing to run: a self-hosted SearXNG needs no
|
// answer depends on what the operator already runs: degoog and SearXNG are self-hosted and need
|
||||||
// key and no third party, and a hosted API needs no container. They differ only in how the
|
// no key and no third party, while brave and tavily need no container. They differ only in how
|
||||||
// request is shaped and where the fields sit in the reply, so everything after parsing is one
|
// the request is shaped and where the fields sit in the reply, so everything after parsing is
|
||||||
// code path.
|
// one code path.
|
||||||
|
//
|
||||||
|
// degoog is the default and the one verified here — it was already running on this host, and it
|
||||||
|
// aggregates several engines and returns them merged. DuckDuckGo is deliberately absent: both
|
||||||
|
// its scraping endpoints answer a challenge page from this network, and its keyless API returns
|
||||||
|
// nothing at all for a real question.
|
||||||
//
|
//
|
||||||
// DESIGN PRINCIPLES
|
// DESIGN PRINCIPLES
|
||||||
// Off by default, and not because it is dangerous.
|
// Off by default, and not because it is dangerous.
|
||||||
@@ -43,7 +48,7 @@
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════════════════════
|
||||||
require_once __DIR__ . '/ai.php';
|
require_once __DIR__ . '/ai.php';
|
||||||
|
|
||||||
const VV_AI_WEB_PROVIDERS = ['searxng', 'brave', 'tavily'];
|
const VV_AI_WEB_PROVIDERS = ['degoog', 'searxng', 'brave', 'tavily'];
|
||||||
const VV_AI_WEB_MAX_QUERY = 300; // characters sent to the provider
|
const VV_AI_WEB_MAX_QUERY = 300; // characters sent to the provider
|
||||||
const VV_AI_WEB_MAX_SNIPPET = 400; // characters kept from each result
|
const VV_AI_WEB_MAX_SNIPPET = 400; // characters kept from each result
|
||||||
|
|
||||||
@@ -53,8 +58,8 @@ function vv_ai_web_enabled(): bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function vv_ai_web_provider(): string {
|
function vv_ai_web_provider(): string {
|
||||||
$p = strtolower(trim((string)(vv_conf_vars()['AI_WEB_SEARCH_PROVIDER'] ?? 'searxng')));
|
$p = strtolower(trim((string)(vv_conf_vars()['AI_WEB_SEARCH_PROVIDER'] ?? 'degoog')));
|
||||||
return in_array($p, VV_AI_WEB_PROVIDERS, true) ? $p : 'searxng';
|
return in_array($p, VV_AI_WEB_PROVIDERS, true) ? $p : 'degoog';
|
||||||
}
|
}
|
||||||
|
|
||||||
function vv_ai_web_results_max(): int {
|
function vv_ai_web_results_max(): int {
|
||||||
@@ -71,6 +76,15 @@ function vv_ai_web_searx_url(): string {
|
|||||||
return rtrim(trim((string)(vv_conf_vars()[strtoupper(vv_detect_host()) . '_SEARXNG_URL'] ?? '')), '/');
|
return rtrim(trim((string)(vv_conf_vars()[strtoupper(vv_detect_host()) . '_SEARXNG_URL'] ?? '')), '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function vv_ai_web_degoog_url(): string {
|
||||||
|
return rtrim(trim((string)(vv_conf_vars()[strtoupper(vv_detect_host()) . '_DEGOOG_URL'] ?? '')), '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which providers need an address on this network rather than a key. Kept as a list because the
|
||||||
|
// readiness check and the error message both need the same answer and disagreeing about it is how
|
||||||
|
// a feature reports itself ready and then does nothing.
|
||||||
|
const VV_AI_WEB_SELF_HOSTED = ['degoog', 'searxng'];
|
||||||
|
|
||||||
function vv_ai_web_api_key(): string {
|
function vv_ai_web_api_key(): string {
|
||||||
return trim((string)(vv_conf_vars()[strtoupper(vv_detect_host()) . '_WEB_SEARCH_API_KEY'] ?? ''));
|
return trim((string)(vv_conf_vars()[strtoupper(vv_detect_host()) . '_WEB_SEARCH_API_KEY'] ?? ''));
|
||||||
}
|
}
|
||||||
@@ -82,11 +96,16 @@ function vv_ai_web_ready(): array {
|
|||||||
return ['ok' => false, 'error' => 'AI_WEB_SEARCH_ENABLED is false'];
|
return ['ok' => false, 'error' => 'AI_WEB_SEARCH_ENABLED is false'];
|
||||||
}
|
}
|
||||||
$p = vv_ai_web_provider();
|
$p = vv_ai_web_provider();
|
||||||
if ($p === 'searxng' && vv_ai_web_searx_url() === '') {
|
$host = strtoupper(vv_detect_host());
|
||||||
return ['ok' => false, 'error' => strtoupper(vv_detect_host()) . '_SEARXNG_URL is empty'];
|
|
||||||
|
if ($p === 'degoog' && vv_ai_web_degoog_url() === '') {
|
||||||
|
return ['ok' => false, 'error' => $host . '_DEGOOG_URL is empty'];
|
||||||
}
|
}
|
||||||
if ($p !== 'searxng' && vv_ai_web_api_key() === '') {
|
if ($p === 'searxng' && vv_ai_web_searx_url() === '') {
|
||||||
return ['ok' => false, 'error' => strtoupper(vv_detect_host()) . '_WEB_SEARCH_API_KEY is empty'];
|
return ['ok' => false, 'error' => $host . '_SEARXNG_URL is empty'];
|
||||||
|
}
|
||||||
|
if (!in_array($p, VV_AI_WEB_SELF_HOSTED, true) && vv_ai_web_api_key() === '') {
|
||||||
|
return ['ok' => false, 'error' => $host . '_WEB_SEARCH_API_KEY is empty'];
|
||||||
}
|
}
|
||||||
return ['ok' => true, 'provider' => $p];
|
return ['ok' => true, 'provider' => $p];
|
||||||
}
|
}
|
||||||
@@ -144,6 +163,25 @@ function vv_ai_web_normalise(array $rows): array {
|
|||||||
// a shape that changes underneath produces zero results rather than wrong ones, because every
|
// a shape that changes underneath produces zero results rather than wrong ones, because every
|
||||||
// field is read by name and anything missing is dropped by vv_ai_web_normalise().
|
// field is read by name and anything missing is dropped by vv_ai_web_normalise().
|
||||||
|
|
||||||
|
// Degoog — self-hosted, no key, and the only provider here whose response has been read from a
|
||||||
|
// live service rather than from documentation. It aggregates several engines and returns them
|
||||||
|
// already merged, so the ranking is its own.
|
||||||
|
//
|
||||||
|
// snippet before content: it fills both, and around one result in six has neither. Those are kept
|
||||||
|
// anyway — a title and a URL is still a citable source, and dropping them would silently shrink
|
||||||
|
// the result set the operator asked for.
|
||||||
|
function vv_ai_web_degoog(string $q): ?array {
|
||||||
|
$base = vv_ai_web_degoog_url();
|
||||||
|
if ($base === '') return null;
|
||||||
|
$d = vv_ai_web_http($base . '/api/search?q=' . rawurlencode($q));
|
||||||
|
if ($d === null) return null;
|
||||||
|
return array_map(fn($r) => ['title' => $r['title'] ?? '',
|
||||||
|
'url' => $r['url'] ?? '',
|
||||||
|
'snippet' => ($r['snippet'] ?? '') !== '' ? $r['snippet']
|
||||||
|
: ($r['content'] ?? '')],
|
||||||
|
array_slice($d['results'] ?? [], 0, vv_ai_web_results_max()));
|
||||||
|
}
|
||||||
|
|
||||||
function vv_ai_web_searxng(string $q): ?array {
|
function vv_ai_web_searxng(string $q): ?array {
|
||||||
$base = vv_ai_web_searx_url();
|
$base = vv_ai_web_searx_url();
|
||||||
if ($base === '') return null;
|
if ($base === '') return null;
|
||||||
@@ -201,7 +239,8 @@ function vv_ai_web_search(string $question): array {
|
|||||||
$raw = match ($provider) {
|
$raw = match ($provider) {
|
||||||
'brave' => vv_ai_web_brave($q),
|
'brave' => vv_ai_web_brave($q),
|
||||||
'tavily' => vv_ai_web_tavily($q),
|
'tavily' => vv_ai_web_tavily($q),
|
||||||
default => vv_ai_web_searxng($q),
|
'searxng' => vv_ai_web_searxng($q),
|
||||||
|
default => vv_ai_web_degoog($q),
|
||||||
};
|
};
|
||||||
|
|
||||||
if ($raw === null) {
|
if ($raw === null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user