scanner.py: универсальный LLM-промпт (LLM сама определяет типы) + fallback-генератор
Deploy drhider / validate (push) Waiting to run
Deploy drhider / validate (push) Waiting to run
This commit is contained in:
+40
-26
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user