Pe4enushko/crappp

Python

0

27 commits

updated Aug 6, 2026

See the code

README

billeval

Построчная форензик-обработка банковских выписок организаций: разбор файлов разного формата в единую таблицу операций, счётные агрегаты по счёту и построчная оценка операций языковой моделью с указанием кода схемы.

Проект описан в двух документах, и они — источник истины, а не этот файл:

документо чём
docs/SPEC.mdчто система делает, промпт судьи, инварианты
docs/PLAN.mdэтапы, критерии готовности, что уже сделано
docs/DOMAIN_TYPOLOGY.mdсхемы C1–C9, за которыми следит судья
docs/OPEN_ITEMS.mdоткрытые вопросы, требующие решения человека
docs/EVAL.mdкак измеряется качество судьи

Установка

Пакеты ставятся только в локальный .venv и только через uv pip — глобальный pip в этом проекте не используется.

uv venv
uv pip install -e .

Модель не устанавливается локально: судья обращается к vLLM-серверу по URL (config/config.yaml, секция judge). Пакет vllm ставить не нужно.

Порядок работы

billeval scan --detect          # найти файлы и определить формат
billeval parse                  # разобрать в таблицу операций
billeval aggregate              # счётные агрегаты по счёту
billeval prefetch-orgs          # справки об организациях (Э6, необязательно)
billeval judge                  # построчная оценка моделью
billeval report                 # отчёт

prefetch-orgs в цепочке необязателен: судья штатно работает в режиме «профиль неизвестен» (§8.8 SPEC). Но профиль держит полноту вердиктов — см. следующий раздел.

Полезное по ходу:

billeval check-config           # что загружено из config/, оба контура моделей
billeval stats                  # что в базе
billeval judge --dry-run        # сколько уйдёт запросов, без обращения к модели
billeval eval                   # метрики судьи на эталоне из БД
billeval arbitrate --dry-run    # файлы, где формат не определился правилом (§5.1)
billeval fix-rows --dry-run     # строки, отсеянные как «ни даты, ни суммы» (§5.6)

Две модели, а не одна

Судья и Э6 ходят в разные эндпоинты, и путать их не следует:

судья (judge, eval)Э6 (prefetch-orgs, arbitrate, fix-rows)
эндпоинтлокальный vLLM, VLLM_*DeepSeek по умолчанию, DEEPSEEK_*
запросов на прогон≈27 000единицы-десятки
зачемследование инструкции, воспроизводимостьзнание о мире (ЕГРЮЛ, ОКВЭД)

Метрики качества судьи (§8.12 SPEC) измерены на Qwen — смена модели судьи обнуляет эталон, поэтому ENRICH_PROVIDER переключает только Э6. Все адреса и ключи живут в .env, в config/config.yaml стоят подстановки ${…}. billeval check-config печатает оба контура рядом.

Сверка профилей организаций

config/org_profiles.yaml — не украшение отчёта. Заполненность этого файла напрямую держит полноту вердиктов suspicious: при пустом профиле включается §8.8 SPEC, уверенность жёстко ограничивается 0.6, и спорная операция уходит с suspicious на attention. Замер 2026-07-30 на эталоне: 0.38 без профилей против 0.62–0.75 с профилями при одном и том же промпте.

Обратная сторона — ошибочный профиль хуже пустого. Пустой заставляет судью сомневаться; неверный заставляет уверенно судить по чужой мерке. Единственный пропуск в замере появился именно с профилем: зная, что организация — ЧОО, модель объяснила 200 000 ₽ на личный счёт физлица «штатной процедурой». Поэтому профили сверяются, а спорное помечается как спорное, а не сглаживается.

Текущее состояние: шесть организаций сверены по агрегаторам ЕГРЮЛ (saby.ru, spark-interfax, РБК, list-org, audit-it) 2026-07-30 и 2026-08-01. История сверки, отклонённые сведения и разбор по каждой организации — docs/changes/2026-08-01-org-profiles-verification.md; сам конфиг содержит только данные. Главное незакрытое: состав участников ОРЛАНа (ответы источников противоречат) и ГРОМ-ТОЛЬЯТТИ (скрыт) — у обоих стоит явный unknown. Связей между организациями набора по формальным основаниям ЕГРЮЛ не установлено, и автоматически связанность не устанавливается (§8а.2).

Автоматическая сверка: billeval prefetch-orgs (Э6)

