decisum / scripts /servir_ensemble.py
lordpizzo's picture
servir_*: device auto (cuda->mps->cpu) — comando do card funciona em qualquer maquina
efb3a40 verified
Raw History Blame Contribute Delete
5.31 kB
#!/usr/bin/env python3
"""
servir_ensemble.py — API do ensemble (média das probs calibradas de N decisors).
Mesmos endpoints do servir_decisor (GET /saude, POST /decidir); carrega 2+
checkpoints, roda cada pergunta em cada modelo e devolve a probabilidade
MÉDIA (calibrada por modelo antes da média — média de distribuições
calibradas continua calibrada). Roteamento: mínima confiança da média.
Uso (host .144, GPU 1, porta de produção ou teste):
python3 scripts/servir_ensemble.py \
--checkpoints models/decisor-v10 models/decisor-v11_cons \
--device cuda --porta 8101
"""
import argparse
import json
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from chamados_para_decisor import CATEGORIAS, DECISOES, URGENCIA # noqa: E402
from inferencia_decisor import DecisorCPU # noqa: E402
PERGUNTAS = {
"categoria": {"tipo": "choice",
"instrucao": "Qual a categoria principal do chamado?",
"opcoes": CATEGORIAS},
"decisao": {"tipo": "choice",
"instrucao": "Qual decisão tomar para este caso?",
"opcoes": DECISOES},
"urgencia": {"tipo": "score",
"instrucao": "Qual a urgência? (0=baixa, 3=crítica)",
"opcoes": URGENCIA},
}
DECISORES = []
def decidir(estado: str, limiar: float) -> dict:
respostas, confs = {}, []
for nome, pergunta in PERGUNTAS.items():
ops = pergunta["opcoes"]
media = None
votos = []
for d in DECISORES:
r = d["modelo"].prever(estado, pergunta)
probs = [r["distribuicao"].get(op, 0.0) for op in ops]
votos.append(probs)
votos.append(None) # placeholder p/ manter par modelo/(peso)
votos.pop()
media = [sum(v[k] for v in votos) / len(votos) for k in range(len(ops))]
i = max(range(len(ops)), key=lambda k: media[k])
respostas[nome] = {
"resposta": ops[i],
"confianca": round(media[i], 4),
"distribuicao": {op: round(media[k], 4)
for op, k in zip(ops, range(len(ops)))},
}
confs.append(media[i])
conf = min(confs)
return {"respostas": respostas,
"confianca_roteamento": round(conf, 4),
"limiar": limiar,
"destino": "automatico" if conf >= limiar else "system2"}
class Handler(BaseHTTPRequestHandler):
def _json(self, obj, status=200):
corpo = json.dumps(obj, ensure_ascii=False).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(corpo)))
self.end_headers()
self.wfile.write(corpo)
def log_message(self, *a):
pass
def do_GET(self):
if self.path == "/saude":
self._json({
"status": "ok",
"ensemble": [d["nome"] for d in DECISORES],
"n_modelos": len(DECISORES),
"calibrado": all(bool(d["modelo"].temps) for d in DECISORES),
"categorias": len(CATEGORIAS),
})
return
self._json({"erro": "use GET /saude ou POST /decidir"}, 404)
def do_POST(self):
if self.path != "/decidir":
self._json({"erro": "rota desconhecida"}, 404)
return
try:
corpo = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
estado = str(corpo["estado"]).strip()
if not estado:
raise ValueError("estado vazio")
limiar = float(corpo.get("limiar", 0.8))
if not 0.0 <= limiar <= 1.0:
raise ValueError("limiar fora de [0,1]")
except (KeyError, ValueError, TypeError) as e:
self._json({"erro": f"payload inválido: {e}",
"formato": {"estado": "texto", "limiar": 0.8}}, 400)
return
self._json(decidir(estado, limiar))
def _device_auto() -> str:
"""Escolhe o melhor device disponível: cuda → mps → cpu."""
import torch
if torch.cuda.is_available():
return "cuda"
if getattr(torch.backends, "mps", None) and torch.backends.mps.is_available():
return "mps"
return "cpu"
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--checkpoints", nargs="+", required=True)
ap.add_argument("--device", default="auto",
choices=["auto", "cuda", "mps", "xpu", "cpu"],
help="default: auto (cuda → mps → cpu)")
ap.add_argument("--porta", type=int, default=8101)
args = ap.parse_args()
device = _device_auto() if args.device == "auto" else args.device
print(f"device: {device}")
for ck in args.checkpoints:
DECISORES.append({"nome": ck.rstrip("/").split("/")[-1],
"modelo": DecisorCPU(ck, device=device)})
print(f"✓ carregado: {ck}")
print(f"ensemble de {len(DECISORES)} modelos na porta {args.porta}")
ThreadingHTTPServer(("0.0.0.0", args.porta), Handler).serve_forever()
if __name__ == "__main__":
main()