Jevásio decision demo
Browse files- README.md +66 -8
- app.py +350 -0
- requirements.txt +3 -0
README.md
CHANGED
|
@@ -1,13 +1,71 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 6.
|
| 8 |
-
python_version:
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Jevásio
|
| 3 |
+
emoji: ⚖️
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: red
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 6.28.0
|
| 8 |
+
python_version: "3.12"
|
| 9 |
app_file: app.py
|
| 10 |
+
short_description: Typed decisions for European Portuguese text
|
| 11 |
+
startup_duration_timeout: 1h
|
| 12 |
+
license: cc-by-nc-sa-4.0
|
| 13 |
+
models:
|
| 14 |
+
- TigreGotico/jevasio-0.8b
|
| 15 |
+
- TigreGotico/jevasio-4b
|
| 16 |
+
- Qwen/Qwen3.5-0.8B-Base
|
| 17 |
+
datasets:
|
| 18 |
+
- TigreGotico/jevasio-decisions
|
| 19 |
+
tags:
|
| 20 |
+
- decision-model
|
| 21 |
+
- european-portuguese
|
| 22 |
+
- pt-pt
|
| 23 |
---
|
| 24 |
|
| 25 |
+
# Jevásio
|
| 26 |
+
|
| 27 |
+
Jevásio is a decision model for European Portuguese (pt-PT). It reads a state, which is a text or a JSON object, and a
|
| 28 |
+
set of typed questions about it. It returns a probability for each option of each question in one forward pass. It
|
| 29 |
+
does not generate text.
|
| 30 |
+
|
| 31 |
+
This Space runs [Jevásio-0.8B](https://huggingface.co/TigreGotico/jevasio-0.8b) on a CPU, in fp32, so one request
|
| 32 |
+
takes a few seconds. [Jevásio-4B](https://huggingface.co/TigreGotico/jevasio-4b) is more accurate and needs a GPU.
|
| 33 |
+
Both models are [Kev](https://github.com/jaredpalmer/kev) fine-tuned on the
|
| 34 |
+
[TigreGotico/jevasio-decisions](https://huggingface.co/datasets/TigreGotico/jevasio-decisions) dataset.
|
| 35 |
+
|
| 36 |
+
## Request shape
|
| 37 |
+
|
| 38 |
+
A request follows Kev's System One format (`POST /v1/systemone`). Keys are English: the state's field names, the
|
| 39 |
+
question ids and the option names. Values are Portuguese.
|
| 40 |
+
|
| 41 |
+
```json
|
| 42 |
+
{
|
| 43 |
+
"state": {"request": "Fui cobrado duas vezes pela encomenda 1182."},
|
| 44 |
+
"questions": {
|
| 45 |
+
"team": {"type": "choice", "instructions": "Que equipa deve tratar deste pedido?",
|
| 46 |
+
"criteria": {"billing": "Cobranças e reembolsos", "shipping": "Entregas", "returns": "Trocas"}},
|
| 47 |
+
"urgent": {"type": "noul", "instructions": "Este pedido precisa de resposta hoje?"},
|
| 48 |
+
"frustration": {"type": "score", "instructions": "Quão frustrado está o cliente?",
|
| 49 |
+
"criteria": ["Calmo", "Incomodado", "Muito zangado"]}
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
There are three question types. A `choice` question picks one of its named options, and each option can have a
|
| 55 |
+
description. A `noul` question is yes or no, and the answer is p(yes). A `score` question picks a level on an ordered
|
| 56 |
+
list, and the answer is the expected level.
|
| 57 |
+
|
| 58 |
+
The **Perguntas** tab builds a request from a form. The **Avançado** tab takes a raw JSON request and shows the raw
|
| 59 |
+
JSON answer. The same two functions are on the Gradio API as `/decide` and `/systemone`.
|
| 60 |
+
|
| 61 |
+
## How it runs
|
| 62 |
+
|
| 63 |
+
The Space installs Kev from GitHub at commit `1f04cfa28e6a708485b28a9585010b9e436b776e` and loads the model with
|
| 64 |
+
Kev's own loader, as `python -m kev.serve --run TigreGotico/jevasio-0.8b` does. The probabilities use the calibration
|
| 65 |
+
temperature stored in the model's `head.pt`.
|
| 66 |
+
|
| 67 |
+
## Licence and limits
|
| 68 |
+
|
| 69 |
+
The models and the dataset are under CC BY-NC-SA 4.0, for non-commercial research only. The model cards list the
|
| 70 |
+
measured accuracy, the calibration error and the known limits. Do not use the model for decisions with legal, medical,
|
| 71 |
+
financial or employment consequences without human review.
|
app.py
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Jevásio on Hugging Face Spaces (Gradio, CPU).
|
| 2 |
+
|
| 3 |
+
A state and a set of typed questions in, a probability per option out, in one forward pass. The model is loaded through
|
| 4 |
+
Kev's own loader (kev.checkpoint), the same path `python -m kev.serve --run TigreGotico/jevasio-0.8b` takes, and
|
| 5 |
+
requests go through kev.api and kev.model.admit exactly as /v1/systemone does. On CPU the weights stay in fp32
|
| 6 |
+
(KEV_DTYPE=bf16 overrides it).
|
| 7 |
+
"""
|
| 8 |
+
import html
|
| 9 |
+
import json
|
| 10 |
+
import os
|
| 11 |
+
import time
|
| 12 |
+
from dataclasses import replace
|
| 13 |
+
|
| 14 |
+
import gradio as gr
|
| 15 |
+
import torch
|
| 16 |
+
from pydantic import ValidationError
|
| 17 |
+
|
| 18 |
+
from kev.api import SystemOneRequest, output_tokens, render, to_answers, to_record
|
| 19 |
+
from kev.checkpoint import Checkpoint, LoadOptions
|
| 20 |
+
from kev.model import admit
|
| 21 |
+
|
| 22 |
+
MODEL_REPO = os.environ.get("JEVASIO_MODEL", "TigreGotico/jevasio-0.8b")
|
| 23 |
+
LINK_08B = "https://huggingface.co/TigreGotico/jevasio-0.8b"
|
| 24 |
+
LINK_4B = "https://huggingface.co/TigreGotico/jevasio-4b"
|
| 25 |
+
LINK_DATA = "https://huggingface.co/datasets/TigreGotico/jevasio-decisions"
|
| 26 |
+
|
| 27 |
+
torch.set_num_threads(os.cpu_count() or 1)
|
| 28 |
+
|
| 29 |
+
t0 = time.perf_counter()
|
| 30 |
+
CHECKPOINT = Checkpoint(MODEL_REPO)
|
| 31 |
+
opts = LoadOptions.from_env()
|
| 32 |
+
if opts.dtype is None:
|
| 33 |
+
opts = replace(opts, dtype=torch.float32)
|
| 34 |
+
TOKENIZER, MODEL = CHECKPOINT.load("cpu", opts)
|
| 35 |
+
LOAD_SECONDS = time.perf_counter() - t0
|
| 36 |
+
print(f"[jevasio] {MODEL_REPO}: base={CHECKPOINT.meta.base}@{CHECKPOINT.meta.base_revision} dtype={MODEL.dtype} "
|
| 37 |
+
f"temperature={MODEL.head.temperature:.2f} loaded in {LOAD_SECONDS:.0f} s", flush=True)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class RequestError(ValueError):
|
| 41 |
+
"""A request the model cannot answer: bad JSON, a bad question, or a state that is too long."""
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def parse_request(payload):
|
| 45 |
+
try:
|
| 46 |
+
return SystemOneRequest.model_validate(payload)
|
| 47 |
+
except ValidationError as e:
|
| 48 |
+
first = e.errors()[0]
|
| 49 |
+
where = ".".join(str(x) for x in first.get("loc", ()))
|
| 50 |
+
raise RequestError(f"Pedido inválido em `{where}`: {first.get('msg')}") from None
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def systemone(req):
|
| 54 |
+
"""The /v1/systemone response body kev.serve returns for this request, plus the latency of the forward pass."""
|
| 55 |
+
rec, meta = to_record(req)
|
| 56 |
+
try:
|
| 57 |
+
enc = admit(MODEL, TOKENIZER, rec)
|
| 58 |
+
except ValueError as e:
|
| 59 |
+
raise RequestError(str(e)) from None
|
| 60 |
+
start = time.perf_counter()
|
| 61 |
+
probs = MODEL.probs(enc)
|
| 62 |
+
ms = (time.perf_counter() - start) * 1000
|
| 63 |
+
answers = to_answers([p.tolist() for p in probs], meta)
|
| 64 |
+
return {"model": MODEL_REPO, "answers": answers,
|
| 65 |
+
"usage": {"input_tokens": len(enc["ids"]), "output_tokens": output_tokens(TOKENIZER, answers)},
|
| 66 |
+
"latency_ms": round(ms, 1)}
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
# ------------------------------------------------------------------ rendering
|
| 70 |
+
|
| 71 |
+
CARD = ("border:1px solid var(--border-color-primary);border-radius:10px;padding:12px 14px;margin-bottom:10px;"
|
| 72 |
+
"background:var(--background-fill-secondary);")
|
| 73 |
+
TRACK = "position:relative;display:block;height:8px;border-radius:999px;background:var(--border-color-primary);overflow:hidden;"
|
| 74 |
+
TYPE_LABEL = {"choice": "escolha", "noul": "sim/não", "score": "escala"}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def bar(label, p, top):
|
| 78 |
+
fill = "var(--color-accent)" if top else "var(--body-text-color-subdued)"
|
| 79 |
+
weight = "600" if top else "400"
|
| 80 |
+
width = max(0.0, min(1.0, p)) * 100
|
| 81 |
+
return ('<div style="display:grid;grid-template-columns:minmax(0,12rem) minmax(0,1fr) 3.2rem;align-items:center;'
|
| 82 |
+
'gap:0 12px;font-size:13px;line-height:24px">'
|
| 83 |
+
f'<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:{weight}" '
|
| 84 |
+
f'title="{html.escape(label)}">{html.escape(label)}</span>'
|
| 85 |
+
f'<span style="{TRACK}"><span style="position:absolute;top:0;bottom:0;left:0;border-radius:999px;'
|
| 86 |
+
f'background:{fill};width:{width:.1f}%"></span></span>'
|
| 87 |
+
f'<span style="text-align:right;font-variant-numeric:tabular-nums;font-weight:{weight}">{p:.2f}</span></div>')
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def render_answers(req, resp):
|
| 91 |
+
usage = resp["usage"]
|
| 92 |
+
parts = ['<div style="font-size:12px;color:var(--body-text-color-subdued);margin:6px 0 10px">'
|
| 93 |
+
f'{len(resp["answers"])} pergunta(s) · {usage["input_tokens"]} tokens de entrada · '
|
| 94 |
+
f'{resp["latency_ms"]:.0f} ms em CPU · nenhum texto gerado</div>']
|
| 95 |
+
for qid, a in resp["answers"].items():
|
| 96 |
+
instructions = render(req.questions[qid].instructions)
|
| 97 |
+
if a["type"] == "noul":
|
| 98 |
+
p = a["noul"]
|
| 99 |
+
headline, detail = ("sim" if p >= 0.5 else "não"), f"p(sim) {p:.2f}"
|
| 100 |
+
rows = bar("sim", p, p >= 0.5) + bar("não", 1 - p, p < 0.5)
|
| 101 |
+
elif a["type"] == "choice":
|
| 102 |
+
headline, detail = a["choice"], f"confiança {a['confidence']:.2f}"
|
| 103 |
+
ranked = sorted(a["probabilities"].items(), key=lambda kv: -kv[1])
|
| 104 |
+
rows = "".join(bar(k, v, k == a["choice"]) for k, v in ranked)
|
| 105 |
+
else:
|
| 106 |
+
legend, dist = a["legend"], a["probabilities"]
|
| 107 |
+
top = max(dist, key=dist.get)
|
| 108 |
+
headline = f"{a['score']:.2f} de {len(legend) - 1}"
|
| 109 |
+
detail = f"confiança {a['confidence']:.2f}"
|
| 110 |
+
rows = "".join(bar(f"{i}. {legend[str(i)]}", dist[str(i)], str(i) == top) for i in range(len(legend)))
|
| 111 |
+
parts.append(f'<div style="{CARD}"><div style="display:flex;align-items:baseline;gap:8px;flex-wrap:wrap">'
|
| 112 |
+
f'<span style="font-family:var(--font-mono);font-size:13px;font-weight:600">{html.escape(qid)}</span>'
|
| 113 |
+
f'<span style="font-size:11px;color:var(--body-text-color-subdued)">· {TYPE_LABEL[a["type"]]}</span></div>'
|
| 114 |
+
f'<div style="font-size:13px;margin:2px 0 6px">{html.escape(instructions)}</div>'
|
| 115 |
+
'<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:8px">'
|
| 116 |
+
f'<span style="font-size:17px;font-weight:600">{html.escape(str(headline))}</span>'
|
| 117 |
+
f'<span style="font-size:12px;color:var(--body-text-color-subdued)">{detail}</span></div>{rows}</div>')
|
| 118 |
+
return "".join(parts)
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def error_html(message):
|
| 122 |
+
return (f'<div style="{CARD}border-color:var(--error-border-color, #d33);">'
|
| 123 |
+
f'<b>Erro:</b> {html.escape(message)}</div>')
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
# ------------------------------------------------------------------ question builder
|
| 127 |
+
|
| 128 |
+
def parse_state(text):
|
| 129 |
+
"""Plain text, or a JSON object or array."""
|
| 130 |
+
t = (text or "").strip()
|
| 131 |
+
if t[:1] in "{[":
|
| 132 |
+
try:
|
| 133 |
+
return json.loads(t)
|
| 134 |
+
except json.JSONDecodeError:
|
| 135 |
+
pass
|
| 136 |
+
return t
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def parse_options(text):
|
| 140 |
+
"""One option per line, `name: description` or just `name`."""
|
| 141 |
+
criteria = {}
|
| 142 |
+
for line in (text or "").splitlines():
|
| 143 |
+
name, _, desc = line.partition(":")
|
| 144 |
+
name, desc = name.strip(), desc.strip()
|
| 145 |
+
if name:
|
| 146 |
+
criteria[name] = desc or None
|
| 147 |
+
return criteria
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def build_request(state, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels):
|
| 151 |
+
"""The System One request the builder describes. A question with an empty id is left out."""
|
| 152 |
+
ids = [i.strip() for i in (choice_id, noul_id, score_id) if i and i.strip()]
|
| 153 |
+
if len(ids) != len(set(ids)):
|
| 154 |
+
raise RequestError("Cada pergunta precisa de um id diferente.")
|
| 155 |
+
questions = {}
|
| 156 |
+
if choice_id.strip():
|
| 157 |
+
questions[choice_id.strip()] = {"type": "choice", "instructions": choice_instr, "criteria": parse_options(choice_opts)}
|
| 158 |
+
if noul_id.strip():
|
| 159 |
+
questions[noul_id.strip()] = {"type": "noul", "instructions": noul_instr}
|
| 160 |
+
if score_id.strip():
|
| 161 |
+
levels = [line.strip() for line in (score_levels or "").splitlines() if line.strip()]
|
| 162 |
+
questions[score_id.strip()] = {"type": "score", "instructions": score_instr, "criteria": levels}
|
| 163 |
+
if not (state or "").strip():
|
| 164 |
+
raise RequestError("Escreva um estado: o texto ou o objeto JSON que o modelo lê.")
|
| 165 |
+
if not questions:
|
| 166 |
+
raise RequestError("Defina pelo menos uma pergunta (um id não vazio).")
|
| 167 |
+
return {"state": parse_state(state), "model": "kev-latest", "questions": questions}
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def decide(state, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels):
|
| 171 |
+
"""Answer the builder's questions about the state. Returns the rendered answers, the request and the response."""
|
| 172 |
+
try:
|
| 173 |
+
payload = build_request(state, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels)
|
| 174 |
+
req = parse_request(payload)
|
| 175 |
+
resp = systemone(req)
|
| 176 |
+
except RequestError as e:
|
| 177 |
+
return error_html(str(e)), {}, {"error": str(e)}
|
| 178 |
+
return render_answers(req, resp), payload, resp
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def decide_raw(request_json):
|
| 182 |
+
"""Answer a raw System One request (JSON text). Returns a status line and the response or an error object."""
|
| 183 |
+
try:
|
| 184 |
+
payload = json.loads(request_json or "")
|
| 185 |
+
except json.JSONDecodeError as e:
|
| 186 |
+
message = f"JSON inválido: {e}"
|
| 187 |
+
return f"**Erro:** {message}", {"error": message}
|
| 188 |
+
if not isinstance(payload, dict):
|
| 189 |
+
message = 'O pedido tem de ser um objeto JSON com "state" e "questions".'
|
| 190 |
+
return f"**Erro:** {message}", {"error": message}
|
| 191 |
+
try:
|
| 192 |
+
resp = systemone(parse_request(payload))
|
| 193 |
+
except RequestError as e:
|
| 194 |
+
return f"**Erro:** {e}", {"error": str(e)}
|
| 195 |
+
return f"Respondido em {resp['latency_ms']:.0f} ms.", resp
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
# ------------------------------------------------------------------ examples
|
| 199 |
+
|
| 200 |
+
EXAMPLES = [
|
| 201 |
+
{
|
| 202 |
+
"name": "Apoio ao cliente",
|
| 203 |
+
"state": "Bom dia. Fui cobrado duas vezes pela encomenda 1182 e o dinheiro já saiu da minha conta. "
|
| 204 |
+
"Preciso que devolvam uma das cobranças ainda hoje, porque tenho a renda para pagar amanhã.",
|
| 205 |
+
"choice": ("team", "Que equipa deve tratar deste pedido?",
|
| 206 |
+
"billing: Cobranças, faturas e reembolsos\nshipping: Entregas e atrasos\n"
|
| 207 |
+
"returns: Trocas e devoluções de artigos\ntechnical: Problemas no site ou na aplicação"),
|
| 208 |
+
"noul": ("urgent", "Este pedido precisa de resposta hoje?"),
|
| 209 |
+
"score": ("frustration", "Quão frustrado está o cliente?", "Calmo\nIncomodado\nMuito zangado"),
|
| 210 |
+
},
|
| 211 |
+
{
|
| 212 |
+
"name": "Sentimento",
|
| 213 |
+
"state": "Fomos jantar ao restaurante novo da Baixa. A comida chegou fria, o empregado esqueceu-se das bebidas "
|
| 214 |
+
"e no fim ainda nos cobraram o couvert que não pedimos. Não voltamos.",
|
| 215 |
+
"choice": ("sentiment", "Qual é o sentimento geral do comentário?",
|
| 216 |
+
"positive: Positivo\nneutral: Neutro ou misto\nnegative: Negativo"),
|
| 217 |
+
"noul": ("recommends", "O autor recomenda o restaurante?"),
|
| 218 |
+
"score": ("intensity", "Com que intensidade o autor exprime a opinião?", "Fraca\nModerada\nForte"),
|
| 219 |
+
},
|
| 220 |
+
{
|
| 221 |
+
"name": "Variedade do português",
|
| 222 |
+
"state": "Estou a escrever-te do autocarro porque o meu telemóvel ficou sem rede no comboio. "
|
| 223 |
+
"Logo à noite passo aí para irmos tomar um pequeno-almoço tardio, está bem?",
|
| 224 |
+
"choice": ("variety", "Em que variedade do português está escrito este texto?",
|
| 225 |
+
"pt_pt: Português europeu\npt_br: Português do Brasil"),
|
| 226 |
+
"noul": ("informal", "O texto usa um tom informal?"),
|
| 227 |
+
"score": ("register", "Qual é o registo do texto?", "Muito informal\nInformal\nNeutro\nFormal\nMuito formal"),
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
"name": "Registo",
|
| 231 |
+
"state": "Exmo. Senhor Diretor, venho por este meio solicitar a V. Ex.ª a marcação de uma reunião, "
|
| 232 |
+
"com a maior brevidade possível, para tratar da renovação do contrato de prestação de serviços.",
|
| 233 |
+
"choice": ("text_type", "Que tipo de texto é este?",
|
| 234 |
+
"letter: Carta ou correio formal\nchat: Mensagem entre amigos\nnews: Notícia\nreview: Crítica ou opinião"),
|
| 235 |
+
"noul": ("asks_action", "O texto pede ao destinatário que faça alguma coisa?"),
|
| 236 |
+
"score": ("register", "Qual é o registo do texto?", "Muito informal\nInformal\nNeutro\nFormal\nMuito formal"),
|
| 237 |
+
},
|
| 238 |
+
]
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def example_row(ex):
|
| 242 |
+
return [ex["state"], *ex["choice"], *ex["noul"], *ex["score"]]
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
RAW_EXAMPLE = json.dumps({
|
| 246 |
+
"state": {"request": "A encomenda 4471 chegou com o ecrã partido. Quero trocar o artigo, não quero o dinheiro de volta.",
|
| 247 |
+
"channel": "email"},
|
| 248 |
+
"model": "kev-latest",
|
| 249 |
+
"questions": {
|
| 250 |
+
"team": {"type": "choice", "instructions": "Que equipa deve tratar deste pedido?",
|
| 251 |
+
"criteria": {"billing": "Cobranças e reembolsos", "shipping": "Entregas", "returns": "Trocas"}},
|
| 252 |
+
"refund_requested": {"type": "noul", "instructions": "O cliente pede o reembolso do dinheiro?"},
|
| 253 |
+
"frustration": {"type": "score", "instructions": "Quão frustrado está o cliente?",
|
| 254 |
+
"criteria": ["Calmo", "Incomodado", "Muito zangado"]},
|
| 255 |
+
},
|
| 256 |
+
}, ensure_ascii=False, indent=2)
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
# ------------------------------------------------------------------ UI
|
| 260 |
+
|
| 261 |
+
INTRO = f"""# Jevásio
|
| 262 |
+
|
| 263 |
+
Modelo de decisão para português europeu. Recebe um **estado** (um texto ou um objeto JSON) e perguntas tipadas, e
|
| 264 |
+
devolve uma probabilidade para cada opção de cada pergunta, numa só passagem pelo modelo. Não gera texto.
|
| 265 |
+
|
| 266 |
+
Este Space corre o [Jevásio-0.8B]({LINK_08B}) em CPU, por isso cada pedido demora alguns segundos. Existe também o
|
| 267 |
+
[Jevásio-4B]({LINK_4B}), mais exato, que precisa de GPU.
|
| 268 |
+
"""
|
| 269 |
+
|
| 270 |
+
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
|
| 271 |
+
escala ordenada). Os nomes das chaves (campos do estado, ids das perguntas, nomes das opções) estão em inglês e os
|
| 272 |
+
valores em português, como nos dados de treino.
|
| 273 |
+
|
| 274 |
+
O Jevásio é o [Kev](https://github.com/jaredpalmer/kev) afinado com o conjunto
|
| 275 |
+
[TigreGotico/jevasio-decisions]({LINK_DATA}). A probabilidade usa a temperatura de calibração guardada no modelo
|
| 276 |
+
(T = {MODEL.head.temperature:.2f}). Antes de definir um limiar, meça o modelo com os seus próprios dados.
|
| 277 |
+
|
| 278 |
+
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,
|
| 279 |
+
médicas, financeiras ou laborais sem revisão humana.
|
| 280 |
+
"""
|
| 281 |
+
|
| 282 |
+
PLACEHOLDER = ('<div style="border:1px dashed var(--border-color-primary);border-radius:10px;padding:28px 16px;'
|
| 283 |
+
'text-align:center;color:var(--body-text-color-subdued);font-size:13px">'
|
| 284 |
+
'As respostas aparecem aqui: um cartão por pergunta, com uma barra por opção.</div>')
|
| 285 |
+
|
| 286 |
+
CSS = """
|
| 287 |
+
#col-container { max-width: 1180px; margin: 0 auto; }
|
| 288 |
+
#decide-btn { min-height: 44px; }
|
| 289 |
+
"""
|
| 290 |
+
|
| 291 |
+
first = EXAMPLES[0]
|
| 292 |
+
|
| 293 |
+
with gr.Blocks(title="Jevásio") as demo:
|
| 294 |
+
with gr.Column(elem_id="col-container"):
|
| 295 |
+
gr.Markdown(INTRO)
|
| 296 |
+
with gr.Tabs():
|
| 297 |
+
with gr.Tab("Perguntas"):
|
| 298 |
+
with gr.Row():
|
| 299 |
+
with gr.Column(scale=1):
|
| 300 |
+
state_box = gr.Textbox(label="Estado (o texto que o modelo lê)", lines=5, max_lines=14,
|
| 301 |
+
value=first["state"],
|
| 302 |
+
placeholder="Uma mensagem, uma crítica, um artigo ou um objeto JSON.")
|
| 303 |
+
with gr.Group():
|
| 304 |
+
gr.Markdown("**Pergunta de escolha** (`choice`)")
|
| 305 |
+
with gr.Row():
|
| 306 |
+
choice_id = gr.Textbox(label="Id", value=first["choice"][0], scale=1)
|
| 307 |
+
choice_instr = gr.Textbox(label="Instrução", value=first["choice"][1], scale=3)
|
| 308 |
+
choice_opts = gr.Textbox(label="Opções (uma por linha, nome: descrição)", lines=4,
|
| 309 |
+
value=first["choice"][2])
|
| 310 |
+
with gr.Group():
|
| 311 |
+
gr.Markdown("**Pergunta de sim/não** (`noul`)")
|
| 312 |
+
with gr.Row():
|
| 313 |
+
noul_id = gr.Textbox(label="Id", value=first["noul"][0], scale=1)
|
| 314 |
+
noul_instr = gr.Textbox(label="Instrução", value=first["noul"][1], scale=3)
|
| 315 |
+
with gr.Group():
|
| 316 |
+
gr.Markdown("**Pergunta de escala** (`score`)")
|
| 317 |
+
with gr.Row():
|
| 318 |
+
score_id = gr.Textbox(label="Id", value=first["score"][0], scale=1)
|
| 319 |
+
score_instr = gr.Textbox(label="Instrução", value=first["score"][1], scale=3)
|
| 320 |
+
score_levels = gr.Textbox(label="Níveis, do mais baixo ao mais alto (um por linha)", lines=3,
|
| 321 |
+
value=first["score"][2])
|
| 322 |
+
gr.Markdown("Deixe o id vazio para omitir uma pergunta.")
|
| 323 |
+
run_btn = gr.Button("Decidir", variant="primary", elem_id="decide-btn")
|
| 324 |
+
with gr.Column(scale=1):
|
| 325 |
+
answers_html = gr.HTML(value=PLACEHOLDER)
|
| 326 |
+
with gr.Accordion("Pedido enviado (formato System One)", open=False):
|
| 327 |
+
request_json = gr.JSON()
|
| 328 |
+
with gr.Accordion("Resposta em bruto", open=False):
|
| 329 |
+
response_json = gr.JSON()
|
| 330 |
+
inputs = [state_box, choice_id, choice_instr, choice_opts, noul_id, noul_instr, score_id, score_instr, score_levels]
|
| 331 |
+
gr.Examples(examples=[example_row(ex) for ex in EXAMPLES], example_labels=[ex["name"] for ex in EXAMPLES],
|
| 332 |
+
inputs=inputs, label="Exemplos")
|
| 333 |
+
run_btn.click(fn=decide, inputs=inputs, outputs=[answers_html, request_json, response_json], api_name="decide")
|
| 334 |
+
|
| 335 |
+
with gr.Tab("Avançado"):
|
| 336 |
+
gr.Markdown("Cole um pedido em JSON no formato System One (`POST /v1/systemone`) e veja a resposta em JSON.")
|
| 337 |
+
with gr.Row():
|
| 338 |
+
with gr.Column(scale=1):
|
| 339 |
+
raw_in = gr.Code(label="Pedido (JSON)", language="json", lines=18, value=RAW_EXAMPLE)
|
| 340 |
+
raw_btn = gr.Button("Enviar", variant="primary")
|
| 341 |
+
with gr.Column(scale=1):
|
| 342 |
+
raw_status = gr.Markdown()
|
| 343 |
+
raw_out = gr.JSON(label="Resposta (JSON)")
|
| 344 |
+
raw_btn.click(fn=decide_raw, inputs=[raw_in], outputs=[raw_status, raw_out], api_name="systemone")
|
| 345 |
+
|
| 346 |
+
with gr.Tab("Sobre"):
|
| 347 |
+
gr.Markdown(ABOUT)
|
| 348 |
+
|
| 349 |
+
if __name__ == "__main__":
|
| 350 |
+
demo.queue(default_concurrency_limit=1).launch(css=CSS)
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
--extra-index-url https://download.pytorch.org/whl/cpu
|
| 2 |
+
torch==2.8.0+cpu
|
| 3 |
+
kev @ git+https://github.com/jaredpalmer/kev@1f04cfa28e6a708485b28a9585010b9e436b776e
|