File size: 25,189 Bytes
a469aa7
89e0793
 
 
a469aa7
 
89e0793
 
 
 
 
 
 
 
a469aa7
89e0793
 
 
 
 
 
 
dd1ee44
 
b881291
 
 
dd1ee44
89e0793
 
1b26be5
dd1ee44
1b26be5
89e0793
 
a469aa7
 
 
 
 
 
 
497a713
1b26be5
dd1ee44
1b26be5
 
 
497a713
1b26be5
 
497a713
a469aa7
 
89e0793
a469aa7
 
 
 
89e0793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a469aa7
89e0793
a469aa7
 
 
89e0793
 
a469aa7
89e0793
 
 
a469aa7
89e0793
 
a469aa7
 
89e0793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a469aa7
89e0793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a469aa7
89e0793
 
 
 
a469aa7
89e0793
 
 
 
 
a469aa7
89e0793
 
 
 
 
 
 
 
 
 
a469aa7
89e0793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1b26be5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89e0793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
dd1ee44
 
 
 
 
89e0793
 
1b26be5
 
89e0793
 
 
 
 
1b26be5
 
 
89e0793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a469aa7
89e0793
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a469aa7
89e0793
 
 
 
 
 
 
 
 
 
a469aa7
89e0793
 
 
 
 
 
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
"""Jevásio on Hugging Face Spaces (Gradio, ZeroGPU).

A state and a set of typed questions in, a probability per option out, in one forward pass. The model is loaded through
Kev's own loader (kev.checkpoint), the same path `python -m kev.serve --run TigreGotico/jevasio-0.8b` takes, and
requests go through kev.api and kev.model.admit exactly as /v1/systemone does. Both sizes load in bf16 at start-up, and
each forward pass runs inside @spaces.GPU, so a request holds a GPU only for that pass.
"""
import html
import json
import os
import time
from dataclasses import replace

import gradio as gr
import spaces
import torch
from pydantic import ValidationError

from kev.api import SystemOneRequest, output_tokens, render, to_answers, to_record
from kev.checkpoint import Checkpoint, LoadOptions
from kev.model import admit

MODELS = {"Jevásio-4B-V2": "TigreGotico/jevasio-4b-v2", "Jevásio-0.8B-V2": "TigreGotico/jevasio-0.8b-v2",
          "Jevásio-4B": "TigreGotico/jevasio-4b", "Jevásio-0.8B": "TigreGotico/jevasio-0.8b"}
# JEVASIO_MODELS="Jevásio-4B-V2,Jevásio-0.8B-V2" serves only the named models.
if os.environ.get("JEVASIO_MODELS"):
    MODELS = {name: MODELS[name] for name in os.environ["JEVASIO_MODELS"].split(",")}
DEFAULT = "Jevásio-4B-V2"
LINK_08B = "https://huggingface.co/TigreGotico/jevasio-0.8b"
LINK_4B = "https://huggingface.co/TigreGotico/jevasio-4b"
LINK_08B_V2 = "https://huggingface.co/TigreGotico/jevasio-0.8b-v2"
LINK_4B_V2 = "https://huggingface.co/TigreGotico/jevasio-4b-v2"
LINK_DATA_V2 = "https://huggingface.co/datasets/TigreGotico/jevasio-decisions-v2"
LINK_DATA = "https://huggingface.co/datasets/TigreGotico/jevasio-decisions"

LOADED = {}
for name, repo in MODELS.items():
    t0 = time.perf_counter()
    checkpoint = Checkpoint(repo)
    opts = LoadOptions.from_env()
    if opts.dtype is None:
        opts = replace(opts, dtype=torch.bfloat16)
    # ZeroGPU has no GPU at start-up: load on the CPU, then move. ZeroGPU defers the move until a @spaces.GPU call, and
    # Kev builds its input tensors on model.device. Off ZeroGPU, JEVASIO_DEVICE_<SIZE> places a model, e.g.
    # JEVASIO_DEVICE_4B=cpu keeps the 4B in RAM on a laptop GPU too small for it (JEVASIO_DEVICE_4B_V2 for the 4B V2).
    key = name.split("-", 1)[1].replace(".", "").replace("-", "_").upper()
    device = "cuda" if os.environ.get("SPACES_ZERO_GPU") else os.environ.get(
        f"JEVASIO_DEVICE_{key}", "cuda" if torch.cuda.is_available() else "cpu")
    tok, model = checkpoint.load("cpu", opts)
    model.to(device)
    model.device = torch.device(device)
    LOADED[name] = tok, model
    print(f"[jevasio] {repo}: base={checkpoint.meta.base}@{checkpoint.meta.base_revision} dtype={model.dtype} "
          f"temperature={model.head.temperature:.2f} loaded in {time.perf_counter() - t0:.0f} s", flush=True)


