322 lines
12 KiB
Python
Executable File
322 lines
12 KiB
Python
Executable File
"""
|
|
extract_knowledge_v3_complete.py - Extração com Validação Problema→Solução
|
|
|
|
Objetivo: Extrair APENAS conhecimento COMPLETO (Problemas + Soluções + Resultados)
|
|
|
|
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
|
|
|
|
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_v3"
|
|
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"
|
|
|
|
# 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:
|
|
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 Extraction v3"
|
|
}
|
|
|
|
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:
|
|
# Se marcou relevante mas não tem casos completos, rejeitar
|
|
knowledge['relevante'] = False
|
|
knowledge['motivo_rejeicao'] = 'sem_casos_completos'
|
|
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:
|
|
# Nenhum caso válido (problema+solução)
|
|
knowledge['relevante'] = False
|
|
knowledge['motivo_rejeicao'] = 'problemas_sem_solucoes'
|
|
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:
|
|
print(f"⚠️ JSON parse error: {source_file} ({e})")
|
|
pass
|
|
|
|
return None
|
|
|
|
elif response.status_code == 429: # Rate limit
|
|
import time
|
|
time.sleep(20)
|
|
continue
|
|
|
|
except Exception as e:
|
|
print(f"❌ Erro: {e}")
|
|
if attempt < retries - 1:
|
|
import time
|
|
time.sleep(5)
|
|
continue
|
|
|
|
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()
|
|
|
|
print(f"🔍 Analisando: {input_file.name}")
|
|
|
|
knowledge = self.extract_knowledge(content, input_file.name)
|
|
|
|
if not knowledge:
|
|
print(f" ⚠️ Falha na extração")
|
|
return False
|
|
|
|
if not knowledge.get('relevante', False):
|
|
motivo = knowledge.get('motivo_rejeicao', knowledge.get('motivo', 'sem_conteudo_util'))
|
|
print(f" ❌ Rejeitado: {motivo}")
|
|
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)
|
|
|
|
# Estatísticas
|
|
n_casos = len(knowledge.get('casos_completos', []))
|
|
categoria = knowledge.get('categoria_aplicacao', 'N/A')
|
|
tipo = knowledge.get('tipo_conteudo', 'N/A')
|
|
|
|
print(f" ✅ COMPLETO! [{categoria}] {tipo} - {n_casos} caso(s)")
|
|
|
|
import time
|
|
time.sleep(2) # Rate limiting
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
print(f" ❌ Erro: {e}")
|
|
return False
|
|
|
|
|
|
def main():
|
|
"""Função principal."""
|
|
print("═══════════════════════════════════════════════════════════")
|
|
print(" CTF CARSTUFF - EXTRAÇÃO v3 (PROBLEMA→SOLUÇÃO→RESULTADO)")
|
|
print(" TESTE: 10 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
|
|
priority_sites = [
|
|
'thehogring.com',
|
|
'forums.pelicanparts.com',
|
|
'thesamba.com',
|
|
'sailrite.com'
|
|
]
|
|
|
|
all_files = []
|
|
for site in priority_sites:
|
|
pattern = f"{site}_*.md"
|
|
files = list(input_path.glob(pattern))
|
|
all_files.extend(files)
|
|
|
|
print(f"📂 Encontrados {len(all_files)} ficheiros")
|
|
print(f"🎯 Testando com 10 ficheiros...")
|
|
print()
|
|
|
|
extractor = KnowledgeExtractor()
|
|
relevant = 0
|
|
processed = 0
|
|
rejected_incomplete = 0
|
|
|
|
for md_file in all_files[:10]: # TESTE: 10 ficheiros
|
|
result = extractor.process_file(md_file, output_path)
|
|
if result:
|
|
relevant += 1
|
|
else:
|
|
# Verificar se foi rejeitado por estar incompleto
|
|
# (para estatísticas)
|
|
rejected_incomplete += 1
|
|
processed += 1
|
|
|
|
print()
|
|
print("═══════════════════════════════════════════════════════════")
|
|
ratio = (relevant / processed * 100) if processed > 0 else 0
|
|
print(f"✓ Teste: {relevant}/{processed} completos ({ratio:.1f}%)")
|
|
print(f"❌ Rejeitados (incompletos): {rejected_incomplete}")
|
|
print(f"📁 Output: {OUTPUT_DIR}")
|
|
print()
|
|
print("CRITÉRIO: Apenas conteúdo com Problema + Solução + Resultado")
|
|
print("═══════════════════════════════════════════════════════════")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|