О проекте
Команда: Melnik
Кейс HackAlem.ai: Спецтрек — Astana Innovations
Стадия: Доведённый продукт
Задача кейса: «Аким на 5 часов»: городской симулятор, распределяющий бюджет по 5 направлениям и 5 районам
Суть
Симулятор бюджета Астаны: пользователь выбирает 5 мер из каталога из 14, получает Score, изменения по районам, heatmap, траекторию по кварталам и AI-объяснение. Добавлены советник акима и голосование жителей за предложения.
Как устроено
simulator.py детерминированно проверяет бюджет 100, направления и несовместимости и считает эффекты с лагами, синергиями и клиппингом, итог — Astana QoL Score. optimizer.py перебирает все допустимые наборы и размещения. advisor.py даёт LLM инструменты валидации, симуляции и поиска через tool calling, после чего все кандидаты пересчитываются кодом. explainer.py передаёт проверенный JSON в OpenAI (gpt-4o-mini, Structured Outputs), при сбое срабатывает fallback. resident.py сопоставляет жалобу жителя с мерой каталога, proposals/digest собирают голоса и народный сценарий.
Ключевые решения
- exhaustive optimizer
- tool calling
- Structured Outputs
- quarterly trajectory Q1–Q8
- heatmap 5×10
- radar chart
- deterministic fallback
Особенности
Цепочка «жалоба жителя → мера каталога → проверенный эффект → голосование → допустимый народный сценарий» с разбором аналогов (Balancing Act, Decidim, vTaiwan). Публичный /api/search и /llms.txt для внешних ИИ-агентов. Главный принцип: «LLM не считает числа».
Стек
Python · FastAPI · Pydantic v2 · vanilla JS · Chart.js · Docker · pytest · Ruff
Модели
OpenAI gpt-4o-mini
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-46e44fd3-melnik.
README
README.mdQalaQadam — «Аким на 5 часов»
AI-симулятор управленческих решений для Астаны, созданный для HackAlem AI 2026. Пользователь распределяет единый бюджет между пятью мероприятиями и получает воспроизводимый Astana Quality of Life Score, изменения по районам и объяснение компромиссов.
От жалобы к решению. Житель приходит с проблемой и уходит с предложением, сопоставленным с мерой каталога и проверенным расчётом.
Deployed-версия: нет, локальный запуск.
Для кого и какую проблему решает
QalaQadam предназначен для жителей, городских аналитиков и управленцев. При ограниченном бюджете трудно заранее сравнить инициативы по нескольким направлениям и понять, где появятся эффекты и какие районы останутся позади. В прототипе все команды получают одинаковый синтетический набор данных и бюджет; человек выбирает приоритеты, код проверяет ограничения и считает последствия, а ИИ помогает интерпретировать результат.
Аналоги и чем мы отличаемся
| Семейство | Пример | Что даёт | Чего не хватает |
|---|---|---|---|
| Бюджетные симуляторы | Balancing Act (Polco), с 2015 г. | Житель сам сводит бюджет и видит компромиссы. | Нет модели последствий по районам; результат совещательный. |
| Бюджет народного участия | Барселона, Decidim 2020–2023: €30 млн, 1 982 предложения → 823 прошли техпроверку → 76 реализованы; Париж: 104 проекта-победителя в 2025 г.; Астана — онлайн-голосование БНУ. | Реальные деньги, решения исполняются. | Проверка ручная; жители голосуют за описание, эффект для района в цифрах не показан. |
| ИИ для обсуждения | vTaiwan / Pol.is: около 200 тыс. участников и 26 законов в 2015–2018 гг.; Talk to the City группирует мнения для власти с помощью LLM. | ИИ понимает и группирует свободный текст. | Не связано с бюджетом и расчётом; решения необязательные. |
| Цифровые двойники | Virtual Singapore, MIT CityScope. | Глубокое моделирование города. | Экспертный инструмент, нужны большие данные или оборудование. |
QalaQadam соединяет три звена в одном потоке: житель пишет обычным языком → ИИ сопоставляет текст с мерой из каталога → код показывает эффект для района → голосование → из поддержанных предложений код собирает допустимый «Народный сценарий». Это инструмент для существующего «Бюджета народного участия» и акимата: проверка и расчёт эффекта до голосования, а не замена решений людей. Мы не обещаем, что «ИИ решит за город».
Ключевой принцип: LLM не считает числа. Формула, ограничения, лаги, синергии и штрафы реализованы детерминированно в Python. OpenAI получает только уже проверенный результат и объясняет его.
Что реализовано
- каталог из 14 мероприятий по пяти направлениям;
- стартовый экран с матрицей состояния города и автоматически найденной точкой роста;
- выбор ровно пяти решений и района для районных мер;
- живая клиентская и обязательная серверная валидация;
- контроль бюджета 100 единиц, повторов, направлений и несовместимостей;
- лаг-скорректированные эффекты, три синергии и единый клиппинг 0–100;
- Astana Quality of Life Score с городской средней, минимальным районом и штрафом за показатели ниже 40;
- разбивка «до → после» по пяти районам и десяти показателям;
- график районных оценок и подсветка критических значений;
- heatmap 5 × 10 в режимах «до», «после» и «изменение»;
- radar-профиль выбранного района по всем десяти показателям;
- единая айдентика Астаны: navy/sky/gold для бренда и отдельная шкала данных;
- структурированное объяснение через OpenAI;
- «Советник акима»: перевод естественно-языковых пожеланий в формальные ограничения;
- точный полный поиск лучших допустимых сценариев по Score или стоимости;
- три AI-инструмента: валидация, симуляция и поиск сценариев;
- поквартальная траектория Score и районного разрыва на горизонте 8 кварталов;
- дорожная карта пяти выбранных мер по кварталам Q1–Q8 с задержкой эффекта, долей реализации к концу горизонта и стоимостью из каталога;
- сравнение выбранной политики, математического максимума и альтернативы с ограничениями;
- «Голос жителя»: перевод предложения в существующую меру, проверенный план и черновик обращения;
- «Голосование жителей»: пустая доска, проверка каталогом, голоса и сборка допустимого народного сценария;
- статусы предложений «Подано», «Проверено кодом», «В народном сценарии», «Не вошло» с причиной исключения;
- «Сводка для акимата»: группировка предложений по темам, проверенные счётчики голосов и выгрузка
.txt; - квартальный прогноз Q1/Q2/Q4/Q8 и переключаемая матрица показателей;
- открытый детерминированный поиск
/api/searchи памятка/llms.txtдля внешних ИИ; - детерминированное резервное объяснение, если API-ключ отсутствует или OpenAI недоступен;
- автоматические тесты, включая эталонные числа организаторов.
Пользовательский сценарий
- Пользователь выбирает пять карточек мероприятий.
- Для районных мероприятий указывает район.
- Интерфейс сразу показывает использованный бюджет и нарушения правил.
- Сервер независимо валидирует набор и рассчитывает все значения.
- Экран результата показывает Score, изменения районов, heatmap показателей, radar выбранного района, эффекты, синергии и траекторию на 8 кварталов.
- Панель сравнивает сценарий с максимумом формулы и альтернативой «без ЛРТ, с экологией».
- LLM объясняет сильные стороны, риски и компромиссы, не меняя рассчитанные числа.
Архитектура
backend/app/simulator.py — источник итогового расчёта сценария. Он не обращается к сети или БД: принимает пять решений, проверяет правила и возвращает полную разбивку. Исходные синтетические данные из брифа находятся в backend/app/data.py.
backend/app/main.py публикует каталог и API симуляции, объяснения, траектории и советника. Перед объяснением сервер не доверяет числам из браузера и повторно запускает симуляцию по selections. backend/app/explainer.py передаёт модели компактный проверенный JSON через Structured Outputs; при сбое внешнего API работает честный deterministic fallback.
backend/app/optimizer.py исчерпывающе перебирает допустимые наборы и размещения районных мер. До раскрытия районов он отбрасывает наборы, нарушающие бюджет, направления, обязательные/исключённые меры и глобальные несовместимости. backend/app/advisor.py даёт LLM инструменты поверх валидатора, симулятора и оптимизатора. Все кандидаты после ответа модели повторно пересчитываются кодом, а числовая часть рекомендации строится только из проверенного результата.
backend/app/trajectory.py переиспользует ту же формулу на шагах Q1–Q8. Финальная точка строго совпадает с /api/simulate; районный разрыв max(D_d) − min(D_d) показан как «Индекс городской справедливости», где меньше — лучше.
backend/app/resident.py сопоставляет текст жителя только с одной из существующих мер каталога. Оптимизатор получает обязательную пару «мера + район», а найденный план повторно проходит симуляцию. Стоимость, эффекты, районные оценки и Score в черновике обращения подставляются кодом; при отсутствии подходящей меры сервис честно возвращает ближайшие варианты без числовых обещаний.
backend/app/public_api.py валидирует ограничения открытого поиска и независимо повторно симулирует каждый возвращаемый кандидат. /llms.txt объясняет внешним агентам главный контракт: не вычислять Score самостоятельно, а получать числа через API.
backend/app/proposals.py хранит предложения и голоса в var/proposals.json с атомарной записью. Клиентские числа не принимаются как доказательство: сценарий формируется оптимизатором и проверяется симулятором. Голосование — демонстрационное и не подтверждает личность участника.
backend/app/digest.py группирует тексты предложений через OpenAI Structured Outputs. Модель получает только идентификаторы и тексты; сервер проверяет принадлежность каждого предложения группе и сам считает голоса, районы, меры и участие в народном сценарии. Без ключа работает группировка по направлениям каталога.
Frontend — один HTML, vanilla JavaScript и CSS без сборщика. theme.js читает единые CSS-токены и передаёт их графикам. Chart.js 4.4.7 хранится локально в frontend/vendor, поэтому графики не зависят от доступа к CDN. Google Fonts используются как улучшение; системные fallback-шрифты сохраняют интерфейс работоспособным без сети.
frontend/roadmap.js строит дорожную карту непосредственно из пяти selections результата /api/simulate и каталога /api/catalog. Она не меняет расчёт: Q1 — старт, первый квартал эффекта следует за лагом, а доля реализации к Q8 равна (8 − lag) / 8.
frontend → POST /api/simulate → validator + simulator
↓
проверенный SimulationResult
↓
frontend → POST /api/explain → повторный simulate → OpenAI/fallback
frontend → POST /api/trajectory → поквартальный детерминированный расчёт
frontend → POST /api/advise → LLM-ограничения → optimizer → повторный simulate
frontend → POST /api/proposals → проверка меры → голоса → допустимый народный сценарий
frontend → POST /api/proposals/digest → темы → проверенные агрегаты
Технологии
- Python 3.10+, FastAPI, Pydantic v2;
- OpenAI Structured Outputs и tool calling; модель задаётся через
OPENAI_MODEL(по умолчаниюgpt-4o-mini); - Chart.js 4.4.7 хранится локально;
- pytest и Ruff;
- Docker и Docker Compose.
Потенциал развития
- заменить синтетические данные брифа официальной городской статистикой после проверки качества и лицензий;
- интеграция с «Бюджетом народного участия» и eOtinish;
- подтверждение личности жителей через eGov вместо демонстрационного localStorage-токена.
Внедрение
Это оценка команды, не часть расчёта симулятора и не подтверждённый план государственных организаций.
Целевая аудитория: управления экономики и бюджетного планирования акимата, жители — участники «Бюджета народного участия», депутаты маслихата и общественные советы. Astana Innovations рассматривается как возможная площадка пилота при согласовании с организацией.
Для пилота потребуются продакт со стороны акимата, два backend-разработчика, один frontend-разработчик, аналитик или городской экономист для калибровки эффектов на реальной статистике, дизайнер на неполной занятости, а также специалисты по интеграции с eGov/eOtinish и безопасности.
Последовательность внедрения: калибровать данные и эффекты одного района → провести пилот вместе с «Бюджетом народного участия» в одном районе → после оценки результатов расширить на город. До работы с реальными обращениями необходимо отдельно решить вопросы доступа, хранения и проверки данных.
Структура проекта
backend/
app/
data.py # районы, показатели, меры и правила из брифа
schemas.py # Pydantic v2 модели
simulator.py # детерминированный расчёт
trajectory.py # траектория Score и районного разрыва Q1–Q8
optimizer.py # точный поиск сценариев с ограничениями
advisor.py # tool-calling «Советник акима»
resident.py # предложение жителя → мера → проверенный план
proposals.py # файловая доска предложений, голосование и народный сценарий
digest.py # тематическая сводка предложений
public_api.py # валидация открытого детерминированного поиска
explainer.py # Structured Outputs и резервное объяснение
main.py # FastAPI API и раздача frontend
tests/ # эталонные и API-тесты
frontend/
index.html
app.js
theme.js
advisor.js
resident.js
votes.js
digest.js
llms.txt # краткий контракт для внешних ИИ
trajectory.js
roadmap.js
matrix.js
radar.js
style.css
advisor.css
resident.css
votes.css
digest.css
trajectory.css
roadmap.css
insights.css
vendor/
chart.umd.min.js # локальный Chart.js 4.4.7
Dockerfile
docker-compose.yml
Быстрый запуск
Требуется Python 3.10+. Команды выполняются из корня репозитория.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # Windows: copy .env.example .env
uvicorn backend.app.main:app --reload
Откройте http://localhost:8000. Документация API: http://localhost:8000/docs.
Frontend и его локальные статические ресурсы раздаются FastAPI по /static; открывайте приложение через адрес backend, а не как file://.
Docker
cp .env.example .env
docker compose up --build
OpenAI необязателен для расчёта. Без действующего OPENAI_API_KEY приложение покажет резервное объяснение и явно отметит его источник.
Проверка корректности
pip install -r requirements-dev.txt
pytest -q
ruff check backend/app backend/tests
Проверяются контрольные значения из брифа:
- базовая городская средняя:
56.86; - минимальный район: Нура,
49.18; - два критических показателя:
S1=38,S2=35; - базовый Score:
52.56; - официальный сценарий стоимостью 95:
56.54307; - синергия
M10 + M12применяется в Нуре.
Для демонстрации нажмите «Загрузить пример из брифа», затем «Рассчитать сценарий».
Для проверки «Голоса жителя» откройте результат сценария и введите «В Нуре не хватает поликлиник». Сервис должен сопоставить текст с M8 в Нуре и показать проверенный план. Запрос без меры каталога должен завершиться сообщением об отсутствии соответствия, без рассчитанных обещаний.
Скриншоты проверочного прогона: прогноз «3 месяца», доска после сборки народного сценария. Для доски использованы четыре тестовых предложения: три с голосами, одно без. После съёмки демо-доска очищена.
API
POST /api/simulate
{
"selections": [
{"measure_id":"M7","district_id":"nura"},
{"measure_id":"M8","district_id":"nura"},
{"measure_id":"M10","district_id":"nura"},
{"measure_id":"M12","district_id":null},
{"measure_id":"M5","district_id":"saryarka"}
]
}
Невалидный сценарий возвращает HTTP 200 и { "valid": false, "reason": "..." }, чтобы интерфейс мог показать точную причину пользователю.
POST /api/explain
Принимает результат /api/simulate. Сервер извлекает только selections, заново рассчитывает сценарий и передаёт модели проверенную компактную версию результата.
POST /api/advise
{
"current_scenario": [
{"measure_id":"M7","district_id":"nura"},
{"measure_id":"M8","district_id":"nura"},
{"measure_id":"M10","district_id":"nura"},
{"measure_id":"M12","district_id":null},
{"measure_id":"M5","district_id":"saryarka"}
],
"message": "без ЛРТ и с экологией"
}
LLM интерпретирует запрос как ограничения, а код выполняет точный поиск и повторную проверку top-3. Без API-ключа поддерживается прозрачный fallback для типовых ограничений (без M3/ЛРТ, обязательное направление, минимальная стоимость).
GET /api/trajectory и POST /api/trajectory
GET возвращает восемь кварталов базового сценария без мер. POST принимает тот же объект selections, что /api/simulate, и возвращает для каждого квартала score, D_avg, минимальную и максимальную районную оценку и balance.
POST /api/resident/propose
{
"district_id": null,
"message": "В Нуре не хватает поликлиник"
}
Сервис выбирает только существующую меру каталога, ищет лучший допустимый план с её точным размещением и возвращает доказательства из повторной симуляции. Явно выбранный район имеет приоритет; при значении «Весь город» район может извлекаться из текста. Если выбранный район отличается от упомянутого в тексте, ответ содержит district_hint, а кнопка под результатом повторяет расчёт для района из текста. Без OPENAI_API_KEY работает детерминированное сопоставление по словам и основам. Если мера не найдена, сервис возвращает ближайшие варианты без расчётных чисел; если районная мера найдена, но район не определён, предлагает выбрать район. Черновик обращения создаётся локально и никуда не отправляется.
POST /api/search
{
"constraints": {
"exclude": ["M3"],
"require_directions": ["Экология"],
"require_placements": [{"measure_id": "M8", "district_id": "nura"}]
},
"objective": "max_score",
"limit": 3
}
Поиск не вызывает LLM. Поддерживаются цели max_score и min_cost; каждый кандидат повторно проходит /api/simulate-эквивалент на сервере. Невалидные идентификаторы, районы, цели и лимиты возвращают HTTP 400 с причиной.
GET/POST/DELETE /api/proposals, POST /api/proposals/{id}/vote
Доска предложений изначально пустая. POST принимает measure_id, district_id и текст; значения эффекта, цены и голоса от клиента игнорируются. Статусы показывают, проверено ли предложение кодом и вошло ли оно в народный сценарий. Голосование учебное: защита от повторного голоса основана на браузерном токене, это не авторизация. POST /api/proposals/people-scenario собирает допустимый план только из предложений хотя бы с одним голосом и возвращает причины исключения с названиями мер; предложения без голосов сохраняют статус. DELETE очищает демо-доску.
POST /api/proposals/digest
Формирует «Сводку для акимата»: темы и краткие текстовые резюме, количество предложений и голосов, районы, меры и число включённых в народный сценарий. При доступном OpenAI модель группирует только тексты и идентификаторы, а все числовые поля вычисляет сервер; без ключа предложения группируются по направлениям. Интерфейс позволяет скачать сводку в .txt.
GET /llms.txt
Краткая plain-text документация для ИИ-агентов со списком эндпоинтов, минимальными примерами и запретом самостоятельно вычислять Score. Полная интерактивная схема доступна на /docs.
Данные и формула
Все районы, веса и мероприятия взяты из синтетического информационного пакета организаторов. Исходный датасет не содержит персональных данных, реляционная БД не используется. Тексты предложений и демонстрационные идентификаторы голосующих хранятся локально в var/proposals.json (путь можно задать через QQ_PROPOSALS_PATH); свободный текст не следует заполнять персональными данными. Внешние сервисы: OpenAI API для текстового объяснения, советника, сопоставления неочевидных запросов жителя и тематической сводки; Google Fonts — только для шрифтов. Chart.js поставляется с репозиторием.
effect_realized = effect × (8 − lag) / 8
effect_realized(t) = 0, если t ≤ lag; иначе effect × (t − lag) / 8
D_d = Σ weight_k × indicator_dk
D_avg = Σ population_share_d × D_d
Score = 0.7 × D_avg + 0.3 × min(D_d) − N_crit
N_crit — число пар «район × показатель» со значением строго меньше 40 после применения всех эффектов.
Знаменатель 8 в поквартальной формуле выбран намеренно: при t=8 траектория воспроизводит официальную лаг-реализованную долю (8 − lag) / 8 и в точности совпадает с основным симулятором. Деление на (8 − lag) означало бы полную реализацию каждой меры к Q8 и противоречило бы эталонному результату 56.54307.
Известные ограничения
- эффекты мероприятий синтетические и не являются прогнозом реальной городской политики;
- географическая карта районов не добавлена: в брифе нет официальной геометрии, а выдумывать её нельзя;
- полный поиск перебирает много комбинаций и может занимать несколько секунд в зависимости от CPU;
- Score оптимизирует заданную формулу и не заменяет общественное обсуждение или экспертную оценку;
- качество LLM-текста зависит от доступности OpenAI, но расчёт от неё не зависит;
- сопоставление свободного текста с каталогом может ошибаться; перед публикацией житель должен проверить найденную меру и район;
- доска голосования хранится в локальном JSON-файле; браузерный токен легко заменить, подтверждения личности и защиты публичного
DELETE /api/proposalsнет, поэтому сервис не подходит для реального голосования без доработки доступа и хранения; - при нескольких процессах сервера локальный файловый lock не обеспечивает межпроцессную синхронизацию записей.
Авторы коммитов
История коммитов
- 2026-09-23 17:42Add quarterly implementation roadmapCodex
- 2026-09-23 17:37Polish resident voting and forecast demoCodex
- 2026-09-23 17:18Fix resident proposal matching and district guidanceCodex
- 2026-09-23 17:08Document positioning and reproducible demoCodex
- 2026-09-23 17:08Add trajectory and resident-facing UICodex
- 2026-09-23 17:08Add city scenario insights and resident participationCodex
- 2026-09-23 16:24Polish city evidence presentationCodex
- 2026-09-23 16:22Expose verified search API for AI clientsCodex
Все коммиты (24)
- 2026-09-23 16:19Add verified resident proposal flowCodex
- 2026-09-23 16:12Add mayor advisor scenario applicationCodex
- 2026-09-23 15:58Redesign QalaQadam with Astana identityCodex
- 2026-09-23 15:35Add district indicator visualizationsCodex
- 2026-09-23 15:07Add quarterly policy trajectoryCodex
- 2026-09-23 14:46Add constraint-aware AI policy advisorCodex
- 2026-09-23 14:23Build Astana city policy simulatorCodex
- 2026-09-23 14:01Start HackAlem hackathonCodex
- 2026-09-23 11:05Add test fileCodex
- 2026-09-23 10:22Disable CI/CD (blocked by org billing, not needed)MelNikVl
- 2026-09-23 10:21Add .dockerignore to speed up buildsMelNikVl
- 2026-09-23 10:18Lazy-init OpenAI client, load .env locallyMelNikVl
- 2026-09-23 10:17Fix lint issuesMelNikVl
- 2026-09-22 13:57Add organizer's README generation prompt for end of hackathonMelNikVl
- 2026-09-22 13:33Add minimal CD: build and push image to GHCR on mainMelNikVl
- 2026-09-22 13:29Base scaffold: FastAPI + SQLite + OpenAI wrapper + CIMelNikVl
Структура репозитория (55 файлов)
.dockerignore .env.example .gitignore Dockerfile README.md backend/__init__.py backend/app/__init__.py backend/app/advisor.py backend/app/data.py backend/app/digest.py backend/app/explainer.py backend/app/main.py backend/app/optimizer.py backend/app/proposals.py backend/app/public_api.py backend/app/resident.py backend/app/schemas.py backend/app/simulator.py backend/app/trajectory.py backend/tests/__init__.py backend/tests/test_advisor.py backend/tests/test_api.py backend/tests/test_digest.py backend/tests/test_optimizer.py backend/tests/test_proposals.py backend/tests/test_public_api.py backend/tests/test_resident.py backend/tests/test_simulator.py backend/tests/test_trajectory.py docker-compose.yml docs/screenshots/q1-forecast.png docs/screenshots/votes-after-build.png frontend/advisor.css frontend/advisor.js frontend/app.js frontend/digest.css frontend/digest.js frontend/index.html frontend/insights.css frontend/llms.txt frontend/matrix.js frontend/radar.js frontend/resident.css frontend/resident.js frontend/roadmap.css frontend/roadmap.js frontend/style.css frontend/theme.js frontend/trajectory.css frontend/trajectory.js frontend/votes.css frontend/votes.js pytest.ini requirements-dev.txt requirements.txt
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.