Стадия Э6 делает то же самое командой. Провайдер по умолчанию — DeepSeek, ключ и адрес берутся из .env (DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL):

billeval prefetch-orgs --dry-run          # о ком будут справки, без обращения к сети
billeval prefetch-orgs                    # запросить и записать в org_profile
billeval prefetch-orgs --provider perplexity   # переключить провайдера
billeval orgs --profiles                  # что получено и что дойдёт до судьи

Что стадия гарантирует, и почему это важнее удобства:

  • справка запрашивается строго по ИНН (R14). Организация без ИНН пропускается: в наборе четыре ЧОО с названиями на «ГРОМ», и поиск по названию подставил бы одному профиль другого;
  • справка, полученная по чужому ИНН, не подставляется — ни из сети, ни из кэша. Без этой сверки ошибка провайдера была бы невидимой: модель уверенно оценивала бы операции по чужому виду деятельности;
  • ключа нет — стадия молча деградирует: цепочка доходит до Null, судья работает по §8.8, billeval check-config называет недостающую переменную;
  • ручной профиль не затирается по полям: сетевая справка выигрывает спор о заполненном поле, но поле, которого в ней нет, остаётся ручным.

После первого прогона профили стоит перечитать глазами. Сейчас в config/org_profiles.yaml руками сверено пять организаций, и там есть то, чего в автоматической справке может не оказаться: конкурсное производство «КОБРЫ» и помеченный спорным статус «ЛЕКС». Для оценки операций эти обстоятельства важнее ОКВЭДа. Если сетевая справка их потеряла — --on-conflict=keep возвращает ручную версию в силу, ничего не удаляя.

Как прогнать сверку вручную через модель с доступом в сеть

Процедура ниже остаётся полезной, когда нужен не автоматический профиль, а проверяемые поля со ссылками и отдельный перечень расхождений между источниками — то есть когда сверку принимает человек.

Шаг 1. Собрать список ИНН из базы.

sqlite3 billeval.db \
  "SELECT inn, name_normalized FROM organization WHERE inn IS NOT NULL AND inn <> '';"

Шаг 2. Отдать модели этот промпт, подставив список ИНН.

Ты сверяешь данные юридических лиц по открытым источникам РФ.

Для каждого ИНН из списка найди в открытых источниках (ЕГРЮЛ и агрегаторы:
rusprofile, list-org, checko, saby, spark-interfax, РБК Компании) и верни
JSON-массив объектов строго такой формы:

{
  "inn": "...",
  "name_full": "полное наименование как в реестре",
  "ogrn": "...",
  "kpp": "...",
  "registered": "ГГГГ-ММ-ДД",
  "address": "юридический адрес",
  "director": "ФИО и должность с датой назначения",
  "capital": "уставный капитал",
  "okved_primary": "код и ТОЧНАЯ формулировка из классификатора",
  "okved_extra": ["код — формулировка", "..."],
  "licenses": "лицензии (Росгвардия, МЧС и пр.) или null",
  "status": "действующая | ликвидирована | в ликвидации | банкротство",
  "status_date": "ГГГГ-ММ-ДД или null",
  "sources": ["URL", "URL"],
  "conflicts": "чем именно источники противоречат друг другу, или null"
}

Требования, от которых зависит польза ответа:

1. НЕ ДОГАДЫВАЙСЯ. Если поля нет в источниках — ставь null. Пустое поле
   безопасно, выдуманное — нет: эти данные идут в промпт, по которому
   оцениваются операции, и ошибка в профиле искажает вывод по всем операциям
   организации.
2. Формулировку ОКВЭД приводи ДОСЛОВНО из классификатора, не по смыслу.
   Например, код 80.10 звучит как «Деятельность охранных служб, в том числе
   частных» — а не «Деятельность частных охранных служб».
3. Каждый объект — минимум ДВА независимых источника. Где источники
   расходятся (статус, адрес, руководитель), не выбирай удобный: заполни
   поле "conflicts" и опиши расхождение с обеими версиями.
4. Различай однофамильцев. В РФ десятки ЧОО с названиями «Кобра», «Орлан»,
   «Гром». Ключ — ИНН и только ИНН; совпадение по названию не принимается.
5. Отдельно отмечай банкротство, ликвидацию и дисквалификацию руководителя:
   для оценки операций это важнее ОКВЭДа.

Список ИНН:
<сюда список из шага 1>

Шаг 3. Перенести ответ в конфиги.

