""" extract_knowledge_batch3_reddit.py - Extração Final Batch 3 + Reddit Extrai conhecimento de: - 65 ficheiros triumphexp.com*.md (Batch 3) - 2 ficheiros reddit_*.md (Reddit scraping) Modelo: google/gemini-2.5-flash-lite (económico e rápido) 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 import glob 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 KnowledgeExtractorFinal: 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-lite" # Modelo económico # Estatísticas self.stats = { 'batch3': {'total': 0, 'processados': 0, 'relevantes': 0, 'erros': 0}, 'reddit': {'total': 0, 'processados': 0, 'relevantes': 0, 'erros': 0} } # Prompt REFORÇADO 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} - Textos muito curtos (<200 caracteres): {"relevante": false} - Apenas CASOS COMPLETOS com problema→solução→resultado devem ser extraídos. """ def extract_knowledge(self, content: str) -> Optional[Dict]: """Extrai conhecimento via OpenRouter API.""" headers = { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", "HTTP-Referer": "https://descomplicar.pt", "X-Title": "CTF Knowledge Base Extractor" } payload = { "model": self.model, "messages": [ { "role": "system", "content": self.extraction_prompt }, { "role": "user", "content": f"Analisa este conteúdo e extrai conhecimento técnico:\n\n{content[:12000]}" } ], "temperature": 0.3, "max_tokens": 2000, "response_format": {"type": "json_object"} } try: response = requests.post(self.api_url, headers=headers, json=payload, timeout=60) response.raise_for_status() result = response.json() content_text = result['choices'][0]['message']['content'] # Parse JSON knowledge = json.loads(content_text) return knowledge except Exception as e: print(f"⚠️ Erro API: {e}") return None def process_file(self, filepath: Path, batch_type: str): """Processa um ficheiro MD e extrai conhecimento.""" print(f"\n📄 A processar: {filepath.name}") self.stats[batch_type]['total'] += 1 try: with open(filepath, 'r', encoding='utf-8') as f: content = f.read() # Validação básica if len(content) < 200: print(f" ⏭️ Ficheiro muito pequeno (<200 chars), ignorado") return # Extração via API knowledge = self.extract_knowledge(content) if not knowledge: print(f" ❌ Erro ao extrair conhecimento") self.stats[batch_type]['erros'] += 1 return # Verificar relevância if not knowledge.get('relevante', False): print(f" ⏭️ Conteúdo não relevante (sem problema→solução)") return # Verificar casos completos casos = knowledge.get('casos_completos', []) if not casos or len(casos) == 0: print(f" ⏭️ Sem casos completos extraídos") return # Guardar JSON output_file = OUTPUT_DIR + f"/knowledge_{filepath.stem}.json" with open(output_file, 'w', encoding='utf-8') as f: json.dump(knowledge, f, ensure_ascii=False, indent=2) print(f" ✅ Extraído: {len(casos)} casos completos → {filepath.stem}.json") self.stats[batch_type]['processados'] += 1 self.stats[batch_type]['relevantes'] += 1 # Rate limiting time.sleep(1) except Exception as e: print(f" ❌ Erro ao processar: {e}") self.stats[batch_type]['erros'] += 1 def run(self): """Executa extração completa Batch 3 + Reddit.""" print("═══════════════════════════════════════════════════════════") print(" EXTRAÇÃO FINAL - BATCH 3 + REDDIT") print(" Modelo: google/gemini-2.5-flash-lite") print(" Descomplicar® Crescimento Digital") print("═══════════════════════════════════════════════════════════") print() # Criar output dir se não existir Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True) # 1. BATCH 3 - Ficheiros triumphexp print("🔵 BATCH 3: A processar ficheiros triumphexp...") print("-" * 60) triumphexp_files = sorted(glob.glob(f"{INPUT_DIR}/triumphexp.com*.md")) print(f"📊 Encontrados {len(triumphexp_files)} ficheiros triumphexp\n") for filepath in triumphexp_files: self.process_file(Path(filepath), 'batch3') # 2. REDDIT - Ficheiros reddit print("\n🟠 REDDIT: A processar ficheiros Reddit...") print("-" * 60) reddit_files = sorted(glob.glob(f"{INPUT_DIR}/reddit_*.md")) print(f"📊 Encontrados {len(reddit_files)} ficheiros Reddit\n") for filepath in reddit_files: self.process_file(Path(filepath), 'reddit') # Resumo final print("\n═══════════════════════════════════════════════════════════") print(" EXTRAÇÃO CONCLUÍDA") print("═══════════════════════════════════════════════════════════") print() print("📊 BATCH 3 (Triumphexp):") print(f" Total ficheiros: {self.stats['batch3']['total']}") print(f" Processados com sucesso: {self.stats['batch3']['processados']}") print(f" Relevantes: {self.stats['batch3']['relevantes']}") print(f" Erros: {self.stats['batch3']['erros']}") print() print("📊 REDDIT:") print(f" Total ficheiros: {self.stats['reddit']['total']}") print(f" Processados com sucesso: {self.stats['reddit']['processados']}") print(f" Relevantes: {self.stats['reddit']['relevantes']}") print(f" Erros: {self.stats['reddit']['erros']}") print() total_novos = self.stats['batch3']['relevantes'] + self.stats['reddit']['relevantes'] print(f"🎯 TOTAL NOVOS CASOS: {total_novos}") print(f"📁 Ficheiros guardados em: {OUTPUT_DIR}") print() if __name__ == "__main__": extractor = KnowledgeExtractorFinal() extractor.run()