О проекте
Команда: Locked in Bois
Кейс HackAlem.ai: Спецтрек — Astana Innovations
Стадия: Доведённый продукт
Задача кейса: «Аким на 5 часов»: городской симулятор, распределяющий бюджет по 5 направлениям и 5 районам
Суть
Симулятор распределения городского бюджета по пяти направлениям и пяти районам Астаны: детерминированный движок считает Astana Quality of Life Score и сравнивает план со всеми 694 395 допустимыми планами, ИИ-аналитик объясняет результат, есть кризисное событие, стресс-тест, модель политического рейтинга акима, совет районов и газета.
Как устроено
Движок на Python (engine/) реализует формулу кейса, валидацию 8 правил, полный перебор допустимых планов (optimizer), кризисные шоки, стресс-тест, «цену обещания», шахматный разбор ходов, fairness с коэффициентом Джини. FastAPI отдаёт эти расчёты через API и хранит лидерборд в SQLite. ИИ-агент с tool calling (OpenAI gpt-5-mini или NVIDIA NIM, цепочка из нескольких ключей с fallback на офлайн-шаблоны) пишет анализ только по числам движка; number guard и jargon guard отклоняют выдуманные числа и внутренние термины. Фронтенд — одностраничник с Chart.js и Tailwind.
Ключевые решения
- brute-force optimization
- tool calling
- number guard
- multi-provider fallback chain
- Pareto analysis
- Gini coefficient
- stress testing
Метрики
Пример из условия: Score 56.54, место 559 из 694 395 (лучше 99.92% планов); глобальный максимум 57.24; лучший план с рейтингом ≥50 — 57.00; ~2 600 тестов
Особенности
Полный перебор пространства решений даёт точный потолок и место плана, а слой политического риска показывает, что формальный оптимум отдаёт все меры одному району (Нура, 16% жителей). Защита от галлюцинаций: проверка всех чисел в тексте LLM против выхода движка и автоматический откат на шаблон.
Стек
Python · FastAPI · SQLite · JavaScript · Chart.js · Tailwind · GitHub Actions · pytest
Модели
gpt-5-mini · NVIDIA NIM
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-f09f40c4-locked-in-bois.
README
README.md«Кабинет акима» — AI-симулятор управления городом
HackAlem AI · кейс «Аким на 5 часов» · команда Locked in Bois
«Кабинет акима» — симулятор распределения городского бюджета между пятью направлениями и пятью районами Астаны. Детерминированный движок рассчитывает Astana Quality of Life Score по формуле кейса и сравнивает план пользователя со всеми 694 395 допустимыми планами. ИИ-аналитик объясняет результат, используя только числа, полученные от движка. Сценарий дополняют кризисное событие в III квартале и оценка политических последствий решений.