До модели-судьи доходят только поля activity, okved_primary, region и source — блок <организация> промпта, см. prompt.py:168-175; собирает их org_view в context.py. Остальное в YAML читает человек. Поэтому:

  • okved_primary — код и дословная формулировка;
  • activity — вид деятельности плюс то, что меняет оценку операций: банкротство, ликвидация, профильные неохранные виды деятельности (у «ЦСМ ГРОМ» это электромонтаж — из-за него платежи за монтаж и материалы профильны, а не посторонни);
  • region — регион и город;
  • source — откуда данные и на какую дату; спорный статус пишется прямо здесь, чтобы судья видел неопределённость, а не мнимую точность;
  • status — состояние юрлица (действующая, ликвидирована, конкурсное производство) с датой; факт, а не повод для вывода;
  • founders, director, management_company — формальные сведения из реестра; ИНН физлица указывается, если раскрыт. Незакрытое пишется явным unknown: «не найдено» ≠ «нет»;
  • address, registration_date — косвенные признаки, сами по себе ничего не доказывающие;
  • ОГРН, КПП, капитал, лицензии — по-прежнему комментариями: в промпте были бы шумом.

Чего в activity быть не должно. Пересказа выписок («по выпискам — охранные услуги»), ранжирования («крупнейшая по обороту») и указаний, что считать подозрительным. Это выводы, которые судья обязан сделать сам; поле не проверяется кодом никак, и что в него вписано — то модель и прочтёт.

Связанность организаций не устанавливается автоматически и списка аффилированных ИНН больше нет. Общие учредители, директора и адреса заносятся в профили как факты, а вывод о группе делает судья и читатель отчёта. Взаимные платежи связанности не доказывают — именно на них держался удалённый список.

Шаг 4. Проверить, что конфиг подхватился, и перемерить.

billeval check-config     # должно показать 6 профилей
billeval eval             # полнота suspicious не должна упасть

Если после сверки полнота падает — смотреть не промпт, а профиль: скорее всего в него попало утверждение, которым модель теперь оправдывает операции.

Тесты

.venv/bin/python -m pytest -q

Тесты судьи работают на MockBackend и в сеть не ходят: обращение к живой модели даёт разброс до 13 % между прогонами и в тестах бесполезно.

Pe4enushko/crappp

Python

0

27 commits

updated Aug 6, 2026

See the code

README

billeval

Построчная форензик-обработка банковских выписок организаций: разбор файлов разного формата в единую таблицу операций, счётные агрегаты по счёту и построчная оценка операций языковой моделью с указанием кода схемы.

Проект описан в двух документах, и они — источник истины, а не этот файл:

документо чём
docs/SPEC.mdчто система делает, промпт судьи, инварианты
docs/PLAN.mdэтапы, критерии готовности, что уже сделано
docs/DOMAIN_TYPOLOGY.mdсхемы C1–C9, за которыми следит судья
docs/OPEN_ITEMS.mdоткрытые вопросы, требующие решения человека
docs/EVAL.mdкак измеряется качество судьи

Установка

Пакеты ставятся только в локальный .venv и только через uv pip — глобальный pip в этом проекте не используется.

uv venv
uv pip install -e .

Модель не устанавливается локально: судья обращается к vLLM-серверу по URL (config/config.yaml, секция judge). Пакет vllm ставить не нужно.

Порядок работы

billeval scan --detect          # найти файлы и определить формат
billeval parse                  # разобрать в таблицу операций
billeval aggregate              # счётные агрегаты по счёту
billeval prefetch-orgs          # справки об организациях (Э6, необязательно)
billeval judge                  # построчная оценка моделью
billeval report                 # отчёт

prefetch-orgs в цепочке необязателен: судья штатно работает в режиме «профиль неизвестен» (§8.8 SPEC). Но профиль держит полноту вердиктов — см. следующий раздел.

Полезное по ходу:

billeval check-config           # что загружено из config/, оба контура моделей
billeval stats                  # что в базе
billeval judge --dry-run        # сколько уйдёт запросов, без обращения к модели
billeval eval                   # метрики судьи на эталоне из БД
billeval arbitrate --dry-run    # файлы, где формат не определился правилом (§5.1)
billeval fix-rows --dry-run     # строки, отсеянные как «ни даты, ни суммы» (§5.6)

Две модели, а не одна

Судья и Э6 ходят в разные эндпоинты, и путать их не следует:

