File size: 7,630 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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
"""Anti-alucinação (5.2): a confiança do modelo (logprobs do Ollama) decide se o app confere ou avisa.
  1. a conta: média dos primeiros tokens, limiar, resposta curta ou sem logprobs não conta;
  2. quais respostas valem conferir (fato em texto sim; ação, código, pedido criativo e bloco de código não);
  3. o laço do chat com um Ollama de mentira: pouca confiança + pesquisa ligada = pesquisa e responde pela busca;
     pouca confiança + sem pesquisa = a resposta fica e o app avisa; confiança alta = nada muda; TARS_CONFIANCA=0 desliga."""
import io
import json
import os
import contextlib
from _util import carregar_tars

tars = carregar_tars()
cl = tars.confianca_lib

# 1) a conta
assert cl.media([-0.1] * 20) == -0.1 and cl.media([]) is None and cl.media(None) is None
assert cl.media([-1.0] * 5) is None, "poucos tokens não dizem nada"
assert abs(cl.media([-0.1] * 10 + [-9.0] * 500) - (-0.1)) < 1e-9 or cl.media([-0.1] * 120 + [-9.0] * 500) == -0.1, "só a janela conta"
assert cl.pouca_confianca([-0.6] * 20) and not cl.pouca_confianca([-0.05] * 20) and not cl.pouca_confianca([-0.6] * 3)
assert cl.pouca_confianca([-0.3] * 20, limiar=-0.2) and not cl.pouca_confianca([-0.3] * 20, limiar=-0.5)
assert cl.logprobs_do_pedaco([{"token": "a", "logprob": -0.5}, {"token": "b"}, "x"]) == [-0.5]
print("OK: média, limiar, janela e pouco token")

# 2) o que vale conferir
longa = "A empresa foi fundada por João Carlos em 2010 e atua com software."
assert cl.vale_conferir("geral", "quem fundou a empresa Zorvintech Brasil?", longa)
assert cl.vale_conferir("simples", "qual a capital da Austrália", longa)
assert not cl.vale_conferir("acao", "abre o firefox por favor", longa)
assert not cl.vale_conferir("codigo", "como faço um loop em python", longa)
assert not cl.vale_conferir("geral", "oi", longa) and not cl.vale_conferir("geral", "quem fundou a empresa X", "Não sei.")
assert not cl.vale_conferir("geral", "escreva um poema sobre o mar", longa)
assert not cl.vale_conferir("geral", "traduz isso para o inglês por favor", longa)
assert not cl.vale_conferir("geral", "como fazer um loop", longa + "\n```python\nfor i in x: pass\n```")
assert not cl.vale_conferir("geral", "me escreva um poema sobre o mar", longa)  # 27/10: o "me" fazia a regra falhar
assert not cl.vale_conferir("geral", "me faça uma piada sobre gatos", longa)
assert not cl.vale_conferir("geral", "write a poem about the sea", longa)
assert cl.vale_conferir("geral", "quem escreveu Dom Casmurro?", longa)  # pergunta de fato com verbo parecido
print("OK: só pergunta de fato respondida em texto")

# 3) o laço do chat, com os turnos do modelo roteirizados
tars.catalogar_modelos = lambda forcar=False: {}
tars.carregar_modelo = lambda mod, silencioso=False: True
tars.contexto_ia = lambda: 8192
tars.modo_modelo = "MANUAL"
tars.listar_janelas_x = lambda: None
PERGUNTA = "quem fundou a empresa Zorvintech Brasil e quando?"
INVENTADO = "A empresa Zorvintech Brasil foi fundada por João Carlos em 2010."
BUSCA = {"content": "", "tool_calls": [{"function": {"name": "search_web", "arguments": {"query": "Zorvintech Brasil fundador"}}}],
         "logprobs": []}
CONFERIDO = {"content": "Não encontrei nada confiável sobre a Zorvintech Brasil nos resultados.", "tool_calls": [], "logprobs": [-0.05] * 30}


def rodar(texto, turnos, pesquisa, tarefa="geral"):
    roteiro = iter(turnos)
    vistas = []
    tars._pesquisa_ligada = lambda: pesquisa
    tars.executar_ferramenta = lambda nome, a: {"sucesso": True, "mensagem": "SUCCESS: searched. Results: nothing found."}
    def turno(modelo, mensagens, tools, pensar=True):
        vistas.append([m.get("content") for m in mensagens if m.get("role") == "user"][-1])
        return next(roteiro), None
    tars._executar_turno_streaming = turno
    tars.conversa = []
    saida = io.StringIO()
    with contextlib.redirect_stdout(saida):
        resposta = tars.perguntar_modelo("ProjectOpenTARS/Gargantua-2:latest", texto, tarefa=tarefa)
    return resposta, saida.getvalue(), vistas


