diff --git a/History/2026-07-12-sonnet-query.md b/History/2026-07-12-sonnet-query.md new file mode 100644 index 0000000..5dcc08b --- /dev/null +++ b/History/2026-07-12-sonnet-query.md @@ -0,0 +1,76 @@ +# Запрос к Соннету — универсальная архитектура DrHider + +## Контекст + +Проект DrHider — обфускация документов (замена персональных данных на фиктивные). Сейчас: +- Python/Flask, без БД, Managed Flask на платформе Штурвал +- Двухпроходная архитектура: сбор сущностей → замена +- Проход 1: regex-паттерны (телефон, email, ИНН, ОГРН, КПП, БИК, счета, паспорт) + LLM NER (ФИО, компании, адреса, паспорта) +- Проход 2: замена по словарю mapping + +## Проблема + +Архитектура **в корне неверна** — жёстко привязана к фиксированному списку типов сущностей: + +```python +# config.py — жёстко зашитые паттерны +ENTITY_PATTERNS = { + "phone": r'...', + "email": r'...', + "inn_fl": r'ИНН\s*\d{12}', + ... +} +``` + +```python +# scanner.py — жёстко зашитый промпт +prompt = ( + "Найди ВСЕ следующие сущности:\n" + "1. ФИО\n2. Компании\n3. Адреса\n4. Паспортные данные\n" +) +``` + +Если новый документ содержит СНИЛС, водительское удостоверение, номер договора, ИНН без префикса «ИНН» — система их НЕ обнаружит. Надо править config, generators, scanner, маппинги. + +## Что нужно + +**Универсальная архитектура**, где система САМА определяет что скрывать в любом документе: +1. Не привязана к списку типов +2. Не требует добавления паттернов под каждый новый вид данных +3. LLM сама решает что является персональными/конфиденциальными данными +4. Генерация фиктивных значений — тоже универсальная (не 11 отдельных функций) + +## Текущая структура (полная) + +``` +drhider/ +├── config.py # ENTITY_PATTERNS (10 regex), словари имён/городов +├── checksum.py # Контрольные суммы ИНН/ОГРН +├── random_utils.py # random_digits, random_letters +├── generators/ # 11 файлов: phone, email, inn, ogrn, kpp, bik, accounts, passport, company, person, address +├── llm_client.py # HTTP-клиент к LLM API +├── extractor.py # Извлечение текста + expand_zips + convert_pdfs_to_docx +├── scanner.py # scan_regex (regex) + scan_llm_ner (LLM NER с жёстким промптом) +├── replacer.py # apply_replacements, replace_in_docx, replace_in_text +├── builder.py # build_zip, build_mapping_csv +├── obfuscator.py # TwoPassObfuscator — оркестратор +├── site/app.py # Flask (3 blueprint'а) +└── site/templates/ # HTML-интерфейс +``` + +## Вопросы к Соннету + +1. Как перестроить архитектуру чтобы обнаружение было универсальным (LLM сама решает что скрывать)? +2. Нужен ли regex вообще или достаточно одного LLM с правильным промптом? +3. Как сделать генерацию фиктивных значений универсальной? (Не 11 функций под каждый тип, а что-то общее) +4. Двухпроходная схема (сбор→замена) — сохранять или перейти на однопроходную (LLM сразу возвращает обфусцированный текст)? +5. Как должен выглядеть промпт чтобы LLM возвращала структурированный результат (что найдено + на что заменить)? +6. Стоит ли сохранять DOCX-форматирование (сейчас runs склеиваются-разделяются) или проще отдать LLM plain text? + +## Ограничения + +- Документы до 200 MB +- Форматы: .docx, .pdf, .txt, .zip +- LLM: OpenAI-совместимое API (aillm.ru, модель gpt-oss-120b, 8000 токенов) +- Без БД, всё в памяти +- Платформа: Managed Flask на Kubernetes diff --git a/History/2026-07-12-sonnet-response.md b/History/2026-07-12-sonnet-response.md new file mode 100644 index 0000000..05d3b38 --- /dev/null +++ b/History/2026-07-12-sonnet-response.md @@ -0,0 +1,78 @@ +# Ответ Соннета — универсальная архитектура DrHider (2026-07-12) + +Кратко: 6 вопросов → 6 ответов → карта изменений. + +--- + +## Q1. Как сделать обнаружение универсальным? + +Промпт должен звучать не «найди вот эти типы», а «найди ВСЁ, что выглядит как приватная информация, и сам назови тип». + +Затрагивает: `scanner.py` (промпт), `builder.py` (тип для mapping.csv). + +--- + +## Q2. Regex или LLM? + +**Гибрид — правильный выбор:** +- **Regex = pre-pass** для структурированных данных (телефон, email, ИНН, БИК). Экономит токены LLM. +- **LLM = post-pass** для неструктурированного (имена, адреса, нестандартные ID). +- Дублирования не страшны — mapping dict сам отсеет. + +--- + +## Q3. Генерация фиктивных значений — универсальная? + +Два уровня: +1. **Известные типы** — специализированные генераторы (оставить как есть). +2. **Неизвестные типы** — fallback-генератор: character-class preserving замена (цифры→цифры, буквы→буквы той же длины). + +Дополнительно: в промпт добавить `"category": "person|org|contact|id|financial|other"` — 6 категорий вместо 10+ типов. + +--- + +## Q4. Двухпроходная или однопроходная? + +**Двухпроходная — обязательно.** Причина: «Иванов» в 5 документах должен заменяться одинаково. Однопроход не может этого гарантировать. + +Текущий `TwoPassObfuscator` — правильный, не трогать. + +--- + +## Q5. Новый промпт + +``` +You are a PII detector. Find ALL sensitive or private information. + +Return JSON array: [{"type": "short_label", "value": "exact_string"}] + +Rules: +- Copy "value" VERBATIM from text +- "type" is snake_case label you invent +- Same value → include once +- Return ONLY JSON +``` + +Ключевое: нет ограничения на типы, value verbatim. + +--- + +## Q6. DOCX или Markdown? + +- **Внутренняя обработка:** Markdown проще парсить, LLM понимает лучше. +- **На выходе:** опция `output_format: "docx" | "md"`. По умолчанию `"docx"`. + +--- + +## Итоговая карта изменений + +``` +scanner.py — новый промпт (убрать список типов, LLM сама называет типы) +generators/ — добавить fallback-генератор для неизвестных типов +obfuscator.py — вызывать fallback если тип неизвестен +api_bp.py — принять параметр output_format +replacer.py — добавить replace_to_markdown() +builder.py — упаковывать .md если output_format=md +``` + +**Не трогать:** двухпроходная схема, checksum-генераторы, ZIP-безопасность. diff --git a/History/2026-07-12-v3-plan.md b/History/2026-07-12-v3-plan.md new file mode 100644 index 0000000..fa89cf0 --- /dev/null +++ b/History/2026-07-12-v3-plan.md @@ -0,0 +1,51 @@ +# План v3 — универсальная архитектура (2026-07-12) + +## Цель + +Убрать жёсткую привязку к фиксированному списку типов сущностей. +Перейти на Markdown как внутренний формат. + +## Изменения по модулям + +### 1. `scanner.py` — универсальный LLM-промпт +- Убрать список типов из промпта +- LLM сама называет типы (snake_case) +- `value` — verbatim из текста + +### 2. `generators/` — fallback-генератор +- Новый `fallback.py`: character-class preserving замена +- `__init__.py`: добавить в ENTITY_GENERATORS +- `obfuscator.py`: использовать fallback для неизвестных типов + +### 3. `extractor.py` — единый MD-пайплайн +- DOCX → MD (python-docx: стили, форматирование) +- PDF → MD (pdfplumber: текст + таблицы) +- TXT → как есть +- Убрать convert_pdfs_to_docx (не нужен) +- Убрать хранение docx-объектов + +### 4. `replacer.py` — упростить +- `apply_replacements(text)` — уже есть +- `replace_in_docx(doc, mapping)` — сохранить как утилиту для DOCX-выхода +- Удалить `replace_in_text()` (заменяется на apply_replacements) + +### 5. `obfuscator.py` — обновить пайплайн +- Pass 1: extract MD → scan_regex + scan_llm_ner +- Pass 2: apply_replacements к MD +- Опционально: replace_in_docx к оригинальному DOCX + +### 6. `api_bp.py` — output_format +- Параметр `output_format: "md" | "docx"` (по умолчанию md) + +### 7. `.doc` — потом +- Добавим convert_doc_to_docx через LibreOffice отдельно + +## Порядок реализации + +1. `generators/fallback.py` + обновить `__init__.py` +2. `scanner.py` — новый промпт +3. `extractor.py` — MD-конвертация +4. `replacer.py` — упростить +5. `obfuscator.py` — новый пайплайн +6. `api_bp.py` — output_format +7. Проверить всё → commit diff --git a/drhider/generators/__init__.py b/drhider/generators/__init__.py index 83fc353..042d3cd 100644 --- a/drhider/generators/__init__.py +++ b/drhider/generators/__init__.py @@ -13,6 +13,7 @@ from .kpp import generate_kpp from .bik import generate_bik from .accounts import generate_rs, generate_ks from .passport import generate_passport +from .fallback import generate_fallback # ═══════════════════════════════════════════════════════════════════════════ # Маппинг: entity_type → функция-генератор @@ -31,3 +32,6 @@ ENTITY_GENERATORS = { "ks": generate_ks, # корреспондентский счёт "passport": generate_passport, } + +# Fallback-генератор — экспортируется отдельно для использования в scanner/obfuscator +FALLBACK_GENERATOR = generate_fallback diff --git a/drhider/generators/fallback.py b/drhider/generators/fallback.py new file mode 100644 index 0000000..955dc6c --- /dev/null +++ b/drhider/generators/fallback.py @@ -0,0 +1,64 @@ +""" +Fallback-генератор фиктивных значений для неизвестных типов сущностей. + +Используется когда LLM находит сущность с типом, которого нет +в специализированных генераторах (например, "contract_number", "employee_id"). + +Принцип: character-class preserving замена. +- Цифры → случайные цифры +- Буквы → случайные буквы (того же алфавита) +- Пробелы/знаки препинания → сохраняются +- Длина строки сохраняется +""" + +import random +import string + + +def generate_fallback(original: str) -> str: + """Сгенерировать фиктивное значение, сохраняя структуру оригинала. + + Правила замены: + - [0-9] → случайная цифра + - [A-Za-z] → случайная буква того же регистра + - [А-Яа-я] → случайная кириллическая буква того же регистра + - Все остальные символы (пробелы, дефисы, спецсимволы) → без изменений + + Args: + original: Оригинальное значение сущности + + Returns: + Фиктивное значение той же длины и структуры + + Example: + "Договор №123/2024" → "Фтщшлпь №847/5921" + "EMP-0042" → "XQJ-8193" + """ + result = [] + + for ch in original: + if ch.isdigit(): + # Цифра → случайная цифра + result.append(random.choice(string.digits)) + + elif 'A' <= ch <= 'Z': + # Заглавная латиница → случайная заглавная латиница + result.append(random.choice(string.ascii_uppercase)) + + elif 'a' <= ch <= 'z': + # Строчная латиница → случайная строчная латиница + result.append(random.choice(string.ascii_lowercase)) + + elif 'А' <= ch <= 'Я': + # Заглавная кириллица → случайная заглавная кириллица + result.append(chr(random.randint(0x0410, 0x042F))) + + elif 'а' <= ch <= 'я' or ch == 'ё': + # Строчная кириллица → случайная строчная кириллица + result.append(chr(random.randint(0x0430, 0x044F))) + + else: + # Всё остальное (пробелы, дефисы, слэши, точки) → сохранить + result.append(ch) + + return ''.join(result)