Spaces:
Running on Zero
Running on Zero
Modal-only inference: app calls deployed case-forge-serve (v3); drop GPU libs; CPU Space
Browse files- app.py +1 -0
- core/infer.py +35 -98
- data/schema.py +12 -12
- pipeline/prompts.py +66 -7
- requirements.txt +6 -18
- shared/i18n.py +4 -0
app.py
CHANGED
|
@@ -270,6 +270,7 @@ def _chips_html(flags: dict, lang: str, demo: bool) -> str:
|
|
| 270 |
chips.append('<span class="cf-chip ' + cls + '" style="animation-delay:' + delay
|
| 271 |
+ '">' + mark + ' ' + labels[key] + '</span>')
|
| 272 |
head = '<div class="cf-chips">' + "".join(chips) + '</div>'
|
|
|
|
| 273 |
if demo:
|
| 274 |
head += '<div class="cf-demo">' + i18n.t("cf.demo_note", lang) + '</div>'
|
| 275 |
return head
|
|
|
|
| 270 |
chips.append('<span class="cf-chip ' + cls + '" style="animation-delay:' + delay
|
| 271 |
+ '">' + mark + ' ' + labels[key] + '</span>')
|
| 272 |
head = '<div class="cf-chips">' + "".join(chips) + '</div>'
|
| 273 |
+
head += '<div class="cf-demo">⚠ ' + i18n.t("cf.disclaimer", lang) + '</div>'
|
| 274 |
if demo:
|
| 275 |
head += '<div class="cf-demo">' + i18n.t("cf.demo_note", lang) + '</div>'
|
| 276 |
return head
|
core/infer.py
CHANGED
|
@@ -1,16 +1,15 @@
|
|
| 1 |
"""Inference for the fine-tuned student — short request → full case+note JSON.
|
| 2 |
|
| 3 |
-
Runtime = ZeroGPU
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
|
| 9 |
Config (env):
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
CASE_FORGE_DEMO=1
|
| 13 |
-
CASE_FORGE_MAX_TOKENS generation cap (default 4096)
|
| 14 |
"""
|
| 15 |
|
| 16 |
from __future__ import annotations
|
|
@@ -27,79 +26,25 @@ for _p in (str(_ROOT), str(_MONOREPO)):
|
|
| 27 |
sys.path.insert(0, _p)
|
| 28 |
|
| 29 |
from data.schema import validate_case # noqa: E402
|
| 30 |
-
from pipeline.prompts import Seed
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
# Published Case Forge LoRA (Well-Tuned). Override via env if needed.
|
| 35 |
-
ADAPTER_REPO = os.environ.get(
|
| 36 |
-
"CASE_FORGE_ADAPTER", "build-small-hackathon/case-forge-qwen3-4b").strip()
|
| 37 |
-
MAX_NEW_TOKENS = int(os.environ.get("CASE_FORGE_MAX_TOKENS", "4096"))
|
| 38 |
FORCE_DEMO = os.environ.get("CASE_FORGE_DEMO", "").strip() in ("1", "true", "yes")
|
| 39 |
|
| 40 |
-
|
| 41 |
-
_TOK = None
|
| 42 |
|
| 43 |
|
| 44 |
-
def
|
| 45 |
-
|
| 46 |
-
import torch
|
| 47 |
-
return torch.cuda.is_available()
|
| 48 |
-
except Exception:
|
| 49 |
-
return False
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def _ensure_model() -> None:
|
| 53 |
-
"""Lazy-load base + LoRA. Called inside the GPU-allocated context."""
|
| 54 |
-
global _MODEL, _TOK
|
| 55 |
-
if _MODEL is not None:
|
| 56 |
-
return
|
| 57 |
-
import torch
|
| 58 |
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 59 |
-
|
| 60 |
-
tok = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True)
|
| 61 |
-
model = AutoModelForCausalLM.from_pretrained(
|
| 62 |
-
BASE_MODEL, torch_dtype=torch.bfloat16, device_map="cuda",
|
| 63 |
-
trust_remote_code=True,
|
| 64 |
-
)
|
| 65 |
-
if ADAPTER_REPO:
|
| 66 |
-
from peft import PeftModel
|
| 67 |
-
model = PeftModel.from_pretrained(model, ADAPTER_REPO)
|
| 68 |
-
model.eval()
|
| 69 |
-
_MODEL, _TOK = model, tok
|
| 70 |
|
| 71 |
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
try:
|
| 79 |
-
text = _TOK.apply_chat_template(
|
| 80 |
-
messages, tokenize=False, add_generation_prompt=True,
|
| 81 |
-
enable_thinking=False,
|
| 82 |
-
)
|
| 83 |
-
except TypeError: # base without the enable_thinking kwarg
|
| 84 |
-
text = _TOK.apply_chat_template(
|
| 85 |
-
messages, tokenize=False, add_generation_prompt=True,
|
| 86 |
-
)
|
| 87 |
-
inputs = _TOK(text, return_tensors="pt").to(_MODEL.device)
|
| 88 |
-
with torch.no_grad():
|
| 89 |
-
out = _MODEL.generate(
|
| 90 |
-
**inputs, max_new_tokens=MAX_NEW_TOKENS,
|
| 91 |
-
do_sample=True, temperature=0.7, top_p=0.95,
|
| 92 |
-
pad_token_id=_TOK.pad_token_id or _TOK.eos_token_id,
|
| 93 |
-
)
|
| 94 |
-
return _TOK.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
def _parse(raw: str) -> dict | None:
|
| 98 |
-
"""Pull the JSON object out of a completion (tolerant of stray prose)."""
|
| 99 |
-
try:
|
| 100 |
-
return json.loads(raw[raw.find("{"): raw.rfind("}") + 1])
|
| 101 |
-
except Exception:
|
| 102 |
-
return None
|
| 103 |
|
| 104 |
|
| 105 |
# --- demo fallback -------------------------------------------------------
|
|
@@ -108,16 +53,12 @@ _DEMO_BANK: dict[str, dict] | None = None
|
|
| 108 |
|
| 109 |
|
| 110 |
def _demo_bank() -> dict[str, dict]:
|
| 111 |
-
"""One valid sample case per language
|
| 112 |
-
|
| 113 |
-
Lets the whole UI (render, quality badges, export) work with real data when
|
| 114 |
-
no GPU/model is available — for local smoke tests and screenshots.
|
| 115 |
-
"""
|
| 116 |
global _DEMO_BANK
|
| 117 |
if _DEMO_BANK is not None:
|
| 118 |
return _DEMO_BANK
|
| 119 |
bank: dict[str, dict] = {}
|
| 120 |
-
# demo_infer.json is a single PT sample produced by the real model.
|
| 121 |
demo = _ROOT / "data" / "synthetic" / "demo_infer.json"
|
| 122 |
if demo.exists():
|
| 123 |
try:
|
|
@@ -126,8 +67,7 @@ def _demo_bank() -> dict[str, dict]:
|
|
| 126 |
bank[obj.get("language", "pt")] = obj
|
| 127 |
except Exception:
|
| 128 |
pass
|
| 129 |
-
|
| 130 |
-
for name in ("pairs_v2.jsonl", "pairs.jsonl"):
|
| 131 |
if len(bank) >= 2:
|
| 132 |
break
|
| 133 |
path = _ROOT / "data" / "synthetic" / name
|
|
@@ -158,10 +98,9 @@ def _demo_result(seed: Seed) -> dict:
|
|
| 158 |
|
| 159 |
def generate(domain: str, topic: str, level: str = "MBA",
|
| 160 |
language: str = "pt", theory: str = "") -> dict:
|
| 161 |
-
"""Forge one case+note from a short request.
|
| 162 |
|
| 163 |
-
Returns {obj, valid, errors, warnings, raw, demo}.
|
| 164 |
-
(or None if parsing failed). `demo` is True when the sample fallback was used.
|
| 165 |
"""
|
| 166 |
seed = Seed(
|
| 167 |
domain=(domain or "administração").strip(),
|
|
@@ -171,20 +110,18 @@ def generate(domain: str, topic: str, level: str = "MBA",
|
|
| 171 |
theory=[t.strip() for t in (theory or "").split(",") if t.strip()],
|
| 172 |
)
|
| 173 |
|
| 174 |
-
if FORCE_DEMO or not (
|
| 175 |
return _demo_result(seed)
|
| 176 |
|
| 177 |
try:
|
| 178 |
-
|
| 179 |
-
except Exception as exc: #
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
return
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
return {"obj": obj, "valid": ok, "errors": errs, "warnings": warns,
|
| 187 |
-
"raw": raw, "demo": False}
|
| 188 |
|
| 189 |
|
| 190 |
__all__ = ["generate", "Seed"]
|
|
|
|
| 1 |
"""Inference for the fine-tuned student — short request → full case+note JSON.
|
| 2 |
|
| 3 |
+
Runtime = **Modal** (no ZeroGPU for this project). The Gradio app calls a deployed
|
| 4 |
+
warm Modal class (`case-forge-serve` / `CaseForge`, see `pipeline/serve_modal.py`)
|
| 5 |
+
that holds base Qwen3-4B + the `qwen3-4b-v3` LoRA. Modal creds come from the Space
|
| 6 |
+
secrets (MODAL_TOKEN_ID / MODAL_TOKEN_SECRET). With no creds / on failure it falls
|
| 7 |
+
back to a real sample case so the UI is fully testable offline.
|
| 8 |
|
| 9 |
Config (env):
|
| 10 |
+
CASE_FORGE_MODAL_APP deployed Modal app name (default case-forge-serve)
|
| 11 |
+
CASE_FORGE_MODAL_CLS class name (default CaseForge)
|
| 12 |
+
CASE_FORGE_DEMO=1 force the demo sample (no Modal call)
|
|
|
|
| 13 |
"""
|
| 14 |
|
| 15 |
from __future__ import annotations
|
|
|
|
| 26 |
sys.path.insert(0, _p)
|
| 27 |
|
| 28 |
from data.schema import validate_case # noqa: E402
|
| 29 |
+
from pipeline.prompts import Seed # noqa: E402
|
| 30 |
+
|
| 31 |
+
MODAL_APP = os.environ.get("CASE_FORGE_MODAL_APP", "case-forge-serve")
|
| 32 |
+
MODAL_CLS = os.environ.get("CASE_FORGE_MODAL_CLS", "CaseForge")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
FORCE_DEMO = os.environ.get("CASE_FORGE_DEMO", "").strip() in ("1", "true", "yes")
|
| 34 |
|
| 35 |
+
_CLS = None # cached Modal class handle
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
+
def _modal_ready() -> bool:
|
| 39 |
+
return bool(os.environ.get("MODAL_TOKEN_ID") and os.environ.get("MODAL_TOKEN_SECRET"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
+
def _cls():
|
| 43 |
+
global _CLS
|
| 44 |
+
if _CLS is None:
|
| 45 |
+
import modal
|
| 46 |
+
_CLS = modal.Cls.from_name(MODAL_APP, MODAL_CLS)
|
| 47 |
+
return _CLS
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
|
| 50 |
# --- demo fallback -------------------------------------------------------
|
|
|
|
| 53 |
|
| 54 |
|
| 55 |
def _demo_bank() -> dict[str, dict]:
|
| 56 |
+
"""One valid sample case per language from the local corpus — lets the whole
|
| 57 |
+
UI (render, badges, export) work without Modal (offline/screenshots)."""
|
|
|
|
|
|
|
|
|
|
| 58 |
global _DEMO_BANK
|
| 59 |
if _DEMO_BANK is not None:
|
| 60 |
return _DEMO_BANK
|
| 61 |
bank: dict[str, dict] = {}
|
|
|
|
| 62 |
demo = _ROOT / "data" / "synthetic" / "demo_infer.json"
|
| 63 |
if demo.exists():
|
| 64 |
try:
|
|
|
|
| 67 |
bank[obj.get("language", "pt")] = obj
|
| 68 |
except Exception:
|
| 69 |
pass
|
| 70 |
+
for name in ("pairs_v3.jsonl", "pairs_v2.jsonl", "pairs.jsonl"):
|
|
|
|
| 71 |
if len(bank) >= 2:
|
| 72 |
break
|
| 73 |
path = _ROOT / "data" / "synthetic" / name
|
|
|
|
| 98 |
|
| 99 |
def generate(domain: str, topic: str, level: str = "MBA",
|
| 100 |
language: str = "pt", theory: str = "") -> dict:
|
| 101 |
+
"""Forge one case+note from a short request, via the Modal server.
|
| 102 |
|
| 103 |
+
Returns {obj, valid, errors, warnings, raw, demo}.
|
|
|
|
| 104 |
"""
|
| 105 |
seed = Seed(
|
| 106 |
domain=(domain or "administração").strip(),
|
|
|
|
| 110 |
theory=[t.strip() for t in (theory or "").split(",") if t.strip()],
|
| 111 |
)
|
| 112 |
|
| 113 |
+
if FORCE_DEMO or not _modal_ready():
|
| 114 |
return _demo_result(seed)
|
| 115 |
|
| 116 |
try:
|
| 117 |
+
res = _cls()().generate.remote(seed.__dict__)
|
| 118 |
+
except Exception as exc: # Modal unreachable / cold-start failure → don't crash UI
|
| 119 |
+
out = _demo_result(seed)
|
| 120 |
+
out["errors"] = [f"falha na geração (Modal): {exc}"] + out["errors"]
|
| 121 |
+
return out
|
| 122 |
+
|
| 123 |
+
res["demo"] = False
|
| 124 |
+
return res
|
|
|
|
|
|
|
| 125 |
|
| 126 |
|
| 127 |
__all__ = ["generate", "Seed"]
|
data/schema.py
CHANGED
|
@@ -195,28 +195,28 @@ def validate_case(obj: dict) -> tuple[bool, list[str], list[str]]:
|
|
| 195 |
"idealmente cada objetivo é coberto por ≥1 questão."
|
| 196 |
)
|
| 197 |
|
| 198 |
-
# 2d)
|
|
|
|
|
|
|
| 199 |
data = case.get("data") or []
|
| 200 |
-
if
|
| 201 |
warnings.append(
|
| 202 |
-
"
|
| 203 |
-
"
|
| 204 |
)
|
| 205 |
|
| 206 |
return (len(errors) == 0, errors, warnings)
|
| 207 |
|
| 208 |
|
| 209 |
-
def
|
| 210 |
-
"""Heurística
|
| 211 |
import re
|
| 212 |
|
| 213 |
t = text.lower()
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
or "%" in t
|
| 219 |
-
)
|
| 220 |
|
| 221 |
|
| 222 |
__all__ = ["CASE_SCHEMA", "validate_case"]
|
|
|
|
| 195 |
"idealmente cada objetivo é coberto por ≥1 questão."
|
| 196 |
)
|
| 197 |
|
| 198 |
+
# 2d) Fontes FABRICADAS (soft) — o gênero NÃO deve inventar citações com
|
| 199 |
+
# nome de relatório/instituto/ano. Sinalizamos pra o auditor limpar. (Antes
|
| 200 |
+
# premiávamos "dado com fonte", o que incentivava o modelo a fabricar — invertido.)
|
| 201 |
data = case.get("data") or []
|
| 202 |
+
if any(_looks_fabricated_source(d) for d in data):
|
| 203 |
warnings.append(
|
| 204 |
+
"algum dado parece citar uma FONTE FABRICADA (ex.: 'Fonte: Relatório …, 2023'); "
|
| 205 |
+
"números são ilustrativos — remova citações inventadas."
|
| 206 |
)
|
| 207 |
|
| 208 |
return (len(errors) == 0, errors, warnings)
|
| 209 |
|
| 210 |
|
| 211 |
+
def _looks_fabricated_source(text: str) -> bool:
|
| 212 |
+
"""Heurística: o dado cita uma 'fonte' com cara de inventada (rótulo + ano)."""
|
| 213 |
import re
|
| 214 |
|
| 215 |
t = text.lower()
|
| 216 |
+
has_cite = ("fonte" in t or "source" in t or "relatório" in t or "report" in t
|
| 217 |
+
or "pesquisa de mercado" in t or "según" in t)
|
| 218 |
+
has_year = bool(re.search(r"\b(19|20)\d{2}\b", t))
|
| 219 |
+
return has_cite and has_year
|
|
|
|
|
|
|
| 220 |
|
| 221 |
|
| 222 |
__all__ = ["CASE_SCHEMA", "validate_case"]
|
pipeline/prompts.py
CHANGED
|
@@ -42,7 +42,7 @@ Responda APENAS com um objeto JSON, sem markdown, sem comentários, exatamente n
|
|
| 42 |
"protagonist": "<o decisor que o leitor veste>",
|
| 43 |
"decision_point":"<a pergunta concreta que o protagonista PRECISA decidir>",
|
| 44 |
"context": "<organização, setor e atores envolvidos>",
|
| 45 |
-
"data": ["<fato
|
| 46 |
"exhibits": [{"title": "<anexo opcional>", "content": "<tabela/quadro>"}],
|
| 47 |
"alternatives": ["<argumento p/ um caminho>", "<argumento p/ outro>", "... (>=2)"],
|
| 48 |
"closing": "<revisita o dilema NO ponto de decisão — NUNCA revela a escolha>",
|
|
@@ -53,7 +53,7 @@ Responda APENAS com um objeto JSON, sem markdown, sem comentários, exatamente n
|
|
| 53 |
"audience": "<curso, nível e pré-requisitos>",
|
| 54 |
"managerial_relevance": "<por que importa pra gestão>",
|
| 55 |
"learning_objectives": ["<objetivo mensurável (verbo de Bloom)>", "... (1 a 4, NUNCA mais de 4)"],
|
| 56 |
-
"data_sources": "<
|
| 57 |
"theoretical_anchor": ["<teoria/conceito a mobilizar>"],
|
| 58 |
"discussion_plan": [{"block": "<bloco>", "minutes": 10, "activity": "<atividade>"}],
|
| 59 |
"discussion_questions": ["<questão alinhada aos objetivos>", "... (>= nº de objetivos)"],
|
|
@@ -71,11 +71,29 @@ Regras invioláveis:
|
|
| 71 |
O desfecho (se houver) vai SÓ no "epilogue" da nota de ensino.
|
| 72 |
2. NO MÁXIMO 4 objetivos de aprendizagem, cada um mensurável e começando por verbo de Bloom
|
| 73 |
(analisar, avaliar, comparar, calcular, propor...).
|
| 74 |
-
3.
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
Não copie nenhuma organização real específica.
|
| 78 |
-
|
| 79 |
|
| 80 |
|
| 81 |
_SEED_EXTRACTION = """\
|
|
@@ -164,5 +182,46 @@ Idioma do conteúdo: {lang_name}{theory_line}{style_line}
|
|
| 164 |
]
|
| 165 |
|
| 166 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
__all__ = ["Seed", "build_generation_prompt", "build_minimal_prompt",
|
| 168 |
-
"build_seed_extraction_prompt"]
|
|
|
|
| 42 |
"protagonist": "<o decisor que o leitor veste>",
|
| 43 |
"decision_point":"<a pergunta concreta que o protagonista PRECISA decidir>",
|
| 44 |
"context": "<organização, setor e atores envolvidos>",
|
| 45 |
+
"data": ["<fato quantitativo ilustrativo, em unidade consistente, SEM citação fabricada>", "... (>=3)"],
|
| 46 |
"exhibits": [{"title": "<anexo opcional>", "content": "<tabela/quadro>"}],
|
| 47 |
"alternatives": ["<argumento p/ um caminho>", "<argumento p/ outro>", "... (>=2)"],
|
| 48 |
"closing": "<revisita o dilema NO ponto de decisão — NUNCA revela a escolha>",
|
|
|
|
| 53 |
"audience": "<curso, nível e pré-requisitos>",
|
| 54 |
"managerial_relevance": "<por que importa pra gestão>",
|
| 55 |
"learning_objectives": ["<objetivo mensurável (verbo de Bloom)>", "... (1 a 4, NUNCA mais de 4)"],
|
| 56 |
+
"data_sources": "<declare que os números são ILUSTRATIVOS/FICTÍCIOS para fins de ensino>",
|
| 57 |
"theoretical_anchor": ["<teoria/conceito a mobilizar>"],
|
| 58 |
"discussion_plan": [{"block": "<bloco>", "minutes": 10, "activity": "<atividade>"}],
|
| 59 |
"discussion_questions": ["<questão alinhada aos objetivos>", "... (>= nº de objetivos)"],
|
|
|
|
| 71 |
O desfecho (se houver) vai SÓ no "epilogue" da nota de ensino.
|
| 72 |
2. NO MÁXIMO 4 objetivos de aprendizagem, cada um mensurável e começando por verbo de Bloom
|
| 73 |
(analisar, avaliar, comparar, calcular, propor...).
|
| 74 |
+
3. NÚMEROS ILUSTRATIVOS, MAS INTERNAMENTE CONSISTENTES (esta é a regra mais importante):
|
| 75 |
+
- Use SEMPRE as mesmas unidades no caso todo: mensal vs anual, por-unidade vs por-período,
|
| 76 |
+
a mesma moeda. Se um custo é "por voo/por mês", a receita comparável também é "por voo/por
|
| 77 |
+
mês" — ou forneça a capacidade/volume para converter. Nunca misture mês e ano para o mesmo item.
|
| 78 |
+
- TODA CONTA TEM QUE FECHAR. Recalcule antes de escrever: somas, percentuais, margens,
|
| 79 |
+
crescimento, ratios, payback. Ex.: só existe "payback" se o investimento GERA economia ou
|
| 80 |
+
receita extra — não calcule payback sobre algo que AUMENTA o custo mensal.
|
| 81 |
+
- Os números do exhibit/tabela TÊM QUE BATER com os do texto. Nada de três valores diferentes
|
| 82 |
+
para o mesmo aumento/indicador.
|
| 83 |
+
- Magnitudes plausíveis para o porte (uma rede de 12 lojas não gasta R$15 mi/ano em mídia digital).
|
| 84 |
+
4. NÃO INVENTE FONTES NEM CITAÇÕES. Proibido "(Fonte: Relatório Interno, 2023)", "Pesquisa de
|
| 85 |
+
Mercado, 2021", nomes de institutos/órgãos como autoridade. O dado é ilustrativo: se indicar
|
| 86 |
+
origem, use rótulo genérico e honesto ("dados internos", "estimativa da equipe"), sem
|
| 87 |
+
relatório/ano/instituição fabricados. Em "data_sources", afirme que os números são
|
| 88 |
+
ILUSTRATIVOS/FICTÍCIOS para fins de ensino.
|
| 89 |
+
5. O PROTAGONISTA (com nome) aparece JÁ no "hook"/"context" — nunca surge só no fechamento.
|
| 90 |
+
Use nomes variados e plausíveis (evite repetir sobrenomes).
|
| 91 |
+
6. A "analysis" da nota, se mostrar contas, usa EXATAMENTE os números do caso e os resolve
|
| 92 |
+
CORRETAMENTE. Confira a aritmética — o professor vai ensinar isso como gabarito.
|
| 93 |
+
7. As "discussion_questions" cobrem os objetivos — pelo menos uma por objetivo.
|
| 94 |
+
8. O caso é FICÇÃO PLAUSÍVEL e ORIGINAL: invente empresa, pessoas e números coerentes.
|
| 95 |
Não copie nenhuma organização real específica.
|
| 96 |
+
9. Escreva todo o conteúdo em {lang_name}. Tom narrativo, concreto, sem moralizar."""
|
| 97 |
|
| 98 |
|
| 99 |
_SEED_EXTRACTION = """\
|
|
|
|
| 182 |
]
|
| 183 |
|
| 184 |
|
| 185 |
+
_AUDIT_SYSTEM = (
|
| 186 |
+
"Você é um editor-auditor meticuloso de casos de ensino. Sua obsessão é "
|
| 187 |
+
"CONSISTÊNCIA NUMÉRICA. Você devolve SEMPRE o objeto JSON inteiro corrigido, "
|
| 188 |
+
"no mesmo formato recebido, sem comentários."
|
| 189 |
+
)
|
| 190 |
+
|
| 191 |
+
_AUDIT_CHECKLIST = """\
|
| 192 |
+
Audite e CORRIJA o caso+nota abaixo. Reescreva o JSON INTEIRO já corrigido (mesmas chaves).
|
| 193 |
+
Não invente conteúdo novo desnecessário — apenas conserte os defeitos:
|
| 194 |
+
|
| 195 |
+
1. UNIDADES: todo número numa unidade coerente. Se um item é "por mês", não trate o mesmo
|
| 196 |
+
valor como anual em outro lugar. Custo "por voo/unidade" só se compara a receita
|
| 197 |
+
"por voo/unidade" (ou inclua a capacidade/volume p/ converter). Conserte mismatches.
|
| 198 |
+
2. ARITMÉTICA: recalcule somas, %, margens, crescimento, ratios e PAYBACK. Payback só existe
|
| 199 |
+
se o investimento gera economia ou receita extra — se os números não sustentam, conserte
|
| 200 |
+
os números OU reescreva a conta corretamente. A "analysis" da nota deve bater com os dados.
|
| 201 |
+
3. EXHIBIT × TEXTO: os números da tabela têm que ser idênticos aos do texto. Um único valor
|
| 202 |
+
por indicador. Elimine linhas idênticas/padding.
|
| 203 |
+
4. MAGNITUDE: valores plausíveis para o porte descrito. Conserte exageros (ex.: orçamento
|
| 204 |
+
gigante para empresa pequena).
|
| 205 |
+
5. FONTES: REMOVA toda citação fabricada ("Fonte: Relatório X, 2023", institutos, anos de
|
| 206 |
+
relatório inventados). Deixe o dado ilustrativo; em "data_sources" diga que os números são
|
| 207 |
+
ilustrativos/fictícios para ensino.
|
| 208 |
+
6. PROTAGONISTA: o nome aparece no "hook"/"context", não só no fechamento.
|
| 209 |
+
7. NÃO revele a decisão no "closing" (o desfecho fica no "epilogue" da nota).
|
| 210 |
+
|
| 211 |
+
JSON a auditar:
|
| 212 |
+
"""
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def build_audit_prompt(pair_obj: dict) -> list[dict]:
|
| 216 |
+
"""Mensagens para o professor AUDITAR e reparar um par caso+nota já gerado —
|
| 217 |
+
foco em consistência numérica e remoção de fontes fabricadas. Retorna JSON inteiro."""
|
| 218 |
+
import json as _json
|
| 219 |
+
payload = _json.dumps(pair_obj, ensure_ascii=False)
|
| 220 |
+
return [
|
| 221 |
+
{"role": "system", "content": _AUDIT_SYSTEM},
|
| 222 |
+
{"role": "user", "content": _AUDIT_CHECKLIST + payload},
|
| 223 |
+
]
|
| 224 |
+
|
| 225 |
+
|
| 226 |
__all__ = ["Seed", "build_generation_prompt", "build_minimal_prompt",
|
| 227 |
+
"build_seed_extraction_prompt", "build_audit_prompt"]
|
requirements.txt
CHANGED
|
@@ -1,22 +1,10 @@
|
|
| 1 |
-
# Case Forge — HF Space runtime (Gradio app
|
| 2 |
#
|
| 3 |
-
#
|
| 4 |
-
#
|
| 5 |
-
#
|
|
|
|
| 6 |
|
| 7 |
-
# --- app runtime ---
|
| 8 |
gradio>=6
|
| 9 |
-
|
| 10 |
-
transformers>=4.49
|
| 11 |
-
peft>=0.13 # stack the LoRA adapter onto the base
|
| 12 |
-
accelerate>=1
|
| 13 |
-
torch
|
| 14 |
jsonschema>=4 # full structural validation of the output contract
|
| 15 |
-
|
| 16 |
-
# --- data pipeline only (collect → generate → train; runs on Modal) ---
|
| 17 |
-
# modal
|
| 18 |
-
# requests
|
| 19 |
-
# beautifulsoup4
|
| 20 |
-
# pypdf
|
| 21 |
-
# datasets
|
| 22 |
-
# bitsandbytes
|
|
|
|
| 1 |
+
# Case Forge — HF Space runtime (Gradio app).
|
| 2 |
#
|
| 3 |
+
# Inference runs on MODAL (no ZeroGPU for this project): the app calls a deployed
|
| 4 |
+
# warm Modal class (case-forge-serve / CaseForge, see pipeline/serve_modal.py),
|
| 5 |
+
# so the Space itself needs NO GPU libs (torch/transformers/peft) — it just renders
|
| 6 |
+
# and calls Modal. Set MODAL_TOKEN_ID / MODAL_TOKEN_SECRET as Space secrets.
|
| 7 |
|
|
|
|
| 8 |
gradio>=6
|
| 9 |
+
modal # calls the deployed Modal inference server
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
jsonschema>=4 # full structural validation of the output contract
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
shared/i18n.py
CHANGED
|
@@ -103,6 +103,10 @@ _T = {
|
|
| 103 |
"en": "Showing a sample case (model not loaded in this environment).",
|
| 104 |
"pt": "Exibindo um caso de amostra (modelo não carregado neste ambiente).",
|
| 105 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
|
| 108 |
|
|
|
|
| 103 |
"en": "Showing a sample case (model not loaded in this environment).",
|
| 104 |
"pt": "Exibindo um caso de amostra (modelo não carregado neste ambiente).",
|
| 105 |
},
|
| 106 |
+
"cf.disclaimer": {
|
| 107 |
+
"en": "Figures are illustrative/fictional — verify them before classroom use.",
|
| 108 |
+
"pt": "Números são ilustrativos/fictícios — confira antes de usar em aula.",
|
| 109 |
+
},
|
| 110 |
}
|
| 111 |
|
| 112 |
|