@spaces.GPU(duration=30)
def forward(name, enc):
    return [p.float().cpu() for p in LOADED[name][1].probs(enc)]


class RequestError(ValueError):
    """A request the model cannot answer: bad JSON, a bad question, or a state that is too long."""


def parse_request(payload):
    try:
        return SystemOneRequest.model_validate(payload)
    except ValidationError as e:
        first = e.errors()[0]
        where = ".".join(str(x) for x in first.get("loc", ()))
        raise RequestError(f"Pedido inválido em `{where}`: {first.get('msg')}") from None


def systemone(req, name=DEFAULT):
    """The /v1/systemone response body kev.serve returns for this request, plus the latency of the forward pass."""
    if name not in LOADED:
        raise RequestError(f"Modelo desconhecido: {name}. Opções: {', '.join(MODELS)}.")
    tokenizer, model = LOADED[name]
    rec, meta = to_record(req)
    try:
        enc = admit(model, tokenizer, rec)
    except ValueError as e:
        raise RequestError(str(e)) from None
    start = time.perf_counter()
    probs = forward(name, enc)
    ms = (time.perf_counter() - start) * 1000
    answers = to_answers([p.tolist() for p in probs], meta)
    return {"model": MODELS[name], "answers": answers,
            "usage": {"input_tokens": len(enc["ids"]), "output_tokens": output_tokens(tokenizer, answers)},
            "latency_ms": round(ms, 1)}


# ------------------------------------------------------------------ rendering

CARD = ("border:1px solid var(--border-color-primary);border-radius:10px;padding:12px 14px;margin-bottom:10px;"
        "background:var(--background-fill-secondary);")
TRACK = "position:relative;display:block;height:8px;border-radius:999px;background:var(--border-color-primary);overflow:hidden;"
TYPE_LABEL = {"choice": "escolha", "noul": "sim/não", "score": "escala"}


def bar(label, p, top):
    fill = "var(--color-accent)" if top else "var(--body-text-color-subdued)"
    weight = "600" if top else "400"
    width = max(0.0, min(1.0, p)) * 100
    return ('<div style="display:grid;grid-template-columns:minmax(0,12rem) minmax(0,1fr) 3.2rem;align-items:center;'
            'gap:0 12px;font-size:13px;line-height:24px">'
            f'<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:{weight}" '
            f'title="{html.escape(label)}">{html.escape(label)}</span>'
            f'<span style="{TRACK}"><span style="position:absolute;top:0;bottom:0;left:0;border-radius:999px;'
            f'background:{fill};width:{width:.1f}%"></span></span>'
            f'<span style="text-align:right;font-variant-numeric:tabular-nums;font-weight:{weight}">{p:.2f}</span></div>')


def render_answers(req, resp):
    usage = resp["usage"]
    parts = ['<div style="font-size:12px;color:var(--body-text-color-subdued);margin:6px 0 10px">'
             f'{len(resp["answers"])} pergunta(s) · {usage["input_tokens"]} tokens de entrada · '
             f'{resp["model"].split("/")[1]} · {resp["latency_ms"]:.0f} ms · nenhum texto gerado</div>']
    for qid, a in resp["answers"].items():
        instructions = render(req.questions[qid].instructions)
        if a["type"] == "noul":
            p = a["noul"]
            headline, detail = ("sim" if p >= 0.5 else "não"), f"p(sim) {p:.2f}"
            rows = bar("sim", p, p >= 0.5) + bar("não", 1 - p, p < 0.5)
        elif a["type"] == "choice":
            headline, detail = a["choice"], f"confiança {a['confidence']:.2f}"
            ranked = sorted(a["probabilities"].items(), key=lambda kv: -kv[1])
            rows = "".join(bar(k, v, k == a["choice"]) for k, v in ranked)
        else:
            legend, dist = a["legend"], a["probabilities"]
            top = max(dist, key=dist.get)
            headline = f"{a['score']:.2f} de {len(legend) - 1}"
            detail = f"confiança {a['confidence']:.2f}"
            rows = "".join(bar(f"{i}. {legend[str(i)]}", dist[str(i)], str(i) == top) for i in range(len(legend)))
        parts.append(f'<div style="{CARD}"><div style="display:flex;align-items:baseline;gap:8px;flex-wrap:wrap">'
                     f'<span style="font-family:var(--font-mono);font-size:13px;font-weight:600">{html.escape(qid)}</span>'
                     f'<span style="font-size:11px;color:var(--body-text-color-subdued)">· {TYPE_LABEL[a["type"]]}</span></div>'
                     f'<div style="font-size:13px;margin:2px 0 6px">{html.escape(instructions)}</div>'
                     '<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:8px">'
                     f'<span style="font-size:17px;font-weight:600">{html.escape(str(headline))}</span>'
                     f'<span style="font-size:12px;color:var(--body-text-color-subdued)">{detail}</span></div>{rows}</div>')
    return "".join(parts)


