О проекте
Команда: 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
Как проверить основной сценарий
- Откройте интерфейс и нажмите «Загрузить пример сценария». Он выбирает M7, M8 и M10 в Нуре, M12 для всего города и M5 в Сарыарке.
- Убедитесь, что бюджет показывает 95 / 100, а кнопка расчёта доступна.
- Нажмите «Рассчитать сценарий». Ожидаемый Score — 56,54 после округления. В таблице видны изменения районов и срабатывание синергии M10+M12.
- Измените одну меру или район и рассчитайте снова. Score и объяснение должны измениться. Попытка превысить бюджет блокируется; несовместимые меры показывают ошибку.
- Для 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.
Авторы коммитов
История коммитов
- 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
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.