Add AI-powered price extraction fallback

- Add AI extraction service supporting Anthropic (Claude) and OpenAI
- Add AI settings UI in Settings page with provider selection
- Add database migration for AI settings columns
- Integrate AI fallback into scraper when standard methods fail
- Add API endpoints for AI settings and test extraction

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
clucraft 2026-01-21 21:49:55 -05:00
parent cfca33b4ea
commit d98138fe7c
11 changed files with 887 additions and 10 deletions

View file

@ -39,8 +39,8 @@ router.post('/', async (req: AuthRequest, res: Response) => {
return;
}
// Scrape product info
const scrapedData = await scrapeProduct(url);
// Scrape product info (pass userId for AI fallback)
const scrapedData = await scrapeProduct(url, userId);
// Allow adding out-of-stock products, but require a price for in-stock ones
if (!scrapedData.price && scrapedData.stockStatus !== 'out_of_stock') {

View file

@ -127,4 +127,89 @@ router.post('/notifications/test/discord', async (req: AuthRequest, res: Respons
}
});
// Get AI settings
router.get('/ai', async (req: AuthRequest, res: Response) => {
try {
const userId = req.userId!;
const settings = await userQueries.getAISettings(userId);
if (!settings) {
res.status(404).json({ error: 'User not found' });
return;
}
// Don't expose full API keys, just indicate if they're set
res.json({
ai_enabled: settings.ai_enabled || false,
ai_provider: settings.ai_provider || null,
anthropic_configured: !!settings.anthropic_api_key,
openai_configured: !!settings.openai_api_key,
});
} catch (error) {
console.error('Error fetching AI settings:', error);
res.status(500).json({ error: 'Failed to fetch AI settings' });
}
});
// Update AI settings
router.put('/ai', async (req: AuthRequest, res: Response) => {
try {
const userId = req.userId!;
const { ai_enabled, ai_provider, anthropic_api_key, openai_api_key } = req.body;
const settings = await userQueries.updateAISettings(userId, {
ai_enabled,
ai_provider,
anthropic_api_key,
openai_api_key,
});
if (!settings) {
res.status(400).json({ error: 'No settings to update' });
return;
}
res.json({
ai_enabled: settings.ai_enabled || false,
ai_provider: settings.ai_provider || null,
anthropic_configured: !!settings.anthropic_api_key,
openai_configured: !!settings.openai_api_key,
message: 'AI settings updated successfully',
});
} catch (error) {
console.error('Error updating AI settings:', error);
res.status(500).json({ error: 'Failed to update AI settings' });
}
});
// Test AI extraction
router.post('/ai/test', async (req: AuthRequest, res: Response) => {
try {
const userId = req.userId!;
const { url } = req.body;
if (!url) {
res.status(400).json({ error: 'URL is required' });
return;
}
const settings = await userQueries.getAISettings(userId);
if (!settings?.ai_enabled) {
res.status(400).json({ error: 'AI extraction is not enabled' });
return;
}
const { extractWithAI } = await import('../services/ai-extractor');
const result = await extractWithAI(url, settings);
res.json({
success: !!result.price,
...result,
});
} catch (error) {
console.error('Error testing AI extraction:', error);
res.status(500).json({ error: 'Failed to test AI extraction' });
}
});
export default router;