Files
inventory/inventory-server/src/routes/ai.js

435 lines
12 KiB
JavaScript

/**
* 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,
groqApiKey: process.env.GROQ_API_KEY,
mysqlConnection: connection,
pool: null, // Will be set by setPool()
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,
groqEnabled: result.groqEnabled
});
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 });
}
});
// ============================================================================
// INLINE AI VALIDATION ENDPOINTS (Groq-powered)
// ============================================================================
/**
* POST /api/ai/validate/inline/name
* Validate a single product name for spelling, grammar, and naming conventions
*
* Body: { product: { name, company_name, company_id, line_name, description } }
* Returns: { isValid, suggestion?, issues[], latencyMs }
*/
router.post('/validate/inline/name', async (req, res) => {
try {
const ready = await ensureInitialized();
if (!ready) {
return res.status(503).json({ error: 'AI service not available' });
}
if (!aiService.hasChatCompletion()) {
return res.status(503).json({
error: 'Chat completion not available - GROQ_API_KEY not configured'
});
}
const { product } = req.body;
if (!product) {
return res.status(400).json({ error: 'Product is required' });
}
// Get pool from app.locals (set by server.js)
const pool = req.app.locals.pool;
const result = await aiService.runTask(aiService.TASK_IDS.VALIDATE_NAME, {
product,
pool
});
if (!result.success) {
return res.status(500).json({
error: result.error || 'Validation failed',
code: result.code
});
}
res.json(result);
} catch (error) {
console.error('[AI Routes] Name validation error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* POST /api/ai/validate/inline/description
* Validate a single product description for quality and guideline compliance
*
* Body: { product: { name, description, company_name, company_id, categories } }
* Returns: { isValid, suggestion?, issues[], latencyMs }
*/
router.post('/validate/inline/description', async (req, res) => {
try {
const ready = await ensureInitialized();
if (!ready) {
return res.status(503).json({ error: 'AI service not available' });
}
if (!aiService.hasChatCompletion()) {
return res.status(503).json({
error: 'Chat completion not available - GROQ_API_KEY not configured'
});
}
const { product } = req.body;
if (!product) {
return res.status(400).json({ error: 'Product is required' });
}
// Get pool from app.locals (set by server.js)
const pool = req.app.locals.pool;
const result = await aiService.runTask(aiService.TASK_IDS.VALIDATE_DESCRIPTION, {
product,
pool
});
if (!result.success) {
return res.status(500).json({
error: result.error || 'Validation failed',
code: result.code
});
}
res.json(result);
} catch (error) {
console.error('[AI Routes] Description validation error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* POST /api/ai/validate/sanity-check
* Run consistency/sanity check on a batch of products
*
* Body: { products: Array<product data> }
* Returns: { issues: Array<{ productIndex, field, issue, suggestion? }>, summary, latencyMs }
*/
router.post('/validate/sanity-check', async (req, res) => {
try {
const ready = await ensureInitialized();
if (!ready) {
return res.status(503).json({ error: 'AI service not available' });
}
if (!aiService.hasChatCompletion()) {
return res.status(503).json({
error: 'Chat completion not available - GROQ_API_KEY not configured'
});
}
const { products } = req.body;
if (!Array.isArray(products) || products.length === 0) {
return res.status(400).json({ error: 'Products array is required' });
}
// Get pool from app.locals (set by server.js)
const pool = req.app.locals.pool;
const result = await aiService.runTask(aiService.TASK_IDS.SANITY_CHECK, {
products,
pool
});
if (!result.success) {
return res.status(500).json({
error: result.error || 'Sanity check failed',
code: result.code
});
}
res.json(result);
} catch (error) {
console.error('[AI Routes] Sanity check error:', error);
res.status(500).json({ error: error.message });
}
});
module.exports = router;