feat: трёхэтапный фильтр мусора — этапы 1-2 (0 токенов) в classify.py

This commit is contained in:
2026-06-27 13:16:30 +04:00
parent a474b4324b
commit 909468b5b7
2 changed files with 45 additions and 1 deletions
+8
View File
@@ -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): def list_pending(batch_id):
"""Documents waiting for classification.""" """Documents waiting for classification."""
return query( return query(
+37 -1
View File
@@ -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_KEY = os.environ.get("LLM_KEY") or os.environ.get("LLM_API_KEY", "")
LLM_MODEL = "gpt-oss-120b" 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): def _call_llm_classify(header_text):
""" """
@@ -70,9 +95,20 @@ def classify_batch(batch_id):
failed = 0 failed = 0
def _classify_one(doc): def _classify_one(doc):
"""Классифицировать один документ: выжимка → LLM → сохранить результат.""" """Классифицировать один документ: фильтр → выжимка → LLM → сохранить."""
try: 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"]) 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) result, raw = _call_llm_classify(text)
db_docs.set_classification( db_docs.set_classification(
doc["id"], doc["id"],