""" Diagnóstico e autoteste do openTARS, pra rodar no PC de quem usa: opentars --diagnostico confere o ambiente (rápido, não mexe em nada) opentars --autoteste diagnóstico + precisão da helper Murph + um teste de verdade: a IA abre a calculadora, faz 7 + 2 clicando nos botões e o resultado é conferido lendo o visor (sem depender do que a IA diz) O relatório vai pra tela e pra ~/opentars-autoteste.txt (texto puro, pra mandar pra quem estiver ajudando). Recebe o núcleo como parâmetro (não importa o tars.py: quando ele roda como script, importar de novo criaria uma segunda cópia do estado). """ import os import platform import re import shutil import sys import tempfile import time from pathlib import Path ARQUIVO_RELATORIO = Path(os.environ.get("TARS_ARQUIVO_AUTOTESTE") or Path.home() / "opentars-autoteste.txt") ACERTO_MINIMO_CLASSIFICADOR = 0.8 # "9" sozinho (não 19, 9.5 ou 0,9); "é 9." no fim da frase vale. _RE_NOVE = re.compile(r"(?= ACERTO_MINIMO_CLASSIFICADOR else None, t("auto.classificador"), t("auto.classificador_resultado", acertos=r["acertos_total"], n=r["n"], pct=f"{100 * taxa:.0f}", murph=r["acertos_local"], ms=f"{r['ms_medio']:.1f}"), ) for frase, esperado, resposta in r["erros_total"][:12]: rel.linhas.append(f" {frase} | {esperado} | {resposta}") def _janela_calculadora(core, nome): """A janela acessível da calculadora (pra ler o visor).""" try: return core.acess.achar_janela(termos=core._termos_para_janela(nome), ignorar_pids=core._pids_protegidos()) except Exception: return None def _teste_calculadora(core, rel): t = core.t rel.secao(t("auto.titulo_calculadora")) nome = t("auto.calculadora") if core.SESSAO_GRAFICA == "nenhuma": rel.item(None, t("auto.calculadora_teste"), t("aviso.sem_display")) return if not core._buscar_app_no_sistema(nome): rel.item(None, t("auto.calculadora_teste"), t("auto.sem_calculadora", nome=nome)) return print(core.c(" " + t("auto.nao_mexa"), core.Cor.AMARELO)) time.sleep(2) chamadas = [] executar_original = core.executar_ferramenta def registrar(nome_ferramenta, argumentos): resultado = executar_original(nome_ferramenta, argumentos) chamadas.append((nome_ferramenta, core._resumo_argumentos(argumentos), bool(resultado.get("sucesso")))) return resultado # A conversa do teste não entra no histórico de quem usa. conversa_antes = [dict(m) for m in core.conversa] arquivo_antes = core.ARQUIVO_SESSOES modo_antes = (core.modo_modelo, core.modelo_manual) temporario = Path(tempfile.mkdtemp(prefix="opentars-teste-")) / "sessao.json" core.ARQUIVO_SESSOES = temporario core.executar_ferramenta = registrar core.modo_modelo, core.modelo_manual = "AUTO", None core.conversa[:] = [{"role": "system", "content": core.montar_system_prompt()}] resposta = None inicio = time.time() try: resposta = core.processar_mensagem(t("auto.pedido_calculadora")) except Exception as e: rel.item(False, t("auto.calculadora_teste"), f"{type(e).__name__}: {e}") finally: segundos = time.time() - inicio core.executar_ferramenta = executar_original core.ARQUIVO_SESSOES = arquivo_antes core.modo_modelo, core.modelo_manual = modo_antes core.conversa[:] = conversa_antes shutil.rmtree(temporario.parent, ignore_errors=True) # Conferência independente: o que o visor mostra de verdade. janela = _janela_calculadora(core, nome) visor = core.acess.listar(janela)["textos"] if janela else [] visor_mostra_9 = any(_RE_NOVE.search(texto) for texto in visor) resposta_diz_9 = bool(resposta and _RE_NOVE.search(resposta)) usadas = [n for n, _r, _ok in chamadas] resumo = ", ".join( f"{n}×{usadas.count(n)}" if usadas.count(n) > 1 else n for n in dict.fromkeys(usadas) ) or "-" detalhe = t("auto.calculadora_detalhe", segundos=f"{segundos:.1f}", ferramentas=resumo) if visor_mostra_9 and resposta_diz_9: rel.item(True, t("auto.calculadora_teste"), t("auto.passou") + " · " + detalhe) elif visor_mostra_9: rel.item(None, t("auto.calculadora_teste"), t("auto.visor_ok_resposta_nao") + " · " + detalhe) elif janela is None and "click_element" not in usadas: rel.item(False, t("auto.calculadora_teste"), t("auto.sem_janela_acessivel") + " · " + detalhe) else: rel.item(False, t("auto.calculadora_teste"), t("auto.falhou", visor=" | ".join(visor[:4]) or "-") + " · " + detalhe) rel.linhas.append(" " + t("auto.resposta_ia") + ": " + " ".join((resposta or "-").split())[:300]) for nome_ferramenta, resumo_args, ok in chamadas: rel.linhas.append(f" {'✓' if ok else '✗'} {nome_ferramenta} {resumo_args}") if janela is not None or core._buscar_app_no_sistema(nome): try: core.fechar_aplicativo(nome) except Exception: pass def _print_da_tela(core, rel, t): inicio = time.time() try: img = core._capturar_tela() ms = (time.time() - inicio) * 1000 if core.SESSAO_GRAFICA == "wayland" and core._imagem_toda_preta(img): rel.item(False, t("diag.print"), core.plataforma.dica_instalar(t("diag.print_preto"))) else: rel.item(True, t("diag.print"), f"{img.size[0]}x{img.size[1]} · {ms:.0f} ms") except Exception as e: rel.item(False, t("diag.print"), str(e)[:200]) def executar(core, completo=False): """Roda o diagnóstico (e, com completo=True, o autoteste). Devolve o código de saída: 0 sem falhas, 1 com alguma.""" t = core.t rel = _Relatorio(core) print(core.c(f"\nopenTARS {core.VERSAO} — {t('auto.titulo') if completo else t('diag.titulo')}", core.Cor.NEGRITO + core.Cor.MAGENTA)) rel.linhas.append(f"openTARS {core.VERSAO} — {t('auto.titulo') if completo else t('diag.titulo')}") rel.linhas.append(time.strftime("%Y-%m-%d %H:%M")) pode_testar = diagnostico(core, rel) if completo: if pode_testar: _teste_classificador(core, rel) _teste_calculadora(core, rel) else: rel.secao(t("auto.titulo_calculadora")) rel.item(False, t("auto.calculadora_teste"), t("auto.sem_ia")) print() resumo = t("diag.resumo", falhas=rel.falhas, avisos=rel.avisos) rel.linhas.append("\n" + resumo) print(core.c(resumo, core.Cor.VERDE if not rel.falhas else core.Cor.AMARELO)) if rel.salvar(): # 5.2: o diagnóstico também salva (é o que o amigo com o Mac manda de volta) print(core.c(t("auto.relatorio_salvo", arquivo=ARQUIVO_RELATORIO), core.Cor.CINZA)) print() sys.stdout.flush() return 1 if rel.falhas else 0