Jarbas commited on
Commit
89e0793
·
verified ·
1 Parent(s): 0eb1ecb

Jevásio decision demo

Browse files
Files changed (3) hide show
  1. README.md +66 -8
  2. app.py +350 -0
  3. requirements.txt +3 -0
README.md CHANGED
@@ -1,13 +1,71 @@
1
  ---
2
- title: Jevasio
3
- emoji: 📉
4
- colorFrom: gray
5
- colorTo: yellow
6
  sdk: gradio
7
- sdk_version: 6.30.0
8
- python_version: '3.13'
9
  app_file: app.py
10
- pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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