187 lines
9.7 KiB
JavaScript
187 lines
9.7 KiB
JavaScript
require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') });
|
|
const axios = require('axios');
|
|
|
|
const WORKFLOW_URL_SCRAPE = process.env.WORKFLOW_URL_SCRAPE;
|
|
const WORKFLOW_URL_TEMPLATE = process.env.WORKFLOW_URL_TEMPLATE;
|
|
const WORKFLOW_URL_CHECK_CURL = process.env.WORKFLOW_URL_CHECK_CURL;
|
|
const WORKFLOW_VALIDATE_FIELDS = process.env.WORKFLOW_VALIDATE_FIELDS;
|
|
|
|
if (!WORKFLOW_URL_SCRAPE) throw new Error('Missing WORKFLOW_URL_SCRAPE environment variable');
|
|
if (!WORKFLOW_URL_TEMPLATE) throw new Error('Missing WORKFLOW_URL_TEMPLATE environment variable');
|
|
if (!WORKFLOW_URL_CHECK_CURL) throw new Error('Missing WORKFLOW_URL_CHECK_CURL environment variable');
|
|
if (!WORKFLOW_VALIDATE_FIELDS) throw new Error('Missing WORKFLOW_VALIDATE_FIELDS environment variable');
|
|
|
|
const TRAI_RULES_TEXT = '1) Max 160 chars. 2) Dynamic vars use {#var#}. 3) Transactional: no promo/URLs unless required. 4) Sender ID DLT-compliant. 5) Allowed punctuation only. 6) Must match event type. 7) Avoid URLs unless explicitly needed. 8) Start with event/order context.';
|
|
|
|
const EVENT_DESCRIPTIONS = {
|
|
placed: 'The customer has successfully placed an order',
|
|
confirmed: 'The order has been confirmed by the seller/warehouse',
|
|
dp_assigned: 'A delivery partner has been assigned to deliver the order',
|
|
pack: 'The order has been packed and is ready for dispatch',
|
|
cancelled: 'The order has been cancelled',
|
|
delivery_done: 'The order has been successfully delivered to the customer',
|
|
};
|
|
|
|
function requestId(prefix) {
|
|
return `${prefix}_${Date.now()}`;
|
|
}
|
|
|
|
function parseJsonField(value, fallback) {
|
|
if (typeof value !== 'string') return value ?? fallback;
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|
|
|
|
async function postWorkflow(url, payload) {
|
|
try {
|
|
const response = await axios.post(url, payload, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
maxBodyLength: Infinity,
|
|
timeout: 60000,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
const details = error.response?.data ? ` | response: ${JSON.stringify(error.response.data)}` : '';
|
|
throw new Error(`Workflow API error (${url}): ${error.message}${details}`);
|
|
}
|
|
}
|
|
|
|
async function parseBrandContext(scrapedData = {}) {
|
|
const representativePages = Array.isArray(scrapedData.representativePages)
|
|
? scrapedData.representativePages.slice(0, 20)
|
|
: [];
|
|
const representativeTextBlocks = Array.isArray(scrapedData.representativeTextBlocks)
|
|
? scrapedData.representativeTextBlocks.slice(0, 20)
|
|
: [];
|
|
const productPages = Array.isArray(scrapedData.productPages)
|
|
? scrapedData.productPages.slice(0, 5)
|
|
: [];
|
|
const contentDigest = representativeTextBlocks
|
|
.map((block) => {
|
|
const title = String(block?.title || '').trim();
|
|
const pageType = String(block?.pageType || '').trim();
|
|
const text = String(block?.text || '').trim();
|
|
return [title, pageType, text].filter(Boolean).join(' | ');
|
|
})
|
|
.filter(Boolean)
|
|
.join('\n\n')
|
|
.slice(0, 14000);
|
|
|
|
const payload = {
|
|
task: 'parse_brand_context',
|
|
request_id: requestId('parse_brand_context'),
|
|
start_url: String(scrapedData.startUrl || ''),
|
|
domain: String(scrapedData.domain || ''),
|
|
site_stats_json: JSON.stringify(scrapedData.siteStats || {}),
|
|
homepage_json: JSON.stringify(scrapedData.homepage || {}),
|
|
about_page_json: JSON.stringify(scrapedData.aboutPage || {}),
|
|
product_pages_json: JSON.stringify(productPages),
|
|
contact_page_json: JSON.stringify(scrapedData.contactPage || {}),
|
|
representative_pages_json: JSON.stringify(representativePages),
|
|
representative_text_blocks_json: JSON.stringify(representativeTextBlocks),
|
|
navigation_json: JSON.stringify(scrapedData.navigation || []),
|
|
policy_pages_json: JSON.stringify(scrapedData.policyPages || []),
|
|
links_json: JSON.stringify(scrapedData.links || []),
|
|
top_images_json: JSON.stringify(scrapedData.topImages || []),
|
|
screenshots_json: JSON.stringify(scrapedData.screenshots || []),
|
|
branding_json: JSON.stringify(scrapedData.branding || {}),
|
|
crawl_summary_json: JSON.stringify(scrapedData || {}),
|
|
content_digest: contentDigest,
|
|
output_schema_text: 'You are given homepage, about-page, product-page, branding, and image evidence for a storefront. Use that evidence to infer brand identity and product language. Return ONLY valid JSON object with exactly these keys: brandName (string), tone (one of: friendly, professional, formal, casual, energetic), taglines (array of strings, max 3), colors (array of hex color strings, or empty array), relevantImageUrls (array of 3-5 absolute image URLs for logo/hero/product images only; no icons/tracking/data URLs), aboutSummary (string, 2-4 sentences, concise customer-facing brand summary that explains what the brand is about, what it sells, and its vibe; do not copy the About Us page verbatim). No markdown, no prose, no extra keys.',
|
|
must_return_json_only: 'true',
|
|
};
|
|
|
|
const data = await postWorkflow(WORKFLOW_URL_SCRAPE, payload);
|
|
const output = typeof data === 'string' ? parseJsonField(data, {}) : (data || {});
|
|
|
|
return {
|
|
brandName: String(output.brandName || '').trim() || 'Unknown Brand',
|
|
tone: ['friendly', 'professional', 'formal', 'casual', 'energetic'].includes(String(output.tone || '').toLowerCase())
|
|
? String(output.tone).toLowerCase()
|
|
: 'professional',
|
|
taglines: Array.isArray(output.taglines) ? output.taglines.slice(0, 3).map(String) : [],
|
|
colors: Array.isArray(output.colors) ? output.colors.map(String) : [],
|
|
relevantImageUrls: Array.isArray(output.relevantImageUrls) ? output.relevantImageUrls.map(String) : [],
|
|
aboutSummary: String(output.aboutSummary || '').trim(),
|
|
};
|
|
}
|
|
|
|
async function generateTemplates(brandContext = {}, eventSlug, eventLabel) {
|
|
const eventDesc = EVENT_DESCRIPTIONS[eventSlug] || `A "${eventLabel}" event in the order lifecycle`;
|
|
|
|
const payload = {
|
|
task: 'generate_sms_templates',
|
|
request_id: requestId('generate_sms_templates'),
|
|
brand_name: String(brandContext.brandName || ''),
|
|
tone: String(brandContext.tone || ''),
|
|
taglines_json: JSON.stringify(Array.isArray(brandContext.taglines) ? brandContext.taglines : []),
|
|
event_slug: String(eventSlug || ''),
|
|
event_label: String(eventLabel || ''),
|
|
event_description: eventDesc,
|
|
trai_rules_text: TRAI_RULES_TEXT,
|
|
templates_count: '3',
|
|
max_chars: '160',
|
|
variable_format: '{#var#}',
|
|
output_schema_text: 'Return ONLY valid JSON object with exactly one key: templates (array of exactly 3 strings). No extra keys.',
|
|
must_return_json_only: 'true',
|
|
};
|
|
|
|
const data = await postWorkflow(WORKFLOW_URL_TEMPLATE, payload);
|
|
const output = typeof data === 'string' ? parseJsonField(data, {}) : (data || {});
|
|
const templates = Array.isArray(output.templates)
|
|
? output.templates
|
|
: parseJsonField(output.templates_json, []);
|
|
|
|
return Array.isArray(templates) ? templates.map(String).slice(0, 3) : [];
|
|
}
|
|
|
|
async function processCurl(rawCurl, approvedTemplate, eventSlug) {
|
|
const payload = {
|
|
task: 'process_provider_curl',
|
|
request_id: requestId('process_provider_curl'),
|
|
raw_curl: String(rawCurl || ''),
|
|
approved_template: String(approvedTemplate || ''),
|
|
event_slug: String(eventSlug || ''),
|
|
instructions_text: 'Identify placeholders, map to semantic field names, normalize placeholders in curl to camelCase, and build positional mapping for DLT placeholder tokens in approved_template. Supported token types include {#var#}, {#numeric#}, {#url#}, and {#cbn#}. Preserve the actual token text in variableMap keys using the format "<token>[index]" based on appearance order within approved_template.',
|
|
output_schema_text: 'Return ONLY valid JSON object with exactly these keys: processedCurl (string), variableMap (object where keys preserve the actual DLT token text in approved_template, such as {#var#}[0], {#numeric#}[1], {#url#}[2], and values are field names in camelCase). No extra keys.',
|
|
must_return_json_only: 'true',
|
|
};
|
|
|
|
const data = await postWorkflow(WORKFLOW_URL_CHECK_CURL, payload);
|
|
const output = typeof data === 'string' ? parseJsonField(data, {}) : (data || {});
|
|
const variableMap = typeof output.variableMap === 'object' && output.variableMap !== null
|
|
? output.variableMap
|
|
: parseJsonField(output.variable_map_json, {});
|
|
|
|
return {
|
|
processedCurl: String(output.processedCurl || ''),
|
|
variableMap: variableMap && typeof variableMap === 'object' ? variableMap : {},
|
|
};
|
|
}
|
|
|
|
async function validateCurlFields(rawCurl) {
|
|
const payload = {
|
|
curl_b64: Buffer.from(String(rawCurl || ''), 'utf8').toString('base64'),
|
|
};
|
|
|
|
const data = await postWorkflow(WORKFLOW_VALIDATE_FIELDS, payload);
|
|
const output = typeof data === 'string' ? parseJsonField(data, {}) : (data || {});
|
|
const isValidCurl = output.is_valid_curl === true || String(output.is_valid_curl).toLowerCase() === 'true';
|
|
|
|
return {
|
|
isValidCurl,
|
|
provider: {
|
|
providerName: String(output.provider_name || '').trim(),
|
|
senderId: String(output.dlt_sender_id || '').trim().toUpperCase(),
|
|
dltEntityId: String(output.dlt_entity_id || '').trim(),
|
|
authKey: String(output.api_auth_key || '').trim(),
|
|
},
|
|
reason: String(output.reason || '').trim(),
|
|
};
|
|
}
|
|
|
|
module.exports = { parseBrandContext, generateTemplates, processCurl, validateCurlFields };
|