О проекте
Команда: Ordabasy
Кейс HackAlem.ai: Спецтрек — Astana Innovations
Стадия: Доведённый продукт
Задача кейса: «Аким на 5 часов»: городской симулятор, распределяющий бюджет по 5 направлениям и 5 районам
Суть
Городской симулятор Астаны. Игрок распределяет 100 единиц бюджета на 5 мер по 5 районам (или на весь город), детерминированный движок пересчитывает Quality of Life Score «до/после», а LLM объясняет сильные стороны, риски и компромиссы сценария.
Как устроено
Каталог районов (10 показателей по 5 направлениям) и 14 инициатив хранится в JSON. Python-движок engine.py валидирует набор решений (бюджет, лимит на направление, дубли, несовместимые пары) и применяет эффекты с учётом лага, синергий и штрафов. Посчитанные факты (evidence) передаются в OpenAI Chat Completions через Structured Outputs json_schema со строгим промптом, который запрещает менять цифры. Модуль numeric_check сверяет каждое число в ответе с evidence: при расхождении делается один повторный запрос, затем возвращается ошибка. Фронтенд на Next.js/Leaflet показывает карту районов и сравнение «до/после».
Ключевые решения
- deterministic scoring engine
- lag/synergy/incompatibility rules
- OpenAI Structured Outputs (json_schema)
- numeric hallucination check with retry
- Feature-Sliced Design
Особенности
Автоматическая проверка каждого числа в ответе LLM против результата расчёта, с одним повтором. Если AI недоступен, числовой результат всё равно показывается. Есть эталонный сценарий для жюри с ожидаемым Score 52.56 → 56.54. OpenAI вызывается через urllib без SDK.
Стек
Python · FastAPI · Pydantic · Uvicorn · Next.js · React · TypeScript · Tailwind CSS · Leaflet · OpenStreetMap · Docker · Docker Compose · pytest · Vitest
Модели
gpt-4o-mini
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-42852219-ordabasy.
README
README.mdАким на 5 часов
AI-симулятор управления городом (кодовое имя пакетов в репозитории: backend
astana-innovation, frontend hackalem).
1. Название проекта
«Аким на 5 часов».
2. Краткое описание
Проблема: у городских управленцев (и у любого, кто хочет понять логику городского бюджетирования) нет простого способа увидеть, как ограниченный бюджет, отложенный эффект инфраструктурных мер и конкурирующие приоритеты районов влияют на качество жизни — до того, как решение уже принято и деньги потрачены.
Решение: интерактивный симулятор на синтетических, но внутренне непротиворечивых данных Астаны. Пользователь получает единый стартовый город, 100 условных единиц бюджета и должен принять ровно пять решений (мер) для пяти районов. Детерминированный движок пересчитывает Astana Quality of Life Score, а AI на основе уже посчитанных фактов объясняет сильные стороны, риски и компромиссы выбранного сценария.
Аудитория: жюри и участники хакатона, преподаватели/студенты, интересующиеся основами городского планирования и объяснимого AI-анализа (LLM, который не считает сам, а комментирует готовый расчёт).
Явная оговорка авторов (см. README.md/frontend/README.md в репозитории):
данные, границы районов и индекс — синтетические. Это учебная модель для
сравнения сценариев, а не официальная статистика и не прогноз реального
развития Астаны.
3. Что реализовано
- Каталог из 5 районов и 14 инициатив (мер) с ценой, направлением,
лагом и эффектом на конкретные показатели (
app/data/,GET /api/bootstrap). - Интерактивная SVG/Leaflet-карта районов с профилем, ключевыми проблемами и
10 показателями каждого района (frontend
widgets/city-map,widgets/district-panel). - Выбор ровно пяти решений с проверкой бюджета, лимита мер на направление,
дублей, несовместимых пар мер и обязательности района для районных мер
(
app/domain/engine.py,frontend/src/features/manage-scenario). - Отдельный режим «Весь город»: меры городского охвата (например, «Единая цифровая платформа обращений») можно выбрать без клика по конкретному району — карта при этом подсвечивает сразу все пять районов.
- Детерминированный расчёт Astana Quality of Life Score «до/после» с разбивкой
по районам, критическими показателями (значение < 40) и сработавшими
синергиями/несовместимостями мер (
POST /api/simulate). - AI-объяснение готового расчёта через OpenAI: сильные стороны, риски,
компромиссы и рекомендации — без права пересчитывать Score или выдумывать
меры/эффекты, которых нет в расчёте (
POST /api/analysis,app/modules/analysis/). - Автоматическая проверка каждого числа в ответе AI против данных расчёта
(
app/modules/analysis/numeric_check.py): при несовпадении запрос к OpenAI повторяется один раз с уточнением, иначе пользователю возвращается явная ошибка, а не непроверенный текст. - Готовый эталонный сценарий («Эталонный план») для быстрой проверки жюри.
- Backend- и frontend-тесты, покрывающие расчёт, валидацию сценария, контракт API и ключевые пользовательские сценарии интерфейса.
4. Как работает решение
- Пользователь открывает карту Астаны и выбирает район (или переключается на вкладку «Весь город» для городских мер) — справа отображается профиль района: QoL-балл, ключевые проблемы, 10 показателей по 5 направлениям.
- По кнопке «Принять решение» открывается каталог из 14 инициатив, отфильтрованный по направлению; для района уже недоступны меры, которые не проходят по бюджету, лимиту направления или конфликтуют с уже выбранными.
- Так собираются ровно 5 решений; бюджет и прогресс видны в реальном времени.
- По кнопке «Запустить симуляцию» решения (
scenarioVersion+decisions) уходят на backend. Единственный источник истины — детерминированный Python-движок (app/domain/engine.py): он валидирует набор, применяет эффекты с учётом лага, синергий и штрафов и считает Score «до/после». - Backend пересчитывает те же самые проверенные факты («evidence») и передаёт их в OpenAI со строгим system-промптом: LLM может только объяснять готовые цифры, не имеет права их менять или придумывать новые. Ответ проверяется на совпадение каждого числа с evidence.
- Пользователь видит итоговый Score, карту «до/после», сравнение по районам и панель AI-анализа (сильные стороны, риски, компромиссы, рекомендации). Если AI недоступен (нет ключа, сеть, таймаут) — числовой результат расчёта всё равно сохраняется и показывается.
5. Технологии
Backend (pyproject.toml, requirements.txt, Python >=3.12,<4.0):
FastAPI 0.141.1, Pydantic 2.13.5, pydantic-settings 2.15.0, Starlette 1.7.0,
Uvicorn 0.53.0. Вызов OpenAI Chat Completions API (Structured Outputs,
response_format: json_schema) реализован через стандартную библиотеку
urllib — без отдельного OpenAI SDK. Модель по умолчанию — gpt-4o-mini
(настраивается через APP_OPENAI_MODEL).
Frontend (frontend/package.json): Next.js 16.3.6 (App Router, Turbopack),
React 19.2.8, TypeScript 5, Tailwind CSS 4, Leaflet 1.9.4 + React-Leaflet 5
(карта и тайлы OpenStreetMap), Vitest 3 + Testing Library (юнит- и
интеграционные тесты).
Инфраструктура: Docker + Docker Compose (два сервиса — backend и
frontend, плюс профиль tests), pytest + pytest-cov на backend.
6. Архитектура проекта
app/domain/engine.py чистый Python: правила, валидация, детерминированный расчёт
app/data/ 5 районов и 14 мероприятий в JSON (источник каталога)
app/modules/simulations/ HTTP-схемы, адаптер движка, публичные маршруты (/api/*)
app/modules/analysis/ system prompt, вызов OpenAI, проверка чисел ответа (/api/analysis)
app/modules/districts/ каталог районов (legacy /api/v1/districts)
app/modules/initiatives/ каталог мер (legacy /api/v1/initiatives)
app/modules/ai_analysis/ сохранённый legacy-модуль (/api/v1/ai-analysis), не связан с /api/analysis
app/main.py сборка FastAPI-приложения, статика web/, регистрация роутеров
web/ минимальный HTML/CSS/ES-модули UI, который backend отдаёт сам по "/"
frontend/ основной UI: Next.js 16 / React 19, Feature-Sliced Design
frontend/src/app|views|widgets|features|entities|shared FSD-слои (app → views → widgets → features → entities → shared)
frontend/src/features/run-simulation/api/backendMapping.ts перевод доменных типов фронтенда в контракт backend и обратно
tests/ pytest: движок, HTTP API, доставка интерфейса
frontend/src/**/*.test.ts(x) vitest: доменная логика, карта, сценарий HomePage
examples/ эталонный запрос и рассчитанный контекст для AI
docs/ контракт AI-интеграции, инструкции по Docker
Поток данных: браузер → frontend (Next.js, порт 3000) → HTTP fetch к
backend (FastAPI, порт 8000) по адресу из NEXT_PUBLIC_API_URL → единственный
Python-движок расчёта → (для AI-анализа) OpenAI Chat Completions → ответ
проверяется и возвращается фронтенду. Движок не хранит пользовательское
состояние и не мутирует исходные данные; повторный расчёт одного и того же
набора решений детерминирован и не зависит от их порядка.
Ключевые HTTP-маршруты (полностью в app/modules/simulations/public_router.py
и app/modules/analysis/):
| Метод | Маршрут | Назначение |
|---|---|---|
| GET | /api/bootstrap | Датасет, правила, каталог и базовый результат |
| POST | /api/validate | Проверка черновика сценария |
| POST | /api/simulate | Полный детерминированный расчёт Score |
| GET | /api/config | Публичные настройки frontend (без секретов) |
| GET | /api/health | Работоспособность backend без зависимости от AI |
| POST | /api/analysis | AI-объяснение расчёта через OpenAI; 404, если не настроено |
7. Установка и запуск
Вариант A — всё через Docker Compose (backend + frontend)
Требования: Docker Engine/Desktop и Docker Compose v2.
docker compose up --build -d
- Frontend (основной UI, Next.js): localhost:3000
- Backend API + встроенный минимальный UI: localhost:8000
- Swagger: localhost:8000/docs
- Health: localhost:8000/api/health
Ключ OpenAI для запуска не обязателен — без него /api/analysis отвечает
404, а расчёт сценария (Score, карта, показатели) работает полностью. Чтобы
включить реальный AI-анализ, задайте переменную окружения перед запуском:
OPENAI_API_KEY=sk-... docker compose up --build -d
Остановить и посмотреть логи:
docker compose logs --tail=100 backend frontend
docker compose down
Вариант B — локально без Docker
Backend (Python 3.12):
python3.12 -m venv .venv
.venv/bin/python -m pip install -r requirements-dev.txt
.venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
На Windows используйте .venv\Scripts\python.exe вместо .venv/bin/python.
Backend уже отдаёт минимальный встроенный UI на http://localhost:8000/.
Frontend (Node.js 20.9+; frontend/README.md):
cd frontend
npm install
cp .env.example .env.local # на Windows: copy .env.example .env.local
npm run dev
Откройте http://localhost:3000. По умолчанию .env.example включает
NEXT_PUBLIC_API_MODE=mock (детерминированное демо без обращения к backend);
чтобы работать с живым backend, поставьте в .env.local
NEXT_PUBLIC_API_MODE=live и убедитесь, что NEXT_PUBLIC_API_URL указывает
на адрес запущенного backend (по умолчанию http://localhost:8000).
8. Как проверить решение
Есть готовый эталонный сценарий (examples/reference-scenario.json):
| Мера | Район | Цена |
|---|---|---|
| M7 — школа + детсад | Нура | 24 |
| M8 — поликлиника | Нура | 20 |
| M10 — освещение и камеры | Нура | 12 |
| M12 — платформа обращений | Весь город | 14 |
| M5 — чистое топливо | Сарыарка | 25 |
Ожидаемый результат: стоимость 95, остаток 5, Score 52.55768 → 56.54307 (прирост +3.98539, на экране округляется до 52,56 → 56,54 (+3,99)), критических показателей после мер — 0, активна синергия M10 + M12 в Нуре.
Через интерфейс: в интерфейсе frontend есть кнопка «Эталонный план» — она заменяет текущий план на этот сценарий одним кликом (с подтверждением, если план уже не пуст). Дальше нажмите «Запустить симуляцию» и сравните цифры с таблицей выше; в панели AI-анализа должны появиться сильные стороны, риски, компромиссы и рекомендации, построенные на этих же цифрах.
Через API, в обход интерфейса:
curl --fail http://localhost:8000/api/bootstrap
curl --fail http://localhost:8000/api/simulate \
-H 'Content-Type: application/json' \
--data-binary @examples/reference-scenario.json
Готовый рассчитанный контекст для сверки — examples/reference-analysis.json.
Автотесты (подтверждают ту же арифметику и контракт API):
.venv/bin/python -m pytest # backend: движок, HTTP API, доставка интерфейса
cd frontend && npm run typecheck && npm run lint && npm run test && npm run build
9. Данные и интеграции
- Каталог районов и мер — статичные JSON-файлы в
app/data/(5 районов, 14 инициатив с ценой/лагом/эффектами). Данные синтетические, придуманы для проекта, не выгружены из внешнего источника. - OpenAI API — единственная внешняя интеграция. Backend делает POST-запрос
к Chat Completions (
https://api.openai.com/v1/chat/completions,app/modules/analysis/client.py) со Structured Outputs и системным промптом, который запрещает модели пересчитывать Score или упоминать меры/районы, которых нет в переданных данных. Ключ (OPENAI_API_KEY) передаётся только backend-контейнеру в рантайме и никогда не попадает во frontend, публичный/api/configили образ. - OpenStreetMap — источник тайлов карты для Leaflet во frontend
(
frontend/src/widgets/city-map); используется только как подложка, границы районов на карте упрощённые и не являются кадастровыми данными. - Внешней базы данных, кэша или очередей проект не использует — весь расчёт идёт в памяти процесса backend и не хранит состояние между запросами.
10. Ограничения
- Данные и индекс качества жизни синтетические: это не официальная статистика и не прогноз реального развития Астаны (явно подтверждается и во frontend, и в system-промпте AI).
- Без
OPENAI_API_KEY(или приAPP_ANALYSIS_MODEотличном отremote) маршрут/api/analysisотвечает 404 — AI-объяснение недоступно, хотя сам расчёт сценария работает в полном объёме. - В репозитории сохранён старый модуль
app/modules/ai_analysis/(/api/v1/ai-analysis) — он не переписан под новый контракт и намеренно возвращает управляемую ошибку миграции (legacy_request), а не считает по старой формуле. - Backend отдаёт также собственный минимальный статический UI (
web/) наhttp://localhost:8000/— это отдельный, более простой интерфейс, не синхронизированный автоматически с основным Next.js-фронтендом вfrontend/. - Нет пользовательской аутентификации, нет сохранения сценариев на сервере
между сессиями (только черновик в
localStorageбраузера) и нет базы данных. - Карта в UI — стилизованная схема с упрощёнными границами районов, не точная GIS/кадастровая модель (отмечено прямо на карте: «Упрощённые границы · не кадастровая карта»).
- Проверка чисел в ответе AI (
numeric_check.py) — эвристика на основе регулярных выражений и округления, а не формальное доказательство; она снижает, но не исключает риск галлюцинации, которая случайно совпадёт с числом из данных.
11. Deployed-версия
Публичного развёрнутого URL у проекта нет — решение запускается локально или через Docker Compose по инструкции из раздела 7.
Авторы коммитов
История коммитов
- 2026-09-23 17:29check 2Yerassyl1008
- 2026-09-23 17:27check 1ykutzhan
- 2026-09-23 17:26checkykutzhan
- 2026-09-23 17:21readme файлErasyl
- 2026-09-23 17:19docs: rewrite README.md for hackathon judgesbagdau
- 2026-09-23 17:11feat(frontend): allow picking a city-wide measure without selecting a districtbagdau
- 2026-09-23 16:58Merge pull request #4 from BAITC-Hacks/feat/frontend-integrationBagdaulet Askhatovich fullstack-developer
- 2026-09-23 16:57feat: integrate the real Next.js frontend and wire it to FastAPIbagdau
Все коммиты (29)
- 2026-09-23 16:50feat: integrate the real Next.js frontend and wire it to FastAPIbagdau
- 2026-09-23 16:36fixed mapykutzhan
- 2026-09-23 16:32connection to backendykutzhan
- 2026-09-23 16:17Merge pull request #3 from BAITC-Hacks/feat/openai-analysisBagdaulet Askhatovich fullstack-developer
- 2026-09-23 16:16feat: implement POST /api/analysis with OpenAI structured outputsbagdau
- 2026-09-23 16:05tried to fix mapykutzhan
- 2026-09-23 15:48csErasyl
- 2026-09-23 15:16webErasyl
- 2026-09-23 15:10feat: add domain engine, docker configs and backend updatesErasyl
- 2026-09-23 15:09basic mpvykutzhan
- 2026-09-23 15:08Merge pull request #1 from BAITC-Hacks/mainBagdaulet Askhatovich fullstack-developer
- 2026-09-23 14:55feat: add composeErasyl
- 2026-09-23 14:54feat: add DockerfileErasyl
- 2026-09-23 14:25docs: add setup and API guidebagdau
- 2026-09-23 14:24test: cover API and simulation rulesbagdau
- 2026-09-23 14:24feat: expose versioned API and health checksbagdau
- 2026-09-23 14:24feat: implement deterministic city simulationbagdau
- 2026-09-23 14:23feat: add district and initiative catalogsbagdau
- 2026-09-23 14:23chore: bootstrap FastAPI project configurationbagdau
- 2026-09-23 14:18Set up a lightweight Feature-Sliced layout so the Next.js app builds and stays maintainable.ykutzhan
- 2026-09-23 13:54Initial commit from Create Next Appykutzhan
Структура репозитория (161 файлов)
.dockerignore .env.example .gitignore Dockerfile README.md app/__init__.py app/api.py app/core/__init__.py app/core/config.py app/core/exceptions.py app/data/districts.json app/data/initiatives.json app/domain/__init__.py app/domain/engine.py app/main.py app/modules/__init__.py app/modules/ai_analysis/__init__.py app/modules/ai_analysis/dependencies.py app/modules/ai_analysis/repository.py app/modules/ai_analysis/router.py app/modules/ai_analysis/schemas.py app/modules/ai_analysis/service.py app/modules/analysis/__init__.py app/modules/analysis/client.py app/modules/analysis/dependencies.py app/modules/analysis/numeric_check.py app/modules/analysis/prompts.py app/modules/analysis/schemas.py app/modules/analysis/service.py app/modules/districts/__init__.py app/modules/districts/dependencies.py app/modules/districts/repository.py app/modules/districts/router.py app/modules/districts/schemas.py app/modules/districts/service.py app/modules/health/__init__.py app/modules/health/dependencies.py app/modules/health/repository.py app/modules/health/router.py app/modules/health/schemas.py app/modules/health/service.py app/modules/initiatives/__init__.py app/modules/initiatives/dependencies.py app/modules/initiatives/repository.py app/modules/initiatives/router.py app/modules/initiatives/schemas.py app/modules/initiatives/service.py app/modules/simulations/__init__.py app/modules/simulations/dependencies.py app/modules/simulations/public_router.py app/modules/simulations/repository.py app/modules/simulations/router.py app/modules/simulations/schemas.py app/modules/simulations/service.py compose.yaml docs/ai-contract.md docs/docker.md examples/reference-analysis.json examples/reference-scenario.json frontend/.cursor/rules/code-quality.mdc frontend/.cursor/rules/fsd.mdc frontend/.cursor/rules/react.mdc frontend/.cursor/rules/typescript.mdc frontend/.dockerignore frontend/.env.example frontend/.gitignore frontend/AGENTS.md frontend/CLAUDE.md frontend/Dockerfile frontend/README.md frontend/eslint.config.mjs frontend/next.config.ts frontend/package-lock.json frontend/package.json frontend/postcss.config.mjs frontend/public/file.svg frontend/public/globe.svg frontend/public/next.svg frontend/public/vercel.svg frontend/public/window.svg frontend/src/app/favicon.ico frontend/src/app/globals.css frontend/src/app/layout.tsx frontend/src/app/page.tsx frontend/src/entities/.gitkeep frontend/src/entities/decision/index.ts frontend/src/entities/decision/model.ts frontend/src/entities/district/index.ts frontend/src/entities/district/model.test.ts frontend/src/entities/district/model.ts frontend/src/entities/initiative/index.ts frontend/src/entities/initiative/model.ts frontend/src/entities/simulation/index.ts frontend/src/entities/simulation/model.ts frontend/src/features/.gitkeep frontend/src/features/manage-scenario/index.ts frontend/src/features/manage-scenario/model/validation.test.ts frontend/src/features/manage-scenario/model/validation.ts frontend/src/features/run-simulation/api/backendMapping.ts frontend/src/features/run-simulation/api/mock.ts frontend/src/features/run-simulation/api/simulationApi.ts frontend/src/features/run-simulation/index.ts frontend/src/shared/api/.gitkeep frontend/src/shared/api/client.ts frontend/src/shared/api/index.ts frontend/src/shared/config/.gitkeep frontend/src/shared/config/domain.ts frontend/src/shared/config/index.ts frontend/src/shared/lib/.gitkeep frontend/src/shared/ui/.gitkeep frontend/src/views/home/index.ts frontend/src/views/home/ui/HomePage.test.tsx frontend/src/views/home/ui/HomePage.tsx frontend/src/widgets/.gitkeep frontend/src/widgets/ai-analysis/index.ts frontend/src/widgets/ai-analysis/ui/AIAnalysisPanel.tsx frontend/src/widgets/city-header/index.ts frontend/src/widgets/city-header/ui/CityHeader.tsx frontend/src/widgets/city-map/index.ts frontend/src/widgets/city-map/model/geography.ts frontend/src/widgets/city-map/model/map.types.ts frontend/src/widgets/city-map/model/mapPresentation.test.ts frontend/src/widgets/city-map/model/mapPresentation.ts frontend/src/widgets/city-map/ui/CityMap.test.tsx frontend/src/widgets/city-map/ui/CityMap.tsx frontend/src/widgets/city-map/ui/LeafletCityMap.tsx frontend/src/widgets/city-map/ui/MapLayerControl.tsx frontend/src/widgets/city-map/ui/MapLegend.tsx frontend/src/widgets/decision-progress/index.ts frontend/src/widgets/decision-progress/ui/DecisionProgress.tsx frontend/src/widgets/district-panel/index.ts frontend/src/widgets/district-panel/ui/DistrictPanel.tsx frontend/src/widgets/initiative-panel/index.ts frontend/src/widgets/initiative-panel/ui/InitiativePanel.tsx frontend/src/widgets/simulation-results/index.ts frontend/src/widgets/simulation-results/ui/ResultDistrictComparison.tsx frontend/src/widgets/simulation-results/ui/SimulationResults.tsx frontend/src/widgets/simulation-timeline/index.ts frontend/src/widgets/simulation-timeline/ui/SimulationTimeline.tsx frontend/test/reactLeafletMock.tsx frontend/tsconfig.json frontend/vitest.config.mts frontend/vitest.setup.ts pyproject.toml requirements-dev.txt requirements.txt tests/__init__.py tests/conftest.py tests/frontend/state.test.mjs tests/test_analysis_api.py tests/test_catalog_api.py tests/test_engine.py tests/test_frontend_delivery.py tests/test_openapi.py tests/test_simulations_api.py web/app.js web/cs.js web/index.html web/mock-analysis.mjs web/state.mjs web/styles.css
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.