Spaces:
Paused
Paused
| """ | |
| Search REST API (universal). | |
| Phase 1 exposes admin/adjudicator read-only search at | |
| ``/admin/api/search``. Read-only search is safe under every assignment | |
| strategy and crowd backend (no self-selection), so it has no config | |
| guard. Annotator search-and-claim is a separate, guarded endpoint. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| from flask import Blueprint, jsonify, request, session | |
| from .service import get_search, search_settings | |
| logger = logging.getLogger(__name__) | |
| search_bp = Blueprint("search", __name__) | |
| def _config() -> dict: | |
| from potato.server_utils.config_module import config | |
| return config | |
| def _annotator_claim_enabled() -> bool: | |
| return bool(search_settings(_config()).get("annotator_claim")) | |
| def _serialize(hits) -> list: | |
| return [ | |
| {"instance_id": h.instance_id, "snippet": h.snippet, "score": h.score} | |
| for h in hits | |
| ] | |
| def _is_privileged() -> bool: | |
| """Admin (API key) or adjudicator may use admin search.""" | |
| username = session.get("username") | |
| try: | |
| from potato.admin import admin_dashboard | |
| if admin_dashboard.check_admin_access(): | |
| return True | |
| except Exception: | |
| pass | |
| if username: | |
| try: | |
| from potato.adjudication import get_adjudication_manager | |
| adj = get_adjudication_manager() | |
| if adj and adj.is_adjudicator(username): | |
| return True | |
| except Exception: | |
| pass | |
| return False | |
| def admin_search(): | |
| if not _is_privileged(): | |
| return jsonify({"error": "Admin or adjudicator access required"}), 403 | |
| backend = get_search() | |
| if backend is None: | |
| return jsonify({ | |
| "error": "Search is not enabled or unavailable in this " | |
| "deployment.", | |
| "hint": "Set search.enabled: true (requires a SQLite build " | |
| "with FTS5).", | |
| }), 503 | |
| q = (request.args.get("q") or "").strip() | |
| if not q: | |
| return jsonify({"error": "Query parameter 'q' is required"}), 400 | |
| try: | |
| limit = max(1, min(int(request.args.get("limit", 50)), 500)) | |
| except (TypeError, ValueError): | |
| limit = 50 | |
| hits = backend.query(q, limit=limit) | |
| return jsonify({ | |
| "query": q, "count": len(hits), "results": _serialize(hits), | |
| }) | |
| def _annotator_ctx(): | |
| """(username, backend) for annotator search/claim, or an error tuple.""" | |
| if not _annotator_claim_enabled(): | |
| return None, None, ("disabled",) | |
| username = session.get("username") | |
| if not username: | |
| return None, None, ("unauth",) | |
| backend = get_search() | |
| if backend is None: | |
| return None, None, ("unavailable",) | |
| return username, backend, None | |
| def _annotator_err(err): | |
| if err == ("disabled",): | |
| return jsonify({ | |
| "error": "Annotator search-and-claim is not enabled.", | |
| "hint": "Set search.annotator_claim: true (subject to the " | |
| "assignment-compatibility guard).", | |
| }), 403 | |
| if err == ("unauth",): | |
| return jsonify({"error": "Not authenticated"}), 401 | |
| if err == ("unavailable",): | |
| return jsonify({"error": "Search is not enabled or unavailable."}), 503 | |
| return None | |
| def annotator_search(): | |
| """Annotator-facing corpus search. Gated by search.annotator_claim; | |
| the startup guard guarantees the assignment design is compatible.""" | |
| username, backend, err = _annotator_ctx() | |
| if err: | |
| return _annotator_err(err) | |
| q = (request.args.get("q") or "").strip() | |
| if not q: | |
| return jsonify({"error": "Query parameter 'q' is required"}), 400 | |
| try: | |
| limit = max(1, min(int(request.args.get("limit", 50)), 200)) | |
| except (TypeError, ValueError): | |
| limit = 50 | |
| hits = backend.query(q, limit=limit) | |
| return jsonify({ | |
| "query": q, "count": len(hits), "results": _serialize(hits), | |
| }) | |
| def annotator_claim(): | |
| """Pull a matching instance into the requesting annotator's queue.""" | |
| username, backend, err = _annotator_ctx() | |
| if err: | |
| return _annotator_err(err) | |
| data = request.get_json(silent=True) or {} | |
| instance_id = data.get("instance_id") | |
| if not instance_id: | |
| return jsonify({"error": "instance_id is required"}), 400 | |
| try: | |
| from potato.item_state_management import get_item_state_manager | |
| from potato.user_state_management import get_user_state_manager | |
| ism = get_item_state_manager() | |
| try: | |
| item = ism.get_item(str(instance_id)) | |
| except Exception: | |
| return jsonify({"error": f"Unknown instance {instance_id}"}), 404 | |
| if item is None: | |
| return jsonify({"error": f"Unknown instance {instance_id}"}), 404 | |
| user_state = get_user_state_manager().get_user_state(username) | |
| already = instance_id in user_state.get_assigned_instance_ids() | |
| user_state.assign_instance(item) | |
| except Exception as e: | |
| logger.error(f"Claim failed for {instance_id}: {e}") | |
| return jsonify({"error": "Could not claim instance"}), 500 | |
| return jsonify({ | |
| "claimed": str(instance_id), | |
| "already_assigned": bool(already), | |
| }) | |