File size: 5,308 Bytes
ce88da3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
efb3a40
 
 
 
 
 
 
 
 
 
ce88da3
 
 
efb3a40
 
 
ce88da3
 
efb3a40
 
ce88da3
 
efb3a40
ce88da3
 
 
 
 
 
 
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/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()