Интерфейс — светлый рабочий стол: графики на миллиметровке, стрелочные шкалы Score и рейтинга, бирюзовые отметки решений и красные сигналы риска. Кабинет, вердикт, кризис и газета адаптированы для телефона; газета оформлена как бумажная первая полоса.
Главная находка. Математически лучший план (Score 57.24) отдаёт все районные меры Нуре: это 16% жителей. Остальные 84% получают только общегородские программы. Модель политического риска оценивает рейтинг акима при таком плане в 48.41 из 100, что ниже порога переизбрания 50. Лучший план с рейтингом не ниже порога даёт 57.00 при рейтинге 53.35. Разница между оптимумом формулы и лучшим политически устойчивым планом составляет 0.24 балла. Приложение показывает пользователю этот компромисс. Рейтинг — наша условная модель риска, не входит в Score.
Для кого: аким и его команда, городской аналитик, участник симулятора. Какую проблему решает: при ограниченном бюджете показывает, как распределение денег между транспортом, экологией, соцсферой, безопасностью и сервисами меняет качество жизни в каждом районе и кто при этом остаётся без внимания.
Установка и запуск
Нужен Python 3.10+. Ключ API не нужен: расчёты и шаблонный анализ работают локально; оформлению, шрифтам и графикам нужен интернет (CDN).
git clone https://github.com/BAITC-Hacks/hack-f09f40c4-locked-in-bois && cd hack-f09f40c4-locked-in-bois
pip install -r requirements.txt
uvicorn api.main:app
Откройте http://localhost:8000. Документация API (Swagger): http://localhost:8000/docs.
Для проверки интерфейса на сохранённых ответах откройте http://localhost:8000/?mock=1. Этот режим воспроизводит фикстуры web/mock/ для примера из условия; произвольные планы проверяйте с работающим API. Офлайн-режим ИИ не требует ключей, но оформление и графики загружают Tailwind, Chart.js и шрифты с CDN.
Режимы ИИ (.env, см. .env.example):
LLM_PROVIDER | Что нужно | Что происходит |
|---|---|---|
offline (по умолчанию) | ничего | Анализ, Совет районов и газета собираются из шаблонов по выходу движка. Все экраны работают. |
openai | OPENAI_API_KEYS | Агент с инструментами (tool calling), модель OPENAI_MODEL (по умолчанию gpt-5-mini). |
nvidia | NVIDIA_API_KEYS | То же через OpenAI-совместимый NVIDIA NIM (NVIDIA_MODEL). |
openai,nvidia | оба | Цепочка резервов: сначала все ключи OpenAI, потом NVIDIA. |
Цепочка резервов. В *_API_KEYS можно указать несколько ключей через запятую. Они пробуются по очереди в одном общем лимите времени (LLM_TIMEOUT). Ключ, который вернул 401/403/404/410, до перезапуска больше не используется. Если не сработал ни один ключ, провайдер не отвечает или ИИ выдумал число, система сама переходит на офлайн-шаблон. Приложение не падает. provider — сработавший провайдер; fallback_chain перечисляет предыдущие отказы; при полном отказе причины — в fallback_reason. Сами ключи не попадают ни в ответы, ни в логи.
Ключи никогда не коммитятся.
.envв.gitignore. Шаблон без секретов —.env.example. Тесты принудительно убирают ключи из окружения и работают офлайн, без сети.
Что реализовано
Обязательные требования кейса
- Единый бюджет 100 и одни исходные данные для всех (
data/dataset.json, отдаются черезGET /api/dataset). - Ровно 5 решений из мер в 5 направлениях: транспорт, экология, соцсфера, безопасность, сервисы; выбирать меру в каждом направлении не обязательно.
- Автоматический контроль бюджета и всех 8 правил. Валидатор называет причину отказа, а превысить бюджет нельзя ни в интерфейсе, ни через API (HTTP 422).
- Итоговый Astana Quality of Life Score по формуле организаторов, проверенный на эталонных числах.
- ИИ-анализ решений: сильные стороны, риски, последствия, компромиссы, рекомендация.
Дополнительно (все пять опциональных пунктов кейса)
- Сравнение команд: лидерборд с серверным пересчётом Score.
- Визуализация изменений по районам: таблица «до → после», график по 8 кварталам, вклад каждой меры, Парето-график.
- ИИ-рекомендации по улучшению: лучший план в том же бюджете и лучший план с рейтингом ≥ 50 в модели.
- Неожиданные городские события: кризис в III квартале с обязательной заменой одной меры в том же бюджете.
- Автогенерация короткой презентации: одностраничник команды в Markdown (
/api/brief).
Сверх кейса
- Полный перебор всех 694 395 допустимых планов: место плана, процентиль, потолок 57.24.
- Слой политического риска «рейтинг акима» (не входит в Score).
- Совет районов и газета «Астана Times» о последствиях решений.
- Ограничитель чисел против галлюцинаций ИИ и цепочка резервных LLM-провайдеров.
- Стресс-тест плана: все отдельные кризисы и «чёрная зима» одновременно, новые критические ячейки, лучшая обязательная замена и план с максимальным Score в худшем отдельном кризисе (
/api/stress). - Цена обещания: точный поиск среди всех допустимых планов с выбранными ограничениями, цена в баллах Score и проверка, выполняет ли обещания ваш план (
/api/promise,/api/promise/catalog). - Разбор партии: оценка каждого решения, доступные замены, ловушки, точность и изменение достижимого потолка после каждого выбора (
/api/grade). Все три доступны в панелях экрана «Вердикт», через API и Swagger. - Чек расчёта: слагаемые Score, эффекты мер и поправки округления; итог примера из условия — 56.54 (
/api/receipt). - Сравнение планов: разбор разницы Score по слагаемым формулы; пример уступает глобальному оптимуму на 0.70 балла (
/api/duel). - Распределение по районам: доли расходов и населения, разрыв районных индексов и коэффициент Джини; пример сокращает разрыв с 13.81 до 10.47 (
/api/fairness). - Календарь обещаний: выход показателей из красной зоны по кварталам; у примера накоплено 8 квартал-ячеек ожидания, у оптимума — 10 (
/api/calendar).
Основной сценарий
Пример из условия кейса, от начала до конца:
- Кабинет. Выберите меры: M7 «Школа + детсад» → Нура, M8 «Центр семейного здоровья» → Нура, M10 «Освещение и камеры» → Нура, M12 «Цифровая платформа обращений» (город), M5 «Чистое топливо» → Сарыарка. Шкала бюджета показывает 95 / 100. Счётчики направлений: 2/1/1/1. Валидатор подсвечивает любое нарушение правил сразу, до отправки.

