feat(brandbook): anadir analizador e ingesta automatica de estrategias LinkedIn B2B con IA desde URL
This commit is contained in:
194
src/lib/b2b-analyzer.ts
Normal file
194
src/lib/b2b-analyzer.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* b2b-analyzer.ts
|
||||
* Ingesta y análisis automático de documentos y estrategias B2B para LinkedIn (4Ventures)
|
||||
* Descarga URLs web (HTML/Markdown/Notion) y usa Claude para extraer campos de Brandbook B2B.
|
||||
*/
|
||||
|
||||
export interface B2BAnalysisResult {
|
||||
linkedin_themes: string[];
|
||||
linkedin_formats: string[];
|
||||
thought_leadership: string[];
|
||||
buyer_roles: string[];
|
||||
case_study_angles: string[];
|
||||
linkedin_cta: string[];
|
||||
abm_notes: string;
|
||||
editorial_summary?: string;
|
||||
source_filename?: string;
|
||||
source_excerpt?: string;
|
||||
}
|
||||
|
||||
function decodeHtml(html: string): string {
|
||||
return html
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/’/g, "'")
|
||||
.replace(/‘/g, "'")
|
||||
.replace(/“/g, '"')
|
||||
.replace(/”/g, '"')
|
||||
.replace(/–/g, '–')
|
||||
.replace(/—/g, '—')
|
||||
.replace(/ /g, ' ');
|
||||
}
|
||||
|
||||
export async function fetchStrategyWebpage(url: string): Promise<{ text: string; filename: string }> {
|
||||
const resp = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (compatible; 4VenturesStrategyBot/1.0; +https://4ventures.es)',
|
||||
Accept: 'text/html,application/xhtml+xml,text/plain;q=0.9,*/*;q=0.8',
|
||||
},
|
||||
signal: AbortSignal.timeout(20000),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Error al acceder a la URL: HTTP ${resp.status}`);
|
||||
}
|
||||
|
||||
const raw = await resp.text();
|
||||
const clean = raw
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<noscript[\s\S]*?<\/noscript>/gi, '')
|
||||
.replace(/<svg[\s\S]*?<\/svg>/gi, '')
|
||||
.replace(/<\/(p|div|h[1-6]|li|tr)>/gi, '\n\n')
|
||||
.replace(/<br\s*[\/]?>/gi, '\n');
|
||||
|
||||
const text = decodeHtml(clean.replace(/<[^>]+>/g, ' '))
|
||||
.replace(/[ \t]+/g, ' ')
|
||||
.replace(/\n\s*\n\s*\n+/g, '\n\n')
|
||||
.trim();
|
||||
|
||||
let filename = 'estrategia-b2b.txt';
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
const pathSegments = parsed.pathname.split('/').filter(Boolean);
|
||||
const lastSeg = pathSegments[pathSegments.length - 1];
|
||||
filename = lastSeg ? (lastSeg.endsWith('.html') ? lastSeg : `${lastSeg}.txt`) : `${parsed.hostname}.txt`;
|
||||
} catch {
|
||||
filename = 'estrategia-b2b.txt';
|
||||
}
|
||||
|
||||
return { text, filename };
|
||||
}
|
||||
|
||||
export async function analyzeB2BWithClaude(
|
||||
content: string,
|
||||
brandName: string,
|
||||
extraContext?: string
|
||||
): Promise<B2BAnalysisResult> {
|
||||
const apiKey =
|
||||
process.env.ANTHROPIC_API_KEY ||
|
||||
process.env.CLAUDE_API_KEY ||
|
||||
'';
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error('Falta ANTHROPIC_API_KEY en variables de entorno.');
|
||||
}
|
||||
|
||||
// Limitar el contenido a ~40.000 caracteres para no desbordar tokens
|
||||
const sample = content.slice(0, 45000);
|
||||
|
||||
const prompt = `Eres el Director de Estrategia B2B y LinkedIn de la agencia 4Ventures.
|
||||
Tu tarea es analizar el siguiente documento o página estratégica de contenidos de la marca "${brandName}" y extraer los campos estructurados necesarios para configurar el Brandbook / Kit de Marca B2B.
|
||||
|
||||
Contexto previo de la marca: ${extraContext || 'Sin contexto adicional'}
|
||||
|
||||
--- DOCUMENTO O ESTRATEGIA B2B ---
|
||||
${sample}
|
||||
--- FIN DOCUMENTO ---
|
||||
|
||||
Extrae y estructura la información en un único objeto JSON válido (sin formato Markdown adicional, solo el JSON puro) con las siguientes claves exactas:
|
||||
|
||||
{
|
||||
"editorial_summary": "Resumen conciso de 2-3 frases de la visión estratégica y posicionamiento de la marca en LinkedIn.",
|
||||
"linkedin_themes": [
|
||||
"Pilar o tema recurrente de contenido B2B 1",
|
||||
"Pilar o tema recurrente de contenido B2B 2",
|
||||
"Pilar o tema recurrente de contenido B2B 3",
|
||||
"Pilar o tema recurrente de contenido B2B 4",
|
||||
"Pilar o tema recurrente de contenido B2B 5"
|
||||
],
|
||||
"linkedin_formats": [
|
||||
"Regla de formato o copy 1 (ej. Formato vertical 4:5, fondo claro institucional)",
|
||||
"Regla de formato o copy 2 (ej. Gancho detén-scroll de 2 líneas)",
|
||||
"Regla de formato o copy 3 (ej. Enlaces siempre en el primer comentario)",
|
||||
"Regla de formato o copy 4 (ej. Carruseles PDF para procesos)",
|
||||
"Regla de formato o copy 5 (ej. Hashtags de nicho)"
|
||||
],
|
||||
"thought_leadership": [
|
||||
"Línea de autoridad o pensamiento sectorial 1",
|
||||
"Línea de autoridad o pensamiento sectorial 2",
|
||||
"Línea de autoridad o pensamiento sectorial 3",
|
||||
"Línea de autoridad o pensamiento sectorial 4"
|
||||
],
|
||||
"buyer_roles": [
|
||||
"Cargo decisor 1 con su dolor o motivación principal",
|
||||
"Cargo decisor 2 con su dolor o motivación principal",
|
||||
"Cargo decisor 3 con su dolor o motivación principal",
|
||||
"Cargo decisor 4 con su dolor o motivación principal"
|
||||
],
|
||||
"case_study_angles": [
|
||||
"Ángulo o caso de éxito sectorial 1",
|
||||
"Ángulo o caso de éxito sectorial 2",
|
||||
"Ángulo o caso de éxito sectorial 3",
|
||||
"Ángulo o caso de éxito sectorial 4"
|
||||
],
|
||||
"linkedin_cta": [
|
||||
"Llamada a la acción de baja fricción 1 (ej. Pre-diagnóstico gratuito en 1er comentario)",
|
||||
"Llamada a la acción de baja fricción 2 (ej. Escríbenos por DM)",
|
||||
"Llamada a la acción de baja fricción 3",
|
||||
"Llamada a la acción de baja fricción 4"
|
||||
],
|
||||
"abm_notes": "Párrafo detallado con la estrategia ABM: universo de cuentas objetivo, sectores prioritarios, disparadores de compra (triggers) y protocolo de contacto."
|
||||
}
|
||||
|
||||
Instrucciones:
|
||||
1. Sé extremadamente fiel y específico a los datos, mercados, números E, normativas, países, dolores y métricas que aparecen en el documento.
|
||||
2. Cada elemento de los arrays debe ser una frase o directriz clara y accionable.
|
||||
3. El resultado debe ser EXCLUSIVAMENTE el JSON válido.`;
|
||||
|
||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'x-api-key': apiKey,
|
||||
'anthropic-version': '2023-06-01',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'claude-haiku-4-5-20251001',
|
||||
max_tokens: 3000,
|
||||
messages: [{ role: 'user', content: prompt }],
|
||||
}),
|
||||
signal: AbortSignal.timeout(30000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errText = await response.text();
|
||||
throw new Error(`Claude API error ${response.status}: ${errText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const text = (data.content || [])
|
||||
.map((c: any) => (c.type === 'text' ? c.text : ''))
|
||||
.join('');
|
||||
|
||||
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
||||
if (!jsonMatch) {
|
||||
throw new Error('Claude no devolvió un JSON reconocible');
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(jsonMatch[0]);
|
||||
|
||||
return {
|
||||
linkedin_themes: Array.isArray(parsed.linkedin_themes) ? parsed.linkedin_themes : [],
|
||||
linkedin_formats: Array.isArray(parsed.linkedin_formats) ? parsed.linkedin_formats : [],
|
||||
thought_leadership: Array.isArray(parsed.thought_leadership) ? parsed.thought_leadership : [],
|
||||
buyer_roles: Array.isArray(parsed.buyer_roles) ? parsed.buyer_roles : [],
|
||||
case_study_angles: Array.isArray(parsed.case_study_angles) ? parsed.case_study_angles : [],
|
||||
linkedin_cta: Array.isArray(parsed.linkedin_cta) ? parsed.linkedin_cta : [],
|
||||
abm_notes: typeof parsed.abm_notes === 'string' ? parsed.abm_notes : '',
|
||||
editorial_summary: typeof parsed.editorial_summary === 'string' ? parsed.editorial_summary : '',
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user