О проекте
Команда: Moggers
Кейс HackAlem.ai: Финансы — Freedom
Стадия: Доведённый продукт
Задача кейса: «Граф денег»: восстановить структуру организованной группы по сети транзакций (AML)
Суть
Пайплайн ролей, кластеров и приоритетов для AML-сети с автономным HTML-вьюером на cytoscape, React-фронтендом и AI-ассистентом с 9 графовыми инструментами.
Как устроено
Parquet → проверка целостности → направленный взвешенный граф → признаки (структура, достижимость от seed, временные паттерны) → Louvain по неориентированной проекции → роли по порогам, привязанным к распределениям датасета (coordinator, consolidator, distributor, transit, terminal, terminal_unverified, peripheral) → взвешенный приоритет. Выгрузки CSV, features.parquet, resilience.csv и самодостаточный viewer.html. AI-ассистент (gpt-4.1-mini) выбирает инструмент (convergence, trace_money, removal_impact, return_flows, data_gaps и др.), числа считает код, валидатор удаляет предложения с несуществующими gid; без ключа работает по шаблону.
Ключевые решения
- Louvain
- rule-based roles
- articulation points
- cycle/return flow detection
- removal simulation top-N
- tool calling
- GID reference validator
- cytoscape
Особенности
Расширение словаря ролью terminal_unverified для узлов на обрыве 4-го колена; побайтово воспроизводимые артефакты; однофайловый офлайн-вьюер, к которому чат подключается при запущенном локальном сервере; guardrails против выдуманных gid.
Стек
Python · pandas · NetworkX · cytoscape.js · React · Vite · TypeScript · Playwright
Модели
gpt-4.1-mini
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-082c1388-moggers.
README
README.mdГраф денег
Восстановление финансовой структуры организованной группы по транзакционной сети.
Вход — выгрузка исходящих переводов 81 клиента на 4 колена (2 248 узлов, 3 119 рёбер, 4 840 транзакций, июль 2026). Выход — роль каждого узла с обоснованием, кластеры сети и ранжированный список: кого из 2 248 клиентов смотреть первым и почему.
Запуск
Сайт и ИИ-ассистент (Windows, macOS, Linux)
После установки зависимостей pip install -e ".[web]" в окружении проекта
и npm ci --prefix frontend выполните python start.py из корня проекта.
Скрипт автоматически выбирает Python из .venv под текущую ОС, создаёт
недостающие результаты анализа и сборку интерфейса.
Сайт: http://127.0.0.1:8000. Чат доступен через пункт «ИИ-ассистент»
или http://127.0.0.1:8000/assistant#tab=chat. Ключ задаётся в локальном .env.
Сервер предназначен для одного пользователя и слушает только localhost.
После изменений frontend пересоберите его: npm run build --prefix frontend.
Пайплайн без сайта
./run.sh
Скрипт создаёт локальный .venv, ставит зависимости и считает всё от сырых
.parquet до выгрузок в out/. Системный Python не трогается: на многих
дистрибутивах он защищён (PEP 668), и установка пакетов туда запрещена.
Сам пересчёт — около 3 секунд на обычном ноутбуке (лимит по ТЗ — 5 минут);
первый запуск дополнительно тратит время на создание окружения.
Ни GPU, ни облака, ни внешних сервисов. Нужен Python ≥ 3.10 с модулем venv
(пакет python3-venv в Debian и Ubuntu).
Вручную, если окружение уже есть:
PYTHONPATH=src python3 -m moneygraph.pipeline --data data --out out
Что на выходе
| Файл | Строк | Содержание |
|---|---|---|
out/nodes_roles.csv | 2 248 | роль, уверенность, кластер, приоритет и обоснование по каждому узлу |
out/clusters.csv | 91 | размер, число seed, внутренний оборот и гипотеза по каждому кластеру |
out/top_nodes.csv | 25 | ранжированный список на проверку с обоснованием |
out/resilience.csv | 6 | что станет с сетью при изъятии топ-N узлов |
out/viewer.html | — | экран просмотра: схема сети, роли, поиск по gid (один файл, офлайн) |
out/features.parquet | 2 248 | все посчитанные признаки — для интерфейса и перепроверки |
out/summary.json | — | сводка прогона: время, распределение ролей, факты о датасете |
nodes_roles.csv содержит обязательные колонки схемы ТЗ (gid, role, role_score,
cluster_id, priority_score, evidence) и все признаки, на которых построена роль —
любую строку можно перепроверить, не заглядывая в код.
Экран просмотра
xdg-open out/viewer.html # или просто открыть файл двойным кликом
Один самодостаточный HTML: cytoscape вшит внутрь, данные встроены в страницу. Ни сервера, ни npm, ни интернета — файл открывается в любом браузере.
Что на экране:
- схема сети со стрелками направления денег и толщиной ребра по сумме;
- роль узла кодируется цветом и формой — цвет не единственный канал;
- поиск по gid: ввод номера сразу показывает узел и его окружение на 2 колена, все его связи подсвечиваются, суммы подписываются;
- фильтры по роли и кластеру, список «кого смотреть первым»;
- карточка узла: обоснование роли, метрики, возвратные потоки, точка сочленения, крупнейшие входящие и исходящие переводы — по ним можно кликать и идти по цепочке;
- панель устойчивости: что станет с сетью при изъятии топ-N узлов.
Для демо есть прямая ссылка на узел: out/viewer.html#gid=100000003684369100.
Схема рисует не все 2 248 узлов сразу — это нечитаемо. По умолчанию показывается топ приоритета с окружением (около 450 узлов), остальное открывается через поиск, фильтры или переключатель охвата.
AI-ассистент аналитика
Два способа: в терминале или прямо в интерфейсе.
moneygraph-ask "кто собирает деньги с 100000003016635100 и 100000000331309100?"
moneygraph-ask # диалоговый режим
moneygraph-serve # схема сети и чат в одном окне -> http://127.0.0.1:8765
Для режима с LLM: pip install -e ".[assistant]". Скопируйте .env.example
в .env в корне проекта и задайте настоящий OPENAI_API_KEY. Промокод на
кредиты не является API-ключом. .env исключён из Git и загружается только
на сервере; переменные окружения имеют приоритет. Модель по умолчанию —
gpt-4.1-mini, изменить её можно через MONEYGRAPH_LLM_MODEL.
Без ключа ассистент тоже работает: разбирает намерение вопроса, вызывает те же инструменты и отвечает по шаблону. Пайплайн и выгрузки от ассистента не зависят и считаются полностью локально — интернет нужен только ему, и ТЗ это допускает.
Ассистент в интерфейсе, без потери самодостаточности
moneygraph-serve поднимает локальный эндпоинт на 127.0.0.1 (только стандартная
библиотека, новых зависимостей нет). Страница определяет его сама:
| Как открыли | Что получаете |
|---|---|
out/viewer.html двойным кликом | схема, роли, поиск по gid — как обычно, чат показывает подсказку |
out/viewer.html при запущенном сервере | то же плюс работающий чат |
http://127.0.0.1:8765 | то же, но одним origin — CORS не нужен вовсе |
Файл остаётся самодостаточным. Must-have 5 не требует ничего запускать: жюри открывает файл двойным кликом, и всё работает. Чат — надстройка, которая включается, если её есть чем обслужить.
Инструменты при этом остаются в Python в единственном экземпляре: переписывать их на JavaScript означало бы два кода, считающих одни и те же числа, — и расхождение между ответом ассистента и выгрузкой было бы вопросом времени. Ключ живёт в переменной окружения сервера и в браузер не попадает; страница никогда не обращается к внешнему API сама.
Номера счетов в ответе кликабельны: клик открывает узел на схеме с его окружением.
Для репетиции демо есть прямые ссылки: #tab=chat и #ask=<вопрос>.
Что он умеет
Ассистент отвечает на вопросы, на которые сама выгрузка ответить не может, — те, что требуют обхода графа:
| Инструмент | Вопрос аналитика |
|---|---|
convergence | «кто собирает деньги с этих пятерых?» — узлы, куда сходятся потоки сразу от нескольких счетов |
trace_money | «как деньги дошли от A до B?» — маршруты с суммами на каждом шаге и узким местом |
removal_impact | «что будет, если заблокировать эти счета?» — сколько сети отрежется от seed |
return_flows | «возвращаются ли деньги обратно?» — встречные переводы и замкнутые цепочки |
data_gaps | «чего мне не хватает и что запросить дальше?» |
node_profile | «почему у этого gid такая роль?» |
counterparties | «кто ему платит и кому платит он?» |
cluster_profile | «что за кластер №N?» |
find_nodes | «покажи координаторов с приоритетом выше 0.5» |
Вопрос про схождение потоков — тот самый, ради которого кейс и заведён: вручную аналитик восстанавливает его обходом на четыре колена, здесь это один запрос.
Guardrails
Числа считает код, не модель. Модель выбирает инструмент, читает его JSON и формулирует ответ. Ни одного вычисления она не делает сама.
Каждое утверждение — со ссылкой на gid. Валидатор удаляет предложения, где упомянут несуществующий узел, и помечает ответ. Если после проверки не осталось текста, ответ собирается детерминированно.
Ограничения данных в системном промпте. Ассистент знает про обрыв на 4-м колене, занижённые входящие у seed и порог 5 000 ₸ — и обязан учитывать их в ответах.
Формулировки — гипотезы. «Признаки консолидации», а не «отмывает деньги».
Сбой внешнего API не ломает демо: ответ собирается детерминированно, в предупреждениях остаётся причина.
Как устроен пайплайн
data/*.parquet
-> loading.py загрузка и проверки целостности
-> graph.py направленный взвешенный граф (все 2 248 узлов, включая 19 без рёбер)
-> features.py структура + достижимость от seed + временны́е паттерны
-> clusters.py Louvain по неориентированной проекции
-> roles.py правила с порогами -> роль, уверенность, evidence
-> priority.py взвешенный приоритет -> кого смотреть первым
-> viewer.py автономный HTML со схемой сети
-> out/*.csv + out/viewer.html
agent/ читает готовые out/features.parquet и граф: отдельный слой поверх пайплайна
Критерии ролей
Правила проверяются в порядке приоритета, первое сработавшее определяет роль.
Все пороги собраны в src/moneygraph/config.py и привязаны к распределениям
в этом датасете, а не взяты произвольно.
| Роль | Правило | Порог и почему именно такой |
|---|---|---|
coordinator | собирает и перераспределяет: in_deg ≥ 5 и out_deg ≥ 10 и in_kzt ≥ 0.3 × out_kzt | требование баланса потоков отсекает узлы, которые не собирают, а вливают средства извне выгрузки |
coordinator | либо структурно: посредничество в верхнем 1%, деньги доходят от ≥ 3 seed, связывает ≥ 2 кластера | узел, через который проходят пути от нескольких независимых seed — это структура, а не совпадение |
consolidator | in_deg ≥ 5 и in_deg ≥ 2 × out_deg | 1 760 узлов имеют ровно одного плательщика; порог 5 отсекает верхние 2.3% (51 узел) — это уже не бытовые переводы |
distributor | out_deg ≥ 10 и out_deg ≥ 2 × in_deg | 1 554 узла не отдают вовсе; 10+ получателей — у 64 узлов (2.8%) |
transit | есть вход и выход, коэффициент пропуска out_kzt/in_kzt в 0.7–1.3, узел не seed | окно вокруг единицы = «принял и отдал столько же»; seed исключены, у них входящие занижены устройством выгрузки |
terminal | depth ≤ 3 и удержано ≥ 80% полученного | глубина ≤ 3 обязательна: на 4-м колене отсутствие исходящих — артефакт обхода |
terminal_unverified | depth = 4, исходящих нет, но получено заметно (≥ 2 плательщика или сумма выше медианы) | расширение словаря, см. раздел ниже |
peripheral | ни один порог не пройден | — |
role_score (0–1) — уверенность в роли. Она снижается, когда вывод опирается на
оборванную выгрузку, и масштабируется объёмом: транзит на одной операции у порога
5 000 ₸ получает низкую уверенность, а не единицу.
Расширение словаря: terminal_unverified
ТЗ допускает расширение словаря при условии документирования. Мы ввели одну дополнительную роль, потому что без неё решение обязано врать.
444 узла лежат на 4-м колене и не имеют исходящих. Наивное правило
«out_deg = 0 ⇒ конечный получатель» назвало бы их стоками — но обход просто
закончился на них, а не деньги. Из этих 444 у 183 узлов входящий профиль
действительно похож на сток; они получают роль terminal_unverified
с прямой формулировкой в evidence:
«Принял 300 тыс ₸ от 1 плательщика, исходящих нет — но узел на 4-м колене, где обход остановлен. Отличить конечного получателя от необследованного нельзя без выгрузки на 5-е колено.»
Настоящий terminal присваивается только при depth ≤ 3, где отсутствие
исходящих — это поведение узла, а не граница выгрузки. Таких узлов 1 185.
Приоритет: кого смотреть первым
priority = ( 0.28·роль + 0.20·seed_sources + 0.16·betweenness + 0.14·оборот
+ 0.13·структура + 0.09·pagerank + 0.05·охват ) × (0.6 + 0.4·role_score)
Компонента «структура» — возвратные потоки и точки сочленения, см. раздел ниже.
Компоненты нормируются на 0–1 по выборке. Множитель по role_score опускает
в списке узлы, у которых роль присвоена на слабом основании.
seed_sources — от скольких разных seed-клиентов деньги доходят до узла.
Это самый содержательный признак в задаче: совпадение нескольких независимых
seed на одном узле означает структуру, а не случайность.
В top_nodes.csv колонка why называет два главных вклада в ранг — чтобы
позицию в списке можно было оспорить по числам.
Возвратные потоки и устойчивость сети
Два дополнительных сигнала, которых нет в степенях и суммах.
Деньги возвращаются отправителю
Ищем простые циклы длиной до 6 (networkx.simple_cycles). В графе их 1 541,
и в них участвуют 303 узла. Самый явный случай — цикл длины 2: счета
переводят друг другу напрямую. Таких узлов 266.
Признак попадает в выгрузку колонками min_cycle_len, cycle_count,
reciprocal_partners и в evidence: «Деньги возвращаются отправителю:
6 встречных счетов». В приоритете прямой возврат весит больше длинной петли —
цепочка из шести узлов может оказаться совпадением, взаимные переводы вряд ли.
23 из 25 узлов топ-листа имеют возвратные потоки — независимое подтверждение того, что приоритет отобрал именно инфраструктуру, а не случайные крупные счета.
Что будет, если изъять топ-N
out/resilience.csv отвечает на вопрос, была ли изъята инфраструктура
или только листья. Главная метрика — сколько узлов остаётся достижимо
от seed-клиентов.
| Изъято | Отрезано от seed | Компонент стало | Оборот затронут |
|---|---|---|---|
| топ-1 | 4% | 93 | 6.6% |
| топ-5 | 16% | 207 | 15.0% |
| топ-10 | 34% | 315 | 21.8% |
| топ-20 | 52% | 414 | 25.0% |
| топ-50 | 63% | 650 | 36.6% |
Десять узлов — это 0.4% сети. Их изъятие отрезает от известной базы треть всех клиентов и дробит сеть с 35 связных компонент до 315. Это и есть ответ на вопрос кейса «кого смотреть первым».
Отдельно считаются точки сочленения (is_articulation, 527 узлов) — те,
чьё изъятие разрывает свою компоненту. Считается за один проход
по неориентированной проекции, в отличие от перебора «удали и посмотри».
Как учтены ограничения датасета
| Ограничение | Что сделано |
|---|---|
| Обход оборван на 4-м колене (444 узла) | terminal только при depth ≤ 3; отдельная роль terminal_unverified |
| Видны только исходящие переводы | pass_through не применяется к seed; узлы, отдающие втрое больше видимых входящих, помечаются в evidence как «источник средств вне выгрузки» |
| У seed входящие занижены | роль transit для seed не назначается вовсе |
| Порог 5 000 ₸ | дробление ниже порога невидимо — заявлено в ограничениях, на этом выводы не строятся |
| 19 seed без единого ребра | попадают в выгрузку как peripheral с явным указанием причины |
| 16 слабосвязных компонент | кластеризация Louvain, 91 сообщество; 8 из них содержат более одного seed |
| Louvain теряет направление | кластеры отвечают только на вопрос «кто с кем связан»; все роли считаются по направленному графу |
Ограничения подхода
Это гипотезы для проверки, а не выводы о виновности. Формулировки в evidence
намеренно даны как «признаки консолидации», «признаки транзита».
Размеченных ролей в данных нет, поэтому точность в привычном смысле не измеряется — критерии защищаются обоснованностью правил, а не accuracy.
Роли назначаются по структуре и суммам за один месяц. Внутрибанковский периметр, порог 5 000 ₸ и глубина 4 колена означают, что часть сети принципиально невидима: у 354 узлов исходящие превышают видимые входящие, и источник этих средств лежит за пределами выгрузки.
Betweenness считается структурно, без весов: веса-суммы нельзя подавать как длину пути, а инвертировать их — значит вводить произвольную шкалу.
Масштабирование до ~1 млн узлов
Текущий пайплайн — 3 секунды на 2 248 узлах, но узкие места известны и упрутся не в загрузку, а в две метрики.
Betweenness — O(V·E), на миллионе узлов неприменим. Замена: приближение
Брандеса по выборке опорных вершин (k семплов вместо всех) либо переход
на graph-tool/igraph с параллельной реализацией. Точное значение здесь не нужно,
нужен ранг.
Louvain на питоне заменяется на Leiden из igraph — он и быстрее, и не даёт
несвязных сообществ.
Достижимость от seed уже масштабируется: это по одному BFS на seed, а не на узел. При росте числа seed до тысяч — переход на разреженное матричное умножение булевых масок достижимости.
Хранение и признаки переезжают с pandas на DuckDB или Polars: агрегации по рёбрам и транзакциям — обычные group-by, они считаются вне памяти.
Что не изменится: правила ролей. Они опираются на локальные признаки узла (степени, суммы, коэффициент пропуска) и считаются за один проход независимо от размера графа. Пороги, привязанные к перцентилям, пересчитываются на новой выборке.
Тесты
make test
62 проверки: схема всех выгрузок, побайтовая воспроизводимость двух прогонов,
запуск CLI как его запустит жюри, и отдельно — ловушки датасета: terminal
никогда не присваивается на 4-м колене, seed никогда не получает роль transit,
19 узлов без рёбер всё равно попадают в выгрузку.
Пороги ролей проверяются юнит-тестами на синтетических узлах: и срабатывание, и несрабатывание на шаг ниже порога.
Ассистент проверяется отдельно: девять инструментов, разбор намерения без LLM, удаление выдуманных gid из ответа и поведение при сбое внешнего API — LLM-ветка покрыта моками, поэтому проверена и без ключа.
Структура репозитория
src/moneygraph/
config.py все пороги с обоснованием
loading.py загрузка parquet и проверки целостности
graph.py построение направленного графа
features.py признаки: структура, seed-достижимость, время
clusters.py Louvain и гипотезы по кластерам
roles.py правила ролей и генерация evidence
priority.py ранжирование
structure.py циклы, точки сочленения, устойчивость сети
viewer.py генерация автономного HTML-экрана
text.py склонение числительных для читаемых формулировок
assets/ вшитый cytoscape.js (чтобы экран работал офлайн)
cli.py CLI ассистента (moneygraph-ask)
serve.py локальный эндпоинт для чата в интерфейсе (moneygraph-serve)
agent/ AI-ассистент: инструменты, промпты, валидатор ссылок, сессия
pipeline.py CLI: один запуск от parquet до выгрузок
data/ предоставленный датасет
starter/ стартовый код организаторов (не используется в пайплайне)
tests/ проверки схемы, воспроизводимости и правил
out/ выгрузки
Авторы коммитов
История коммитов
- 2026-09-23 17:53Correct floating-point precision in node role metricsaikinovatomiris
- 2026-09-23 17:32feat: подключить ИИ-ассистента и защиту от оффтопаaikinovatomiris
- 2026-09-23 16:37Ассистент в интерфейсе: локальный эндпоинт, автоопределение, кликабельные gid в ответахAndrey Rudov
- 2026-09-23 16:28AI-ассистент аналитика: 9 инструментов над графом, валидация ссылок на узлы, работа без ключаAndrey Rudov
- 2026-09-23 16:19feat: add analyst frontend, local API and progressive graph explorationNUKI1223
- 2026-09-23 16:00feat: export a reproducible dashboard snapshot with lossless identifiersNUKI1223
- 2026-09-23 15:34fix: use internal edge turnover in cluster hypothesesNUKI1223
- 2026-09-23 15:31Передача работы: карта репозитория, порядок входа для нового человекаAndrey Rudov
Все коммиты (16)
- 2026-09-23 14:41run.sh ставит зависимости в локальный venv: системный Python защищён PEP 668Andrey Rudov
- 2026-09-23 14:40Контекст проекта в репозитории: CLAUDE.md, проектные скиллы, ТЗ, датасет, журнал решенийAndrey Rudov
- 2026-09-23 14:32Все артефакты воспроизводятся побайтово, включая parquet и viewerAndrey Rudov
- 2026-09-23 14:29Возвратные потоки и устойчивость сети: циклы, точки сочленения, симуляция изъятия топ-NAndrey Rudov
- 2026-09-23 14:21Документ передачи работы коллегамAndrey Rudov
- 2026-09-23 14:20Экран просмотра: автономный HTML со схемой сети, ролями и поиском по gidAndrey Rudov
- 2026-09-23 14:08Детерминированные выгрузки, тесты пайплайна, READMEAndrey Rudov
- 2026-09-23 14:02Граф денег: пайплайн ролей, кластеров и приоритетовAndrey Rudov
Структура репозитория (83 файлов)
.claude/skills/moneygraph-demo/SKILL.md .claude/skills/moneygraph-pipeline/SKILL.md .env.example .gitignore CLAUDE.md HANDOFF.md Makefile README.md data/edges.parquet data/nodes.parquet data/transactions.parquet docs/DATASET.md docs/DEBUG_AUDIT_2026-09-23.md docs/DECISIONS.md docs/TZ.md docs/data-checksums.sha256 docs/requirements-verified.txt frontend/.nvmrc frontend/.prettierrc.json frontend/index.html frontend/package-lock.json frontend/package.json frontend/playwright.config.ts frontend/playwright.unit.config.ts frontend/src/App.module.css frontend/src/App.tsx frontend/src/GraphCanvas.tsx frontend/src/Network.module.css frontend/src/Network.tsx frontend/src/data.ts frontend/src/global.css frontend/src/graph.ts frontend/src/main.tsx frontend/src/types.ts frontend/src/vite-env.d.ts frontend/tests/app.spec.ts frontend/tests/graph.spec.ts frontend/tsconfig.json frontend/vite.config.ts out/clusters.csv out/features.parquet out/nodes_roles.csv out/resilience.csv out/summary.json out/top_nodes.csv out/viewer.html pyproject.toml requirements.txt run.sh src/moneygraph/__init__.py src/moneygraph/agent/__init__.py src/moneygraph/agent/llm.py src/moneygraph/agent/prompts.py src/moneygraph/agent/scope.py src/moneygraph/agent/session.py src/moneygraph/agent/tools.py src/moneygraph/agent/validator.py src/moneygraph/api.py src/moneygraph/assets/cytoscape.min.js src/moneygraph/cli.py src/moneygraph/clusters.py src/moneygraph/config.py src/moneygraph/features.py src/moneygraph/graph.py src/moneygraph/loading.py src/moneygraph/pipeline.py src/moneygraph/priority.py src/moneygraph/roles.py src/moneygraph/serve.py src/moneygraph/structure.py src/moneygraph/text.py src/moneygraph/viewer.py start.py starter/README.md starter/requirements.txt starter/starter.py tests/test_mg_agent.py tests/test_mg_api.py tests/test_mg_pipeline.py tests/test_mg_roles.py tests/test_mg_scope.py tests/test_mg_serve.py web.sh
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.