def error_html(message):
    return (f'<div style="{CARD}border-color:var(--error-border-color, #d33);">'
            f'<b>Erro:</b> {html.escape(message)}</div>')


# ------------------------------------------------------------------ question builder

def parse_state(text):
    """Plain text, or a JSON object or array."""
    t = (text or "").strip()
    if t[:1] in "{[":
        try:
            return json.loads(t)
        except json.JSONDecodeError:
            pass
    return t


def parse_options(text):
    """One option per line, `name: description` or just `name`."""
    criteria = {}
    for line in (text or "").splitlines():
        name, _, desc = line.partition(":")
        name, desc = name.strip(), desc.strip()
        if name:
            criteria[name] = desc or None
    return criteria


def build_request(state, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels):
    """The System One request the builder describes. A question with an empty id is left out."""
    ids = [i.strip() for i in (choice_id, noul_id, score_id) if i and i.strip()]
    if len(ids) != len(set(ids)):
        raise RequestError("Cada pergunta precisa de um id diferente.")
    questions = {}
    if choice_id.strip():
        questions[choice_id.strip()] = {"type": "choice", "instructions": choice_instr, "criteria": parse_options(choice_opts)}
    if noul_id.strip():
        questions[noul_id.strip()] = {"type": "noul", "instructions": noul_instr}
    if score_id.strip():
        levels = [line.strip() for line in (score_levels or "").splitlines() if line.strip()]
        questions[score_id.strip()] = {"type": "score", "instructions": score_instr, "criteria": levels}
    if not (state or "").strip():
        raise RequestError("Escreva um estado: o texto ou o objeto JSON que o modelo lê.")
    if not questions:
        raise RequestError("Defina pelo menos uma pergunta (um id não vazio).")
    return {"state": parse_state(state), "model": "kev-latest", "questions": questions}


def decide(name, state, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels):
    """Answer the builder's questions about the state. Returns the rendered answers, the request and the response."""
    try:
        payload = build_request(state, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels)
        req = parse_request(payload)
        resp = systemone(req, name)
    except RequestError as e:
        return error_html(str(e)), {}, {"error": str(e)}
    return render_answers(req, resp), payload, resp


def decide_raw(name, request_json):
    """Answer a raw System One request (JSON text). Returns a status line and the response or an error object."""
    try:
        payload = json.loads(request_json or "")
    except json.JSONDecodeError as e:
        message = f"JSON inválido: {e}"
        return f"**Erro:** {message}", {"error": message}
    if not isinstance(payload, dict):
        message = 'O pedido tem de ser um objeto JSON com "state" e "questions".'
        return f"**Erro:** {message}", {"error": message}
    try:
        resp = systemone(parse_request(payload), name)
    except RequestError as e:
        return f"**Erro:** {e}", {"error": str(e)}
    return f"Respondido em {resp['latency_ms']:.0f} ms.", resp


# ------------------------------------------------------------------ examples