- Вердикт. «Принять решения» → Score 56.54 (+3.98 к базе 52.56). Сработала синергия M10 + M12 (B1 +2 в Нуре). Критических ячеек было 2 (S1 и S2 в Нуре), не осталось ни одной.
- Счётчик упущенного: место 559 из 694 395 (лучше 99.92% планов). Максимум при этих правилах 57.24. При том же бюджете 95 можно получить 57.23.
- Парето-график «стоимость → лучший Score» с точкой вашего плана. План за 66 единиц даёт 56.57, то есть больше, чем пример из условия за 95.
- Таблица районов «до → после» (ячейки < 40 красные), график по 8 кварталам, вклад каждой меры (remove-one).
- Рейтинг акима: 50.78. Аким остаётся на посту.

- ИИ-аналитик: сильные стороны, риски, последствия, компромиссы и рекомендация. Кнопка «Применить рекомендацию» загружает рекомендованный план обратно в кабинет.
С ключом (
LLM_PROVIDER=openaiиOPENAI_API_KEYSв.env) анализ пишет ИИ-агент; без ключа — шаблонный анализ.
- Совет районов: пять депутатов, по одному от района, реагируют на план и цитируют реальные изменения своего района.
- Кризис «5-го часа». В III квартале случается событие: прорыв теплотрассы в Алматы (C1 −12), смоговая зима в Сарыарке (E2 −10) или переполнение школ в Есиле (S1 −8). Нужно заменить ровно одну меру, не выходя из бюджета. Система показывает цену кризиса, сколько вы отыграли и была ли ваша замена лучшей из возможных.

