from __future__ import annotations import logging import os from flask import Flask, jsonify, request from .bank import QuestionBank from .config import load_settings from .generator import QuestionGenerator logging.basicConfig(level=logging.INFO) log = logging.getLogger("multiq.api") _SMOKE = os.getenv("MQG_SMOKE", "false").lower() in ("1", "true", "yes") _POINTS = {"easy": 10, "medium": 25, "hard": 50} _BANK: QuestionBank | None = None _GEN: QuestionGenerator | None = None def _get_gen() -> QuestionGenerator: global _GEN if _GEN is None: _GEN = QuestionGenerator(load_settings(), smoke=_SMOKE) return _GEN def _get_bank() -> QuestionBank: global _BANK if _BANK is None: _BANK = QuestionBank(load_settings().bank_path) return _BANK def _game_meta(q: dict) -> dict: return { "points": _POINTS.get(q.get("difficulty", "easy"), 10), "tags": [q.get("lang"), q.get("type", q.get("qtype")), q.get("difficulty")], } def create_app(name: str = "multiqgen") -> Flask: app = Flask(name) app.config["JSON_AS_ASCII"] = False @app.get("/") def root(): return jsonify({"service": "MultiQGen", "version": "0.1.0", "status": "ok"}) @app.route("/api/v1/health") def health(): return jsonify({"status": "healthy", "bank": _get_bank().stats()}) @app.post("/api/v1/generate") def generate_endpoint(): payload = request.get_json(force=True, silent=True) or {} topic = payload.get("topic", "science") lang = payload.get("lang", "en") difficulty = payload.get("difficulty") qtype = payload.get("type", "multiple_choice") n_distractors = payload.get("distractors", 3) temperature = payload.get("temperature") q = _get_gen().generate( topic=topic, lang=lang, difficulty=difficulty, qtype=qtype, n_distractors=n_distractors, temperature=temperature, ) d = q.to_dict() return jsonify({"question": d, "game_meta": _game_meta(d)}), 200 @app.post("/api/v1/next") def next_endpoint(): payload = request.get_json(force=True, silent=True) or {} q = _get_bank().draw( lang=payload.get("lang"), difficulty=payload.get("difficulty"), qtype=payload.get("type"), topic=payload.get("topic"), ) if q is None: return jsonify({"error": "No pre-generated question matches. Run /api/v1/prefill first."}), 404 return jsonify({"question": q, "game_meta": _game_meta(q)}), 200 @app.post("/api/v1/prefill") def prefill_endpoint(): payload = request.get_json(force=True, silent=True) or {} topics = payload.get("topics", []) langs = payload.get("langs", ["en", "ar"]) difficulties = payload.get("difficulties", ["easy", "medium", "hard"]) types = payload.get("types", ["multiple_choice"]) per_cell = int(payload.get("per_cell", 3)) gen = _get_gen() bank = _get_bank() generated = 0 failed = [] for topic in topics: for lang in langs: for diff in difficulties: for qtype in types: made = 0 attempts = 0 while made < per_cell and attempts < per_cell * 4: attempts += 1 try: q = gen.generate(topic=topic, lang=lang, difficulty=diff, qtype=qtype) except Exception as e: failed.append( {"topic": topic, "lang": lang, "difficulty": diff, "error": str(e)[:200]} ) break if bank.add(q.to_dict()): made += 1 generated += 1 else: attempts -= 1 if made < per_cell: failed.append( {"topic": topic, "lang": lang, "difficulty": diff, "made": made, "target": per_cell} ) return jsonify({"generated": generated, "failed": failed, "bank": bank.stats()}) @app.get("/api/v1/bank/stats") def bank_stats_endpoint(): bank = _get_bank() return jsonify({"stats": bank.stats(), "topics": bank.topics()}) @app.post("/api/v1/batch") def batch_endpoint(): payload = request.get_json(force=True, silent=True) or {} topics = payload.get("topics", []) langs = payload.get("langs", ["en", "ar"]) difficulties = payload.get("difficulties", ["easy", "medium", "hard"]) gen = _get_gen() results = [] failed = [] for t in topics: for lang in langs: for diff in difficulties: try: q = gen.generate(topic=t, lang=lang, difficulty=diff) results.append(q.to_dict()) except Exception as e: log.warning("Failed to gen Q for %s %s %s: %s", t, lang, diff, e) failed.append({"topic": t, "lang": lang, "difficulty": diff, "error": str(e)}) return jsonify({"generated": results, "failed": failed}) return app