Files

1 line
20 KiB
JSON

{"repo": "ateuatado/cerebro", "files": {"app/Services/DeepSeekService.php": {"c6e5667957cdb7a83f1840bc3027673dab31372c": {"t": 1785745779.0777183, "n": 12821, "h": "90b928b34894fa36", "c": "<?php\n\nnamespace App\\Services;\n\n/**\n * DeepSeekService — Integração com a API do DeepSeek para extração de conhecimento histórico\n * Suporta análise de imagens (manuscritos) via DeepSeek Vision (deepseek-chat com image_url)\n */\nclass DeepSeekService\n{\n private string $apiKey;\n private string $apiUrl = 'https://api.deepseek.com/chat/completions';\n private string $model = 'deepseek-chat';\n\n public function __construct()\n {\n $this->apiKey = getenv('DEEPSEEK_API_KEY') ?: '';\n\n $envPath = defined('ROOTPATH') ? ROOTPATH . '.env' : __DIR__ . '/../../.env';\n if (empty($this->apiKey) && file_exists($envPath)) {\n $lines = file($envPath);\n foreach ($lines as $line) {\n if (strpos(trim($line), 'DEEPSEEK_API_KEY') === 0) {\n $parts = explode('=', $line, 2);\n $this->apiKey = trim($parts[1] ?? '');\n break;\n }\n }\n }\n }\n\n /**\n * Envia requisição para a API DeepSeek\n */\n private function callApi(array $messages): array\n {\n if (empty($this->apiKey)) {\n throw new \\RuntimeException('A chave DEEPSEEK_API_KEY não está configurada no arquivo .env.');\n }\n\n $data = [\n 'model' => $this->model,\n 'messages' => $messages,\n 'temperature' => 0.1,\n 'response_format' => ['type' => 'json_object']\n ];\n\n $ch = \\curl_init($this->apiUrl);\n \\curl_setopt($ch, \\CURLOPT_RETURNTRANSFER, true);\n \\curl_setopt($ch, \\CURLOPT_POST, true);\n \\curl_setopt($ch, \\CURLOPT_HTTPHEADER, [\n 'Content-Type: application/json',\n 'Authorization: Bearer ' . $this->apiKey\n ]);\n \\curl_setopt($ch, \\CURLOPT_POSTFIELDS, \\json_encode($data, \\JSON_UNESCAPED_UNICODE));\n \\curl_setopt($ch, \\CURLOPT_SSL_VERIFYPEER, false);\n \\curl_setopt($ch, \\CURLOPT_TIMEOUT, 90);\n\n $response = \\curl_exec($ch);\n $httpCode = \\curl_getinfo($ch, \\CURLINFO_HTTP_CODE);\n $error = \\curl_error($ch);\n \\curl_close($ch);\n\n if ($error) {\n throw new \\RuntimeException('Erro na chamada da API DeepSeek: ' . $error);\n }\n\n if ($httpCode !== 200) {\n throw new \\RuntimeException('A API DeepSeek retornou status ' . $httpCode . ': ' . \\mb_substr($response, 0, 500));\n }\n\n $json = \\json_decode($response, true);\n $content = $json['choices'][0]['message']['content'] ?? '';\n\n if (empty($content)) {\n throw new \\RuntimeException('A resposta da API DeepSeek veio vazia.');\n }\n\n $cleanedContent = \\preg_replace('/^```json\\s*|\\s*```$/i', '', \\trim($content));\n $extractedData = \\json_decode($cleanedContent, true);\n\n if (\\json_last_error() !== \\JSON_ERROR_NONE) {\n throw new \\RuntimeException('Falha ao decodificar JSON retornado pela IA: ' . \\json_last_error_msg());\n }\n\n return $extractedData ?: [];\n }\n\n /**\n * Lê uma imagem de recorte (Base64) diretamente via DeepSeek Vision\n * e extrai entidades e relações do manuscrito.\n * \n * @param string $docTitle Título do documento\n * @param string $imageBase64 Dados da imagem em Base64 (com prefixo data:image/...)\n * @return array ['transcription', 'entities', 'relationships']\n */\n public function extractFromCropImage(string $docTitle, string $imageBase64): array\n {\n $systemPrompt = <<<PROMPT\nVocê é um historiador e paleógrafo especialista em leitura de documentos manuscritos cursivos do Brasil das décadas de 1920 e 1930 (boletins de batalhões militares, registros de prisões, jornais e processos judiciais).\n\nA imagem a seguir é um recorte de um documento manuscrito em caligrafia cursiva antiga, com possíveis abreviações da época (ex: \"Ten.\" = Tenente, \"Sgt.\" = Sargento, \"Alferes\", \"2º Batalhão\", \"Mappa diario\", \"preso_em\").\n\nLEIA DIRETAMENTE A IMAGEM e:\n1. \"transcription\": Faça a transcrição completa e fiel do texto manuscrito visível.\n2. \"entities\": Extraia todas as pessoas (com patentes/cargos em atributos), locais, eventos e organizações.\n3. \"relationships\": Extraia todas as conexões entre essas entidades.\n\nRetorne EXCLUSIVAMENTE um JSON:\n{\n \"transcription\": \"Texto completo transcrito do manuscrito na imagem...\",\n \"entities\": [\n {\"name\": \"Nome\", \"type\": \"person|location|event\", \"attributes\": {\"cargo\": \"...\"}}\n ],\n \"relationships\": [\n {\n \"source_name\": \"Nome Origem\",\n \"target_name\": \"Nome Destino\",\n \"relationship_type\": \"lotado_em\",\n \"direction\": \"directed\",\n \"confidence\": 0.90,\n \"excerpt\": \"trecho...\"\n }\n ]\n}\nPROMPT;\n\n $messages = [\n ['role' => 'system', 'content' => $systemPrompt],\n [\n 'role' => 'user',\n 'content' => [\n [\n 'type' => 'text',\n 'text' => \"Documento: {$docTitle}\\n\\nTranscreva e extraia entidades/relações deste recorte de manuscrito histórico.\"\n ],\n [\n 'type' => 'image_url',\n 'image_url' => [\n 'url' => $imageBase64\n ]\n ]\n ]\n ]\n ];\n\n $result = $this->callApi($messages);\n\n return [\n 'transcription' => $result['transcription'] ?? '',\n 'entities' => $result['entities'] ?? [],\n 'relationships' => $result['relationships'] ?? [],\n ];\n }\n\n /**\n * Transcreve e extrai entidades/relações a partir do texto bruto de um recorte de imagem manuscrita.\n */\n public function extractFromCropText(string $docTitle, string $rawOcrText): array\n {\n if (empty($this->apiKey)) {\n throw new \\RuntimeException('A chave DEEPSEEK_API_KEY não está configurada no arquivo .env.');\n }\n\n $systemPrompt = <<<PROMPT\nVocê é um historiador e paleógrafo especialista em leitura de documentos manuscritos cursivos do Brasil das décadas de 1920 e 1930 (boletins de batalhões militares, registros de prisões, jornais e processos judiciais).\n\nO texto a seguir veio de um recorte de imagem de manuscrito em caligrafia cursiva antiga e pode conter ruídos ou abreviações da época (ex: \"Ten.\" = Tenente, \"Sgt.\" = Sargento, \"Alferes\", \"2º Batalhão\", \"Mappa diario\", \"preso_em\").\n\nSua missão é:\n1. \"transcription\": Corrigir e restaurar o texto em português respeitando a ortografia/conteúdo original do manuscrito.\n2. \"entities\": Extrair todas as pessoas (com patentes/cargos em atributos), locais, eventos e organizações.\n3. \"relationships\": Extrair todas as conexões entre essas entidades (ex: lotado_em, preso_em, comandado_por, discursou_em).\n\nEstrutura JSON esperada:\n{\n \"transcription\": \"Texto manuscrito restaurado...\",\n \"entities\": [\n {\"name\": \"Nome\", \"type\": \"person|location|event\", \"attributes\": {\"cargo\": \"...\"}}\n ],\n \"relationships\": [\n {\n \"source_name\": \"Nome Origem\",\n \"target_name\": \"Nome Destino\",\n \"relationship_type\": \"lotado_em\",\n \"direction\": \"directed\",\n \"confidence\": 0.90,\n \"excerpt\": \"trecho...\"\n }\n ]\n}\nPROMPT;\n\n $userPrompt = \"Título do Documento: {$docTitle}\\n\\nTexto Bruto do Recorte:\\n{$rawOcrText}\";\n\n $messages = [\n ['role' => 'system', 'content' => $systemPrompt],\n ['role' => 'user', 'content' => $userPrompt]\n ];\n\n $result = $this->callApi($messages);\n\n return [\n 'transcription' => $result['transcription'] ?? $rawOcrText,\n 'entities' => $result['entities'] ?? [],\n 'relationships' => $result['relationships'] ?? [],\n ];\n }\n\n /**\n * Extração de alta densidade em documentos longos (ex: jornais, processos extensos),\n * dividindo o texto em blocos sequenciais para extrair dezenas/centenas de relações sem truncamento da IA.\n */\n public function extractKnowledgeChunked(string $docTitle, string $docText, array $extraAttributes = [], int $chunkSize = 3000): array\n {\n if (\\strlen($docText) <= $chunkSize) {\n return $this->extractKnowledge($docTitle, $docText, $extraAttributes);\n }\n\n $lines = \\explode(\"\\n\", $docText);\n $chunks = [];\n $currentChunk = '';\n\n foreach ($lines as $line) {\n if (\\strlen($currentChunk) + \\strlen($line) > $chunkSize && !empty($currentChunk)) {\n $chunks[] = $currentChunk;\n $currentChunk = '';\n }\n $currentChunk .= $line . \"\\n\";\n }\n if (!empty(\\trim($currentChunk))) {\n $chunks[] = $currentChunk;\n }\n\n $allEntities = [];\n $allRelationships = [];\n\n foreach ($chunks as $index => $chunkText) {\n $chunkTitle = \"{$docTitle} (Parte \" . ($index + 1) . \" de \" . \\count($chunks) . \")\";\n try {\n $res = $this->extractKnowledge($chunkTitle, $chunkText, $extraAttributes);\n\n foreach ($res['entities'] as $entity) {\n $name = \\trim($entity['name'] ?? '');\n $type = $entity['type'] ?? 'person';\n if (!empty($name)) {\n $key = \\mb_strtolower($name) . '|' . $type;\n if (!isset($allEntities[$key])) {\n $allEntities[$key] = $entity;\n } else {\n $allEntities[$key]['attributes'] = \\array_merge(\n $allEntities[$key]['attributes'] ?? [],\n $entity['attributes'] ?? []\n );\n }\n }\n }\n\n foreach ($res['relationships'] as $rel) {\n $allRelationships[] = $rel;\n }\n\n } catch (\\Exception $e) {\n // Log da falha no bloco e continua nos próximos\n }\n }\n\n return [\n 'entities' => \\array_values($allEntities),\n 'relationships' => $allRelationships,\n ];\n }\n\n /**\n * Analisa o texto de um documento histórico e extrai entidades e relações em formato estruturado.\n */\n public function extractKnowledge(string $docTitle, string $docText, array $extraAttributes = []): array\n {\n $systemPrompt = <<<PROMPT\nVocê é um historiador especialista em análise exaustiva e de alta densidade de jornais e documentos do Brasil das décadas de 1920 e 1930 (movimento operário, anarquismo, repressão policial, greves, edições de jornais).\n\nSua missão é LER EXAUSTIVAMENTE o texto e EXTRAIR O MÁXIMO POSSÍVEL de entidades e relações. Seja extremamente detalhista — extraia dezenas de nomes, locais, jornais, organizações, sindicatos, prisioneiros e eventos mencionados no texto!\n\n1. ENTIDADES:\n - \"person\": Pessoas (ex: militantes, oradores, prisioneiros, policiais, redateis, colaboradores, operários, autoridades).\n - \"location\": Locais (ex: cidades, ruas, praças, prisões, sedes de sindicatos, redações, auditórios).\n - \"event\": Eventos (ex: greves, prisões, sessões de leitura, comícios, perseguições, edições, reuniões, conferências).\n\n2. RELAÇÕES entre essas entidades:\n - relationship_type (snake_case): ex: publicou, editou, assinou, denunciou, preso_em, discursou_em, militante_de, participou_de, localizado_em, apoia, reprimiu.\n - source_name: Nome exato da entidade origem.\n - target_name: Nome exato da entidade destino.\n - direction: \"directed\" ou \"symmetric\".\n - confidence: Valor decimal entre 0.60 e 0.99.\n - excerpt: Trecho original do texto (máx 150 caracteres).\n\nREGRAS OBRIGATÓRIAS:\n- Retorne EXCLUSIVAMENTE um objeto JSON válido.\n- Seja exaustivo: não resuma ou pule nomes citados no texto.\n- Estrutura JSON esperada:\n{\n \"entities\": [\n {\"name\": \"Nome da Entidade\", \"type\": \"person|location|event\", \"attributes\": {\"cargo\": \"...\", \"ocupacao\": \"...\"}}\n ],\n \"relationships\": [\n {\n \"source_name\": \"Nome Origem\",\n \"target_name\": \"Nome Destino\",\n \"relationship_type\": \"discursou_em\",\n \"direction\": \"directed\",\n \"confidence\": 0.90,\n \"excerpt\": \"trecho do jornal...\"\n }\n ]\n}\nPROMPT;\n\n $userPrompt = \"Título do Documento: {$docTitle}\\n\\n\";\n if (!empty($extraAttributes)) {\n $userPrompt .= \"Metadados: \" . \\json_encode($extraAttributes, \\JSON_UNESCAPED_UNICODE) . \"\\n\\n\";\n }\n $userPrompt .= \"Conteúdo do Documento:\\n\" . $docText;\n\n $messages = [\n ['role' => 'system', 'content' => $systemPrompt],\n ['role' => 'user', 'content' => $userPrompt]\n ];\n\n return $this->callApi($messages);\n }\n}\n"}, "b7f154ff76c886d4cbd7f84b7e2a32ede78ce0dd": {"t": 1785756470.051282, "n": 5897, "h": "f12b0d271c34c069", "c": "<?php\n\nnamespace App\\Services;\n\n/**\n * DeepSeekService — Integração com a API do DeepSeek para extração de conhecimento histórico\n */\nclass DeepSeekService\n{\n private string $apiKey;\n private string $apiUrl = 'https://api.deepseek.com/chat/completions';\n private string $model = 'deepseek-chat';\n\n public function __construct()\n {\n $this->apiKey = getenv('DEEPSEEK_API_KEY') ?: '';\n\n $envPath = defined('ROOTPATH') ? ROOTPATH . '.env' : __DIR__ . '/../../.env';\n if (empty($this->apiKey) && file_exists($envPath)) {\n $lines = file($envPath);\n foreach ($lines as $line) {\n if (strpos(trim($line), 'DEEPSEEK_API_KEY') === 0) {\n $parts = explode('=', $line, 2);\n $this->apiKey = trim($parts[1] ?? '');\n break;\n }\n }\n }\n }\n\n /**\n * Analisa o texto de um documento histórico e extrai entidades e relações em formato estruturado.\n *\n * @param string $docTitle Título do documento\n * @param string $docText Conteúdo/transcrição do documento\n * @param array $extraAttributes Atributos bibliográficos do documento\n * @return array Array estruturado com ['entities' => [...], 'relationships' => [...]]\n */\n public function extractKnowledge(string $docTitle, string $docText, array $extraAttributes = []): array\n {\n if (empty($this->apiKey)) {\n throw new \\RuntimeException('A chave DEEPSEEK_API_KEY não está configurada no arquivo .env.');\n }\n\n $systemPrompt = <<<PROMPT\nVocê é um especialista em análise documental histórica e extração de redes e grafos de conhecimento, com foco na história do Brasil nas décadas de 1920 e 1930 (período de conflitos políticos, repressão e fragilização democrática).\n\nSua tarefa é analisar o texto do documento fornecido e extrair:\n1. ENTIDADES mencionadas ou implicitamente presentes:\n - \"person\": Pessoas (ex: indiciados, delegados, testemunhas, militantes, autoridades). Atributos possíveis: ocupacao, apelido, cargo, filiacao.\n - \"location\": Locais (ex: cidades, prisões, quarteis, ruas, órgãos públicos). Atributos possíveis: municipio, estado, tipo_local.\n - \"event\": Eventos datados ou marcantes (ex: prisões, depoimentos, julgamentos, manifestações, confrontos). Atributos possíveis: data, tipo_evento, descricao.\n\n2. RELAÇÕES entre essas entidades:\n - relationship_type: Nome da relação em snake_case (ex: participou_de, foi_preso_em, investigou, denunciou, liderou, presente_em, associado_a, submetido_a).\n - source_name: Nome exato da entidade origem (como extraída no array de entidades).\n - target_name: Nome exato da entidade destino (como extraída no array de entidades).\n - direction: \"directed\" ou \"symmetric\".\n - confidence: Valor decimal entre 0.50 e 0.99 indicando o grau de certeza histórica da inferência a partir do texto.\n - excerpt: Trecho curto do texto original de onde a relação foi extraída (máximo 150 caracteres).\n\nREGRAS OBRIGATÓRIAS:\n- Retorne EXCLUSIVAMENTE um objeto JSON válido, sem trechos em markdown ```json ... ``` ou texto explicativo extra.\n- Estrutura de saída esperada:\n{\n \"entities\": [\n {\"name\": \"Nome da Entidade\", \"type\": \"person|location|event\", \"attributes\": {\"chave\": \"valor\"}}\n ],\n \"relationships\": [\n {\n \"source_name\": \"Nome Origem\",\n \"target_name\": \"Nome Destino\",\n \"relationship_type\": \"participou_de\",\n \"direction\": \"directed\",\n \"confidence\": 0.85,\n \"excerpt\": \"trecho do documento...\"\n }\n ]\n}\nPROMPT;\n\n $userPrompt = \"Título do Documento: {$docTitle}\\n\\n\";\n if (!empty($extraAttributes)) {\n $userPrompt .= \"Metadados: \" . json_encode($extraAttributes, JSON_UNESCAPED_UNICODE) . \"\\n\\n\";\n }\n $userPrompt .= \"Conteúdo do Documento:\\n\" . $docText;\n\n $data = [\n 'model' => $this->model,\n 'messages' => [\n ['role' => 'system', 'content' => $systemPrompt],\n ['role' => 'user', 'content' => $userPrompt]\n ],\n 'temperature' => 0.1,\n 'response_format' => ['type' => 'json_object']\n ];\n\n $ch = curl_init($this->apiUrl);\n curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\n curl_setopt($ch, CURLOPT_POST, true);\n curl_setopt($ch, CURLOPT_HTTPHEADER, [\n 'Content-Type: application/json',\n 'Authorization: Bearer ' . $this->apiKey\n ]);\n curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));\n curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);\n curl_setopt($ch, CURLOPT_TIMEOUT, 60);\n\n $response = curl_exec($ch);\n $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\n $error = curl_error($ch);\n curl_close($ch);\n\n if ($error) {\n throw new \\RuntimeException('Erro na chamada da API DeepSeek: ' . $error);\n }\n\n if ($httpCode !== 200) {\n throw new \\RuntimeException('A API DeepSeek retornou o status HTTP ' . $httpCode . ': ' . $response);\n }\n\n $json = json_decode($response, true);\n $content = $json['choices'][0]['message']['content'] ?? '';\n\n if (empty($content)) {\n throw new \\RuntimeException('A resposta da API DeepSeek veio vazia.');\n }\n\n // Limpar possíveis blocos de markdown se retornados\n $cleanedContent = preg_replace('/^```json\\s*|\\s*```$/i', '', trim($content));\n $extractedData = json_decode($cleanedContent, true);\n\n if (json_last_error() !== JSON_ERROR_NONE) {\n throw new \\RuntimeException('Falha ao decodificar JSON retornado pela IA: ' . json_last_error_msg());\n }\n\n return [\n 'entities' => $extractedData['entities'] ?? [],\n 'relationships' => $extractedData['relationships'] ?? [],\n ];\n }\n}\n"}}}}