Add Groq as AI provider + new inline AI tasks, extend database to support more prompt types

This commit is contained in:
2026-01-20 10:04:01 -05:00
parent 7218e7cc3f
commit 167c13c572
24 changed files with 3521 additions and 315 deletions

View File

@@ -51,66 +51,64 @@ router.get('/:id', async (req, res) => {
}
});
// Get prompt by type (general, system, company_specific)
// Get prompt by type (any prompt_type value - extensible)
router.get('/by-type', async (req, res) => {
try {
const { type, company } = req.query;
const pool = req.app.locals.pool;
if (!pool) {
throw new Error('Database pool not initialized');
}
// Validate prompt type
if (!type || !['general', 'system', 'company_specific'].includes(type)) {
return res.status(400).json({
error: 'Valid type query parameter is required (general, system, or company_specific)'
});
}
// For company_specific type, company ID is required
if (type === 'company_specific' && !company) {
// Validate type is provided
if (!type || typeof type !== 'string' || type.trim().length === 0) {
return res.status(400).json({
error: 'Company ID is required for company_specific prompt type'
error: 'Valid type query parameter is required'
});
}
// For general and system types, company should not be provided
if ((type === 'general' || type === 'system') && company) {
// For company_specific types, company ID is required
const isCompanySpecificType = type.endsWith('_company_specific');
if (isCompanySpecificType && !company) {
return res.status(400).json({
error: 'Company ID should not be provided for general or system prompt types'
error: 'Company ID is required for company_specific prompt types'
});
}
// Build the query based on the type
// For non-company-specific types, company should not be provided
if (!isCompanySpecificType && company) {
return res.status(400).json({
error: 'Company ID should not be provided for non-company-specific prompt types'
});
}
// Build the query based on whether company is provided
let query, params;
if (type === 'company_specific') {
if (company) {
query = 'SELECT * FROM ai_prompts WHERE prompt_type = $1 AND company = $2';
params = [type, company];
params = [type.trim(), company];
} else {
query = 'SELECT * FROM ai_prompts WHERE prompt_type = $1';
params = [type];
query = 'SELECT * FROM ai_prompts WHERE prompt_type = $1 AND company IS NULL';
params = [type.trim()];
}
// Execute the query
const result = await pool.query(query, params);
// Check if any prompt was found
if (result.rows.length === 0) {
let errorMessage;
if (type === 'company_specific') {
errorMessage = `AI prompt not found for company ${company}`;
} else {
errorMessage = `${type.charAt(0).toUpperCase() + type.slice(1)} AI prompt not found`;
}
const errorMessage = company
? `AI prompt '${type}' not found for company ${company}`
: `AI prompt '${type}' not found`;
return res.status(404).json({ error: errorMessage });
}
// Return the first matching prompt
res.json(result.rows[0]);
} catch (error) {
console.error('Error fetching AI prompt by type:', error);
res.status(500).json({
res.status(500).json({
error: 'Failed to fetch AI prompt',
details: error instanceof Error ? error.message : 'Unknown error'
});
@@ -130,27 +128,28 @@ router.post('/', async (req, res) => {
if (!prompt_text || !prompt_type) {
return res.status(400).json({ error: 'Prompt text and type are required' });
}
// Validate prompt type
if (!['general', 'company_specific', 'system'].includes(prompt_type)) {
return res.status(400).json({ error: 'Prompt type must be either "general", "company_specific", or "system"' });
}
// Validate company is provided for company-specific prompts
if (prompt_type === 'company_specific' && !company) {
return res.status(400).json({ error: 'Company is required for company-specific prompts' });
// Validate prompt_type is a non-empty string (no hardcoded list - extensible)
if (typeof prompt_type !== 'string' || prompt_type.trim().length === 0) {
return res.status(400).json({ error: 'Prompt type must be a non-empty string' });
}
// Validate company is not provided for general or system prompts
if ((prompt_type === 'general' || prompt_type === 'system') && company) {
return res.status(400).json({ error: 'Company should not be provided for general or system prompts' });
// For company-specific types (ending with _company_specific), require company
const isCompanySpecificType = prompt_type.endsWith('_company_specific');
if (isCompanySpecificType && !company) {
return res.status(400).json({ error: 'Company is required for company-specific prompt types' });
}
// For non-company-specific types, company should not be provided
if (!isCompanySpecificType && company) {
return res.status(400).json({ error: 'Company should not be provided for non-company-specific prompt types' });
}
const pool = req.app.locals.pool;
if (!pool) {
throw new Error('Database pool not initialized');
}
const result = await pool.query(`
INSERT INTO ai_prompts (
prompt_text,
@@ -160,35 +159,30 @@ router.post('/', async (req, res) => {
RETURNING *
`, [
prompt_text,
prompt_type,
company
prompt_type.trim(),
company || null
]);
res.status(201).json(result.rows[0]);
} catch (error) {
console.error('Error creating AI prompt:', error);
// Check for unique constraint violations
if (error instanceof Error && error.message.includes('unique constraint')) {
if (error.message.includes('unique_company_prompt')) {
return res.status(409).json({
error: 'A prompt already exists for this company',
if (error instanceof Error && error.message.includes('unique')) {
if (error.message.includes('idx_singleton_with_company')) {
return res.status(409).json({
error: 'A prompt of this type already exists for this company',
details: error.message
});
} else if (error.message.includes('idx_unique_general_prompt')) {
return res.status(409).json({
error: 'A general prompt already exists',
details: error.message
});
} else if (error.message.includes('idx_unique_system_prompt')) {
return res.status(409).json({
error: 'A system prompt already exists',
} else if (error.message.includes('idx_singleton_no_company')) {
return res.status(409).json({
error: 'A prompt of this type already exists',
details: error.message
});
}
}
res.status(500).json({
res.status(500).json({
error: 'Failed to create AI prompt',
details: error instanceof Error ? error.message : 'Unknown error'
});
@@ -209,73 +203,70 @@ router.put('/:id', async (req, res) => {
if (!prompt_text || !prompt_type) {
return res.status(400).json({ error: 'Prompt text and type are required' });
}
// Validate prompt type
if (!['general', 'company_specific', 'system'].includes(prompt_type)) {
return res.status(400).json({ error: 'Prompt type must be either "general", "company_specific", or "system"' });
// Validate prompt_type is a non-empty string (no hardcoded list - extensible)
if (typeof prompt_type !== 'string' || prompt_type.trim().length === 0) {
return res.status(400).json({ error: 'Prompt type must be a non-empty string' });
}
// Validate company is provided for company-specific prompts
if (prompt_type === 'company_specific' && !company) {
return res.status(400).json({ error: 'Company is required for company-specific prompts' });
// For company-specific types, require company
const isCompanySpecificType = prompt_type.endsWith('_company_specific');
if (isCompanySpecificType && !company) {
return res.status(400).json({ error: 'Company is required for company-specific prompt types' });
}
// Validate company is not provided for general or system prompts
if ((prompt_type === 'general' || prompt_type === 'system') && company) {
return res.status(400).json({ error: 'Company should not be provided for general or system prompts' });
// For non-company-specific types, company should not be provided
if (!isCompanySpecificType && company) {
return res.status(400).json({ error: 'Company should not be provided for non-company-specific prompt types' });
}
const pool = req.app.locals.pool;
if (!pool) {
throw new Error('Database pool not initialized');
}
// Check if the prompt exists
const checkResult = await pool.query('SELECT * FROM ai_prompts WHERE id = $1', [id]);
if (checkResult.rows.length === 0) {
return res.status(404).json({ error: 'AI prompt not found' });
}
const result = await pool.query(`
UPDATE ai_prompts
SET
UPDATE ai_prompts
SET
prompt_text = $1,
prompt_type = $2,
company = $3
company = $3,
updated_at = CURRENT_TIMESTAMP
WHERE id = $4
RETURNING *
`, [
prompt_text,
prompt_type,
company,
prompt_type.trim(),
company || null,
id
]);
res.json(result.rows[0]);
} catch (error) {
console.error('Error updating AI prompt:', error);
// Check for unique constraint violations
if (error instanceof Error && error.message.includes('unique constraint')) {
if (error.message.includes('unique_company_prompt')) {
return res.status(409).json({
error: 'A prompt already exists for this company',
if (error instanceof Error && error.message.includes('unique')) {
if (error.message.includes('idx_singleton_with_company')) {
return res.status(409).json({
error: 'A prompt of this type already exists for this company',
details: error.message
});
} else if (error.message.includes('idx_unique_general_prompt')) {
return res.status(409).json({
error: 'A general prompt already exists',
details: error.message
});
} else if (error.message.includes('idx_unique_system_prompt')) {
return res.status(409).json({
error: 'A system prompt already exists',
} else if (error.message.includes('idx_singleton_no_company')) {
return res.status(409).json({
error: 'A prompt of this type already exists',
details: error.message
});
}
}
res.status(500).json({
res.status(500).json({
error: 'Failed to update AI prompt',
details: error instanceof Error ? error.message : 'Unknown error'
});