Server : LiteSpeed System : Linux server335.web-hosting.com 4.18.0-553.62.1.lve.el8.x86_64 #1 SMP Mon Jul 21 17:50:35 UTC 2025 x86_64 User : cardxfeb ( 2452) PHP Version : 8.1.34 Disable Function : NONE Directory : /home/cardxfeb/public_html/app/Services/ |
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class LlmAnalyzerService
{
/**
* Analyze MULTIPLE source data (social + corporate) with Claude API.
* Strategy: corporate branding for colors, social personality for bio.
*/
public function analyzeMultiple(array $multiSourceData, string $region = 'BR'): array
{
$apiKey = config('services.claude.api_key');
if (empty($apiKey) || $apiKey === 'sk-ant-CONFIGURE_ME') {
Log::warning('[LlmAnalyzer] CLAUDE_API_KEY not configured using fallback');
return $this->fallbackResult($multiSourceData['merged'] ?? []);
}
$lang = $region === 'PT' ? 'portugus europeu formal' : 'portugus brasileiro profissional';
$merged = $multiSourceData['merged'] ?? [];
$social = $multiSourceData['social'] ?? [];
$corporate = $multiSourceData['corporate'] ?? [];
// Build color hints string
$colorHints = '';
$brandColors = $merged['brand_colors'] ?? [];
if (!empty($brandColors)) {
$colorParts = [];
if (!empty($brandColors['theme_color'])) $colorParts[] = "theme-color: {$brandColors['theme_color']}";
if (!empty($brandColors['css_primary'])) $colorParts[] = "CSS primary: {$brandColors['css_primary']}";
if (!empty($brandColors['tile_color'])) $colorParts[] = "tile-color: {$brandColors['tile_color']}";
if (!empty($brandColors['body_bg'])) $colorParts[] = "body-bg: {$brandColors['body_bg']}";
$colorHints = implode(', ', $colorParts);
}
// Build social profiles summary
$socialSummary = '';
foreach ($social as $s) {
$socialSummary .= "- [{$s['platform']}] Ttulo: \"{$s['title']}\" | Bio: \"{$s['description']}\"\n";
}
// Build corporate summary
$corporateSummary = '';
foreach ($corporate as $c) {
$corporateSummary .= "- [Website] Nome da empresa: \"{$c['title']}\" | Descrio: \"{$c['description']}\" | Keywords: \"{$c['keywords']}\"\n";
}
$sourcesCount = ($merged['sources'] ?? 1);
$prompt = "Voc um especialista snior em branding digital e identidade visual corporativa.
Voc recebeu dados de {$sourcesCount} fonte(s) distintas para criar um carto de visita digital profissional.
=== FONTES DE DADOS ===
PERFIS SOCIAIS (Use para: personalidade, bio, tom de voz do indivduo):
{$socialSummary}
SITE CORPORATIVO (Use para: cores da marca, nome da empresa, posicionamento):
{$corporateSummary}
CORES DETECTADAS NO SITE DA EMPRESA:
{$colorHints}
=== INSTRUO CRTICA ===
- Use as CORES DO SITE CORPORATIVO como base para color_primary e color_secondary.
- Se no houver cores do site, use um tom que combine com a personalidade social detectada.
- Mantenha a BIO e PROFISSO baseadas nos perfis sociais (voz autntica do indivduo).
- O NOME deve priorizar o perfil social; use o site apenas como confirmao.
- O TEMPLATE deve refletir o posicionamento: corporativo=1-8, criativo=9-16, empreendedor=17-24.
RETORNE APENAS este JSON vlido (sem markdown, sem texto extra):
{
\"name\": \"nome completo ou profissional\",
\"profession\": \"cargo ou rea de atuao\",
\"bio\": \"bio profissional em at 160 caracteres em {$lang}\",
\"company\": \"nome da empresa ou marca\",
\"color_primary\": \"#HEXCOLOR da marca corporativa ou personalidade\",
\"color_secondary\": \"#HEXCOLOR complementar\",
\"template_id\": \"1 a 24\",
\"links\": [\"URLs relevantes\"],
\"social_handles\": {},
\"brand_personality\": \"1 palavra: corporativo|criativo|empreendedor|profissional\",
\"confidence\": \"high|medium|low\"
}";
try {
$response = Http::withHeaders([
'x-api-key' => $apiKey,
'anthropic-version' => '2023-06-01',
'content-type' => 'application/json',
])->timeout(35)->post('https://api.anthropic.com/v1/messages', [
'model' => config('services.claude.model', 'claude-haiku-4-5'),
'max_tokens' => 700,
'messages' => [['role' => 'user', 'content' => $prompt]],
]);
if (!$response->ok()) {
Log::error('[LlmAnalyzer] API error: ' . $response->status() . ' - ' . $response->body());
return $this->fallbackResult($merged);
}
$text = $response->json('content.0.text', '');
// Strip markdown code blocks if present
$text = preg_replace('/^```(?:json)?\s*/m', '', $text);
$text = preg_replace('/\s*```$/m', '', $text);
$text = trim($text);
$data = json_decode($text, true);
if (json_last_error() !== JSON_ERROR_NONE) {
Log::warning('[LlmAnalyzer] Invalid JSON from LLM: ' . substr($text, 0, 200));
return $this->fallbackResult($merged);
}
// Sanitize template_id
$data['template_id'] = max(1, min(24, (int)($data['template_id'] ?? 1)));
// If no valid color from LLM, use detected brand color
if (empty($data['color_primary']) || !preg_match('/^#[0-9a-fA-F]{3,6}$/', $data['color_primary'])) {
$data['color_primary'] = $brandColors['theme_color']
?? $brandColors['css_primary']
?? '#2563EB';
}
return $data;
} catch (\Exception $e) {
Log::error('[LlmAnalyzer] Exception: ' . $e->getMessage());
return $this->fallbackResult($merged);
}
}
/**
* Legacy single-source analyze (backward compatibility)
*/
public function analyze(array $scrapedData, string $region = 'BR'): array
{
// Wrap single source as multi-source format
$wrapped = [
'social' => [$scrapedData],
'corporate' => [],
'merged' => [
'name' => $scrapedData['title'] ?? '',
'bio_hint' => $scrapedData['description'] ?? '',
'brand_name' => '',
'brand_desc' => '',
'brand_colors' => [],
'platforms' => [$scrapedData['platform'] ?? 'website'],
'sources' => 1,
],
];
return $this->analyzeMultiple($wrapped, $region);
}
private function fallbackResult(array $merged): array
{
$brandColor = $merged['brand_colors']['theme_color']
?? $merged['brand_colors']['css_primary']
?? '#2563EB';
return [
'name' => $merged['name'] ?? 'Meu Carto Digital',
'profession' => '',
'bio' => $merged['bio_hint'] ?? '',
'company' => $merged['brand_name'] ?? '',
'color_primary' => $brandColor,
'color_secondary' => '#F59E0B',
'template_id' => 1,
'links' => [],
'social_handles' => [],
'brand_personality' => 'profissional',
'confidence' => 'low',
];
}
}