# a) pouca confiança + pesquisa ligada: o app manda conferir pela busca, e a resposta final é a da busca
r, saida, vistas = rodar(PERGUNTA, [{"content": INVENTADO, "tool_calls": [], "logprobs": [-0.9] * 30}, BUSCA, CONFERIDO], True)
assert r == CONFERIDO["content"], r
assert tars.t("msg.verificando_fato") in saida and tars.t("msg.pouca_confianca") not in saida
assert any("Call search_web" in v for v in vistas), vistas
print("OK: pouca confiança com a pesquisa ligada: confere pela busca e responde com ela")

# b) pouca confiança sem pesquisa: a resposta fica e o app avisa embaixo
r, saida, vistas = rodar(PERGUNTA, [{"content": INVENTADO, "tool_calls": [], "logprobs": [-0.9] * 30}], False)
assert r == INVENTADO and tars.t("msg.pouca_confianca") in saida and len(vistas) == 1
print("OK: pouca confiança sem pesquisa: a resposta fica e o app avisa")

# c) confiança alta: nada muda
r, saida, vistas = rodar(PERGUNTA, [{"content": "Canberra é a capital da Austrália, e fica no território da capital.", "tool_calls": [],
                                      "logprobs": [-0.02] * 30}], True)
assert tars.t("msg.pouca_confianca") not in saida and tars.t("msg.verificando_fato") not in saida and len(vistas) == 1
print("OK: confiança alta: a resposta sai como veio")

# d) sem logprobs (Ollama antigo, modelo de API) ou desligado por TARS_CONFIANCA=0: nada muda
r, saida, _ = rodar(PERGUNTA, [{"content": INVENTADO, "tool_calls": []}], True)
assert tars.t("msg.verificando_fato") not in saida and r == INVENTADO
os.environ["TARS_CONFIANCA"] = "0"
try:
    r, saida, _ = rodar(PERGUNTA, [{"content": INVENTADO, "tool_calls": [], "logprobs": [-0.9] * 30}], True)
    assert r == INVENTADO and tars.t("msg.verificando_fato") not in saida and tars.t("msg.pouca_confianca") not in saida
finally:
    del os.environ["TARS_CONFIANCA"]
print("OK: sem logprobs ou com TARS_CONFIANCA=0 o app não mexe")

# e) pedido de ação não é conferido, mesmo com confiança baixa (o app tem as outras travas para ação)
TURNO_ACAO = {"content": "Pronto, abri o navegador e entrei no GitHub, tudo certo.", "tool_calls": [], "logprobs": [-0.9] * 30}
r, saida, _ = rodar("abre o navegador e entra no github", [TURNO_ACAO] * 4, True, tarefa="acao")
assert tars.t("msg.verificando_fato") not in saida and tars.t("msg.pouca_confianca") not in saida
print("\ntest_confianca: TUDO OK.")

# f) 27/10: a mesma busca não roda duas vezes no pedido, e há um teto de buscas por pedido
execucoes = []
tars._pesquisa_ligada = lambda: True
tars.executar_ferramenta = lambda nome, a: (execucoes.append(json.dumps(a, sort_keys=True)),
                                           {"sucesso": True, "mensagem": "SUCCESS: searched. Results: nothing found."})[1]
def busca(q):
    return {"content": "", "tool_calls": [{"function": {"name": "search_web", "arguments": {"query": q}}}], "logprobs": []}
def turnos(lista):
    roteiro = iter(lista)
    tars._executar_turno_streaming = lambda modelo, mensagens, tools, pensar=True: (next(roteiro), None)
    tars.conversa = []
    with contextlib.redirect_stdout(io.StringIO()):
        return tars.perguntar_modelo("ProjectOpenTARS/Gargantua-2:latest", "quem fundou a empresa Zorvintech Brasil?", tarefa="geral")
r = turnos([busca("a"), busca("a"), busca("a"), CONFERIDO])
assert len(execucoes) == 1 and r == CONFERIDO["content"], (execucoes, r)
execucoes.clear()
r = turnos([busca("q1"), busca("q2"), busca("q3"), busca("q4"), busca("q5"), CONFERIDO])
assert len(execucoes) == 3, execucoes
print("OK: busca repetida não roda de novo, e o pedido para em três buscas")