145 lines
5.2 KiB
TypeScript
145 lines
5.2 KiB
TypeScript
|
|
import { NextRequest, NextResponse } from 'next/server';
|
||
|
|
import { createMetaMCPClient } from '@/lib/mcp/client';
|
||
|
|
import { getCampaignDetails } from '@/lib/mcp/tools';
|
||
|
|
import { getAccessToken } from '@/lib/auth/session';
|
||
|
|
import { getAIClient } from '@/lib/ai/client';
|
||
|
|
|
||
|
|
const ANALYSIS_MODEL = 'gpt-5.4-mini';
|
||
|
|
|
||
|
|
export async function POST(request: NextRequest) {
|
||
|
|
try {
|
||
|
|
const token = await getAccessToken();
|
||
|
|
if (!token) {
|
||
|
|
return NextResponse.json({ success: false, error: 'Not authenticated' }, { status: 401 });
|
||
|
|
}
|
||
|
|
|
||
|
|
const { campaignId } = await request.json();
|
||
|
|
if (!campaignId) {
|
||
|
|
return NextResponse.json({ success: false, error: 'Campaign ID required' }, { status: 400 });
|
||
|
|
}
|
||
|
|
|
||
|
|
const client = await createMetaMCPClient(token);
|
||
|
|
let campaignData: any;
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await getCampaignDetails(client, campaignId);
|
||
|
|
campaignData = Array.isArray(result) ? result[0] : result;
|
||
|
|
} finally {
|
||
|
|
await client.close();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!campaignData || !campaignData.id) {
|
||
|
|
return NextResponse.json({ success: false, error: 'Campaign not found on Meta' }, { status: 404 });
|
||
|
|
}
|
||
|
|
|
||
|
|
// Prepare data for the AI model
|
||
|
|
const insights = campaignData.insights?.data?.[0] || {};
|
||
|
|
const adsets = campaignData.adsets?.data || [];
|
||
|
|
|
||
|
|
let totalSpend = parseFloat(insights.spend || '0');
|
||
|
|
let roas = parseFloat(insights.purchase_roas?.[0]?.value || '0');
|
||
|
|
|
||
|
|
// Extract Ads & Creatives
|
||
|
|
const adsToAnalyze = [];
|
||
|
|
for (const adset of adsets) {
|
||
|
|
const ads = adset.ads?.data || [];
|
||
|
|
for (const ad of ads) {
|
||
|
|
const adInsights = ad.insights?.data?.[0] || {};
|
||
|
|
const creative = ad.creative || {};
|
||
|
|
|
||
|
|
// Extract texts
|
||
|
|
let body = creative.body || '';
|
||
|
|
let title = creative.title || '';
|
||
|
|
let imageUrl = creative.image_url || creative.thumbnail_url || '';
|
||
|
|
|
||
|
|
// Handle asset_feed_spec (Dynamic Creative)
|
||
|
|
if (creative.asset_feed_spec) {
|
||
|
|
const feed = creative.asset_feed_spec;
|
||
|
|
body = feed.bodies?.map((b: any) => b.text).join(' | ') || body;
|
||
|
|
title = feed.titles?.map((t: any) => t.text).join(' | ') || title;
|
||
|
|
if (feed.images && feed.images.length > 0) {
|
||
|
|
imageUrl = feed.images[0].url || feed.images[0].thumbnail_url || imageUrl; // Just take the first image for now
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
adsToAnalyze.push({
|
||
|
|
ad_name: ad.name,
|
||
|
|
spend: adInsights.spend || '0',
|
||
|
|
roas: adInsights.purchase_roas?.[0]?.value || '0',
|
||
|
|
cpc: adInsights.cpc || '0',
|
||
|
|
ctr: adInsights.ctr || '0',
|
||
|
|
body,
|
||
|
|
title,
|
||
|
|
image_url: imageUrl,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const openai = getAIClient();
|
||
|
|
|
||
|
|
const prompt = `
|
||
|
|
Você é um Especialista Sênior em Tráfego Pago e Copywriting.
|
||
|
|
Abaixo estão os dados reais extraídos de uma Campanha no Facebook Ads.
|
||
|
|
Por favor, faça uma auditoria completa.
|
||
|
|
|
||
|
|
DADOS DA CAMPANHA:
|
||
|
|
Nome: ${campaignData.name}
|
||
|
|
Gasto Total (30d): R$ ${totalSpend}
|
||
|
|
ROAS Total: ${roas}
|
||
|
|
|
||
|
|
ANÚNCIOS ATIVOS (CRIATIVOS E MÉTRICAS):
|
||
|
|
${JSON.stringify(adsToAnalyze, null, 2)}
|
||
|
|
|
||
|
|
Sua tarefa é retornar um JSON ESTRITAMENTE VÁLIDO com a seguinte estrutura de análise (NÃO adicione formatação markdown \`\`\`json no início ou no fim).
|
||
|
|
Importante: Para as análises e ações, retorne um ARRAY de strings (cada string é um bullet point claro, curto e direto).
|
||
|
|
{
|
||
|
|
"diagnostico_geral": "Um parágrafo forte resumindo a saúde da campanha.",
|
||
|
|
"analise_copy": ["Ponto 1 sobre a copy...", "Sugestão de melhoria de texto..."],
|
||
|
|
"analise_imagem": ["O que achei do visual...", "Ideia de novo formato (vídeo/carrossel)..."],
|
||
|
|
"analise_metricas": ["ROAS está X...", "CPA está Y..."],
|
||
|
|
"acao_recomendada": ["Ação imediata 1 (ex: Pausar Ad X)...", "Ação 2 (ex: Testar público Y)..."],
|
||
|
|
"nova_estrategia_sugerida": {
|
||
|
|
"nicho_sugerido": "ex: ecommerce, info_produto, negocios_locais, advogado...",
|
||
|
|
"produto_foco": "Nome ou tipo do produto que deve ser focado na próxima campanha",
|
||
|
|
"publico_alvo": "Qual deve ser o público alvo ideal",
|
||
|
|
"angulos_de_venda": ["Ângulo 1", "Ângulo 2"]
|
||
|
|
}
|
||
|
|
}
|
||
|
|
`;
|
||
|
|
|
||
|
|
const completion = await openai.chat.completions.create({
|
||
|
|
model: ANALYSIS_MODEL,
|
||
|
|
messages: [{ role: 'user', content: prompt }],
|
||
|
|
response_format: { type: 'json_object' },
|
||
|
|
});
|
||
|
|
const textRes = completion.choices[0]?.message?.content || '';
|
||
|
|
let jsonRes;
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Clean up markdown fences if model ignored instruction
|
||
|
|
const cleanedText = textRes.replace(/^```json/m, '').replace(/```$/m, '').trim();
|
||
|
|
jsonRes = JSON.parse(cleanedText);
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Failed to parse AI response:', textRes);
|
||
|
|
return NextResponse.json({ success: false, error: 'Failed to parse AI response' }, { status: 500 });
|
||
|
|
}
|
||
|
|
|
||
|
|
return NextResponse.json({
|
||
|
|
success: true,
|
||
|
|
data: {
|
||
|
|
campaign_metrics: { spend: totalSpend, roas },
|
||
|
|
ads_count: adsToAnalyze.length,
|
||
|
|
ads: adsToAnalyze,
|
||
|
|
analysis: jsonRes
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
} catch (error) {
|
||
|
|
console.error('[Analysis] API Error:', error);
|
||
|
|
return NextResponse.json(
|
||
|
|
{ success: false, error: error instanceof Error ? error.message : 'Unknown error' },
|
||
|
|
{ status: 500 }
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|