scanner.py: универсальный LLM-промпт (LLM сама определяет типы) + fallback-генератор
Deploy drhider / validate (push) Waiting to run

This commit is contained in:
2026-07-12 09:11:29 +04:00
parent 579f9c8334
commit 2ceeb05a8b
+40 -26
View File
@@ -2,8 +2,11 @@
Проход 1: обнаружение сущностей в тексте документов. Проход 1: обнаружение сущностей в тексте документов.
Два метода сканирования: Два метода сканирования:
1. _scan_regex — быстрое regex-обнаружение (телефоны, email, ИНН, счета, компании, ФИО) 1. scan_regex — быстрое regex-обнаружение (телефоны, email, ИНН, счета, компании, ФИО)
2. _scan_llm_ner — LLM-обнаружение (имена, адреса, паспорта — то что regex не ловит) 2. scan_llm_ner — универсальное LLM-обнаружение (любые приватные данные)
LLM САМА определяет какие типы сущностей присутствуют в документе.
Никакого фиксированного списка типов.
""" """
import re import re
@@ -12,15 +15,25 @@ import logging
from typing import Dict from typing import Dict
from .config import ENTITY_PATTERNS, COMPANY_PATTERN, PERSON_PATTERN from .config import ENTITY_PATTERNS, COMPANY_PATTERN, PERSON_PATTERN
from .generators import ENTITY_GENERATORS from .generators import ENTITY_GENERATORS, FALLBACK_GENERATOR
from .generators.company import generate_company from .generators.company import generate_company
from .generators.person import generate_person from .generators.person import generate_person
from .generators.address import generate_address
from .generators.passport import generate_passport
log = logging.getLogger("drhider") log = logging.getLogger("drhider")
def _get_generator(entity_type: str):
"""Получить генератор по типу сущности. Если тип неизвестен — fallback.
Args:
entity_type: Строковый идентификатор типа (например, "phone", "person_name")
Returns:
Функция-генератор: (str) -> str
"""
return ENTITY_GENERATORS.get(entity_type, FALLBACK_GENERATOR)
def scan_regex(text: str, mapping: Dict[str, str]) -> None: def scan_regex(text: str, mapping: Dict[str, str]) -> None:
"""Сканировать текст regex-паттернами, заполнить словарь замен. """Сканировать текст regex-паттернами, заполнить словарь замен.
@@ -43,8 +56,8 @@ def scan_regex(text: str, mapping: Dict[str, str]) -> None:
if not original or original in mapping: if not original or original in mapping:
continue continue
# Выбираем генератор по типу сущности # Генератор: специализированный (если есть) или fallback
generator = ENTITY_GENERATORS.get(entity_type, lambda x: "XXX") generator = _get_generator(entity_type)
mapping[original] = generator(original) mapping[original] = generator(original)
# ── Названия компаний ── # ── Названия компаний ──
@@ -86,17 +99,21 @@ def scan_llm_ner(all_texts: Dict[str, str], mapping: Dict[str, str], llm_client)
f"FILE: {fname}\n{t[:3000]}" for fname, t in all_texts.items() f"FILE: {fname}\n{t[:3000]}" for fname, t in all_texts.items()
) )
# Промпт для LLM # ── Универсальный промпт — без фиксированного списка типов ──
prompt = ( prompt = (
"Ты — система обнаружения персональных данных в документах. " "You are a PII (Personally Identifiable Information) detector. "
"Найди ВСЕ следующие сущности в тексте ниже:\n\n" "Find ALL sensitive or private information in the text below.\n\n"
"1. ФИО (полные и сокращённые — 'Иванов И.И.', 'Петров А.С.')\n" "Return a JSON array. Each item must have:\n"
"2. Названия компаний-контрагентов (не 'НУБЕС')\n" ' - "type": short snake_case label describing the entity\n'
"3. Почтовые адреса\n" ' (e.g. person_name, phone, email, address, inn, company, passport,\n'
"4. Паспортные данные\n\n" ' contract_number, employee_id, bank_account — WHATEVER you see)\n'
"Формат ответа — JSON-массив:\n" ' - "value": the EXACT string from the text (copy verbatim)\n\n'
'[{"type": "person"|"company"|"address"|"passport", "value": "найденный текст"}]\n\n' "Rules:\n"
f"Текст:\n{combined[:8000]}" "- Copy value VERBATIM — same spaces, punctuation, case as in text\n"
"- If same value appears multiple times — include only once\n"
"- Invent the type label yourself based on what you see\n"
"- Return ONLY the JSON array, no other text, no markdown wrapping\n\n"
f"Text:\n{combined[:8000]}"
) )
try: try:
@@ -118,15 +135,12 @@ def scan_llm_ner(all_texts: Dict[str, str], mapping: Dict[str, str], llm_client)
if not val or val in mapping: if not val or val in mapping:
continue continue
ent_type = ent.get("type", "") # Тип — любой (LLM сама придумала)
if ent_type == "person": ent_type = ent.get("type", "").strip().lower().replace(" ", "_")
mapping[val] = generate_person(val)
elif ent_type == "company": # Генератор: специализированный (если тип совпал) или fallback
mapping[val] = generate_company(val) generator = _get_generator(ent_type)
elif ent_type == "address": mapping[val] = generator(val)
mapping[val] = generate_address(val)
elif ent_type == "passport":
mapping[val] = generate_passport(val)
except Exception as e: except Exception as e:
log.warning("LLM NER failed: %s", e) log.warning("LLM NER failed: %s", e)