feat(05-03): Integrate AI fallback system into research search (Exa → Ollama/Gemini/OpenRouter/OpenCode)
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
const { generateWithFallback } = require('../utils/gemini-fallback');
|
||||||
|
|
||||||
const DEFAULT_EXA_API_URL = 'https://api.exa.ai/search';
|
const DEFAULT_EXA_API_URL = 'https://api.exa.ai/search';
|
||||||
|
|
||||||
const buildSummary = (results) => {
|
const buildSummary = (results) => {
|
||||||
@@ -20,56 +22,106 @@ const buildSummary = (results) => {
|
|||||||
return snippets.slice(0, 3).join(' ');
|
return snippets.slice(0, 3).join(' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const generateFallbackSummary = async (query, numResults) => {
|
||||||
|
try {
|
||||||
|
console.log('🔄 Fallback: Generating AI summary for query:', query);
|
||||||
|
const prompt = `Provide a concise summary of exercise form and technique for: "${query}". Include key points about proper form, common mistakes, and benefits. Keep it to 2-3 sentences.`;
|
||||||
|
|
||||||
|
const response = await generateWithFallback(prompt, {
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 256
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success && response.data) {
|
||||||
|
console.log(`✅ AI fallback summary generated (provider: ${response.provider})`);
|
||||||
|
// Extract text from the appropriate response format
|
||||||
|
let summaryText = '';
|
||||||
|
if (response.data.response) {
|
||||||
|
summaryText = response.data.response;
|
||||||
|
} else if (response.data.choices?.[0]?.message?.content) {
|
||||||
|
summaryText = response.data.choices[0].message.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
summary: summaryText,
|
||||||
|
results: [], // No source links from AI fallback
|
||||||
|
provider: response.provider
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('AI fallback summary generation failed:', err.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort: simple template
|
||||||
|
return {
|
||||||
|
summary: `Exercise information for ${query}. Consult a fitness professional for proper form and safety guidelines.`,
|
||||||
|
results: [],
|
||||||
|
provider: 'template'
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const searchExerciseResearch = async ({ query, numResults = 5 }) => {
|
const searchExerciseResearch = async ({ query, numResults = 5 }) => {
|
||||||
if (!query || typeof query !== 'string') {
|
if (!query || typeof query !== 'string') {
|
||||||
throw new Error('Query must be a non-empty string');
|
throw new Error('Query must be a non-empty string');
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = process.env.EXA_API_KEY;
|
const apiKey = process.env.EXA_API_KEY;
|
||||||
if (!apiKey) {
|
|
||||||
throw new Error('EXA_API_KEY is not configured');
|
// Tier 1: Try Exa API if configured
|
||||||
|
if (apiKey) {
|
||||||
|
try {
|
||||||
|
console.log('📍 Tier 1: Attempting Exa search API...');
|
||||||
|
const apiUrl = process.env.EXA_API_URL || DEFAULT_EXA_API_URL;
|
||||||
|
|
||||||
|
const response = await fetch(apiUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-api-key': apiKey
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query,
|
||||||
|
numResults,
|
||||||
|
type: 'neural',
|
||||||
|
useAutoprompt: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Exa search failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const results = (data.results || []).map((result) => ({
|
||||||
|
id: result.id,
|
||||||
|
title: result.title,
|
||||||
|
url: result.url,
|
||||||
|
snippet: Array.isArray(result.highlights) && result.highlights.length > 0
|
||||||
|
? result.highlights[0]
|
||||||
|
: result.snippet,
|
||||||
|
highlight: result.highlight,
|
||||||
|
publishedDate: result.publishedDate,
|
||||||
|
score: result.score
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('✅ Exa API success');
|
||||||
|
return {
|
||||||
|
summary: buildSummary(results),
|
||||||
|
results,
|
||||||
|
provider: 'exa'
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ Exa API failed: ${err.message}, falling back to AI...`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('⚠️ EXA_API_KEY not configured, using AI fallback...');
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiUrl = process.env.EXA_API_URL || DEFAULT_EXA_API_URL;
|
// Tier 2: Fallback to AI-generated summary
|
||||||
|
return generateFallbackSummary(query, numResults);
|
||||||
const response = await fetch(apiUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
'x-api-key': apiKey
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
query,
|
|
||||||
numResults,
|
|
||||||
type: 'neural',
|
|
||||||
useAutoprompt: true
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const text = await response.text();
|
|
||||||
throw new Error(`Exa search failed: ${response.status} ${text}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
const results = (data.results || []).map((result) => ({
|
|
||||||
id: result.id,
|
|
||||||
title: result.title,
|
|
||||||
url: result.url,
|
|
||||||
snippet: Array.isArray(result.highlights) && result.highlights.length > 0
|
|
||||||
? result.highlights[0]
|
|
||||||
: result.snippet,
|
|
||||||
highlight: result.highlight,
|
|
||||||
publishedDate: result.publishedDate,
|
|
||||||
score: result.score
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
summary: buildSummary(results),
|
|
||||||
results
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
searchExerciseResearch
|
searchExerciseResearch,
|
||||||
|
generateFallbackSummary
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user