From fdd7d89ce5484ebef0abd8efe6a8cb4f66a04333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Mon, 15 Jun 2026 07:41:13 +0400 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B4=D0=B2=D1=83=D1=85=D1=88=D0=B0?= =?UTF-8?q?=D0=B3=D0=BE=D0=B2=D1=8B=D0=B9=20=D0=BF=D1=80=D0=BE=D1=86=D0=B5?= =?UTF-8?q?=D1=81=D1=81=20=E2=80=94=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7?= =?UTF-8?q?=D0=BA=D0=B0=20(=D0=B1=D1=8B=D1=81=D1=82=D1=80=D0=BE)=20?= =?UTF-8?q?=E2=86=92=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=20(LLM)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Шаг 1: POST / → сохранить файлы, парсинг → редирект на /?id=X Шаг 2: POST /process/X → LLM extract + diff → редирект на /?id=X Нет таймаута — загрузка мгновенная, LLM по отдельной кнопке. --- site/app.py | 145 +++++++++++++++++++++---------------- site/templates/upload.html | 27 +++++++ 2 files changed, 111 insertions(+), 61 deletions(-) diff --git a/site/app.py b/site/app.py index 9ba99b1..222a663 100644 --- a/site/app.py +++ b/site/app.py @@ -1,7 +1,7 @@ -"""app.py — Точка входа и сборка слоёв. Бизнес-логики НОЛЬ.""" +"""app.py — Точка входа и сборка слоёв.""" from dotenv import load_dotenv -from flask import Flask, render_template, request +from flask import Flask, render_template, request, redirect import json import schema @@ -25,7 +25,8 @@ class ContractsApp: self._add_routes() def _add_routes(self): - self.app.add_url_rule("/", "index", self._upload_page, methods=["GET", "POST"]) + self.app.add_url_rule("/", "index", self._index, methods=["GET", "POST"]) + self.app.add_url_rule("/process/", "process", self._process, methods=["POST"]) self.app.add_url_rule("/health", "health", self._health) self.app.register_blueprint(test_bp) self.app.register_blueprint(upload_bp) @@ -34,17 +35,22 @@ class ContractsApp: def _health(self): return "OK", 200, {"Content-Type": "text/plain"} - def _upload_page(self): + # ── Шаг 1: загрузка файлов ────────────────────────────────── + + def _index(self): if request.method == "POST": - return self._process_upload() + return self._upload_files() + cid = request.args.get("id") + if cid: + return self._show_contract(cid) return render_template("upload.html") - def _process_upload(self): + def _upload_files(self): + """Только сохранить файлы, без LLM.""" files = request.files.getlist("files") if not files or not any(f.filename for f in files): return render_template("upload.html", error="Нет файлов") - # Создать договор conn, err = db.connect() if err: return render_template("upload.html", error=f"БД: {err}") @@ -58,9 +64,6 @@ class ContractsApp: cur.close() conn.close() - results = [] - total_rows = 0 - for f in files: if not f.filename: continue @@ -68,7 +71,6 @@ class ContractsApp: mime = mimeutil.guess_mime(filename) or f.content_type or "application/octet-stream" file_bytes = f.read() - # Сохранить документ db.execute( "INSERT INTO documents (filename, mime_type, original_bytes, status) VALUES (%s,%s,%s,'uploaded')", (filename, mime, file_bytes), @@ -79,72 +81,105 @@ class ContractsApp: ) doc_id = doc["id"] if doc else None if not doc_id: - results.append({"name": filename, "ok": False, "error": "doc save failed"}) continue - # Парсинг + # Парсинг (быстро, без LLM) pr = parser_mod.parse(file_bytes, mime) - - # ZIP: объединяем элементы из всех файлов + elements = [] if mime == "application/zip" and "files" in pr: - all_el = [] - zip_errs = [] for zf in pr["files"]: - if zf.get("errors"): - zip_errs.extend(zf["errors"]) - all_el.extend(zf.get("elements", [])) - if zip_errs and not all_el: - results.append({"name": filename, "ok": False, "error": "; ".join(zip_errs)}) - continue - elements = all_el + elements.extend(zf.get("elements", [])) else: elements = pr.get("elements", []) - if not elements: - errs = pr.get("errors", []) - results.append({"name": filename, "ok": False, "error": "; ".join(errs) if errs else "нет таблиц"}) - continue - - text = textify.to_text(elements) + text = textify.to_text(elements) if elements else "" db.execute("UPDATE documents SET parsed_text=%s, status='parsed' WHERE id=%s", (text, str(doc_id))) - # Допник db.execute( "INSERT INTO supplements (contract_id, document_id, type) VALUES (%s,%s,'initial')", (str(contract_id), str(doc_id)), ) - supp, _ = db.query_one( - "SELECT id FROM supplements WHERE document_id=%s ORDER BY created_at DESC LIMIT 1", - (str(doc_id),), - ) - supp_id = supp["id"] if supp else None - if not supp_id: - results.append({"name": filename, "ok": False, "error": "supplement save failed"}) + + return redirect("/?id=" + str(contract_id)) + + def _show_contract(self, cid): + """Показать договор с кнопкой «Обработать».""" + contract, _ = db.query_one("SELECT id,number,created_at FROM contracts WHERE id=%s", (cid,)) + if not contract: + return render_template("upload.html", error="Договор не найден") + + supps, _ = db.query( + "SELECT s.id, s.created_at, d.filename, d.status, d.parsed_text IS NOT NULL as has_text " + "FROM supplements s JOIN documents d ON s.document_id=d.id " + "WHERE s.contract_id=%s ORDER BY s.created_at", + (cid,), + ) + supp_list = [dict(zip(supps["columns"], r)) for r in supps["rows"]] if supps else [] + + # Есть ли уже результаты? + rows_res, _ = 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 sr.supplement_id=s.id " + "WHERE s.contract_id=%s ORDER BY sr.row_num", + (cid,), + ) + all_rows = [dict(zip(rows_res["columns"], r)) for r in rows_res["rows"]] if rows_res else [] + + # Есть необработанные допники? + unprocessed = [s for s in supp_list if s["status"] in ("uploaded", "parsed")] + + return render_template("upload.html", + contract=contract, supplements=supp_list, + all_rows=all_rows, unprocessed=unprocessed) + + # ── Шаг 2: LLM-обработка ──────────────────────────────────── + + def _process(self, cid): + """Запустить LLM-обработку для всех необработанных допников договора.""" + supps, _ = db.query( + "SELECT s.id, d.id as doc_id, d.parsed_text, d.filename " + "FROM supplements s JOIN documents d ON s.document_id=d.id " + "WHERE s.contract_id=%s AND d.parsed_text IS NOT NULL", + (cid,), + ) + if not supps: + return redirect("/?id=" + cid) + + supp_rows = [dict(zip(supps["columns"], r)) for r in supps["rows"]] + total_rows = 0 + + for s in supp_rows: + text = s["parsed_text"] + if not text: + continue + + # Проверим, не обработан ли уже + existing, _ = db.query("SELECT 1 FROM spec_rows WHERE supplement_id=%s LIMIT 1", (s["id"],)) + if existing and existing["rows"]: continue - # LLM-извлечение ext = extractor.extract(text) if "error" in ext: - results.append({"name": filename, "ok": False, "error": ext["error"]}) + db.execute("UPDATE documents SET status='extract_error', error_message=%s WHERE id=%s", + (ext["error"], s["doc_id"])) continue rows = ext.get("rows", []) total_rows += len(rows) - # Сохранить spec_rows for row in rows: db.execute( "INSERT INTO spec_rows (supplement_id,row_num,name,price,qty,sum,date_start) VALUES (%s,%s,%s,%s,%s,%s,%s)", - (str(supp_id), row.get("row_num"), row.get("name"), + (s["id"], row.get("row_num"), row.get("name"), row.get("price"), row.get("qty"), row.get("sum"), row.get("date_start")), ) - # Diff с предыдущим + # Diff prev_res, _ = 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 sr.supplement_id=s.id " - "WHERE s.contract_id=%s AND s.id!=%s ORDER BY sr.row_num", - (str(contract_id), str(supp_id)), + "FROM spec_rows sr JOIN supplements sup ON sr.supplement_id=sup.id " + "WHERE sup.contract_id=%s AND sup.id!=%s ORDER BY sr.row_num", + (cid, s["id"]), ) prev_rows = [dict(zip(prev_res["columns"], r)) for r in prev_res["rows"]] if prev_res else [] diff_r = differ.diff(prev_rows, rows) @@ -153,26 +188,14 @@ class ContractsApp: db.execute( "INSERT INTO spec_history (contract_id,supplement_id,row_num,change_type,old_values,new_values) " "VALUES (%s,%s,%s,%s,%s,%s)", - (str(contract_id), str(supp_id), ch["row_num"], ch["change_type"], + (cid, s["id"], ch["row_num"], ch["change_type"], json.dumps(ch.get("old_values"), ensure_ascii=False, default=str) if ch.get("old_values") else None, json.dumps(ch.get("new_values"), ensure_ascii=False, default=str) if ch.get("new_values") else None), ) - db.execute("UPDATE documents SET status='extracted' WHERE id=%s", (str(doc_id),)) - results.append({"name": filename, "ok": True, "rows": len(rows), "summary": diff_r.get("summary", {})}) + db.execute("UPDATE documents SET status='extracted' WHERE id=%s", (s["doc_id"],)) - # Все строки для отображения - all_rows = [] - rows_res, _ = 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 sr.supplement_id=s.id " - "WHERE s.contract_id=%s ORDER BY sr.row_num", - (str(contract_id),), - ) - if rows_res: - all_rows = [dict(zip(rows_res["columns"], r)) for r in rows_res["rows"]] - - return render_template("upload.html", results=results, all_rows=all_rows, total_rows=total_rows) + return redirect("/?id=" + cid) def run(self): self.app.run(host="0.0.0.0", port=5000) diff --git a/site/templates/upload.html b/site/templates/upload.html index 1e10128..8fff497 100644 --- a/site/templates/upload.html +++ b/site/templates/upload.html @@ -106,6 +106,33 @@ + + {% if contract %} +
+
+ + Договор {{ contract.number }} — файлы загружены +
+
+ {% for s in supplements %} +
+ {{ '✅' if s.status == 'extracted' else '⏳' }} + {{ s.filename }} + {{ s.status }} +
+ {% endfor %} + + {% if unprocessed %} +
+ +
+ {% endif %} +
+
+ {% endif %} + {% if results %}