feature/05-exercise-encyclopedia #4

Merged
sphinxen merged 28 commits from feature/05-exercise-encyclopedia into main 2026-03-06 12:29:20 +01:00
Showing only changes of commit ab87e54630 - Show all commits
+93 -41
View File
@@ -1,3 +1,5 @@
const { generateWithFallback } = require('../utils/gemini-fallback');
const DEFAULT_EXA_API_URL = 'https://api.exa.ai/search';
const buildSummary = (results) => {
@@ -20,56 +22,106 @@ const buildSummary = (results) => {
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 }) => {
if (!query || typeof query !== 'string') {
throw new Error('Query must be a non-empty string');
}
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;
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
};
// Tier 2: Fallback to AI-generated summary
return generateFallbackSummary(query, numResults);
};
module.exports = {
searchExerciseResearch
searchExerciseResearch,
generateFallbackSummary
};