CoolFace
Apppublic

sanchitttt/Precision_Coder

sourceHugging Faceupdated 4mo agoView on Hugging Face
0likes
app.py240 linesDownload Raw Back to root
1import os2import json3import time4import requests5import pandas as pd6from flask import Flask, request, jsonify, send_from_directory7from werkzeug.utils import secure_filename8 9app = Flask(__name__)10 11OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")12 13# ─────────────────────────────────────────────14# FRONTEND15# ─────────────────────────────────────────────16@app.route("/")17def home():18    return send_from_directory(".", "index.html")19 20 21# ─────────────────────────────────────────────22# FILE READER23# ─────────────────────────────────────────────24@app.route('/read-file', methods=['POST'])25def read_file():26    if 'file' not in request.files:27        return jsonify({'error': 'No file uploaded'}), 40028 29    file     = request.files['file']30    filename = secure_filename(file.filename)31 32    try:33        if filename.endswith('.xlsx'):34            df = pd.read_excel(file, keep_default_na=False, na_values=[''])35        elif filename.endswith('.csv'):36            df = pd.read_csv(file, keep_default_na=False, na_values=[''])37        else:38            return jsonify({'error': 'Only CSV and XLSX files are supported'}), 40039 40        cols_lower = [str(c).lower().strip() for c in df.columns]41 42        def find_col(candidates):43            for p in candidates:44                if p in cols_lower:45                    return df.columns[cols_lower.index(p)]46            return None47 48        resp_col    = find_col(['response','responses','verbatim','verbatims',49                                 'answer','answers','openend','open_end','open end',50                                 'oe','text','comment','comments','mention']) or df.columns[0]51        country_col = find_col(['country','market','region','geo','geography','nation'])52        id_col      = find_col(['id','resp_id','respondent_id','respondentid',53                                 'uid','uuid','caseid','case_id','rid'])54 55        keep = [c for c in [id_col, resp_col, country_col] if c is not None]56        sub  = df[keep].copy()57        sub[resp_col] = sub[resp_col].astype(str).str.strip()58        sub = sub[~sub[resp_col].isin(['', 'nan', 'NaN'])]59 60        if len(sub) > 10000:61            return jsonify({'error': 'Maximum 10,000 responses per upload'}), 40062 63        records = []64        for _, row in sub.iterrows():65            rec = {'response': str(row[resp_col])}66            if id_col:      rec['id']      = str(row[id_col])67            if country_col: rec['country'] = str(row[country_col])68            records.append(rec)69 70        return jsonify({71            'records':     records,72            'has_country': country_col is not None,73            'has_id':      id_col      is not None,74            'total':       len(records)75        })76 77    except Exception as e:78        return jsonify({'error': str(e)}), 50079 80 81# ─────────────────────────────────────────────82# OPENAI CALL  (single shared function)83# Returns (content_string, error_string)84# ─────────────────────────────────────────────85def call_openai(messages, max_tokens=4000):86    if not OPENAI_API_KEY:87        return None, "OPENAI_API_KEY is not set"88 89    for attempt in range(4):90        try:91            resp = requests.post(92                "https://api.openai.com/v1/chat/completions",93                headers={94                    "Authorization": f"Bearer {OPENAI_API_KEY}",95                    "Content-Type":  "application/json"96                },97                json={98                    "model":       "gpt-4o-mini",99                    "messages":    messages,100                    "temperature": 0.1,101                    "max_tokens":  max_tokens102                    # NOTE: no response_format — avoids extra overhead103                    #       and works fine because we parse flexibly104                },105                timeout=120106            )107 108            if resp.status_code == 429:109                wait = int(resp.headers.get("Retry-After", 20))110                time.sleep(min(wait, 60))111                continue112 113            result = resp.json()114 115            if resp.status_code != 200:116                err_msg = result.get("error", {}).get("message", str(result))117                return None, f"OpenAI error {resp.status_code}: {err_msg}"118 119            content = (result.get("choices") or [{}])[0] \120                           .get("message", {}) \121                           .get("content", "")122 123            return content.strip(), None124 125        except requests.exceptions.Timeout:126            if attempt == 3:127                return None, "Request timed out after 120s — try a smaller batch size"128            time.sleep(5)129 130        except Exception as e:131            if attempt == 3:132                return None, str(e)133            time.sleep(3)134 135    return None, "Max retries exceeded"136 137 138# ─────────────────────────────────────────────139# CHAT  (main coding endpoint)140# ─────────────────────────────────────────────141@app.route("/chat", methods=["POST"])142def chat():143    try:144        data    = request.json or {}145        content, err = call_openai(data.get("messages", []))146 147        if err:148            return jsonify({"error": err}), 500149 150        # Return in the shape the frontend expects151        return jsonify({"choices": [{"message": {"content": content}}]})152 153    except Exception as e:154        return jsonify({"error": str(e)}), 500155 156 157# ─────────────────────────────────────────────158# REVIEW  (self-correction pass)159# ─────────────────────────────────────────────160@app.route("/review", methods=["POST"])161def review():162    try:163        data           = request.json or {}164        to_review      = data.get("items", [])165        codebook_text  = data.get("codebook_text", "")166        question       = data.get("question", "")167        catch_all_names = data.get("catch_all_names", ["other"])168 169        if not to_review:170            return jsonify({"results": []})171 172        catch_list = ", ".join(f'"{n}"' for n in catch_all_names)173        q_line     = f'Survey question: "{question}"\n' if question else ""174 175        system = (176            "You are reviewing potentially wrong open-end coding decisions.\n"177            + q_line178            + "CODING FRAME:\n" + codebook_text + "\n\n"179            + f"For each item: if the coder's reason shows they identified a specific match "180            f"but still assigned a catch-all code ({catch_list}), correct it. "181            "Only keep the catch-all when nothing in the frame genuinely fits.\n\n"182            "Return ONLY a JSON array (no markdown):\n"183            '[{"index":0,"response":"...","corrected":"...","codes":["Name"],"ids":["ID"],"reason":"...","confidence":"HIGH"}]'184        )185 186        lines = [187            f'{i}. response="{r["response"]}" | corrected="{r.get("corrected","")}" | '188            f'coded_as="{",".join(r.get("codes",[]))}" | reason="{r.get("reason","")}"'189            for i, r in enumerate(to_review)190        ]191 192        content, err = call_openai([193            {"role": "system", "content": system},194            {"role": "user",   "content": f"Review {len(to_review)} items:\n" + "\n".join(lines)}195        ])196 197        if err:198            return jsonify({"error": err}), 500199 200        # Parse array from response201        clean = content.replace("```json","").replace("```","").strip()202        try:203            parsed = json.loads(clean)204        except Exception:205            m = clean.find('[')206            if m != -1:207                parsed = json.loads(clean[m:clean.rfind(']')+1])208            else:209                parsed = []210 211        if isinstance(parsed, dict):212            parsed = parsed.get("results", list(parsed.values())[0] if parsed else [])213 214        # Ensure index field exists215        for i, item in enumerate(parsed):216            if "index" not in item:217                item["index"] = to_review[i].get("_review_index", i) if i < len(to_review) else i218 219        return jsonify({"results": parsed})220 221    except Exception as e:222        return jsonify({"error": str(e)}), 500223 224 225# ─────────────────────────────────────────────226# BUILD-PROMPT  (kept for compatibility)227# ─────────────────────────────────────────────228@app.route("/build-prompt", methods=["POST"])229def build_prompt():230    # Prompt is now built client-side; this is a no-op kept for safety231    data = request.json or {}232    return jsonify({"prompt": "", "question": data.get("question", "")})233 234 235# ─────────────────────────────────────────────236# RUN237# ─────────────────────────────────────────────238if __name__ == "__main__":239    app.run(host="0.0.0.0", port=7860)240