AAYMA
RU
Войти

Аким на 5 часов

Городской симулятор Астаны. Игрок распределяет 100 единиц бюджета на 5 мер по 5 районам (или на весь город), детерминированный движок пересчитывает…

Доведённый продукт
Команда «Ordabasy» 13💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: 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. Как работает решение

  1. Пользователь открывает карту Астаны и выбирает район (или переключается на вкладку «Весь город» для городских мер) — справа отображается профиль района: QoL-балл, ключевые проблемы, 10 показателей по 5 направлениям.
  2. По кнопке «Принять решение» открывается каталог из 14 инициатив, отфильтрованный по направлению; для района уже недоступны меры, которые не проходят по бюджету, лимиту направления или конфликтуют с уже выбранными.
  3. Так собираются ровно 5 решений; бюджет и прогресс видны в реальном времени.
  4. По кнопке «Запустить симуляцию» решения (scenarioVersion + decisions) уходят на backend. Единственный источник истины — детерминированный Python-движок (app/domain/engine.py): он валидирует набор, применяет эффекты с учётом лага, синергий и штрафов и считает Score «до/после».
  5. Backend пересчитывает те же самые проверенные факты («evidence») и передаёт их в OpenAI со строгим system-промптом: LLM может только объяснять готовые цифры, не имеет права их менять или придумывать новые. Ответ проверяется на совпадение каждого числа с evidence.
  6. Пользователь видит итоговый 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/analysisAI-объяснение расчёта через OpenAI; 404, если не настроено

7. Установка и запуск

Вариант A — всё через Docker Compose (backend + frontend)

Требования: Docker Engine/Desktop и Docker Compose v2.

docker compose up --build -d

Ключ 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.

Звёзды
⭐ 0
Форки
0
Язык
TypeScript
Последний push
в прошлом месяце
Коммиты
29
Авторы
5
Файлы
161
Ветки
5

Авторы коммитов

bagdau 14Yerassyl1008 8Erasyl 5ykutzhan 1Erasyl 1

История коммитов

  • 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
Ветки (5)

Обсуждение 0 комментариев

Войдите, чтобы комментировать

Пока нет комментариев — будьте первым.