EXAMPLES = [
    {
        "name": "Apoio ao cliente",
        "state": "Bom dia. Fui cobrado duas vezes pela encomenda 1182 e o dinheiro já saiu da minha conta. "
                 "Preciso que devolvam uma das cobranças ainda hoje, porque tenho a renda para pagar amanhã.",
        "choice": ("team", "Que equipa deve tratar deste pedido?",
                   "billing: Cobranças, faturas e reembolsos\nshipping: Entregas e atrasos\n"
                   "returns: Trocas e devoluções de artigos\ntechnical: Problemas no site ou na aplicação"),
        "noul": ("urgent", "Este pedido precisa de resposta hoje?"),
        "score": ("frustration", "Quão frustrado está o cliente?", "Calmo\nIncomodado\nMuito zangado"),
    },
    {
        "name": "Sentimento",
        "state": "Fomos jantar ao restaurante novo da Baixa. A comida chegou fria, o empregado esqueceu-se das bebidas "
                 "e no fim ainda nos cobraram o couvert que não pedimos. Não voltamos.",
        "choice": ("sentiment", "Qual é o sentimento geral do comentário?",
                   "positive: Positivo\nneutral: Neutro ou misto\nnegative: Negativo"),
        "noul": ("recommends", "O autor recomenda o restaurante?"),
        "score": ("intensity", "Com que intensidade o autor exprime a opinião?", "Fraca\nModerada\nForte"),
    },
    {
        "name": "Variedade do português",
        "state": "Estou a escrever-te do autocarro porque o meu telemóvel ficou sem rede no comboio. "
                 "Logo à noite passo aí para irmos tomar um pequeno-almoço tardio, está bem?",
        "choice": ("variety", "Em que variedade do português está escrito este texto?",
                   "pt_pt: Português europeu\npt_br: Português do Brasil"),
        "noul": ("informal", "O texto usa um tom informal?"),
        "score": ("register", "Qual é o registo do texto?", "Muito informal\nInformal\nNeutro\nFormal\nMuito formal"),
    },
    {
        "name": "Registo",
        "state": "Exmo. Senhor Diretor, venho por este meio solicitar a V. Ex.ª a marcação de uma reunião, "
                 "com a maior brevidade possível, para tratar da renovação do contrato de prestação de serviços.",
        "choice": ("text_type", "Que tipo de texto é este?",
                   "letter: Carta ou correio formal\nchat: Mensagem entre amigos\nnews: Notícia\nreview: Crítica ou opinião"),
        "noul": ("asks_action", "O texto pede ao destinatário que faça alguma coisa?"),
        "score": ("register", "Qual é o registo do texto?", "Muito informal\nInformal\nNeutro\nFormal\nMuito formal"),
    },
    {
        "name": "Mensagem a um serviço (V2)",
        "state": '{"message": "Boa tarde. Recebi a fatura de março com um valor de 89 euros, quando o meu plano é de 35. '
                 'Já liguei duas vezes e ninguém resolve. Se não corrigirem até amanhã, cancelo o contrato."}',
        "choice": ("department", "Que departamento deve tratar desta mensagem?",
                   "delivery: Entregas e envios\ncancellation: Cancelamentos e rescisões\nsales: Vendas e novos serviços\n"
                   "technical: Assistência técnica\ncomplaints: Reclamações\nbilling: Faturação e pagamentos"),
        "noul": ("", ""),
        "score": ("urgency", "Quão urgente é esta mensagem?",
                  "Pode esperar dias\nResponder nos próximos dias\nResponder hoje\nImediato: há risco ou dano"),
    },
    {
        "name": "Comando de voz (V2)",
        "state": '{"request": "acorda-me amanhã às sete e meia que tenho de apanhar o comboio"}',
        "choice": ("scenario", "A que domínio pertence este comando?",
                   "alarm: Alarmes\ncalendar: Agenda\ntransport: Transportes\nweather: Meteorologia\n"
                   "music: Preferências de música\niot: Casa inteligente"),
        "noul": ("", ""),
        "score": ("", "", ""),
    },
    {
        "name": "Secção do jornal (V2)",
        "state": '{"title": "Câmara do Porto aprova novas regras para o alojamento local na Baixa", '
                 '"text": "A Câmara Municipal do Porto aprovou esta terça-feira um regulamento que suspende novos registos de '
                 'alojamento local em seis freguesias do centro histórico durante dois anos."}',
        "choice": ("section", "Em que secção do jornal saiu este artigo?",
                   "sports: Desporto\nlocal: Notícias locais\ntechnology: Tecnologia\neconomy: Economia\n"
                   "culture: Cultura\npolitics: Política nacional\ntravel: Viagens e lazer\nsociety: Sociedade"),
        "noul": ("", ""),
        "score": ("", "", ""),
    },
    {
        "name": "Área do direito (V2)",
        "state": '{"text": "Acordam no Supremo Tribunal de Justiça: a autora pede a condenação da ré no pagamento das '
                 'retribuições em dívida e da indemnização pelo despedimento, que considera ilícito por falta de processo '
                 'disciplinar."}',
        "choice": ("legal_area", "Em que área do direito se insere este acórdão?",
                   "labour: Direito do trabalho\ndisputes: Contencioso\ncivil: Direito civil\ncriminal: Direito penal"),
        "noul": ("", ""),
        "score": ("", "", ""),
    },
    {
        "name": "Relação entre frases (V2)",
        "state": '{"premise": "O concerto da banda no Coliseu de Lisboa esgotou em menos de uma hora.", '
                 '"hypothesis": "Os bilhetes para o concerto no Coliseu acabaram muito depressa."}',
        "choice": ("relation", "Que relação há entre as duas frases?",
                   "entailment: A primeira implica a segunda\nnone: Nenhuma: a segunda não decorre da primeira\n"
                   "paraphrase: As duas dizem o mesmo"),
        "noul": ("", ""),
        "score": ("similarity", "Quão parecido é o sentido das duas frases?",
                  "sem relação\npouca relação\nalguma relação\nmuito parecidas\npraticamente iguais"),
    },
    {
        "name": "Tradução com um facto trocado (V2)",
        "state": '{"original": "The museum opens at 9 a.m. on weekdays and is closed on Mondays.", '
                 '"translation": "O museu abre às 10 horas nos dias úteis e está fechado às segundas-feiras."}',
        "choice": ("", "", ""),
        "noul": ("faithful", "As duas versões dizem o mesmo?"),
        "score": ("", "", ""),
    },
    {
        "name": "Linguagem ofensiva (V2)",
        "state": "Este árbitro é um palhaço, devia ter vergonha de aparecer em campo. Corja de incompetentes!",
        "choice": ("", "", ""),
        "noul": ("offensive", "A mensagem contém insultos ou linguagem ofensiva?"),
        "score": ("", "", ""),
    },
    {
        "name": "Trocadilho (V2)",
        "state": "Qual é o animal que anda com uma pata? O pato.",
        "choice": ("", "", ""),
        "noul": ("pun", "A piada joga com um duplo sentido ou com o som das palavras?"),
        "score": ("", "", ""),
    },
]


