File size: 3,568 Bytes
2695555
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
"""openTARS: sinal de que uma resposta de conversa pode ser invenção (anti-alucinação, 5.2).

O Ollama devolve o logprob de cada token que o modelo escreveu. Quando o Gargantua responde de cabeça a algo que não sabe
(pessoa, empresa ou livro que não existe, um número, um dado de agora), a média desses logprobs tende a cair. Medido com
`fonte/kit/prova_alucinacao.py` (modo app, temperatura 0,1, 120 primeiros tokens): AUC 0,93 nas 48 perguntas em que o limiar
foi escolhido, mas só 0,73 (2B) e 0,70 (Lite) nas 28 perguntas de validação, que ele nunca viu. Com o limiar -0,25 a validação
pega 7 de 10 respostas erradas do 2B e 17 de 21 do Lite, e marca 3 de 10 certas do 2B e 2 de 5 do Lite. É um sinal moderado,
não um detector: por isso o app só PESQUISA (pesquisa web ligada) ou AVISA, nunca bloqueia nem reescreve a resposta. Sem geração
extra: custo zero. Só biblioteca padrão, sem Qt. Reconferir com a prova do Lab depois de cada treino.
"""
import os
import re
import unicodedata

JANELA = 120          # tokens do começo da resposta que entram na média (os mesmos da prova)
LIMIAR = -0.25        # média abaixo disso = pouca confiança
MIN_TOKENS = 8        # resposta curta demais não diz nada ("oi!")
MIN_CARACTERES = 25

_NAO_E_FATO = re.compile(  # pedido criativo ou de texto, em qualquer posição (antes só valia no começo da frase)
    r"\b(escreva|escreve|poema|poesia|conto|historia|cancao|musica|letra|verso|rima|piada|crie|cria|invente|inventa|"
    r"faca|faz|gere|gera|sugira|traduza|traduz|resuma|resume|reescreva|corrija|"
    r"write|poem|poetry|story|joke|song|lyrics|translate|summari[sz]e|rewrite|fix|improve|draft|make up|tell me)\b", re.I)
_PERGUNTA = re.compile(  # a forma de pergunta de fato: termina com "?" ou começa com palavra interrogativa
    r"\?\s*$|^(quem|qual|quais|quando|quanto|quanta|quantos|quantas|onde|por que|porque|o que|que|como|existe|"
    r"who|what|when|where|which|how|is|are|does|did)\b")


def ligado():
    """TARS_CONFIANCA=0 desliga (por exemplo, para medir o modelo sem o app mexer na resposta)."""
    return os.environ.get("TARS_CONFIANCA", "1") != "0"


def _sem_acento(texto):
    t = unicodedata.normalize("NFKD", str(texto or ""))
    return "".join(c for c in t if not unicodedata.combining(c)).lower().strip()


def media(logprobs):
    """Média dos logprobs dos primeiros JANELA tokens (None se não vieram: Ollama antigo ou modelo de API)."""
    valores = [x for x in (logprobs or [])[:JANELA] if isinstance(x, (int, float))]
    return sum(valores) / len(valores) if len(valores) >= MIN_TOKENS else None


def pouca_confianca(logprobs, limiar=None):
    m = media(logprobs)
    return m is not None and m < (LIMIAR if limiar is None else limiar)


def vale_conferir(tarefa, pedido, resposta):
    """Só pergunta de fato respondida em texto: nada de ação no computador, código ou visão, nem pedido de texto criativo
    (escrever, traduzir, resumir), nem resposta com bloco de código."""
    if tarefa in ("acao", "visao", "codigo", "tecnico"):
        return False
    if len((resposta or "").strip()) < MIN_CARACTERES or "```" in (resposta or ""):
        return False
    pedido = _sem_acento(pedido)
    return len(pedido.split()) >= 3 and _PERGUNTA.search(pedido) is not None and not _NAO_E_FATO.search(pedido)


def logprobs_do_pedaco(pedaco):
    """Os logprobs de um pedaço do streaming do Ollama ([{'token','logprob',...}])."""
    return [x["logprob"] for x in (pedaco or []) if isinstance(x, dict) and isinstance(x.get("logprob"), (int, float))]