AAYMA
RU
Войти

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

Городской симулятор: пользователь выбирает ровно 5 мер из 14 в пределах бюджета 100, детерминированный движок считает Astana Quality of Life Score…

Рабочий MVP
Команда «SODA Team» 4💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: SODA Team
Кейс HackAlem.ai: Спецтрек — Astana Innovations
Стадия: Рабочий MVP

Задача кейса: «Аким на 5 часов»: городской симулятор, распределяющий бюджет по 5 направлениям и 5 районам

Суть

Городской симулятор: пользователь выбирает ровно 5 мер из 14 в пределах бюджета 100, детерминированный движок считает Astana Quality of Life Score по пяти районам, а AI объясняет результат и предлагает замены.

Как устроено

Данные районов, весов, мер, синергий и несовместимостей лежат в data/*.json. Движок на FastAPI валидирует выбор и считает показатели с учётом лага ((8−L)/8), синергий и clip 0–100, затем Score = 0.7·D_avg + 0.3·min(D) − N_crit. Полный перебор 694 395 допустимых наборов находит оптимум и допустимые замены одной меры. Объяснение строит OpenAI Responses API через серверный инструмент get_scenario_analysis: числа и замены берутся только из движка, а если модель выдаёт свои числа, ответ заменяется fallback-шаблоном. UI на React с Recharts.

Ключевые решения

  • exhaustive enumeration optimizer
  • deterministic scoring model
  • function calling (get_scenario_analysis)
  • template fallback
  • Pydantic validation

Метрики

Score: базовое состояние 52.558, пример 56.543, лучший набор полного перебора 57.237 (стоимость 98)

Особенности

Полный перебор всех 694 395 допустимых планов даёт точный оптимум для сравнения. LLM запрещено выдавать числа и замены: при нарушении срабатывает детерминированный fallback.

Стек

Python 3.11 · FastAPI · Pydantic v2 · React · TypeScript · Vite · Recharts · Docker Compose · Nginx · pytest

Модели

OpenAI Responses API


Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-596cde86-soda-team.

README

README.md

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

AI-симулятор городских решений для хакатона HackAlem AI. Управленец или аналитик распределяет общий виртуальный бюджет между инициативами, выбирает ровно пять мер и видит, как по правилам синтетической модели меняется качество жизни в пяти условных районах Астаны.

Система рассчитывает Astana Quality of Life Score детерминированно. AI объясняет уже вычисленный результат, сильные стороны, риски и компромиссы. Без OpenAI-ключа то же действие работает через шаблонное объяснение, собранное из результатов движка.

Что реализовано

  • Одинаковые исходные данные и бюджет 100 условных единиц для всех пользователей.
  • Каталог 14 мероприятий по транспорту, экологии, социальной инфраструктуре, безопасности и городским сервисам.
  • Выбор ровно 5 разных мер; район обязателен для районной меры. На одно направление допускается не более 2 мер.
  • Живой счётчик бюджета. UI не даёт добавить меру сверх бюджета, а API отклоняет любые превышения и остальные нарушения.
  • Учёт задержки эффекта, синергий и несовместимостей; показатели и Score меняются при изменении решений.
  • Визуализация районных оценок и матрица десяти показателей до и после, выделение значений ниже критического порога.
  • Полный перебор 694 395 допустимых наборов, сравнение с лучшим и предложения допустимых замен одной меры или района.
  • Объяснение через OpenAI Responses API с вызовом серверного инструмента; fallback при отсутствии ключа, таймауте или ошибке API.
  • Кнопка «Загрузить пример сценария» для быстрой проверки.

Быстрый запуск

Нужен Docker с Compose. В корне проекта выполните:

docker compose up --build

Интерфейс: http://localhost:8080. Документация API: http://localhost:8000/docs. Ключ не требуется.

Для AI-объяснений создайте локальный .env по образцу .env.example и задайте OPENAI_API_KEY. Модель настраивается через OPENAI_MODEL. Файл .env игнорируется Git; ключ никогда не вводится во frontend.

Запуск без Docker

Нужны Python 3.11+, Node.js 24+ и pnpm 11.19.0. Из корня проекта:

python -m venv .venv
# Активируйте .venv для вашей ОС
python -m pip install -r backend/requirements.txt
python -m uvicorn backend.api.main:app --reload --port 8000

В другом терминале:

cd frontend
pnpm install --frozen-lockfile
pnpm run dev

Откройте http://localhost:5173. Vite проксирует /api к локальному backend. Для production-проверки используйте pnpm run build и pnpm run preview.

В Windows, если PowerShell блокирует файлы npm.ps1 или pnpm.ps1, используйте .cmd-обёртки:

cd C:\path\to\hackalem\frontend
pnpm.cmd install --frozen-lockfile
pnpm.cmd run dev

Backend и frontend запускаются в разных окнах терминала. Для backend в Windows можно использовать py вместо python:

cd C:\path\to\hackalem
py -m pip install -r backend\requirements.txt
py -m uvicorn backend.api.main:app --reload --port 8000

Как проверить основной сценарий

  1. Откройте интерфейс и нажмите «Загрузить пример сценария». Он выбирает M7, M8 и M10 в Нуре, M12 для всего города и M5 в Сарыарке.
  2. Убедитесь, что бюджет показывает 95 / 100, а кнопка расчёта доступна.
  3. Нажмите «Рассчитать сценарий». Ожидаемый Score — 56,54 после округления. В таблице видны изменения районов и срабатывание синергии M10+M12.
  4. Измените одну меру или район и рассчитайте снова. Score и объяснение должны измениться. Попытка превысить бюджет блокируется; несовместимые меры показывают ошибку.
  5. Для API-проверки откройте /docs или отправьте POST /api/simulate:
{
  "selection": [
    {"measure_id": "M7", "district": "nura"},
    {"measure_id": "M8", "district": "nura"},
    {"measure_id": "M10", "district": "nura"},
    {"measure_id": "M12", "district": null},
    {"measure_id": "M5", "district": "saryarka"}
  ]
}

Невалидный набор для /simulate и /explain получает HTTP 422 с телом {"errors": [{"rule": "…", "message": "…"}]}. /validate возвращает все найденные ошибки без расчёта Score.

Как работает модель

В data/ лежат исходные показатели пяти районов, доли населения, веса, каталог мер, синергии и несовместимости. Все десять показателей имеют шкалу 0–100, где больше — лучше. Горизонт расчёта — 8 кварталов. Для меры с лагом L учтённая доля полного эффекта равна (8 − L) / 8.

Для района d и показателя k:

I'[d,k] = clip(I[d,k] + Σ effect[m,k] × (8 − lag[m]) / 8 + synergy[d,k], 0, 100)
D[d]   = Σ weight[k] × I'[d,k]
D_avg  = Σ population_share[d] × D[d]
Score  = 0.7 × D_avg + 0.3 × min(D[d]) − N_crit

N_crit — число пар «район × показатель» со значением строго ниже 40. Синергия добавляется до clip и не масштабируется лагом. Округление применяется только при показе; расчёт идёт с полной точностью. Остаток бюджета не даёт бонуса. Вес слабейшего района не позволяет улучшать лишь благополучные районы.

Эталоны движка:

СценарийScore
Исходное состояние без мер52.558
Пример из интерфейса56.543
Лучший набор: M2, M3 Нура, M8 Нура, M9 Нура, M14 (стоимость 98)57.237

Набор без мер — только справочная база. Пользовательский сценарий без пяти мер невалиден и Score не получает. Две меры одного направления разрешены, если направлений не меньше трёх в результате соблюдения лимита.

Пример качественного ответа агента для примерного сценария: он объясняет, что сценарий улучшает выбранные направления, отдельно отмечает эффект синергии M10+M12, указывает на сохраняющиеся слабые места районов и предлагает допустимую замену, если она повышает Score. Числа, Score, стоимость, разрыв с лучшим набором и конкретные замены берутся из результатов детерминированного инструмента; модель не вычисляет их самостоятельно. При отсутствии ключа тот же анализ возвращается проверяемым fallback-шаблоном.

Архитектура

flowchart LR
  JSON["data/*.json"] --> ENGINE["Детерминированный engine"]
  ENGINE --> API["FastAPI"]
  API --> UI["React UI"]
  API --> TOOL["Серверный инструмент"]
  TOOL --> LLM["OpenAI Responses API"]
  TOOL --> FALLBACK["Fallback без ключа"]
  LLM --> API
  FALLBACK --> API

backend/engine отвечает за валидацию, эффекты, Score, вклад мер, сравнение, полный перебор и замены. backend/api отдаёт данные и результаты. backend/ai передаёт AI проверенные факты через функцию get_scenario_analysis; промпт хранится в backend/ai/prompts/system.md. Числа и допустимую замену сервер всегда формирует из движка. Модель добавляет только качественный комментарий без чисел и предложений заменить меру; нарушение этого ограничения переводит ответ на fallback. frontend показывает данные API и не вычисляет Score.

Маршруты API

МетодМаршрутНазначение
GET/api/dataИсходные данные, правила и базовый результат
POST/api/validateВсе ошибки выбора
POST/api/simulateПолный детерминированный расчёт
POST/api/explainАнализ, вклады, сравнение с лучшим и замены
GET/api/best?top_n=5Лучшие наборы полного перебора
GET/api/healthПроверка доступности

Технологии и тесты

Backend: Python 3.11, FastAPI, Pydantic v2, OpenAI SDK, pytest, Ruff. Frontend: React, TypeScript strict, Vite, Recharts. Развёртывание: Docker Compose, Nginx.

python -m pytest backend -q
python -m ruff check backend
cd frontend && pnpm run build

Тесты проверяют каждый класс ошибки валидатора, эталоны Score, синергию, независимость от порядка, число допустимых наборов, худший набор ниже базы, API 422, fallback и вызов инструмента AI. Первый полный перебор на машине разработки занимает десятки секунд; результат кешируется в памяти процесса API.

Данные, интеграции и ограничения

Данные — подготовленный синтетический набор из задания, без персональных сведений. Единственный внешний сервис — OpenAI API, и он необязателен. Формула и эффекты заданы для демонстрации компромиссов и не являются прогнозом реального развития Астаны.

В текущей версии нет учётных записей, сохранения результатов команд, неожиданных событий и автоматической презентации. Кеш оптимизатора сбрасывается при перезапуске backend. Развёрнутой публичной версии пока нет.

Никогда не коммитьте настоящий API-ключ в Git. Заполните OPENAI_API_KEY только в локальном .env; файл .env добавлен в .gitignore. Если ключ уже попал в репозиторий или был опубликован, его нужно отозвать и выпустить новый в панели OpenAI.

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

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

artray1 6HanoiClown 3rifestudio 2Batyrkhan 1

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

  • 2026-09-23 17:48Update README.mdrifestudio
  • 2026-09-23 17:46Update .env.examplerifestudio
  • 2026-09-23 17:34ai: load configuration from env fileartray1
  • 2026-09-23 17:32Merge remote-tracking branch 'origin/frontend' # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starartray1
  • 2026-09-23 17:26frontend: smooth interface animationsartray1
  • 2026-09-23 15:21Merge remote-tracking branch 'origin/frontend' :wq#artray1
  • 2026-09-23 15:18Add example OpenAI API keyHanoiClown
  • 2026-09-23 15:18frontend updatesartray1
Все коммиты (12)
  • 2026-09-23 15:12feat: add backend scenario comparison endpointBatyrkhan
  • 2026-09-23 14:24feat: add Akim city simulatorHanoiClown
  • 2026-09-23 14:21feat: add deterministic city simulator, API, AI explanation and UIHanoiClown
  • 2026-09-23 13:36initialartray1
Структура репозитория (96 файлов)
.dockerignore
.env.example
.gitignore
AGENTS.md
README.md
backend/Dockerfile
backend/__init__.py
backend/ai/__init__.py
backend/ai/agent.py
backend/ai/fallback.py
backend/ai/prompts/system.md
backend/ai/tools.py
backend/api/__init__.py
backend/api/main.py
backend/engine/__init__.py
backend/engine/data_loader.py
backend/engine/models.py
backend/engine/optimizer.py
backend/engine/score.py
backend/engine/simulation.py
backend/engine/tests/__init__.py
backend/engine/tests/test_optimizer.py
backend/engine/tests/test_simulation.py
backend/engine/tests/test_validator.py
backend/engine/validator.py
backend/requirements.txt
backend/tests/test_agent.py
backend/tests/test_api.py
data/districts.json
data/measures.json
data/weights.json
docker-compose.yml
docs/superpowers/plans/2026-09-23-city-simulator.md
docs/superpowers/specs/2026-09-23-city-simulator-design.md
frontend/.impeccable/design.json
frontend/DESIGN.md
frontend/Dockerfile
frontend/README.md
frontend/frontend_new/.dockerignore
frontend/frontend_new/.gitignore
frontend/frontend_new/Dockerfile
frontend/frontend_new/README-frontend.md
frontend/frontend_new/index.html
frontend/frontend_new/nginx.conf
frontend/frontend_new/package-lock.json
frontend/frontend_new/package.json
frontend/frontend_new/src/App.tsx
frontend/frontend_new/src/api/client.ts
frontend/frontend_new/src/api/types.ts
frontend/frontend_new/src/components/Catalog.tsx
frontend/frontend_new/src/components/DistrictMap.tsx
frontend/frontend_new/src/components/ExplainDrawer.tsx
frontend/frontend_new/src/components/Hud.tsx
frontend/frontend_new/src/components/Icons.tsx
frontend/frontend_new/src/components/IsoIcon.tsx
frontend/frontend_new/src/components/MeasureCard.tsx
frontend/frontend_new/src/components/SetPanel.tsx
frontend/frontend_new/src/components/TopNav.tsx
frontend/frontend_new/src/hooks/useAnimatedNumber.ts
frontend/frontend_new/src/hooks/useGameData.ts
frontend/frontend_new/src/hooks/useLiveSimulation.ts
frontend/frontend_new/src/lib/constants.ts
frontend/frontend_new/src/lib/directions.ts
frontend/frontend_new/src/lib/format.ts
frontend/frontend_new/src/lib/rules.ts
frontend/frontend_new/src/main.tsx
frontend/frontend_new/src/styles.css
frontend/frontend_new/tsconfig.json
frontend/frontend_new/vite.config.ts
frontend/index.html
frontend/nginx.conf
frontend/package.json
frontend/pnpm-lock.yaml
frontend/pnpm-workspace.yaml
frontend/src/App.tsx
frontend/src/api.ts
frontend/src/components/Catalog.tsx
frontend/src/components/DistrictDropdown.tsx
frontend/src/components/DistrictMap.tsx
frontend/src/components/DistrictPicker.tsx
frontend/src/components/Hud.tsx
frontend/src/components/Icons.tsx
frontend/src/components/IsoIcon.tsx
frontend/src/components/MeasureCard.tsx
frontend/src/components/ScenarioResult.tsx
frontend/src/components/SetPanel.tsx
frontend/src/lib/directions.ts
frontend/src/lib/format.ts
frontend/src/lib/rules.ts
frontend/src/main.tsx
frontend/src/styles.css
frontend/src/types.ts
frontend/tests/motion.browser.mjs
frontend/tests/rules.test.mjs
frontend/tsconfig.json
frontend/vite.config.ts
Ветки (4)

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

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

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