def example_row(ex):
    return [ex["state"], *ex["choice"], *ex["noul"], *ex["score"]]


RAW_EXAMPLE = json.dumps({
    "state": {"request": "A encomenda 4471 chegou com o ecrã partido. Quero trocar o artigo, não quero o dinheiro de volta.",
              "channel": "email"},
    "model": "kev-latest",
    "questions": {
        "team": {"type": "choice", "instructions": "Que equipa deve tratar deste pedido?",
                 "criteria": {"billing": "Cobranças e reembolsos", "shipping": "Entregas", "returns": "Trocas"}},
        "refund_requested": {"type": "noul", "instructions": "O cliente pede o reembolso do dinheiro?"},
        "frustration": {"type": "score", "instructions": "Quão frustrado está o cliente?",
                        "criteria": ["Calmo", "Incomodado", "Muito zangado"]},
    },
}, ensure_ascii=False, indent=2)


# ------------------------------------------------------------------ UI

INTRO = f"""# Jevásio

Modelo de decisão para português europeu. Recebe um **estado** (um texto ou um objeto JSON) e perguntas tipadas, e
devolve uma probabilidade para cada opção de cada pergunta, numa só passagem pelo modelo. Não gera texto.

Escolha o modelo: os 4B são mais exatos, os 0.8B são mais pequenos e rápidos. O [Jevásio-4B-V2]({LINK_4B_V2}) e o
[Jevásio-0.8B-V2]({LINK_08B_V2}) são o [Jevásio-4B]({LINK_4B}) e o [Jevásio-0.8B]({LINK_08B}) treinados com mais tarefas:
variedade do português por domínio, comandos de voz, secções de jornal, áreas do direito, relação entre frases,
linguagem ofensiva, trocadilhos e traduções ou resumos com um facto trocado. Os exemplos marcados com (V2) são dessas
tarefas.
"""

TEMPERATURES = ", ".join(f"T = {m.head.temperature:.2f} no {n}" for n, (_, m) in LOADED.items())

ABOUT = f"""Há três tipos de pergunta: `choice` (escolher uma opção com nome), `noul` (sim ou não) e `score` (um nível numa
escala ordenada). Os nomes das chaves (campos do estado, ids das perguntas, nomes das opções) estão em inglês e os
valores em português, como nos dados de treino.

O Jevásio é o [Kev](https://github.com/jaredpalmer/kev) afinado com o conjunto
[TigreGotico/jevasio-decisions]({LINK_DATA}), e o V2 com [TigreGotico/jevasio-decisions-v2]({LINK_DATA_V2}). A
probabilidade usa a temperatura de calibração guardada no modelo ({TEMPERATURES}).
Antes de definir um limiar, meça o modelo com os seus próprios dados.

Licença CC BY-NC-SA 4.0: só para investigação não comercial. Não use o modelo em decisões com consequências legais,
médicas, financeiras ou laborais sem revisão humana.
"""

