Add Brave Search as primary web search with NanoGPT fallback
Deploy to NAS / deploy (push) Has been cancelled

- Brave Search API is now primary (if BRAVE_API_KEY set)
- NanoGPT web search is fallback if Brave fails
- Add BRAVE_API_KEY to .env, docker-compose, and deploy workflow
- Tool calling still works for AI to request searches
This commit is contained in:
2026-03-09 16:51:43 -04:00
parent a73a8901f2
commit 7ff95a3d7f
3 changed files with 86 additions and 16 deletions
+1
View File
@@ -43,6 +43,7 @@ jobs:
CLIENT_ID=${{ secrets.DISCORD_CLIENT_ID }} CLIENT_ID=${{ secrets.DISCORD_CLIENT_ID }}
NANOGPT_API_KEY=${{ secrets.NANOGPT_API_KEY }} NANOGPT_API_KEY=${{ secrets.NANOGPT_API_KEY }}
ZAI_API_KEY=${{ secrets.ZAI_API_KEY }} ZAI_API_KEY=${{ secrets.ZAI_API_KEY }}
BRAVE_API_KEY=${{ secrets.BRAVE_API_KEY }}
NANO_MODEL=${{ secrets.NANO_MODEL }} NANO_MODEL=${{ secrets.NANO_MODEL }}
NODE_ENV=production NODE_ENV=production
EOF EOF
+1
View File
@@ -10,6 +10,7 @@ services:
environment: environment:
- NANO_MODEL=${NANO_MODEL} - NANO_MODEL=${NANO_MODEL}
- ZAI_API_KEY=${ZAI_API_KEY} - ZAI_API_KEY=${ZAI_API_KEY}
- BRAVE_API_KEY=${BRAVE_API_KEY}
volumes: volumes:
# Mount logs directory if you want to persist logs # Mount logs directory if you want to persist logs
- ./logs:/app/logs - ./logs:/app/logs
+84 -16
View File
@@ -7,6 +7,45 @@ const DEFAULT_MODEL = 'GLM-4.5-Air-Derestricted';
const FALLBACK_MODEL = 'GLM-4.7-Flash'; const FALLBACK_MODEL = 'GLM-4.7-Flash';
const FALLBACK_API_URL = 'https://api.z.ai/api/coding/paas/v4/chat/completions'; const FALLBACK_API_URL = 'https://api.z.ai/api/coding/paas/v4/chat/completions';
// Brave Search API
const BRAVE_API_URL = 'https://api.search.brave.com/res/v1/web/search';
async function performBraveSearch(query, count = 10) {
const response = await axios.get(BRAVE_API_URL, {
params: {
q: query,
count: count,
extra_snippets: true
},
headers: {
'X-Subscription-Token': process.env.BRAVE_API_KEY,
'Accept': 'application/json'
},
timeout: 15000
});
const results = response.data.web?.results || [];
if (results.length === 0) {
return 'No search results found.';
}
let formatted = 'Search results:\n\n';
results.slice(0, 8).forEach((result, i) => {
formatted += `${i + 1}. ${result.title}\n`;
formatted += ` ${result.url}\n`;
if (result.description) {
formatted += ` ${result.description.substring(0, 200)}${result.description.length > 200 ? '...' : ''}\n`;
}
if (result.extra_snippets && result.extra_snippets.length > 0) {
formatted += ` ${result.extra_snippets[0].substring(0, 150)}...\n`;
}
formatted += '\n';
});
return formatted;
}
// Tool definitions for function calling // Tool definitions for function calling
const WEB_SEARCH_TOOL = { const WEB_SEARCH_TOOL = {
type: 'function', type: 'function',
@@ -77,7 +116,15 @@ function buildMessages(history, prompt, mentionedUsername, systemPrompt) {
return messages; return messages;
} }
async function performWebSearch(query) { // Primary: Brave Search
async function webSearchWithBrave(query) {
console.log('Using Brave Search for:', query);
return await performBraveSearch(query);
}
// Fallback: NanoGPT web search
async function webSearchWithNanoGPT(query) {
console.log('Using NanoGPT web search for:', query);
const response = await axios.post( const response = await axios.post(
'https://nano-gpt.com/api/web', 'https://nano-gpt.com/api/web',
{ {
@@ -96,31 +143,52 @@ async function performWebSearch(query) {
); );
const data = response.data.data; const data = response.data.data;
const metadata = response.data.metadata;
// Format the response nicely
let formattedResult = '';
if (typeof data === 'string') { if (typeof data === 'string') {
formattedResult = data; return data;
} else if (data.answer) { } else if (data.answer) {
formattedResult = data.answer; let result = data.answer;
if (data.sources && data.sources.length > 0) { if (data.sources && data.sources.length > 0) {
formattedResult += '\n\nSources:\n'; result += '\n\nSources:\n';
data.sources.forEach((source, i) => { data.sources.forEach((source, i) => {
formattedResult += `${i + 1}. ${source.name || source.url}\n`; result += `${i + 1}. ${source.name || source.url}\n`;
}); });
} }
return result;
} else if (data.results) { } else if (data.results) {
formattedResult = 'Search results:\n'; let result = 'Search results:\n';
data.results.slice(0, 5).forEach((result, i) => { data.results.slice(0, 8).forEach((r, i) => {
formattedResult += `${i + 1}. ${result.title || result.name}\n`; result += `${i + 1}. ${r.title || r.name}\n`;
if (result.url) formattedResult += ` ${result.url}\n`; if (r.url) result += ` ${r.url}\n`;
if (result.content) formattedResult += ` ${result.content.substring(0, 200)}...\n`; if (r.content) result += ` ${r.content.substring(0, 200)}...\n`;
}); });
return result;
} }
return formattedResult; return 'Could not parse search results.';
}
// Unified web search function (Brave first, NanoGPT fallback)
async function performWebSearch(query) {
// Try Brave first if API key is available
if (process.env.BRAVE_API_KEY) {
try {
return await webSearchWithBrave(query);
} catch (error) {
console.error('Brave Search failed:', error.message);
}
}
// Fallback to NanoGPT
if (process.env.NANOGPT_API_KEY) {
try {
return await webSearchWithNanoGPT(query);
} catch (error) {
console.error('NanoGPT search failed:', error.message);
}
}
throw new Error('No web search provider available');
} }
async function callNanoGPTWithTools(messages, model, maxRetries = 2) { async function callNanoGPTWithTools(messages, model, maxRetries = 2) {
@@ -156,7 +224,7 @@ async function callNanoGPTWithTools(messages, model, maxRetries = 2) {
const args = JSON.parse(toolCall.function.arguments); const args = JSON.parse(toolCall.function.arguments);
console.log('AI requested web search for:', args.query); console.log('AI requested web search for:', args.query);
// Perform the search // Perform the search (Brave primary, NanoGPT fallback)
const searchResult = await performWebSearch(args.query); const searchResult = await performWebSearch(args.query);
// Add tool result to messages and continue // Add tool result to messages and continue