Spaces:
Running
Running
| from __future__ import annotations | |
| import json | |
| from pathlib import Path | |
| from typing import Any | |
| from cert_study_app.services.parse_quality_service import load_parsed_questions | |
| BLOCKING_ISSUES = { | |
| "missing_stem", | |
| "number_duplicate", | |
| "embedded_next_question", | |
| "page_regression", | |
| "duplicate_raw_text", | |
| } | |
| REPARSE_ISSUES = {"number_gap", "embedded_next_question", "chunk_too_long", "duplicate_raw_text"} | |
| REVIEW_ISSUES = { | |
| "short_stem", | |
| "missing_answer", | |
| "missing_options", | |
| "option_label_gap", | |
| "answer_not_in_options", | |
| "answer_leaked_to_stem", | |
| "chunk_too_short", | |
| "image_missing", | |
| } | |
| def default_gate_report_path(output_json: str | Path) -> str: | |
| path = Path(output_json) | |
| return path.with_name(f"{path.stem}.gate.json").as_posix() | |
| def default_quarantine_path(output_json: str | Path) -> str: | |
| path = Path(output_json) | |
| return path.with_name(f"{path.stem}.quarantine.json").as_posix() | |
| def apply_quality_gate( | |
| parsed_json_path: str | Path, | |
| quality_report: dict[str, Any], | |
| *, | |
| gate_report_path: str | Path | None = None, | |
| quarantine_path: str | Path | None = None, | |
| pass_score: int = 85, | |
| warn_score: int = 70, | |
| ) -> dict[str, Any]: | |
| questions = load_parsed_questions(parsed_json_path) | |
| issues_by_index = { | |
| int(row["index"]): row.get("issues") or [] | |
| for row in quality_report.get("question_issues") or [] | |
| if isinstance(row, dict) and str(row.get("index", "")).isdigit() | |
| } | |
| issue_counts = quality_report.get("issue_counts") or {} | |
| blocking_count = sum(int(issue_counts.get(code, 0) or 0) for code in BLOCKING_ISSUES) | |
| reparse_count = sum(int(issue_counts.get(code, 0) or 0) for code in REPARSE_ISSUES) | |
| score = int(quality_report.get("score") or 0) | |
| question_count = int(quality_report.get("question_count") or len(questions)) | |
| if question_count <= 0: | |
| action = "hold" | |
| status = "empty" | |
| reason = "ํ์ฑ๋ ๋ฌธํญ์ด ์์ต๋๋ค." | |
| elif blocking_count: | |
| action = "hold" | |
| status = "reparse_needed" | |
| reason = "์ฒญํน/๋ฒํธ/์ค๋ณต ๊ด๋ จ ์ฐจ๋จ ์ด์๊ฐ ์์ต๋๋ค." | |
| elif score >= pass_score: | |
| action = "proceed" | |
| status = "pass" | |
| reason = "์๋ ์ ์ฌ ๊ฐ๋ฅํ ํ์ง์ ๋๋ค." | |
| elif score >= warn_score: | |
| action = "proceed_with_review" | |
| status = "warning" | |
| reason = "์ ์ฌ๋ ๊ฐ๋ฅํ์ง๋ง ์ผ๋ถ ๋ฌธํญ์ ์๋ ๊ฒ์ ๋์์ผ๋ก ํ์ํฉ๋๋ค." | |
| else: | |
| action = "hold" | |
| status = "low_quality" | |
| reason = "ํ์ง ์ ์๊ฐ ๋ฎ์ DB ์ ์ฌ๋ฅผ ๋ณด๋ฅํฉ๋๋ค." | |
| annotated = [] | |
| status_counts: dict[str, int] = {} | |
| for index, item in enumerate(questions): | |
| row = dict(item) | |
| issues = issues_by_index.get(index, []) | |
| codes = [str(issue.get("code")) for issue in issues if isinstance(issue, dict)] | |
| if action == "hold": | |
| parse_status = "needs_reparse" if set(codes) & REPARSE_ISSUES else "needs_review" | |
| quality_status = "blocked" | |
| elif set(codes) & REPARSE_ISSUES: | |
| parse_status = "needs_reparse" | |
| quality_status = "reparse_candidate" | |
| elif set(codes) & REVIEW_ISSUES: | |
| parse_status = "needs_review" | |
| quality_status = "review" | |
| else: | |
| parse_status = row.get("parse_status") or "draft" | |
| quality_status = "pass" | |
| row["parse_status"] = parse_status | |
| row["quality_status"] = quality_status | |
| row["quality_issues"] = issues | |
| row["quality_score"] = score | |
| annotated.append(row) | |
| status_counts[quality_status] = status_counts.get(quality_status, 0) + 1 | |
| Path(parsed_json_path).write_text(json.dumps(annotated, ensure_ascii=False, indent=2), encoding="utf-8") | |
| quarantine = None | |
| if action == "hold": | |
| quarantine = str(quarantine_path or default_quarantine_path(parsed_json_path)) | |
| Path(quarantine).parent.mkdir(parents=True, exist_ok=True) | |
| Path(quarantine).write_text(json.dumps(annotated, ensure_ascii=False, indent=2), encoding="utf-8") | |
| gate = { | |
| "schema_version": 1, | |
| "source_json": Path(parsed_json_path).as_posix(), | |
| "action": action, | |
| "status": status, | |
| "reason": reason, | |
| "score": score, | |
| "pass_score": pass_score, | |
| "warn_score": warn_score, | |
| "question_count": question_count, | |
| "blocking_count": blocking_count, | |
| "reparse_count": reparse_count, | |
| "status_counts": status_counts, | |
| "quarantine_json": quarantine, | |
| "llm_review_policy": { | |
| "enabled_by_default": False, | |
| "recommended_only_for": sorted(REPARSE_ISSUES | {"answer_not_in_options", "missing_answer"}), | |
| }, | |
| } | |
| if gate_report_path: | |
| Path(gate_report_path).parent.mkdir(parents=True, exist_ok=True) | |
| Path(gate_report_path).write_text(json.dumps(gate, ensure_ascii=False, indent=2), encoding="utf-8") | |
| return gate | |
| def summarize_quality_gate(gate: dict[str, Any]) -> str: | |
| return ( | |
| f"ํ์ง ๊ฒ์ดํธ {gate.get('status')} ยท {gate.get('action')} ยท " | |
| f"์ฐจ๋จ {gate.get('blocking_count', 0)}๊ฐ ยท ์ฌํ์ฑ ํ๋ณด {gate.get('reparse_count', 0)}๊ฐ" | |
| ) | |