/** * AI Routes * * API endpoints for AI-powered product validation features. * Provides embedding generation and similarity-based suggestions. */ const express = require('express'); const router = express.Router(); const aiService = require('../services/ai'); const { getDbConnection, closeAllConnections } = require('../utils/dbConnection'); // Track initialization state let initializationPromise = null; /** * Ensure AI service is initialized * Uses lazy initialization on first request */ async function ensureInitialized() { if (aiService.isReady()) { return true; } if (initializationPromise) { await initializationPromise; return aiService.isReady(); } initializationPromise = (async () => { try { console.log('[AI Routes] Initializing AI service...'); // Get database connection for taxonomy const { connection } = await getDbConnection(); const result = await aiService.initialize({ openaiApiKey: process.env.OPENAI_API_KEY, mysqlConnection: connection, logger: console }); if (!result.success) { console.error('[AI Routes] AI service initialization failed:', result.message); return false; } console.log('[AI Routes] AI service initialized:', result.stats); return true; } catch (error) { console.error('[AI Routes] Failed to initialize AI service:', error); return false; } })(); await initializationPromise; return aiService.isReady(); } /** * GET /api/ai/status * Get AI service status */ router.get('/status', async (req, res) => { try { const status = aiService.getStatus(); res.json(status); } catch (error) { res.status(500).json({ error: error.message }); } }); /** * POST /api/ai/initialize * Manually trigger initialization (also happens automatically on first use) */ router.post('/initialize', async (req, res) => { try { const ready = await ensureInitialized(); const status = aiService.getStatus(); res.json({ success: ready, ...status }); } catch (error) { console.error('[AI Routes] Initialize error:', error); res.status(500).json({ error: error.message }); } }); /** * GET /api/ai/taxonomy * Get all taxonomy data (categories, themes, colors) without embeddings */ router.get('/taxonomy', async (req, res) => { try { const ready = await ensureInitialized(); if (!ready) { return res.status(503).json({ error: 'AI service not available' }); } const taxonomy = aiService.getTaxonomyData(); res.json(taxonomy); } catch (error) { console.error('[AI Routes] Taxonomy error:', error); res.status(500).json({ error: error.message }); } }); /** * POST /api/ai/embedding * Generate embedding for a single product * * Body: { product: { name, description, company_name, line_name } } * Returns: { embedding: number[], latencyMs: number } */ router.post('/embedding', async (req, res) => { try { const ready = await ensureInitialized(); if (!ready) { return res.status(503).json({ error: 'AI service not available' }); } const { product } = req.body; if (!product) { return res.status(400).json({ error: 'Product is required' }); } const result = await aiService.getProductEmbedding(product); res.json(result); } catch (error) { console.error('[AI Routes] Embedding error:', error); res.status(500).json({ error: error.message }); } }); /** * POST /api/ai/embeddings * Generate embeddings for multiple products * * Body: { products: Array<{ name, description, company_name, line_name }> } * Returns: { embeddings: Array<{ index, embedding }>, latencyMs } */ router.post('/embeddings', async (req, res) => { try { const ready = await ensureInitialized(); if (!ready) { return res.status(503).json({ error: 'AI service not available' }); } const { products } = req.body; if (!Array.isArray(products)) { return res.status(400).json({ error: 'Products array is required' }); } const result = await aiService.getProductEmbeddings(products); res.json(result); } catch (error) { console.error('[AI Routes] Embeddings error:', error); res.status(500).json({ error: error.message }); } }); /** * POST /api/ai/suggestions * Get category/theme/color suggestions for a single product * Generates embedding and finds similar taxonomy items * * Body: { product: { name, description, company_name, line_name }, options?: { topCategories, topThemes, topColors } } * Returns: { categories: Array, themes: Array, colors: Array, latencyMs } */ router.post('/suggestions', async (req, res) => { try { const ready = await ensureInitialized(); if (!ready) { return res.status(503).json({ error: 'AI service not available' }); } const { product, options } = req.body; if (!product) { return res.status(400).json({ error: 'Product is required' }); } const suggestions = await aiService.getSuggestionsForProduct(product, options); res.json(suggestions); } catch (error) { console.error('[AI Routes] Suggestions error:', error); res.status(500).json({ error: error.message }); } }); /** * POST /api/ai/suggestions/batch * Get suggestions for multiple products * More efficient than calling /suggestions multiple times * * Body: { products: Array, options?: { topCategories, topThemes, topColors } } * Returns: { results: Array<{ index, categories, themes, colors }>, latencyMs } */ router.post('/suggestions/batch', async (req, res) => { try { const ready = await ensureInitialized(); if (!ready) { return res.status(503).json({ error: 'AI service not available' }); } const { products, options } = req.body; if (!Array.isArray(products)) { return res.status(400).json({ error: 'Products array is required' }); } const startTime = Date.now(); // Generate all embeddings at once const { embeddings, latencyMs: embeddingLatency } = await aiService.getProductEmbeddings(products); // Find suggestions for each embedding const results = embeddings.map(({ index, embedding }) => { const suggestions = aiService.findSimilarTaxonomy(embedding, options); return { index, ...suggestions }; }); const totalLatency = Date.now() - startTime; res.json({ results, latencyMs: totalLatency, embeddingLatencyMs: embeddingLatency, searchLatencyMs: totalLatency - embeddingLatency, productCount: products.length, embeddingCount: embeddings.length }); } catch (error) { console.error('[AI Routes] Batch suggestions error:', error); res.status(500).json({ error: error.message }); } }); /** * POST /api/ai/similar * Find similar taxonomy items given a pre-computed embedding * Useful when frontend has cached the embedding * * Body: { embedding: number[], options?: { topCategories, topThemes, topColors } } * Returns: { categories, themes, colors } */ router.post('/similar', async (req, res) => { try { const ready = await ensureInitialized(); if (!ready) { return res.status(503).json({ error: 'AI service not available' }); } const { embedding, options } = req.body; if (!embedding || !Array.isArray(embedding)) { return res.status(400).json({ error: 'Embedding array is required' }); } const startTime = Date.now(); const suggestions = aiService.findSimilarTaxonomy(embedding, options); res.json({ ...suggestions, latencyMs: Date.now() - startTime }); } catch (error) { console.error('[AI Routes] Similar error:', error); res.status(500).json({ error: error.message }); } }); module.exports = router;