На экране «Вердикт» ниже аналитика — панели «Разбор партии», «Стресс-тест кризисами», «Цена обещания» (API-запросы — в контракте):
- Стресс-тест,
POST /api/stress. Худший отдельный кризис — прорыв теплотрассы в Алматы: 55.34, потеря 1.20. Лучшая замена M5 → M14 даёт 56.78, отыгрывая 1.44. «Чёрная зима» даёт 54.02, потеря 2.52. Ответ также содержит антикризисный план: M5 в Сарыарке, M8 и M9 в Нуре, городские M6 и M14; стоимость 91, обычный Score 56.49, худший отдельный кризис 56.29.
- Обещания,
POST /api/promise. Передайтеpromises: [{"type":"min_approval","value":50}]и исходныйplan: обещание «Рейтинг ≥ 50» стоит 0.24, лучший план даёт 57.00 при рейтинге 53.35; ваш пример обещание выполняет. Дляpromises: [{"type":"min_districts","value":5}]цена «Каждому району — свой проект» — 2.42, лучший Score 54.82; пример обещание нарушает. Готовые варианты возвращаетGET /api/promise/catalog.
- Разбор,
POST /api/grade. Точность примера — 91.94%. M7 и M8 — «Лучший ход», M10 и M12 — «Отличный ход», M5 — «Хороший ход». Достижимый потолок: 57.24 → 57.21 → 57.21 → 57.21 → 57.21 → 56.54. Это разбор порядка выбора; сам итоговый Score от порядка не зависит.
Архитектура
┌──────────────── web/index.html (Tailwind + Chart.js, без сборки) ────────────────┐
│ Кабинет → Вердикт (Score, упущенное, Парето, районы, рейтинг) → Кризис → Газета │
└──────────────────────────────────────┬──────────────────────────────────────────────┘
│ JSON (api/CONTRACT.md)
┌──────────────────────────── api/main.py (FastAPI) ───────────────────────────────┐
│ /validate /score /approval /optimize /shock /shock/resolve /submit /leaderboard │
│ /analyze /narrative /brief /stress /promise /promise/catalog /grade │
└───────┬───────────────────────────────┬──────────────────────────────┬───────────┘
│ │ │
┌───────▼──────── engine/ ───────┐ ┌────▼──── api/agent.py ────────┐ ┌──▼── api/db.py ──┐
│ validate.py правила 1–8 │ │ LLM-агент (openai | nvidia) │ │ SQLite лидерборд │
│ score.py формула, таймлайн,│◄┤ инструменты: score, validate, │ └──────────────────┘
│ remove-one │ │ optimize_same_budget, what_if,│
│ optimize.py полный перебор ───┼─┤ remove_one, stress_test, │
│ → optimizer_cache │ │ grade_plan, price_of_promise │
│ approval.py слой полит. риска │ │ ограничитель чисел (regex) │
│ shock.py кризисы │ │ офлайн-шаблоны │
│ │ └────────────────────────────────┘
└──────────────┬─────────────────┘
data/dataset.json · data/events.json · data/optimizer_cache.json
engine/— чистый Python без зависимостей. Все числа, которые видит пользователь, считает он. Данные лежат вdata/dataset.json, в коде их нет.- Новые расчёты:
stress.pyсравнивает кризисные сценарии,promise.pyфильтрует заранее рассчитанную таблицу обещаний,grading.pyоценивает ходы и точные потолки продолжения плана. Официальная формула Score общая для всех модулей. api/— FastAPI. Отдаёт фронтенд как статику, так что запускается одной командой.web/—web/index.htmlи модулиweb/panels/(grade, stress, promise), без сборки; Tailwind и Chart.js с CDN. Если сервер недоступен, интерфейс переключается на записанные ответы движка для примера из условия (web/mock/, принудительно —?mock=1).- ИИ объясняет, но не считает. Агент получает цифры только через инструменты движка. Всё, что он пишет, проходит через ограничитель чисел (см. ниже). Ограничитель проверяет, что число есть в источниках, но не его привязку к конкретной метрике.
Технологии
| Слой | Что используется |
|---|---|
| Язык | Python 3.10+ (проверено на 3.12) |
| Движок | чистый Python, без зависимостей: перебор itertools.combinations, кэши JSON и сжатая таблица XZ (lzma) |
| API | FastAPI, Uvicorn, SQLite (sqlite3 из стандартной библиотеки) для лидерборда |
| Фронтенд | web/index.html и модули web/panels/ (grade, stress, promise), без сборки; Tailwind CSS и Chart.js с CDN |
| ИИ | OpenAI API (gpt-5-mini, tool calling) через SDK openai; резерв — NVIDIA NIM (OpenAI-совместимый endpoint); офлайн-шаблоны |
| Тесты | pytest, FastAPI TestClient (160+ тестов, работают без сети и без ключей) |
| Разработка | с AI-агентами: Codex и Claude Code |
Данные и интеграции
- Данные: синтетический датасет организаторов «Датасет районов»: 5 районов × 10 показателей, 14 мер, 3 синергии, 3 несовместимости. Перенесён в
data/dataset.jsonбез изменений и сверен с документом по каждой ячейке. Добавлены только падежные формы названий районов для русских текстов. Персональных данных нет. - Кризисные события:
data/events.json, 3 события, придуманы нами. - Внешние ИИ-сервисы (необязательные): OpenAI API и NVIDIA NIM, только если в
.envзаданы ключи. Без ключей сервер использует офлайн-шаблоны; браузер загружает оформление, графики и шрифты с CDN. - Хранилище: локальный файл SQLite
data/leaderboard.db, создаётся при первом обращении к лидерборду.
Формула и правила
Реализовано ровно по условию, в engine/score.py:
I'_dk = clip(I_dk + Σ эффект_m,k × (8 − L_m)/8 + синергии, 0, 100). Городские меры действуют во всех пяти районах.D_d = Σ w_k × I'_dk, веса T1 .10 · T2 .10 · E1 .09 · E2 .11 · S1 .11 · S2 .11 · B1 .09 · B2 .09 · C1 .10 · C2 .10.D_avg = Σ pop_d × D_d.Score = 0.7 × D_avg + 0.3 × min(D_d) − N_crit, где N_crit — число ячеек «район × показатель» строго меньше 40.
Правила проверяются в engine/validate.py в фиксированном порядке. Возвращается первое нарушение с понятной причиной на русском: ровно 5 решений, без повторов, бюджет ≤ 100, район указан только у районных мер, не более 2 мер на направление, M1×M3 нигде вместе, M4×M7 и M5×M13 не в одном районе.
Проверка движка: эталонные числа из условия и наши собственные проверки — это тесты, которые обязаны проходить.
| План | Стоимость | Score |
|---|---|---|
| Без действий (база) | 0 | 52.56 |
| Пример из условия (M7, M8, M10 Нура · M12 · M5 Сарыарка) | 95 | 56.54 |
| Самый дешёвый (M9, M11, M10, M4 Нура · M12) | 61 | 55.67 |
| Глобальный оптимум (M2 · M3, M8, M9 Нура · M14) | 98 | 57.24 |
Наши интерпретации (в условии этого нет, решение принимали мы):
- Лаг по кварталам. Условие задаёт только итог на 8-м квартале. Для графика по кварталам мы считаем долю эффекта линейной:
max(0, q − L) / 8. На 8-м квартале это ровно(8 − L)/8, как в условии. Синергия включается, когда работают обе меры (q > max(L_a, L_b)). - Синергия даётся в районе меры, названной первой в паре: M1+M2 → район M1, M10+M12 → район M10, M5+M6 → район M5.
- Вклад меры (remove-one) = Score плана минус Score того же плана без этой меры (4 решения; правило «ровно 5» обходится только для этого расчёта).
- Кризис сдвигает исходный показатель района (C1 −12 и т. д.). Score под кризисом считается той же формулой. Цена кризиса = Score до кризиса − Score после. Отыграно = Score после замены − Score после кризиса без замены.
- Рейтинг акима — отдельный слой политического риска. В Score он не входит. По району:
clip(50 + 4·ΔD_d + (10, если в районе есть районная мера, иначе −12) − 3·крит.ячейки_d, 0, 100). Городской рейтинг — среднее, взвешенное по населению. Порог переизбрания — 50. Константы подобраны так, чтобы голый оптимум формулы давал рейтинг ниже 50. Подробности вDECISIONS.md. - Стресс-тест отдельно сравнивает одиночные кризисы и их одновременное наступление. Антикризисный план максимизирует худший Score среди одиночных кризисов до замены. «Страховка» в ответе — реальная обязательная замена: её эффект может быть отрицательным.
- Цена обещания —
diff2(лучший Score без ограничений, лучший Score с обещаниями). Обещания действуют одновременно; охват района засчитывается только при наличии районной меры. Если допустимого плана нет, возвращаютсяfeasible: false,best: null,price: nullи причина. - Разбор партии сравнивает решение с допустимыми заменами при остальных фиксированных решениях. Если план обеспечивает переизбрание, а максимум по Score его лишает, движок ищет лучшую сохраняющую пост замену; «Жертва» ставится только при отсутствии такой замены с более высоким отображаемым Score. Точность основана на сумме локальных потерь; эта сумма не равна отставанию от глобального оптимума.
eval_barпоказывает точные потолки для последовательных префиксов, и сумма их падений равна общему отставанию. Все отображаемые разницы считаются черезdiff2— разность округлённых значений.
Агентный слой и защита от галлюцинаций
Агент (api/agent.py) — это LLM с вызовом инструментов (tool calling). Инструменты:
| Инструмент | Что делает (всё — функции движка) |
|---|---|
score(plan) | Score, изменения по районам, критические ячейки, вклад мер |
validate(plan) | проверка правил |
optimize_same_budget(plan) | лучший план в том же бюджете, место, процентиль, лучший план с рейтингом ≥ 50 |
what_if(plan, swap) | что будет, если заменить одну меру |
remove_one(plan) | вклад каждой меры |
stress_test(plan) | уязвимость к кризисам, страхующие замены, устойчивый план |
grade_plan(plan) | оценки ходов, потери, альтернативы, точность |
price_of_promise(promises) | цена обещаний в баллах Score и лучший план с ними |
Агент делает не больше 6 вызовов инструментов и возвращает строгий JSON: summary, strengths, risks, consequences, tradeoffs, recommendation. Рекомендованный план потом снова проверяет и пересчитывает движок. expected_score всегда берётся из движка, а не из ответа модели.
Ограничитель чисел. Из ответа модели вытаскиваются все числа. Каждое число с дробной частью или ≥ 10 должно встречаться в результатах инструментов или в датасете, с учётом округления. Для числа 694 395 разделители разрядов учитываются. Если модель выдумала число, ответ генерируется заново с сообщением «ты использовал числа, которых нет в результатах инструментов: …». Если выдумка повторяется, система отдаёт офлайн-шаблон с пометкой grounded: false. Ответ анализа содержит grounded, guard (сколько было попыток и какие числа отклонены) и tool_trace (какие инструменты агент вызвал и с какими аргументами), так что всё это можно проверить. Первый ход агента всегда настоящий вызов optimize_same_budget: анализ начинается с данных движка (место плана, лучший план в том же бюджете, лучший план с рейтингом ≥ 50), а не с одного промпта. Весь цикл агента ограничен по времени (LLM_TIMEOUT, по умолчанию 60 с); если время вышло, возвращается офлайн-анализ с полем fallback_reason. Живой прогон на примере из условия: gpt-5-mini, 3 вызова инструментов (optimize_same_budget → remove_one → what_if), ограничитель чисел пройден с первой попытки, около 30 с.
Офлайн-режим. Анализ, Совет районов, газета и одностраничник собираются детерминированно из выхода движка; соответствие чисел ограничителю проверяется тестами.
Результаты анализа данных
Перебраны все 694 395 допустимых планов (python -m engine.optimize, около 21 секунды).
- Потолок — 57.24. Это всего +4.68 к базе. Пример из условия (56.54) — место 559, лучше 99.92% планов, но он тратит 95 единиц. План за 66 единиц (M8, M9, M10, M11 в Нуре + M12) даёт больше: 56.57.
- Эффект Нуры. Все 11 точек Парето-фронта кладут все районные меры в Нуру. Причина в формуле: 30% веса у самого слабого района (Нура, D = 49.18) и штраф за её две критические ячейки (S1 = 38, S2 = 35). M8 «Центр семейного здоровья» в Нуре входит в каждый оптимальный план стоимостью от 66.
- Оптимум формулы не проходит порог переизбрания. У плана со Score 57.24 рейтинг акима 48.41. Лучший план с рейтингом ≥ 50 (M3, M7, M8 в Нуре · M11 в Есиле · M14) даёт 57.00 при рейтинге 53.35. Разница по Score составляет 0.24 балла.
- M11 в Алматы снижает Score. «Безопасные переходы» снижают T1 на 2 (с учётом лага на 1.75). В Алматы T1 = 40 ровно на пороге. После меры он падает до 38.25, и появляется новая критическая ячейка, которая снижает Score на 1. ИИ-аналитик отмечает этот случай.
- Влияние лага. M3 (ЛРТ) и M13 (теплосети) реализуются за 8 кварталов лишь на 50%, M6 — тоже на 50%. Поэтому дорогие долгосрочные проекты дают меньший вклад в Score, чем быстрые недорогие меры.
API
Все маршруты, поля и примеры — в api/CONTRACT.md и в Swagger на /docs.
Как проверить решение
Одна команда, без сервера, сети и ключей (≈1 с): python scripts/judge_check.py — PASS/FAIL по эталонным числам и критериям кейса.
Автоматически, по критериям проверки кейса (одна команда, один тест на каждый критерий):
python -m pytest tests/test_criteria.py -v
| Критерий кейса | Тест |
|---|---|
| 1. Все команды начинают с одного бюджета и одних данных | test_1_same_budget_and_initial_data_for_everyone |
| 2. Система не даёт превысить бюджет | test_2_budget_cannot_be_exceeded (план за 129 → отказ с причиной, 422 на /score и /submit) |
| 3. Решения влияют на итоговые показатели | test_3_decisions_change_the_indicators (S1 в Нуре: 38 → 48) |
| 4. ИИ объясняет результат и компромиссы | test_4_ai_explains_result_and_tradeoffs |
| 5. Другой набор решений даёт другой Score | test_5_changing_decisions_changes_the_score |
Вручную: запустите приложение, соберите пример из условия (M7, M8, M10 в Нуре, M12, M5 в Сарыарке) и получите 56.54. Затем замените M5 на M14 и получите 56.99. Снова нажмите «Пример из ТЗ». Замените M12 на M3 «Линия ЛРТ» (30) в Нуре: стоимость станет 111: шкала бюджета покраснеет, интерфейс покажет «Перерасход: 111 из 100 ед.», а кнопка «Принять решения» станет неактивной. API на такой план отвечает 422 с причиной «Превышен бюджет: 111 > 100».
Готовая ссылка на пример из условия: http://localhost:8000/?plan=M7:Нура,M8:Нура,M10:Нура,M12,M5:Сарыарка — план загружается сразу, панель плана показывает прогноз Score до принятия решений. На экране вердикта кнопка «Скопировать ссылку на план» даёт такую же ссылку для любого плана, а «Одностраничник команды (.md)» скачивает автоматическую сводку из /api/brief.
Сквозной тест интерфейса (Playwright, проходит весь сценарий на ширине ноутбука и телефона): node web/tools/smoke.cjs "http://127.0.0.1:8000/?seed=1". Параметр ?seed=1 фиксирует кризис (смоговая зима в Сарыарке), чтобы сценарий повторялся один в один.
Ограничения
- Данные синтетические (датасет организаторов). Реальные показатели Астаны не подключены.
- Рейтинг акима — наша иллюстративная модель политического риска с подобранными константами (см.
DECISIONS.md), а не социологическая модель. - Линейное нарастание эффекта по кварталам и сдвиг исходных показателей при кризисе — наши интерпретации, в условии их нет.
- Живой ИИ-анализ занимает около 30 секунд, газета около 18. Без ключей и при сбое провайдера используются офлайн-шаблоны: они точны по числам, но менее разнообразны по тексту.
- Ключи NVIDIA, выданные команде, при проверке вернули 401. Путь NVIDIA реализован и покрыт тестами, но вживую не подтверждён. OpenAI проверен вживую.
- Интерфейс загружает Tailwind, Chart.js и шрифты с CDN: без интернета страница откроется без стилей и графиков.
- Лидерборд хранится в локальном SQLite, без авторизации: он рассчитан на одну площадку, а не на публичный интернет.
- Кэш оптимизатора нужно пересобрать (
python -m engine.optimize), если меняется датасет. - При изменении данных пересоберите также кэши стресс-теста, обещаний и разбора; кэши обещаний и разбора проверяют и связанные файлы движка.
Развёрнутая версия
Публичного деплоя нет. Приложение запускается локально одной командой (см. «Установка и запуск») и не требует ключей. Интернет нужен только интерфейсу, чтобы загрузить Tailwind, Chart.js и шрифты с CDN.
Развитие
- Реальные данные акимата: те же структуры
dataset.jsonдля настоящих районов Астаны и настоящих KPI. - Многолетний бюджет: решения по годам с переносом остатка. Перебор заменяется целочисленной оптимизацией или RL-поиском политики.
- Агенты-жители: обратная связь от синтетических групп жителей по каждому району, чтобы рейтинг считался не формулой, а по реакции людей.
- Режим совещания: несколько акимов (команд) делят один бюджет в реальном времени; лидерборд уже есть.
English summary
Akim's Cabinet is an AI city-management simulator for the HackAlem "Akim for 5 hours" case. The player makes exactly 5 decisions within a budget of 100 across transport, ecology, social infrastructure, safety and city services. A deterministic Python engine computes the official Astana Quality of Life Score (52.56 baseline, 56.54 for the case example, verified by tests). An LLM agent then explains the result. It reaches numbers only through tools (score, validate, optimize_same_budget, what_if, remove_one, stress_test, grade_plan, price_of_promise), and a regex number guard rejects any number that didn't come from the engine. Calculations and template-based analysis run locally; browser styling, fonts and charts require internet access (CDNs).
Авторы коммитов
История коммитов
- 2026-09-23 17:55docs: README ~25% shorter (API list -> CONTRACT.md, tests merged into verification, repo tree and duplicate findings removed)Yeldar2609
- 2026-09-23 17:53web: remove the tool-call list from the analyst panel; README + screenshot updatedYeldar2609
- 2026-09-23 17:44web: remove the engine/provider status text from the headerYeldar2609
- 2026-09-23 17:42docs: agent screenshot from a live run — clean Russian, compact tool trace (4 tool calls)Yeldar2609
- 2026-09-23 17:37web: final review fixes — AI swap verdict shown, compact tool trace, CDN-failure fallbacks, crisis race fixes, integer budgetYeldar2609
- 2026-09-23 17:37docs: README final review fixes — python -m pytest, judge check up top, new screenshots, all 8 agent tools, honest offline/CDN noteYeldar2609
- 2026-09-23 17:35web: receipt shows each critical penalty once so lines always sum to the totalZombie1209
- 2026-09-23 17:34api: request hardening — NaN/Infinity, broken Unicode and malformed bodies return a Russian 4xx, never 500Yeldar2609
Все коммиты (45)
- 2026-09-23 17:31web: «Чек расчёта Score» (engine receipt as a paper tape) and «Последний ход» (exact final Score on every card at 4/5)Zombie1209
- 2026-09-23 17:26agent: jargon guard allows public words/codes with '=' (Score = 57.0, S1 = 48)Yeldar2609
- 2026-09-23 17:24agent: jargon guard — internal field/tool names in LLM text trigger one regeneration, then the offline templateYeldar2609
- 2026-09-23 17:19docs: screenshots of the chess grading, crisis stress test and price-of-promise panelsYeldar2609
- 2026-09-23 17:19agent: number guard checks signs and ignores numbers echoed in tool-call argumentsYeldar2609
- 2026-09-23 17:18web: accessibility + copy polish for stress and promise panelsYeldar2609
- 2026-09-23 17:18docs: refresh README screenshots after panel integration; compact verdict shotZombie1209
- 2026-09-23 17:16docs: mark PLAN.md as the original build plan; README is the source of truthYeldar2609
- 2026-09-23 17:14fixes from 11 Codex reviewers: new routes, honest AI badge, visible tool trace, copy, CI matrixYeldar2609
- 2026-09-23 17:05features: chess grading, UI panels, agent tools, AI swap verdict, receipt, fairness, duel, calendar, judge check, CI + 2,600 testsYeldar2609
- 2026-09-23 17:05web: live Score forecast in the plan panel, shareable plan links, one-pager downloadZombie1209
- 2026-09-23 16:50README: neutral, professional toneZombie1209
- 2026-09-23 16:40README: match the current frontend (validator message, offline fallback, smoke test, CDN dependency)Zombie1209
- 2026-09-23 16:30engine+api: crisis stress test & crisis-proof plan; price of a promiseYeldar2609
- 2026-09-23 16:22web: polish pass from 9 lens reviewers (copy, cabinet, crisis, districts, dials, type, charts, newspaper, color); README screenshotsZombie1209
- 2026-09-23 16:06web: rebuild app in the chart-recorder direction (board 1), crisis per board 2, district board per board 3Zombie1209
- 2026-09-23 15:48docs: drop tunnel/demo note (no live presentation)Yeldar2609
- 2026-09-23 15:48decisions: no live presentation, repo-only judgingYeldar2609
- 2026-09-23 15:48Revert "api: leaderboard rooms (session) for live QR audience play"Yeldar2609
- 2026-09-23 15:46api: leaderboard rooms (session) for live QR audience playYeldar2609
- 2026-09-23 15:29web/variants: 8 rough design directions as static boards + gallery (impeccable)Zombie1209
- 2026-09-23 15:23docs+tests: README covers all 11 organizer README points; one test per case verification criterionYeldar2609
- 2026-09-23 15:15agent: multi-key fallback chain (openai -> nvidia -> offline), forced first tool call, sharper promptsYeldar2609
- 2026-09-23 15:06web: label post-crisis rank in newspaperZombie1209
- 2026-09-23 15:06build: pin requirements to the exact tested versionsYeldar2609
- 2026-09-23 15:05web: wire frontend to real API contract (score/optimize/analyze/narrative/shock/submit), fixture-based mockZombie1209
- 2026-09-23 15:04docs: README + CONTRACT match the built agent (guard, tool_trace, timeout, Windows curl)Yeldar2609
- 2026-09-23 15:03agent: polished Russian offline text: cases, newspaper front page, five deputy voicesYeldar2609
- 2026-09-23 14:54web: single-page frontend with cabinet, verdict, crisis, newspaper, leaderboard on mocksZombie1209
- 2026-09-23 14:51data: Russian case forms for district names (additive, for templates)Yeldar2609
- 2026-09-23 14:51agent: LLM_TIMEOUT (default 60s) + low reasoning effort for gpt-5 modelsYeldar2609
- 2026-09-23 14:50agent: tool-calling analyst, number guard, council + newspaper, offline templates, briefYeldar2609
- 2026-09-23 14:44docs: README (RU + EN): launch, scenario, architecture, formula, findingsYeldar2609
- 2026-09-23 14:43api: FastAPI routes per CONTRACT.md, SQLite leaderboard, static web mountYeldar2609
- 2026-09-23 14:42engine: formula, validation, approval layer, brute-force optimizer, crisis shocks + testsYeldar2609
- 2026-09-23 14:36api: CONTRACT.md (PLAN §4 + approval/narrative), requirements, .env.example, DECISIONS.mdYeldar2609
- 2026-09-23 14:31data: dataset.json (districts, measures, rules) + crisis events + PLAN.mdYeldar2609
Структура репозитория (99 файлов)
.env.example .github/workflows/tests.yml .gitignore DECISIONS.md PLAN.md README.md api/CONTRACT.md api/__init__.py api/agent.py api/db.py api/main.py api/narrative.py api/prompts.py data/dataset.json data/events.json data/fairness_cache.json data/grading_cache.json data/optimizer_cache.json data/promise_table.xz data/stress_cache.json docs/screenshots/agent.png docs/screenshots/cabinet.png docs/screenshots/crisis.png docs/screenshots/grade.png docs/screenshots/hero.png docs/screenshots/newspaper.png docs/screenshots/promise.png docs/screenshots/stress.png docs/screenshots/verdict.png engine/__init__.py engine/approval.py engine/calendar.py engine/duel.py engine/fairness.py engine/grading.py engine/model.py engine/optimize.py engine/promise.py engine/receipt.py engine/score.py engine/shock.py engine/stress.py engine/validate.py requirements.txt scripts/judge_check.py tests/conftest.py tests/test_agent.py tests/test_agent_tools.py tests/test_api.py tests/test_api_fuzz.py tests/test_calendar.py tests/test_criteria.py tests/test_duel.py tests/test_engine.py tests/test_fairness.py tests/test_grading.py tests/test_guard_hardening.py tests/test_hardening.py tests/test_jargon_guard.py tests/test_judge_check.py tests/test_new_routes.py tests/test_promise.py tests/test_properties.py tests/test_readme.py tests/test_receipt.py tests/test_rules_census.py tests/test_stress.py web/API_REQUESTS.md web/DECISIONS.md web/index.html web/mock/analyze.json web/mock/dataset.json web/mock/grade.json web/mock/leaderboard.json web/mock/narrative.json web/mock/narrative_crisis.json web/mock/optimize.json web/mock/promise.json web/mock/receipt.json web/mock/resolve.json web/mock/score.json web/mock/shock.json web/mock/stress.json web/panels/grade.js web/panels/promise.js web/panels/stress.js web/tools/capture_fixtures.py web/tools/shoot-variants.cjs web/tools/smoke.cjs web/variants/_kit.js web/variants/index.html web/variants/v1-samopisets.html web/variants/v2-shahmaty.html web/variants/v3-tablo.html web/variants/v4-hypercard.html web/variants/v5-plastinka.html web/variants/v6-vybory.html web/variants/v7-genplan.html web/variants/v8-lrt.html
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.