Files
decision-models-benchmark/datagen/generate.py
Marcos Lohmann 84ea9a5307 setup models
2026-10-07 13:43:09 -03:00

148 lines
7.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Geração sintética dos enunciados (tarefas 24 e 27).
Cada lote = (intenção ou categoria fora de escopo) × perfil, com N enunciados distintos.
O lote é o `group_id`: enunciados do mesmo lote nunca são separados entre partições.
Geradores alternados (fornecedores distintos dos anotadores). Retomável: lotes já gravados
em <out>/generated.jsonl são pulados.
Uso (na VM): python -m datagen.generate --out /data/runs/datagen [--per-batch 12] [--workers 8]
"""
from __future__ import annotations
import argparse
import json
import random
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from dmb.llm import chat, extract_json
from datagen import taxonomy
GENERATORS = ["meta.llama-4-maverick-17b-128e-instruct-fp8", "cohere.command-a-03-2025"]
SEED = 20261007
PROFILES = {
"curto_neutro": "mensagens curtas e diretas (3 a 12 palavras), tom neutro",
"informal_abreviado": "linguagem informal de chat, com abreviações e gírias (vc, pq, q, tb, blz, mds, pfv)",
"erros_digitacao": "MUITOS erros de digitação em toda mensagem: letras trocadas, faltando ou repetidas, sem acentuação, sem pontuação, tudo minúsculo, palavras grudadas",
"regionalismo": "regionalismos e expressões de diferentes regiões do Brasil (Nordeste, Sul, Minas, Rio, São Paulo, Norte)",
"emocional": "cliente irritado, frustrado, ansioso ou indignado; pode usar caixa alta, exclamações e xingamentos leves, mas com o pedido identificável",
"longo_contexto": "mensagens longas (2 a 5 frases) contando a história do problema antes do pedido",
"indireto_multiplo": "pedido indireto ou implícito; em metade das mensagens inclua também um segundo pedido secundário diferente",
}
OOS_PROFILES = ["curto_neutro", "informal_abreviado", "erros_digitacao", "regionalismo", "emocional",
"longo_contexto", "indireto_multiplo", "curto_neutro", "emocional"]
SYSTEM = (
"Você gera dados sintéticos realistas em português do Brasil para avaliar um classificador de "
"intenções do atendimento digital de uma operadora de telefonia móvel. Escreva como clientes reais "
"digitam no chat ou no aplicativo de mensagens. Nunca use nomes de empresas, marcas ou produtos reais, "
"nem dados pessoais reais: se precisar de números, use valores claramente fictícios. "
"Responda somente com JSON válido."
)
def batch_specs(per_batch: int) -> list[dict]:
tax = taxonomy.load()
specs = []
for intent, info in tax["intents"].items():
for profile in PROFILES:
specs.append({"kind": "intent", "target": intent, "domain": info["domain"], "profile": profile})
for oos in tax["out_of_scope"]:
for i, profile in enumerate(OOS_PROFILES):
specs.append({"kind": "oos", "target": oos, "domain": "fora_de_escopo", "profile": profile, "rep": i})
for i, s in enumerate(specs):
s["group_id"] = f"g{i:04d}-{s['target']}-{s['profile']}" + (f"-{s['rep']}" if "rep" in s else "")
s["generator"] = GENERATORS[i % len(GENERATORS)]
s["n"] = per_batch
return specs
def prompt(spec: dict) -> str:
tax = taxonomy.load()
if spec["kind"] == "intent":
info = tax["intents"][spec["target"]]
others = [k for k in tax["intents"] if k != spec["target"]]
rng = random.Random(f"{SEED}-{spec['group_id']}")
hints = info["confusable_with"] + rng.sample(others, 3)
target = (f"Intenção que TODAS as mensagens devem expressar como pedido principal: "
f"`{spec['target']}` — {info['description']}.\n"
f"Cuidado para não escrever mensagens que se encaixem melhor nestas outras intenções: "
+ "; ".join(f"`{h}` ({tax['intents'][h]['description']})" for h in hints) + ".")
if spec["profile"] == "indireto_multiplo":
secondary = ("Em metade das mensagens inclua um segundo pedido, de intenção diferente da principal, "
"e informe o id em `secundaria` escolhendo entre: " + ", ".join(sorted(tax["intents"]))
+ ". Nas demais, `secundaria`: null.")
else:
secondary = "Cada mensagem deve conter UM ÚNICO pedido, sem pedidos adicionais; `secundaria`: null."
else:
target = (f"As mensagens NÃO devem ter nenhum pedido de atendimento de telefonia. Categoria: "
f"`{spec['target']}` — {tax['out_of_scope'][spec['target']]}.")
secondary = "Use `secundaria`: null."
return (
f"{target}\n\nEstilo do lote: {PROFILES[spec['profile']]}.\n\n"
f"Gere {spec['n']} mensagens diferentes entre si (vocabulário, estrutura e situação variados; "
f"não repita o mesmo início). {secondary}\n\n"
"Formato: lista JSON de objetos com as chaves `texto` (a mensagem), `sentimento` "
"(neutro, positivo, frustrado, irritado, ansioso ou indignado) e `secundaria` (id ou null)."
)
def run_batch(spec: dict) -> list[dict]:
raw = chat(spec["generator"], SYSTEM, prompt(spec), temperature=0.9, max_tokens=4000)
items = extract_json(raw)
rows = []
for j, it in enumerate(items[: spec["n"]]):
text = (it.get("texto") or "").strip()
if not text:
continue
rows.append({
"id": f"{spec['group_id']}-{j:02d}", "group_id": spec["group_id"], "text": text,
"gen_label": spec["target"] if spec["kind"] == "intent" else "sem_correspondencia",
"gen_target": spec["target"], "kind": spec["kind"], "domain": spec["domain"],
"profile": spec["profile"], "sentiment": it.get("sentimento"),
"gen_secondary": it.get("secundaria"), "generator": spec["generator"],
"taxonomy_version": taxonomy.load()["version"],
})
return rows
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--out", required=True)
ap.add_argument("--per-batch", type=int, default=12)
ap.add_argument("--workers", type=int, default=8)
ap.add_argument("--limit", type=int)
args = ap.parse_args()
out = Path(args.out)
out.mkdir(parents=True, exist_ok=True)
path = out / "generated.jsonl"
done = set()
if path.exists():
done = {json.loads(l)["group_id"] for l in path.open()}
specs = [s for s in batch_specs(args.per_batch) if s["group_id"] not in done][: args.limit]
print(f"{len(specs)} lotes a gerar ({len(done)} já prontos)", flush=True)
lock = threading.Lock()
failures = []
with ThreadPoolExecutor(args.workers) as pool, path.open("a") as f:
futures = {pool.submit(run_batch, s): s for s in specs}
for i, fut in enumerate(as_completed(futures), 1):
spec = futures[fut]
try:
rows = fut.result()
except Exception as error: # um lote ruim não derruba a geração; é refeito na próxima rodada
failures.append((spec["group_id"], str(error)[:120]))
continue
with lock:
for r in rows:
f.write(json.dumps(r, ensure_ascii=False) + "\n")
f.flush()
if i % 20 == 0:
print(f"{i}/{len(specs)} lotes", flush=True)
print(f"falhas: {len(failures)}", *failures[:10], sep="\n", flush=True)
if __name__ == "__main__":
main()