PLACEHOLDER = ('<div style="border:1px dashed var(--border-color-primary);border-radius:10px;padding:28px 16px;'
               'text-align:center;color:var(--body-text-color-subdued);font-size:13px">'
               'As respostas aparecem aqui: um cartão por pergunta, com uma barra por opção.</div>')

CSS = """
#col-container { max-width: 1180px; margin: 0 auto; }
#decide-btn { min-height: 44px; }
"""

first = EXAMPLES[0]

with gr.Blocks(title="Jevásio") as demo:
    with gr.Column(elem_id="col-container"):
        gr.Markdown(INTRO)
        model_choice = gr.Radio(list(MODELS), value=DEFAULT, label="Modelo")
        with gr.Tabs():
            with gr.Tab("Perguntas"):
                with gr.Row():
                    with gr.Column(scale=1):
                        state_box = gr.Textbox(label="Estado (o texto que o modelo lê)", lines=5, max_lines=14,
                                               value=first["state"],
                                               placeholder="Uma mensagem, uma crítica, um artigo ou um objeto JSON.")
                        with gr.Group():
                            gr.Markdown("**Pergunta de escolha** (`choice`)")
                            with gr.Row():
                                choice_id = gr.Textbox(label="Id", value=first["choice"][0], scale=1)
                                choice_instr = gr.Textbox(label="Instrução", value=first["choice"][1], scale=3)
                            choice_opts = gr.Textbox(label="Opções (uma por linha, nome: descrição)", lines=4,
                                                     value=first["choice"][2])
                        with gr.Group():
                            gr.Markdown("**Pergunta de sim/não** (`noul`)")
                            with gr.Row():
                                noul_id = gr.Textbox(label="Id", value=first["noul"][0], scale=1)
                                noul_instr = gr.Textbox(label="Instrução", value=first["noul"][1], scale=3)
                        with gr.Group():
                            gr.Markdown("**Pergunta de escala** (`score`)")
                            with gr.Row():
                                score_id = gr.Textbox(label="Id", value=first["score"][0], scale=1)
                                score_instr = gr.Textbox(label="Instrução", value=first["score"][1], scale=3)
                            score_levels = gr.Textbox(label="Níveis, do mais baixo ao mais alto (um por linha)", lines=3,
                                                      value=first["score"][2])
                        gr.Markdown("Deixe o id vazio para omitir uma pergunta.")
                        run_btn = gr.Button("Decidir", variant="primary", elem_id="decide-btn")
                    with gr.Column(scale=1):
                        answers_html = gr.HTML(value=PLACEHOLDER)
                        with gr.Accordion("Pedido enviado (formato System One)", open=False):
                            request_json = gr.JSON()
                        with gr.Accordion("Resposta em bruto", open=False):
                            response_json = gr.JSON()
                inputs = [state_box, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels]
                gr.Examples(examples=[example_row(ex) for ex in EXAMPLES], example_labels=[ex["name"] for ex in EXAMPLES],
                            inputs=inputs, label="Exemplos")
                run_btn.click(fn=decide, inputs=[model_choice, *inputs], outputs=[answers_html, request_json, response_json], api_name="decide")

            with gr.Tab("Avançado"):
                gr.Markdown("Cole um pedido em JSON no formato System One (`POST /v1/systemone`) e veja a resposta em JSON.")
                with gr.Row():
                    with gr.Column(scale=1):
                        raw_in = gr.Code(label="Pedido (JSON)", language="json", lines=18, value=RAW_EXAMPLE)
                        raw_btn = gr.Button("Enviar", variant="primary")
                    with gr.Column(scale=1):
                        raw_status = gr.Markdown()
                        raw_out = gr.JSON(label="Resposta (JSON)")
                raw_btn.click(fn=decide_raw, inputs=[model_choice, raw_in], outputs=[raw_status, raw_out], api_name="systemone")

            with gr.Tab("Sobre"):
                gr.Markdown(ABOUT)

if __name__ == "__main__":
    demo.queue(default_concurrency_limit=1).launch(css=CSS)