Spaces:
Running on Zero
Running on Zero
Download schema_editor.py from mrfakename/clef: direct link, hf CLI and curl.
- Browser
- Download file 5.95 kB
-
https://huggingface.co/spaces/mrfakename/clef/resolve/main/schema_editor.py
- Command line
-
hf download hf://spaces/mrfakename/clef/schema_editor.py
-
curl -L -o schema_editor.py https://huggingface.co/spaces/mrfakename/clef/resolve/main/schema_editor.py
5.95 kB
| """Conversion between the Clef question schema (JSON) and the visual editor's state. | |
| Editor state is a list of questions, each: | |
| {"id": str, "type": "noul"|"choice"|"score", "instructions": str, | |
| "options": [{"id": str, "desc": str}, ...], # choice: id + description; score: ordered levels | |
| "true": str, "false": str} # noul: optional descriptions | |
| Text fields hold strings; a value that parses as a JSON object/array is exported as JSON. | |
| """ | |
| import json | |
| QUESTION_TYPES = ("noul", "choice", "score") | |
| QUESTION_KEYS = {"type", "instructions", "criteria"} | |
| def dumps(value): | |
| return json.dumps(value, indent=2, ensure_ascii=False) | |
| def to_text(value): | |
| if value is None: | |
| return "" | |
| return value if isinstance(value, str) else json.dumps(value, ensure_ascii=False) | |
| def to_value(text): | |
| stripped = (text or "").strip() | |
| if not stripped: | |
| return None | |
| if stripped[:1] in "{[": | |
| try: | |
| return json.loads(stripped) | |
| except json.JSONDecodeError: | |
| pass | |
| return text | |
| def new_question(existing_ids, qtype="noul"): | |
| n = len(existing_ids) + 1 | |
| while f"question_{n}" in existing_ids: | |
| n += 1 | |
| question = {"id": f"question_{n}", "type": qtype, "instructions": "", "options": [], "true": "", "false": ""} | |
| seed_options(question) | |
| return question | |
| def seed_options(question): | |
| """Give choice/score questions two empty options if they have none.""" | |
| if question["type"] == "choice" and not question["options"]: | |
| question["options"] = [{"id": "option_1", "desc": ""}, {"id": "option_2", "desc": ""}] | |
| elif question["type"] == "score" and not question["options"]: | |
| question["options"] = [{"id": "0", "desc": ""}, {"id": "1", "desc": ""}] | |
| def new_option(question): | |
| ids = {o["id"] for o in question["options"]} | |
| n = len(ids) + 1 | |
| if question["type"] == "choice": | |
| while f"option_{n}" in ids: | |
| n += 1 | |
| return {"id": f"option_{n}", "desc": ""} | |
| return {"id": str(len(question["options"])), "desc": ""} | |
| def from_schema(schema): | |
| """Parse a questions object into editor state. Raises ValueError with a readable message.""" | |
| if not isinstance(schema, dict) or not schema: | |
| raise ValueError("Top level must be a non-empty JSON object mapping question ID to question.") | |
| questions = [] | |
| for qid, q in schema.items(): | |
| if not isinstance(q, dict): | |
| raise ValueError(f"`{qid}`: each question must be a JSON object.") | |
| qtype = q.get("type") | |
| if qtype not in QUESTION_TYPES: | |
| raise ValueError(f"`{qid}`: `type` must be noul, choice, or score (got {json.dumps(qtype)}).") | |
| unknown = sorted(set(q) - QUESTION_KEYS) | |
| if unknown: | |
| raise ValueError(f"`{qid}`: unsupported keys {unknown}; allowed keys are type, instructions, criteria.") | |
| criteria = q.get("criteria") | |
| item = { | |
| "id": str(qid), | |
| "type": qtype, | |
| "instructions": to_text(q.get("instructions")), | |
| "options": [], | |
| "true": "", | |
| "false": "", | |
| } | |
| if qtype == "choice": | |
| if not isinstance(criteria, dict): | |
| raise ValueError(f"`{qid}`: choice `criteria` must be an object of option ID to description.") | |
| item["options"] = [{"id": str(k), "desc": to_text(v)} for k, v in criteria.items()] | |
| elif qtype == "score": | |
| if not isinstance(criteria, list): | |
| raise ValueError(f"`{qid}`: score `criteria` must be a list of level descriptions.") | |
| item["options"] = [{"id": str(i), "desc": to_text(v)} for i, v in enumerate(criteria)] | |
| elif criteria is not None: | |
| if not isinstance(criteria, dict) or set(criteria) - {"true", "false"}: | |
| raise ValueError(f"`{qid}`: noul `criteria` may only contain `true` and `false` descriptions.") | |
| item["true"] = to_text(criteria.get("true")) | |
| item["false"] = to_text(criteria.get("false")) | |
| questions.append(item) | |
| return questions | |
| def to_schema(questions): | |
| schema = {} | |
| for q in questions: | |
| out = {"type": q["type"]} | |
| instructions = to_value(q["instructions"]) | |
| if instructions is not None: | |
| out["instructions"] = instructions | |
| if q["type"] == "choice": | |
| out["criteria"] = {o["id"]: to_value(o["desc"]) for o in q["options"]} | |
| elif q["type"] == "score": | |
| out["criteria"] = [to_value(o["desc"]) for o in q["options"]] | |
| else: | |
| criteria = {k: to_value(q[k]) for k in ("true", "false") if to_value(q[k]) is not None} | |
| if criteria: | |
| out["criteria"] = criteria | |
| schema[q["id"]] = out | |
| return schema | |
| def validate(questions): | |
| problems = [] | |
| if not questions: | |
| problems.append("Add at least one question.") | |
| seen = set() | |
| for i, q in enumerate(questions, 1): | |
| name = f"Question {i} (`{q['id']}`)" if q["id"].strip() else f"Question {i}" | |
| if not q["id"].strip(): | |
| problems.append(f"{name}: ID is empty.") | |
| elif q["id"] in seen: | |
| problems.append(f"{name}: duplicate ID; only the last one is kept.") | |
| seen.add(q["id"]) | |
| if q["type"] in ("choice", "score") and not q["options"]: | |
| problems.append(f"{name}: needs at least one option.") | |
| if q["type"] == "choice": | |
| option_ids = [o["id"] for o in q["options"]] | |
| if any(not oid.strip() for oid in option_ids): | |
| problems.append(f"{name}: an option ID is empty.") | |
| if len(set(option_ids)) != len(option_ids): | |
| problems.append(f"{name}: duplicate option IDs.") | |
| return problems | |
| def status_markdown(questions): | |
| problems = validate(questions) | |
| if not problems: | |
| return "" | |
| return "**Schema issues**\n\n" + "\n".join(f"- {p}" for p in problems) | |