Add isolated teach flow on VM
This commit is contained in:
+208
@@ -0,0 +1,208 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user