судья (judge, eval)Э6 (prefetch-orgs, arbitrate, fix-rows)
эндпоинтлокальный vLLM, VLLM_*DeepSeek по умолчанию, DEEPSEEK_*
запросов на прогон≈27 000единицы-десятки
зачемследование инструкции, воспроизводимостьзнание о мире (ЕГРЮЛ, ОКВЭД)

Метрики качества судьи (§8.12 SPEC) измерены на Qwen — смена модели судьи обнуляет эталон, поэтому ENRICH_PROVIDER переключает только Э6. Все адреса и ключи живут в .env, в config/config.yaml стоят подстановки ${…}. billeval check-config печатает оба контура рядом.

Сверка профилей организаций

config/org_profiles.yaml — не украшение отчёта. Заполненность этого файла напрямую держит полноту вердиктов suspicious: при пустом профиле включается §8.8 SPEC, уверенность жёстко ограничивается 0.6, и спорная операция уходит с suspicious на attention. Замер 2026-07-30 на эталоне: 0.38 без профилей против 0.62–0.75 с профилями при одном и том же промпте.

Обратная сторона — ошибочный профиль хуже пустого. Пустой заставляет судью сомневаться; неверный заставляет уверенно судить по чужой мерке. Единственный пропуск в замере появился именно с профилем: зная, что организация — ЧОО, модель объяснила 200 000 ₽ на личный счёт физлица «штатной процедурой». Поэтому профили сверяются, а спорное помечается как спорное, а не сглаживается.

Текущее состояние: шесть организаций сверены по агрегаторам ЕГРЮЛ (saby.ru, spark-interfax, РБК, list-org, audit-it) 2026-07-30 и 2026-08-01. История сверки, отклонённые сведения и разбор по каждой организации — docs/changes/2026-08-01-org-profiles-verification.md; сам конфиг содержит только данные. Главное незакрытое: состав участников ОРЛАНа (ответы источников противоречат) и ГРОМ-ТОЛЬЯТТИ (скрыт) — у обоих стоит явный unknown. Связей между организациями набора по формальным основаниям ЕГРЮЛ не установлено, и автоматически связанность не устанавливается (§8а.2).

Автоматическая сверка: billeval prefetch-orgs (Э6)

Стадия Э6 делает то же самое командой. Провайдер по умолчанию — DeepSeek, ключ и адрес берутся из .env (DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL):

billeval prefetch-orgs --dry-run          # о ком будут справки, без обращения к сети
billeval prefetch-orgs                    # запросить и записать в org_profile
billeval prefetch-orgs --provider perplexity   # переключить провайдера
billeval orgs --profiles                  # что получено и что дойдёт до судьи

Что стадия гарантирует, и почему это важнее удобства:

  • справка запрашивается строго по ИНН (R14). Организация без ИНН пропускается: в наборе четыре ЧОО с названиями на «ГРОМ», и поиск по названию подставил бы одному профиль другого;
  • справка, полученная по чужому ИНН, не подставляется — ни из сети, ни из кэша. Без этой сверки ошибка провайдера была бы невидимой: модель уверенно оценивала бы операции по чужому виду деятельности;
  • ключа нет — стадия молча деградирует: цепочка доходит до Null, судья работает по §8.8, billeval check-config называет недостающую переменную;
  • ручной профиль не затирается по полям: сетевая справка выигрывает спор о заполненном поле, но поле, которого в ней нет, остаётся ручным.

После первого прогона профили стоит перечитать глазами. Сейчас в config/org_profiles.yaml руками сверено пять организаций, и там есть то, чего в автоматической справке может не оказаться: конкурсное производство «КОБРЫ» и помеченный спорным статус «ЛЕКС». Для оценки операций эти обстоятельства важнее ОКВЭДа. Если сетевая справка их потеряла — --on-conflict=keep возвращает ручную версию в силу, ничего не удаляя.

Как прогнать сверку вручную через модель с доступом в сеть

Процедура ниже остаётся полезной, когда нужен не автоматический профиль, а проверяемые поля со ссылками и отдельный перечень расхождений между источниками — то есть когда сверку принимает человек.

Шаг 1. Собрать список ИНН из базы.

sqlite3 billeval.db \
  "SELECT inn, name_normalized FROM organization WHERE inn IS NOT NULL AND inn <> '';"

Шаг 2. Отдать модели этот промпт, подставив список ИНН.

Ты сверяешь данные юридических лиц по открытым источникам РФ.

