feat: Added Google PSE as an alternative web search provider (#65)

* Added Google PSE as an alternative web search tool

* Removed unecessary comments

* chore(ConfigManager): moved Google PSE input next to API key

---------

Co-authored-by: AnotiaWang <anotia0202@gmail.com>
This commit is contained in:
Jonathan Rampersad
2025-04-05 23:18:08 -04:00
committed by GitHub
parent 651f445803
commit 5097c639d2
6 changed files with 97 additions and 1 deletions

View File

@ -91,6 +91,12 @@
link: 'https://www.firecrawl.dev/app/api-keys',
supportsCustomApiBase: true,
},
{
label: 'Google PSE',
value: 'google-pse',
help: 'settings.webSearch.providers.google-pse.help',
link: 'https://programmablesearchengine.google.com/', // Link to Google PSE console
},
])
const tavilySearchTopicOptions = ['general', 'news', 'finance']
const selectedAiProvider = computed(() =>
@ -315,6 +321,26 @@
:placeholder="$t('settings.webSearch.apiKey')"
/>
</UFormField>
<template v-if="config.webSearch.provider === 'google-pse'">
<UFormField
:label="
$t('settings.webSearch.providers.google-pse.pseIdLabel')
"
required
>
<UInput
v-model="config.webSearch.googlePseId"
class="w-full"
:placeholder="
$t(
'settings.webSearch.providers.google-pse.pseIdPlaceholder',
)
"
/>
</UFormField>
</template>
<UFormField
v-if="selectedWebSearchProvider?.supportsCustomApiBase"
:label="$t('settings.webSearch.apiBase')"

View File

@ -46,6 +46,50 @@ export const useWebSearch = (): WebSearchFunction => {
}))
}
}
case 'google-pse': {
const apiKey = config.webSearch.apiKey
const pseId = config.webSearch.googlePseId
return async (q: string, o: WebSearchOptions) => {
if (!apiKey || !pseId) {
throw new Error('Google PSE API key or ID not set')
}
// Construct Google PSE API URL
// Ref: https://developers.google.com/custom-search/v1/using_rest
const searchParams = new URLSearchParams({
key: apiKey,
cx: pseId,
q: q,
num: o.maxResults?.toString() || '5',
});
if (o.lang) {
searchParams.append('lr', `lang_${o.lang}`);
}
const apiUrl = `https://www.googleapis.com/customsearch/v1?${searchParams.toString()}`;
try {
const response = await $fetch<{ items?: Array<{ title: string; link: string; snippet: string }> }>(apiUrl, { method: 'GET' });
if (!response.items) {
return [];
}
// Map response to WebSearchResult format
return response.items.map((item) => ({
content: item.snippet, // Use snippet as content
url: item.link,
title: item.title,
}));
} catch (error: any) {
console.error('Google PSE search failed:', error);
// Attempt to parse Google API error format
const errorMessage = error?.data?.error?.message || error.message || 'Unknown error';
throw new Error(`Google PSE Error: ${errorMessage}`);
}
}
}
case 'tavily':
default: {
const tvly = tavily({

View File

@ -9,7 +9,7 @@ export type ConfigAiProvider =
| 'deepseek'
| 'ollama'
export type ConfigWebSearchProvider = 'tavily' | 'firecrawl'
export type ConfigWebSearchProvider = 'tavily' | 'firecrawl' | 'google-pse'
export interface ConfigAi {
provider: ConfigAiProvider
@ -31,6 +31,7 @@ export interface ConfigWebSearch {
tavilyAdvancedSearch?: boolean
/** Tavily: search topic. Defaults to `general` */
tavilySearchTopic?: 'general' | 'news' | 'finance'
googlePseId?: string; // Google PSE ID
}
export interface Config {
@ -47,6 +48,7 @@ function validateConfig(config: Config) {
if (ws.provider === 'tavily' && !ws.apiKey) return false
// Either apiBase or apiKey is required for firecrawl
if (ws.provider === 'firecrawl' && !ws.apiBase && !ws.apiKey) return false
if (ws.provider === 'google-pse' && (!ws.apiKey || !ws.googlePseId)) return false; // Require API Key and PSE ID
if (typeof ws.concurrencyLimit !== 'undefined' && ws.concurrencyLimit! < 1)
return false
return true