From 909468b5b7d7883302385966d3bf34ad846e5d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CNaeel=E2=80=9D?= Date: Sat, 27 Jun 2026 13:16:30 +0400 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=82=D1=80=D1=91=D1=85=D1=8D=D1=82?= =?UTF-8?q?=D0=B0=D0=BF=D0=BD=D1=8B=D0=B9=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82?= =?UTF-8?q?=D1=80=20=D0=BC=D1=83=D1=81=D0=BE=D1=80=D0=B0=20=E2=80=94=20?= =?UTF-8?q?=D1=8D=D1=82=D0=B0=D0=BF=D1=8B=201-2=20(0=20=D1=82=D0=BE=D0=BA?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=D0=B2)=20=D0=B2=20classify.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/db/documents.py | 8 ++++++++ deploy/services/classify.py | 38 ++++++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/deploy/db/documents.py b/deploy/db/documents.py index 527b5fa..e6d5d33 100644 --- a/deploy/db/documents.py +++ b/deploy/db/documents.py @@ -53,6 +53,14 @@ def set_classify_failed(doc_id, error): ) +def set_classify_garbage(doc_id, reason=""): + """Mark document as garbage (Stage 1-2 filter, no LLM call).""" + return execute( + "UPDATE documents SET doc_type='garbage', classify_status='garbage', error_message=%s WHERE id=%s", + (f"garbage: {reason}", doc_id), + ) + + def list_pending(batch_id): """Documents waiting for classification.""" return query( diff --git a/deploy/services/classify.py b/deploy/services/classify.py index aec3f23..1262c4c 100644 --- a/deploy/services/classify.py +++ b/deploy/services/classify.py @@ -27,6 +27,31 @@ LLM_URL = "https://api.aillm.ru/v1/chat/completions" LLM_KEY = os.environ.get("LLM_KEY") or os.environ.get("LLM_API_KEY", "") LLM_MODEL = "gpt-oss-120b" +# ── Garbage filter (Stage 1: filename regex) ──────────────────── +_GARBAGE_FILENAME_RE = re.compile( + r'(сч[её]т|акт|плат[её]ж|УПД|сверк|инвойс|invoice|payment|act)', + re.IGNORECASE, +) + +# ── Garbage filter (Stage 2: header keywords) ─────────────────── +_GARBAGE_HEADER_MARKERS = [ + 'СЧЕТ-ФАКТУРА', 'СЧЕТ НА ОПЛАТУ', 'АКТ СВЕРКИ', + 'АКТ ОКАЗАННЫХ УСЛУГ', 'АКТ ВЫПОЛНЕННЫХ РАБОТ', + 'ПЛАТЁЖНОЕ ПОРУЧЕНИЕ', 'УНИВЕРСАЛЬНЫЙ ПЕРЕДАТОЧНЫЙ', + 'УПД', 'ПЛАТЕЖНОЕ ПОРУЧЕНИЕ', +] + + +def _is_garbage_by_filename(filename: str) -> bool: + """Stage 1: regex по имени файла — быстро, 0 токенов.""" + return bool(_GARBAGE_FILENAME_RE.search(filename)) + + +def _is_garbage_by_header(text: str) -> bool: + """Stage 2: ключевые слова в первых 2KB текста — быстро, 0 токенов.""" + header = text[:2000].upper() + return any(marker in header for marker in _GARBAGE_HEADER_MARKERS) + def _call_llm_classify(header_text): """ @@ -70,9 +95,20 @@ def classify_batch(batch_id): failed = 0 def _classify_one(doc): - """Классифицировать один документ: выжимка → LLM → сохранить результат.""" + """Классифицировать один документ: фильтр → выжимка → LLM → сохранить.""" try: + # Stage 1: garbage by filename (0 tokens) + if _is_garbage_by_filename(doc["filename"]): + db_docs.set_classify_garbage(doc["id"], "filename_regex") + return True + + # Stage 2: garbage by header keywords (0 tokens) text = _smart_extract(doc["elements_json"]) + if _is_garbage_by_header(text): + db_docs.set_classify_garbage(doc["id"], "header_keywords") + return True + + # Stage 3: LLM classification (only for remaining) result, raw = _call_llm_classify(text) db_docs.set_classification( doc["id"],