""" teach.py — Isolated teaching/feedback page. This blueprint is intentionally separate from the working compare pipeline. It renders a standalone UI and stores feedback only in the feedback table. """ import json import os from flask import Blueprint, jsonify, render_template, request import db teach_bp = Blueprint("teach", __name__) @teach_bp.route("/teach/api/meta", methods=["GET"]) def teach_meta_api(): """Return default metadata for the isolated teaching page.""" return jsonify({ "ok": True, "prompt_version": os.getenv("TEACH_PROMPT_VERSION", "vm-teach-v1"), "model_name": os.getenv("TEACH_MODEL_NAME", "gpt-oss-120b"), }) @teach_bp.route("/teach", methods=["GET"]) def teach_page(): """Standalone feedback page for reviewing comparison output.""" return render_template("teach.html") @teach_bp.route("/teach/api/contracts", methods=["GET"]) def teach_contracts_api(): """List contracts and supplements for the teaching page.""" result, err = db.query( "SELECT c.id AS contract_id, c.number AS contract_number, c.client, c.date_signed, " "s.id AS supplement_id, s.number AS supplement_number, s.date_signed AS supplement_date, " "s.type AS supplement_type, s.document_id, d.filename AS document_filename " "FROM contracts c " "LEFT JOIN supplements s ON s.contract_id = c.id " "LEFT JOIN documents d ON d.id = s.document_id " "ORDER BY c.created_at DESC, s.date_signed DESC NULLS LAST, s.created_at DESC" ) if err: return jsonify({"ok": False, "error": err}), 500 contracts = {} for row in result["rows"]: item = dict(zip(result["columns"], row)) cid = str(item["contract_id"]) if cid not in contracts: contracts[cid] = { "contract_id": cid, "contract_number": item.get("contract_number"), "client": item.get("client"), "date_signed": item.get("date_signed"), "supplements": [], } if item.get("supplement_id"): contracts[cid]["supplements"].append({ "supplement_id": str(item["supplement_id"]), "supplement_number": item.get("supplement_number"), "supplement_date": item.get("supplement_date"), "supplement_type": item.get("supplement_type"), "document_id": str(item["document_id"]) if item.get("document_id") else None, "document_filename": item.get("document_filename"), }) return jsonify({"ok": True, "contracts": list(contracts.values())}) @teach_bp.route("/teach/api/context", methods=["GET"]) def teach_context_api(): """Load rows and history for a chosen supplement.""" supplement_id = request.args.get("supplement_id") if not supplement_id: return jsonify({"ok": False, "error": "supplement_id required"}), 400 supp, err = db.query_one( "SELECT s.id AS supplement_id, s.contract_id, s.number, s.date_signed, s.type, " "c.number AS contract_number, c.client, d.filename AS document_filename " "FROM supplements s " "JOIN contracts c ON c.id = s.contract_id " "LEFT JOIN documents d ON d.id = s.document_id " "WHERE s.id = %s", (supplement_id,), ) if err: return jsonify({"ok": False, "error": err}), 500 if not supp: return jsonify({"ok": False, "error": "not found"}), 404 rows_result, err = db.query( "SELECT id, row_num, name, price, qty, sum, date_start, date_end, created_at " "FROM spec_rows WHERE supplement_id = %s ORDER BY row_num", (supplement_id,), ) if err: return jsonify({"ok": False, "error": err}), 500 prev_rows_result, err = db.query( "SELECT sr.row_num, sr.name, sr.price, sr.qty, sr.sum, sr.date_start " "FROM spec_rows sr " "JOIN supplements s ON s.id = sr.supplement_id " "WHERE s.contract_id = %s AND s.id <> %s " "ORDER BY s.date_signed DESC NULLS LAST, sr.row_num", (supp["contract_id"], supplement_id), ) if err: return jsonify({"ok": False, "error": err}), 500 rows = [dict(zip(rows_result["columns"], row)) for row in rows_result["rows"]] prev_rows = [dict(zip(prev_rows_result["columns"], row)) for row in prev_rows_result["rows"]] return jsonify({ "ok": True, "supplement": supp, "current_rows": rows, "previous_rows": prev_rows, }) @teach_bp.route("/teach/api/feedback", methods=["GET", "POST"]) def teach_feedback_api(): """Read or append isolated feedback records.""" if request.method == "GET": contract_id = request.args.get("contract_id") supplement_id = request.args.get("supplement_id") sql = ( "SELECT id, created_at, contract_id, supplement_id, event_seq, scope, verdict, " "error_type, field, service_name, llm_value, correct_value, prompt_version, model_name, doc_mode, comment " "FROM feedback" ) params = [] where = [] if contract_id: where.append("contract_id = %s") params.append(contract_id) if supplement_id: where.append("supplement_id = %s") params.append(supplement_id) if where: sql += " WHERE " + " AND ".join(where) sql += " ORDER BY created_at DESC LIMIT 200" result, err = db.query(sql, tuple(params) if params else None) if err: return jsonify({"ok": False, "error": err}), 500 rows = [dict(zip(result["columns"], row)) for row in result["rows"]] return jsonify({"ok": True, "rows": rows, "count": len(rows)}) data = request.get_json(silent=True) or {} required = ["scope", "verdict"] for key in required: if not data.get(key): return jsonify({"ok": False, "error": f"{key} is required"}), 400 sql = ( "INSERT INTO feedback (contract_id, supplement_id, event_seq, scope, verdict, error_type, field, " "service_name, llm_value, correct_value, prompt_version, model_name, doc_mode, comment) " "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s::jsonb, %s::jsonb, %s, %s, %s, %s) RETURNING id" ) params = ( data.get("contract_id"), data.get("supplement_id"), data.get("event_seq"), data.get("scope"), data.get("verdict"), data.get("error_type"), data.get("field"), data.get("service_name"), _json_or_none(data.get("llm_value")), _json_or_none(data.get("correct_value")), data.get("prompt_version"), data.get("model_name"), data.get("doc_mode"), data.get("comment"), ) conn, err = db.connect() if err: return jsonify({"ok": False, "error": err}), 500 try: cur = conn.cursor() cur.execute(sql, params) row = cur.fetchone() conn.commit() cur.close() return jsonify({"ok": True, "id": str(row[0]) if row else None}) except Exception as exc: try: conn.rollback() except Exception: pass return jsonify({"ok": False, "error": str(exc)}), 500 finally: db.put_conn(conn) def _json_or_none(value): """Prepare JSONB payload for psycopg2.""" if value is None or value == "": return None return json.dumps(value, ensure_ascii=False)