Для каждого ИНН из списка найди в открытых источниках (ЕГРЮЛ и агрегаторы:
rusprofile, list-org, checko, saby, spark-interfax, РБК Компании) и верни
JSON-массив объектов строго такой формы:

{
  "inn": "...",
  "name_full": "полное наименование как в реестре",
  "ogrn": "...",
  "kpp": "...",
  "registered": "ГГГГ-ММ-ДД",
  "address": "юридический адрес",
  "director": "ФИО и должность с датой назначения",
  "capital": "уставный капитал",
  "okved_primary": "код и ТОЧНАЯ формулировка из классификатора",
  "okved_extra": ["код — формулировка", "..."],
  "licenses": "лицензии (Росгвардия, МЧС и пр.) или null",
  "status": "действующая | ликвидирована | в ликвидации | банкротство",
  "status_date": "ГГГГ-ММ-ДД или null",
  "sources": ["URL", "URL"],
  "conflicts": "чем именно источники противоречат друг другу, или null"
}

Требования, от которых зависит польза ответа:

1. НЕ ДОГАДЫВАЙСЯ. Если поля нет в источниках — ставь null. Пустое поле
   безопасно, выдуманное — нет: эти данные идут в промпт, по которому
   оцениваются операции, и ошибка в профиле искажает вывод по всем операциям
   организации.
2. Формулировку ОКВЭД приводи ДОСЛОВНО из классификатора, не по смыслу.
   Например, код 80.10 звучит как «Деятельность охранных служб, в том числе
   частных» — а не «Деятельность частных охранных служб».
3. Каждый объект — минимум ДВА независимых источника. Где источники
   расходятся (статус, адрес, руководитель), не выбирай удобный: заполни
   поле "conflicts" и опиши расхождение с обеими версиями.
4. Различай однофамильцев. В РФ десятки ЧОО с названиями «Кобра», «Орлан»,
   «Гром». Ключ — ИНН и только ИНН; совпадение по названию не принимается.
5. Отдельно отмечай банкротство, ликвидацию и дисквалификацию руководителя:
   для оценки операций это важнее ОКВЭДа.

Список ИНН:
<сюда список из шага 1>

Шаг 3. Перенести ответ в конфиги.

До модели-судьи доходят только поля activity, okved_primary, region и source — блок <организация> промпта, см. prompt.py:168-175; собирает их org_view в context.py. Остальное в YAML читает человек. Поэтому:

  • okved_primary — код и дословная формулировка;
  • activity — вид деятельности плюс то, что меняет оценку операций: банкротство, ликвидация, профильные неохранные виды деятельности (у «ЦСМ ГРОМ» это электромонтаж — из-за него платежи за монтаж и материалы профильны, а не посторонни);
  • region — регион и город;
  • source — откуда данные и на какую дату; спорный статус пишется прямо здесь, чтобы судья видел неопределённость, а не мнимую точность;
  • status — состояние юрлица (действующая, ликвидирована, конкурсное производство) с датой; факт, а не повод для вывода;
  • founders, director, management_company — формальные сведения из реестра; ИНН физлица указывается, если раскрыт. Незакрытое пишется явным unknown: «не найдено» ≠ «нет»;
  • address, registration_date — косвенные признаки, сами по себе ничего не доказывающие;
  • ОГРН, КПП, капитал, лицензии — по-прежнему комментариями: в промпте были бы шумом.

Чего в activity быть не должно. Пересказа выписок («по выпискам — охранные услуги»), ранжирования («крупнейшая по обороту») и указаний, что считать подозрительным. Это выводы, которые судья обязан сделать сам; поле не проверяется кодом никак, и что в него вписано — то модель и прочтёт.

Связанность организаций не устанавливается автоматически и списка аффилированных ИНН больше нет. Общие учредители, директора и адреса заносятся в профили как факты, а вывод о группе делает судья и читатель отчёта. Взаимные платежи связанности не доказывают — именно на них держался удалённый список.

Шаг 4. Проверить, что конфиг подхватился, и перемерить.

billeval check-config     # должно показать 6 профилей
billeval eval             # полнота suspicious не должна упасть

Если после сверки полнота падает — смотреть не промпт, а профиль: скорее всего в него попало утверждение, которым модель теперь оправдывает операции.

Тесты

.venv/bin/python -m pytest -q

Тесты судьи работают на MockBackend и в сеть не ходят: обращение к живой модели даёт разброс до 13 % между прогонами и в тестах бесполезно.