""" upload.py — Слой загрузки документов. Blueprint /upload: принимает файлы, сохраняет в БД, запускает парсинг. Поток: POST /upload (multipart/form-data: file) → сохраняем original_bytes в documents (status='uploaded') → parser.parse() → elements → textify.to_text() → parsed_text → UPDATE documents SET parsed_text, status='parsed' → возвращаем document_id Не зависит от LLM-слоёв. Только от parser.py, textify.py, db.py. """ from flask import Blueprint, request, jsonify import db import parser import textify upload_bp = Blueprint("upload", __name__) @upload_bp.route("/upload", methods=["POST"]) def upload(): """ Загрузить файл договора/допника. Ожидает multipart/form-data с полем 'file'. Возвращает {document_id, filename, status}. """ # 1. Проверить что файл передан if "file" not in request.files: return jsonify({"error": "no file in request"}), 400 file = request.files["file"] if not file.filename: return jsonify({"error": "empty filename"}), 400 # 2. Определить MIME-тип (приоритет: расширение, а не content_type от браузера) filename = file.filename mime_type = _guess_mime(filename) or file.content_type or "application/octet-stream" file_bytes = file.read() if not file_bytes: return jsonify({"error": "empty file"}), 400 # 3. Сохранить исходный файл в БД rowcount, err = db.execute( "INSERT INTO documents (filename, mime_type, original_bytes, status) " "VALUES (%s, %s, %s, 'uploaded')", (filename, mime_type, file_bytes), ) if err: return jsonify({"error": f"db insert: {err}"}), 500 # 4. Получить ID созданного документа doc, err = db.query_one( "SELECT id FROM documents " "WHERE filename = %s AND status = 'uploaded' " "ORDER BY created_at DESC LIMIT 1", (filename,), ) if err or not doc: return jsonify({"error": f"document not found after insert: {err}"}), 500 document_id = doc["id"] # 5. Парсинг: bytes → elements → текст parse_result = parser.parse(file_bytes, mime_type) if parse_result.get("errors"): # Парсинг частично не удался — сохраняем что смогли, помечаем ошибку db.execute( "UPDATE documents SET status = 'error', error_message = %s WHERE id = %s", ("; ".join(parse_result["errors"]), str(document_id)), ) return jsonify({ "document_id": str(document_id), "filename": filename, "status": "error", "errors": parse_result["errors"], }), 422 # 6. JSON elements → линейный текст для LLM elements = parse_result.get("elements", []) parsed_text = textify.to_text(elements) # 7. Обновить документ: текст + статус db.execute( "UPDATE documents SET parsed_text = %s, status = 'parsed' WHERE id = %s", (parsed_text, str(document_id)), ) return jsonify({ "document_id": str(document_id), "filename": filename, "status": "parsed", "elements_count": len(elements), }) def _guess_mime(filename: str) -> str: """ Угадать MIME-тип по расширению файла. Используется когда браузер не передал content_type. """ ext = filename.lower().rsplit(".", 1)[-1] if "." in filename else "" mime_map = { "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "doc": "application/msword", "pdf": "application/pdf", "zip": "application/zip", } return mime_map.get(ext, "application/octet-stream")