""" extract_knowledge_production.py - Extração COMPLETA com Validação Problema→Solução Objetivo: Processar TODOS os 3,285 ficheiros MD com critério rigoroso Author: Descomplicar® Crescimento Digital Link: https://descomplicar.pt Copyright: 2025 Descomplicar® """ import os import json import requests from pathlib import Path from typing import Dict, Optional from dotenv import load_dotenv import time from datetime import datetime load_dotenv() # Configurações INPUT_DIR = "/media/ealmeida/Dados/GDrive/Cloud/Clientes_360/CTF_Carstuff/KB/Scrapper/sites/output_md" OUTPUT_DIR = "/media/ealmeida/Dados/GDrive/Cloud/Clientes_360/CTF_Carstuff/KB/Scrapper/sites/knowledge_base_final" API_KEY = os.getenv("OPENROUTER_API_KEY") class KnowledgeExtractor: def __init__(self): self.api_key = API_KEY self.api_url = "https://openrouter.ai/api/v1/chat/completions" self.model = "google/gemini-2.5-flash" # Estatísticas self.stats = { 'total': 0, 'processados': 0, 'relevantes': 0, 'rejeitados_pequenos': 0, 'rejeitados_incompletos': 0, 'erros': 0 } # Prompt REFORÇADO - Exige Problemas + Soluções self.extraction_prompt = """ És um especialista em estofamento automotivo, náutico, ferroviário e aeronáutico. ⚠️ CRITÉRIO CRÍTICO DE RELEVÂNCIA: O conteúdo SÓ É RELEVANTE se contiver o fluxo COMPLETO: PROBLEMA → SOLUÇÃO → RESULTADO Se o texto apenas descreve problemas SEM as suas soluções, retorna: {{"relevante": false}} IGNORAR COMPLETAMENTE: - Navegação de site - Publicidade - Comentários genéricos ("obrigado", "bom post") - Conversas off-topic - Problemas mencionados sem soluções correspondentes - Links sem contexto EXTRAIR APENAS SE EXISTIR FLUXO COMPLETO: 1. **Problema técnico específico** identificado claramente 2. **Solução prática** aplicada ou recomendada para esse problema 3. **Resultado obtido** ou esperado (se mencionado) FORMATO JSON DE SAÍDA: {{ "relevante": true/false, "categoria_aplicacao": "automovel|automovel-classico|mobiliario|nautica|ferroviaria|aeronautica|geral", "tipo_conteudo": "problema-tecnico|tutorial|caso-pratico|comparacao-materiais", "casos_completos": [ {{ "problema": {{ "descricao": "Problema específico identificado", "contexto": "Tipo de veículo/aplicação/situação", "severidade": "baixa|media|alta" }}, "solucao": {{ "material_usado": "Material específico aplicado", "tecnica": "Técnica ou método usado", "passos": "Passos principais (se mencionados)" }}, "resultado": {{ "obtido": "Resultado concreto alcançado", "qualidade": "Avaliação da solução" }} }} ], "materiais_discutidos": {{ "principais": ["materiais eficazes mencionados"], "nao_recomendados": ["materiais que falharam ou são evitados"] }}, "keywords_tecnicas": ["termos", "tecnicos", "relevantes"], "aplicabilidade": ["tipos de veículos/situações"], "nivel_expertise": "iniciante|intermedio|avancado" }} ⚠️ IMPORTANTE: - Se o texto só menciona problemas sem soluções: {{"relevante": false}} - Se menciona soluções sem contexto de problema: {{"relevante": false}} - Só marca relevante se tiver AMBOS problema E solução claramente relacionados TEXTO PARA ANALISAR: --- {content} --- Responde APENAS com o JSON, sem texto adicional. """ def extract_knowledge(self, content: str, source_file: str, retries: int = 3) -> Optional[Dict]: """Extrai conhecimento útil usando AI.""" # Pré-filtro if len(content) < 1000: self.stats['rejeitados_pequenos'] += 1 return {"relevante": False, "motivo": "conteudo_pequeno"} if len(content) > 50000: content = content[:50000] for attempt in range(retries): try: headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", "HTTP-Referer": "https://descomplicar.pt", "X-Title": "CTF Knowledge Production" } prompt = self.extraction_prompt.format(content=content) data = { "model": self.model, "messages": [{"role": "user", "content": prompt}], "temperature": 0.2, "max_tokens": 3500 } response = requests.post( self.api_url, headers=headers, json=data, timeout=60 ) if response.status_code == 200: result = response.json() if 'choices' in result and len(result['choices']) > 0: content_text = result['choices'][0]['message']['content'] # Parsing robusto try: # Remover blocos markdown if '```json' in content_text: content_text = content_text.split('```json')[1].split('```')[0] elif '```' in content_text: content_text = content_text.split('```')[1].split('```')[0] # Limpeza content_text = content_text.strip() # Parse knowledge = json.loads(content_text) # ✅ VALIDAÇÃO CRÍTICA: Verificar completude if knowledge.get('relevante', False): # Verificar se tem casos completos (problema + solução) casos = knowledge.get('casos_completos', []) if not casos or len(casos) == 0: knowledge['relevante'] = False knowledge['motivo_rejeicao'] = 'sem_casos_completos' self.stats['rejeitados_incompletos'] += 1 return knowledge # Verificar se cada caso tem problema E solução casos_validos = [] for caso in casos: tem_problema = bool(caso.get('problema', {}).get('descricao')) tem_solucao = bool(caso.get('solucao', {}).get('material_usado') or caso.get('solucao', {}).get('tecnica')) if tem_problema and tem_solucao: casos_validos.append(caso) if not casos_validos: knowledge['relevante'] = False knowledge['motivo_rejeicao'] = 'problemas_sem_solucoes' self.stats['rejeitados_incompletos'] += 1 return knowledge # Atualizar com apenas casos válidos knowledge['casos_completos'] = casos_validos return knowledge except json.JSONDecodeError as e: # Fallback: extrair { ... } manualmente try: start = content_text.find('{') end = content_text.rfind('}') + 1 if start != -1 and end > start: clean_json = content_text[start:end] knowledge = json.loads(clean_json) return knowledge except: self.stats['erros'] += 1 return None return None elif response.status_code == 429: # Rate limit time.sleep(20) continue except Exception as e: if attempt < retries - 1: time.sleep(5) continue else: self.stats['erros'] += 1 return None def process_file(self, input_file: Path, output_dir: Path) -> bool: """Processa um ficheiro e extrai conhecimento.""" try: with open(input_file, 'r', encoding='utf-8') as f: content = f.read() knowledge = self.extract_knowledge(content, input_file.name) if not knowledge: return False if not knowledge.get('relevante', False): return False # RELEVANTE - Guardar! output_file = output_dir / f"knowledge_{input_file.stem}.json" with open(output_file, 'w', encoding='utf-8') as f: json.dump(knowledge, f, indent=2, ensure_ascii=False) self.stats['relevantes'] += 1 # Rate limiting time.sleep(2) return True except Exception as e: self.stats['erros'] += 1 return False def print_progress(self, current: int, total: int): """Imprime progresso.""" percent = (current / total * 100) if total > 0 else 0 relevance_rate = (self.stats['relevantes'] / current * 100) if current > 0 else 0 print(f"\r⏳ Progresso: {current}/{total} ({percent:.1f}%) | " f"✅ Relevantes: {self.stats['relevantes']} ({relevance_rate:.1f}%) | " f"❌ Rejeitados: {self.stats['rejeitados_incompletos']} | " f"⚠️ Erros: {self.stats['erros']}", end='', flush=True) def main(): """Função principal.""" print("═══════════════════════════════════════════════════════════") print(" CTF CARSTUFF - EXTRAÇÃO PRODUÇÃO (PROBLEMA→SOLUÇÃO)") print(" PROCESSAMENTO COMPLETO: ~3,285 ficheiros") print("═══════════════════════════════════════════════════════════") print() if not API_KEY: print("❌ OPENROUTER_API_KEY não configurada no .env") return input_path = Path(INPUT_DIR) output_path = Path(OUTPUT_DIR) if not input_path.exists(): print(f"❌ Diretório não existe: {INPUT_DIR}") return output_path.mkdir(parents=True, exist_ok=True) # Sites prioritários (ordenados por volume de ficheiros) priority_sites = [ # Batch 1 - Já processados (118 casos extraídos) 'thehogring.com', # 264 ficheiros → 105 casos 'forums.pelicanparts.com', # 1636 ficheiros → 11 casos 'thesamba.com', # 158 ficheiros → 2 casos 'sailrite.com', # 41 ficheiros → 0 casos # Batch 2 - Novos sites (1186 ficheiros a processar) 'relicate.com', # 359 ficheiros - PRIORIDADE ALTA 'trawlerforum.com', # 193 ficheiros 'alfabb.com', # 165 ficheiros 'vansairforce.net', # 132 ficheiros 'mgexp.com', # 91 ficheiros 'cruisersforum.com', # 78 ficheiros 'ultrafabricsinc.com', # 51 ficheiros 'sunbrella.com', # 46 ficheiros 'camirafabrics.com', # 44 ficheiros 'keystonbros.com' # 27 ficheiros ] all_files = [] for site in priority_sites: pattern = f"{site}_*.md" files = list(input_path.glob(pattern)) all_files.extend(files) total_files = len(all_files) print(f"📂 Encontrados {total_files} ficheiros") print(f"🎯 Iniciando extração completa...") print() extractor = KnowledgeExtractor() extractor.stats['total'] = total_files start_time = datetime.now() for idx, md_file in enumerate(all_files, 1): extractor.process_file(md_file, output_path) extractor.stats['processados'] = idx # Atualizar progresso a cada 10 ficheiros if idx % 10 == 0 or idx == total_files: extractor.print_progress(idx, total_files) end_time = datetime.now() duration = end_time - start_time print("\n") print("═══════════════════════════════════════════════════════════") print(" EXTRAÇÃO CONCLUÍDA") print("═══════════════════════════════════════════════════════════") print(f"📊 Total processado: {extractor.stats['processados']}/{total_files}") print(f"✅ Conhecimento COMPLETO: {extractor.stats['relevantes']} ficheiros") print(f"❌ Rejeitados (pequenos): {extractor.stats['rejeitados_pequenos']}") print(f"❌ Rejeitados (incompletos): {extractor.stats['rejeitados_incompletos']}") print(f"⚠️ Erros: {extractor.stats['erros']}") print() relevance_rate = (extractor.stats['relevantes'] / extractor.stats['processados'] * 100) if extractor.stats['processados'] > 0 else 0 print(f"📈 Taxa de relevância: {relevance_rate:.1f}%") print(f"⏱️ Duração: {duration}") print(f"📁 Output: {OUTPUT_DIR}") print() print("CRITÉRIO: Apenas Problema + Solução + Resultado") print("═══════════════════════════════════════════════════════════") if __name__ == '__main__': main()