О проекте
Команда: VELTARQ
Кейс HackAlem.ai: Спецтрек — Astana Innovations
Стадия: Доведённый продукт
Задача кейса: «Аким на 5 часов»: городской симулятор, распределяющий бюджет по 5 направлениям и 5 районам
Суть
Городской симулятор «Аким на 5 часов» с картой 2GIS: пользователь выбирает 5 мероприятий в бюджете 100, размещает их по 5 районам и сравнивает показатели качества жизни и Score до и после (горизонт 8 кварталов).
Как устроено
Бэкенд на FastAPI проверяет план: ровно 5 мер, бюджет не больше 100, не больше 2 мер одного направления, несовместимые пары. Затем он считает эффекты с учётом лага (8 − лаг)/8 и синергий, ограничивает показатели диапазоном 0–100 и вычисляет Score = 0.7·средний D с весами населения + 0.3·минимальный D − штраф за значения ниже 40. Поиск улучшений перебирает замену или перенос одной меры. LLM только возвращает ссылки на рассчитанные факты для объяснения, бэкенд их проверяет; без LLM объяснение строится автоматически.
Ключевые решения
- rule-based scenario simulation
- weighted Score formula
- local search (замена или перенос одной меры)
- fact-reference LLM explanation с валидацией
- 2GIS MapGL
Метрики
Контрольный пример из ТЗ: Score 56.54307 против исходного 52.55768 при бюджете 95/100
Особенности
LLM не генерирует текст напрямую: он возвращает ссылки на факты, а текст собирает бэкенд, поэтому числа нельзя подменить. На карте 2GIS видны слой пробок («до») и объекты мер («после»). Эталонный пример из ТЗ воспроизводится через Swagger. Есть QA-план и CI.
Стек
React · TypeScript · Vite · 2GIS MapGL · Python · FastAPI · SQLite · Docker · GitHub Actions · Node.js
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-8b6eaa99-veltarq.
README
README.mdAstana Atlas / VELTARQ
Учебный симулятор управления Астаной «Аким на 5 часов». Пользователь выбирает пять мероприятий в пределах бюджета, распределяет их по районам и сравнивает исходные показатели качества жизни с результатом через восемь кварталов.
Проект подходит для демонстрации городских сценариев и разработки приложений с картой, расчётным API и необязательным ИИ-анализом. В модели пять игровых районов, десять показателей и четырнадцать мероприятий. Это фиксированный учебный набор данных: границы и размещение объектов демонстрационные, результаты не являются прогнозом реального развития города.
Интерфейс построен на React, TypeScript и Vite, карта — на 2GIS MapGL. FastAPI проверяет планы, рассчитывает показатели, сравнивает сценарии и ищет улучшения. LLM помогает упорядочить объяснение уже рассчитанных фактов; числа и Score определяет backend.
Быстрый старт
Что понадобится
- Git и доступ к репозиторию.
- Node.js 24.x и npm — эта версия Node используется в CI и подходит зависимостям из lock-файла.
- Python 3.11+ с поддержкой
venvиpip. - Доступ к реестрам npm и PyPI для установки зависимостей.
- Для карты: браузер с WebGL, сеть и ключ 2ГИС с доступом к MapGL JS API. Первый расчёт можно выполнить без ключа карты и без LLM.
Установка и запуск
Клонируйте проект. Все последующие команды выполняются из корня репозитория, где находится package.json:
git clone https://github.com/BAITC-Hacks/hack-8b6eaa99-veltarq.git
cd hack-8b6eaa99-veltarq
npm ci
npm run setup:backend
setup:backend создаёт backend/.venv и устанавливает Python-зависимости, включая инструменты разработки. Активация окружения вручную не нужна. Если Python не найден, задайте переменную окружения PYTHON с полным путём к исполняемому файлу Python 3.11+ и повторите команду.
Скопируйте .env.example в .env.local, если такого файла ещё нет:
cp .env.example .env.local
В Windows PowerShell используйте Copy-Item .env.example .env.local. Для карты заполните VITE_2GIS_KEY своим ключом. Для первого расчёта оставьте его пустым: появится сообщение о ненастроенной карте, но районы, редактор плана и расчёт будут доступны.
npm run dev
Команда запускает обе части приложения:
- сайт — http://127.0.0.1:5175/;
- API — http://127.0.0.1:8000/docs, интерактивная документация;
- проверка сервера — http://127.0.0.1:8000/health, ответ с
"status": "ok".
Порты 5175 и 8000 должны быть свободны. Ctrl+C завершает запущенные дочерние процессы. После изменения .env.local или backend/.env перезапустите соответствующие серверы.
Первый результат
- Откройте сайт и нажмите «Мой план».
- В пустом плане нажмите «Загрузить пример из ТЗ». Если восстановился прежний план, сначала удалите его мероприятия.
- Нажмите «Рассчитать сценарий».
Пример содержит M7, M8 и M10 в Нуре, M12 для всего города и M5 в Сарыарке. Признак успеха: включён режим «После — сценарий», бюджет равен 95 / 100, верхняя панель показывает Score 56.54307 вместо исходного 52.55768. Карточки районов показывают изменения показателей.
Снова откройте «Мой план»: в разделе «Анализ и улучшение плана» доступны объяснение, «Найти улучшения», сравнение с текущим планом и применение предложенного варианта. Без LLM объяснение помечено «Автоматическое объяснение без ИИ» — это штатный режим.
Работа со своим планом
Выберите район на карте или в нижней панели, затем нажмите «Подобрать меры». Общий список открывается кнопкой «Каталог мероприятий». Каталог показывает стоимость, направление, лаг и серверный предпросмотр изменений при добавлении меры. Для районных мер выберите район; городские действуют на все пять районов.
Для итогового расчёта действуют правила:
- ровно 5 разных мероприятий и бюджет не более 100 условных единиц;
- не более 2 мероприятий одного направления; охватывать все направления не обязательно;
- M1 и M3 несовместимы во всём плане;
- M4 и M7, а также M5 и M13 несовместимы при размещении в одном районе.
Редактор объясняет недопустимые добавления. Неполный план можно редактировать и просматривать, но итоговый Score для него не рассчитывается. Изменение плана отменяет текущие запросы, скрывает прежний результат и возвращает режим «До».
Выбранные меры и районы автоматически сохраняются в localStorage текущего браузера под ключом astana-atlas-plan-v1. После перезагрузки результаты нужно рассчитать заново. Синхронизации между браузерами в интерфейсе нет.
Настройки
Карта 2ГИС
В корневом .env.local задайте:
VITE_2GIS_KEY=ваш_ключ_2гис
В настройках своего ключа разрешите Origin сайта, например http://127.0.0.1:5175; при использовании localhost, другого порта или рабочего домена добавьте соответствующий адрес. При отказе доступа проверьте ключ, доступ к MapGL JS API и ограничения Origin.
Ключ VITE_2GIS_KEY встраивается в браузерную сборку и виден её пользователям. .env.local исключён из Git, но не делает браузерный ключ секретным. Для сборки с картой ключ должен быть задан до npm run build.
В режиме «До» карта показывает дорожный слой пробок, в режиме «После» — объекты выбранных мероприятий. Реальные пробки 2ГИС не участвуют в вычислении учебного показателя T1 или Score. Ошибка загрузки карты не блокирует редактор и серверный расчёт.
Адрес backend и порты
| Настройка | Где задаётся | Назначение |
|---|---|---|
VITE_API_BASE_URL | .env.local / окружение сборки | Префикс API для браузера; по умолчанию /api/v1 |
BACKEND_URL | .env.local | Адрес API для прокси Vite; в примере http://127.0.0.1:8000 |
BACKEND_PORT, BACKEND_HOST | Окружение процесса запуска npm | Порт и адрес прослушивания Python-сервера; по умолчанию 8000 и 127.0.0.1 |
VELTARQ_CORS_ORIGINS | backend/.env / окружение сервера | Разрешённые Origin через запятую при прямых запросах браузера к API |
VELTARQ_DB_PATH | backend/.env / окружение сервера | Путь SQLite; при запуске через npm по умолчанию backend/data/veltarq.db |
Vite проксирует /api и /health на backend. Если порт 8000 занят, задайте, например, BACKEND_PORT=18000 в окружении оболочки и измените BACKEND_URL в .env.local на http://127.0.0.1:18000. Явный BACKEND_URL имеет приоритет для прокси. Одного BACKEND_PORT в .env.local недостаточно для изменения порта Python.
В PowerShell перед запуском выполните $env:BACKEND_PORT = '18000'; в Bash — export BACKEND_PORT=18000. Другой порт сайта можно передать командой npm run dev -- --port 15175; для карты потребуется разрешить новый Origin ключа 2ГИС.
При обращении к отдельно размещённому API задайте полный VITE_API_BASE_URL, заканчивающийся на /api/v1, и разрешите Origin сайта через VELTARQ_CORS_ORIGINS. Переменные VITE_* фиксируются при сборке, поэтому после их изменения опубликованный frontend нужно пересобрать.
Необязательный ИИ-анализ
Скопируйте backend/.env.example в backend/.env и настройте своего провайдера:
VELTARQ_LLM_BASE_URL— базовый адрес API;VELTARQ_LLM_MODEL— доступная вам модель;VELTARQ_LLM_API_KEY— серверный ключ;VELTARQ_LLM_API_MODE—responsesилиchat_completions(последний используется, если переменная не задана);VELTARQ_LLM_TIMEOUT_SECONDS— ожидание ответа от 1 до 30 секунд, по умолчанию 20.
Значения провайдера и модели в файле-примере нужно согласовать с доступом вашего аккаунта. Скрипты dev, dev:backend и start загружают backend/.env. Храните LLM-ключ только на сервере: без префикса VITE_, без добавления в URL или Git.
LLM получает рассчитанный результат и каталог фактов, а возвращает ссылки на факты для разделов объяснения. Backend проверяет эти ссылки и формирует отображаемый текст. При отсутствии настройки, таймауте, ошибке провайдера или некорректном ответе используется автоматическое объяснение без ИИ. Ошибка объяснения не удаляет уже полученный Score.
Для отдельной проверки настроенного провайдера из корня проекта:
npm run test:llm
Команда делает реальные запросы объяснения и сравнения; они могут тарифицироваться провайдером. Успех требует provider=llm в обоих ответах, автоматическое объяснение без ИИ успехом не считается. Без полной настройки сетевой запрос не выполняется.
Как устроен расчёт
Горизонт — 8 кварталов. Эффект меры умножается на (8 − лаг) / 8. Синергии M1 + M2 для T1, M10 + M12 для B1 и M5 + M6 для E2 добавляют по 2 пункта без лага в районе соответствующей районной меры. После применения эффектов значения ограничиваются диапазоном 0–100.
D района = сумма показателей с их весами
Score = 0,7 × средний D с учётом долей населения
+ 0,3 × минимальный D
− число пар «район × показатель» со значением строго ниже 40
Значение ровно 40 штрафа не даёт. Итоговые значения рассчитывает backend; frontend проверяет совместимость каталогов. При недоступном API, несовпадении каталогов или отклонённом плане результат не показывается, а план остаётся доступным для исправления и повторного запроса.
Поиск улучшений перебирает замену или перенос одной меры и оценивает варианты по Score. Глобальный максимум не гарантируется. Геометрия объектов условная: улучшение T1 не означает рассчитанное сокращение времени поездки по конкретной дороге. Ответ содержит итог горизонта, а не поквартальный прогноз.
API для разработчика
После запуска откройте Swagger UI или OpenAPI JSON. При другом порте замените 8000 в адресах.
Чтобы получить тот же контрольный результат без интерфейса, в Swagger UI выберите POST /api/v1/scenarios/simulate → Try it out, вставьте тело запроса и нажмите Execute:
{
"decisions": [
{"measure_id": "M7", "district_id": "nura"},
{"measure_id": "M8", "district_id": "nura"},
{"measure_id": "M10", "district_id": "nura"},
{"measure_id": "M12"},
{"measure_id": "M5", "district_id": "saryarka"}
]
}
Ожидается HTTP 200, budget_used: 95, score.score_before: 52.55768, score.score_after: 56.54307 и score.score_delta: 3.98539.
API также предоставляет каталог, валидацию, предпросмотр, анализ, сравнение, рекомендации, сохранение сценариев и таблицу лидеров. Список маршрутов — в документации backend. Серверное сохранение использует SQLite и доступно через API; редактор сайта сохраняет план отдельно, в браузере. Учётные записи и авторизация в API не реализованы.
Разработка и сборка
Команды из корня репозитория после установки зависимостей:
| Команда | Что делает |
|---|---|
npm run dev:web | Запускает только Vite; backend запускается отдельно |
npm run dev:backend | Запускает только API с перезагрузкой при изменении Python-кода |
npm test | Запускает frontend-тесты Vitest |
npm run test:backend | Запускает backend-тесты pytest |
npm run test:integration | Поднимает изолированный API на свободном порту и проверяет HTTP-контракт, контрольный пример, поиск и сравнение |
npm run validate:data | Проверяет GeoJSON игровых районов и демонстрационных объектов |
npm run build | Проверяет данные и TypeScript, собирает frontend в dist/ |
Обычные тесты не требуют ключей карты и LLM; HTTP-проверка использует автоматическое объяснение без ИИ. Конфигурация CI дополнительно запускает Ruff и проверяет покрытие backend с порогом 90%. Это порог проверки, а не заявление о текущем покрытии.
Для запуска собранного сайта и API одним сервером:
npm run build
npm start
По умолчанию сайт доступен на http://127.0.0.1:8000/. Добавьте этот Origin к разрешённым для ключа карты. npm start требует установленный backend и готовый dist; для изменения адреса прослушивания используйте BACKEND_HOST / BACKEND_PORT.
При отдельном статическом размещении dist/ API нужно запускать отдельно и указывать его через VITE_API_BASE_URL при сборке. npm run preview — локальный просмотр сборки через Vite, backend он не запускает. Docker Compose запускает только API из каталога backend/; инструкции находятся в backend/README.md.
Основные каталоги
| Путь | Назначение |
|---|---|
| src/ | Интерфейс, карта, клиент API и frontend-тесты |
| src/modelData.ts, src/model.ts | Локальный каталог, исходное состояние, валидация и контрольные расчёты |
| src/data/ | Игровые границы и демонстрационные объекты; metrics.json — архивные значения, основной расчёт их не использует |
| backend/app/ | FastAPI, числовой движок, поиск, объяснения и SQLite |
| backend/tests/ | Тесты backend |
| scripts/ | Установка, запуск и проверки проекта |
Дополнительные материалы
- Документация backend — API, формула и отдельный запуск сервиса.
- Происхождение backend и изменения интеграции.
- План QA и шаблон баг-репорта.
- Отчёт о ранее выполненной проверке ИИ-интеграции — исторический результат; доступ к своему провайдеру проверяйте отдельно.
Отдельная лицензия проекта в репозитории не указана. Для включённого шрифта Onest сохранён файл лицензии OFL.
Авторы коммитов
История коммитов
- 2026-09-23 17:52docs: prepare Russian README with verified quick startanuarmaksut711
- 2026-09-23 17:43Integrate Astana Atlas frontend and verified LLM scenario analysisanuarmaksut711
- 2026-09-23 17:26docs: add backend developer introductionAbs01ute000
- 2026-09-23 17:15docs: add QA testing resultsShiroHatS
- 2026-09-23 16:30Merge pull request #5 from BAITC-Hacks/integrationAbs01ute000
- 2026-09-23 16:20Add hover drawers, pinned district selection and hamburger menuanuarmaksut711
- 2026-09-23 16:17Merge pull request #4 from BAITC-Hacks/codex/ilhanAbs01ute000
- 2026-09-23 16:17docs: expand QA READMEShiroHatS
Все коммиты (21)
- 2026-09-23 16:17Merge pull request #3 from BAITC-Hacks/codex/aidarAbs01ute000
- 2026-09-23 16:15Merge pull request #1 from BAITC-Hacks/codex/aidarAbs01ute000
- 2026-09-23 16:06Add Astana map frontend, shared datasets and reviewed design prototypesanuarmaksut711
- 2026-09-23 15:52Реализовать backend симулятора: API, расчёты, AI-интеграция и тестыAbs01ute000
- 2026-09-23 15:14Add QA test planShiroHatS
- 2026-09-23 15:12Обновил файлы проектаAbs01ute000
- 2026-09-23 15:10Add files via uploadDeep
- 2026-09-23 14:16Merge branch 'main' into codex/aidarShiroHatS
- 2026-09-23 14:13Merge pull request #2 from BAITC-Hacks/codex/ilhanAbs01ute000
- 2026-09-23 14:13Merge branch 'main' into codex/ilhanAbs01ute000
- 2026-09-23 14:08docs: add QA participant READMEShiroHatS
- 2026-09-23 14:02Modify README to include team member informationDeep
- 2026-09-23 14:00Update README with backend developer responsibilitiesAbs01ute000
Структура репозитория (118 файлов)
.env.example .frontend-workbench/sessions/astana-planning-map/product-design/preview/index.html .frontend-workbench/sessions/astana-planning-map/product-design/preview/main.js .frontend-workbench/sessions/astana-planning-map/product-design/preview/style.css .frontend-workbench/sessions/astana-planning-map/product-design/visual-direction.json .frontend-workbench/sessions/astana-playfield-redesign/artifacts/after-selected-desktop.zip .frontend-workbench/sessions/astana-playfield-redesign/authority/intent.json .frontend-workbench/sessions/astana-playfield-redesign/coverage.json .frontend-workbench/sessions/astana-playfield-redesign/product-design/DESIGN-NOTES.md .frontend-workbench/sessions/astana-playfield-redesign/product-design/after-selected-desktop.critique.json .frontend-workbench/sessions/astana-playfield-redesign/product-design/preview/fonts/OFL.txt .frontend-workbench/sessions/astana-playfield-redesign/product-design/preview/fonts/Onest.ttf .frontend-workbench/sessions/astana-playfield-redesign/product-design/preview/index.html .frontend-workbench/sessions/astana-playfield-redesign/product-design/preview/main.js .frontend-workbench/sessions/astana-playfield-redesign/product-design/preview/style.css .frontend-workbench/sessions/astana-playfield-redesign/product-design/visual-direction.json .frontend-workbench/sessions/astana-playfield-redesign/state.json .frontend-workbench/sessions/astana-playfield-redesign/structure.json .github/workflows/ci.yml .gitignore .openai/hosting.json AI_VERIFICATION.md README.md backend/.dockerignore backend/.env.example backend/.github/workflows/ci.yml backend/.gitignore backend/Dockerfile backend/QA_TEST_PLAN.md backend/README.md backend/UPSTREAM.md backend/app/__init__.py backend/app/api/__init__.py backend/app/api/preview_routes.py backend/app/api/routes.py backend/app/catalog.py backend/app/domain.py backend/app/main.py backend/app/schemas.py backend/app/services/__init__.py backend/app/services/analysis.py backend/app/services/llm.py backend/app/services/preview.py backend/app/services/recommendation.py backend/app/services/repository.py backend/app/services/simulation.py backend/app/services/validation.py backend/compose.yaml backend/pyproject.toml backend/scripts/check_llm.py backend/tests/test_ai_flow.py backend/tests/test_api.py backend/tests/test_catalog.py backend/tests/test_hosting.py backend/tests/test_llm.py backend/tests/test_preview.py backend/tests/test_repository.py backend/tests/test_simulation.py backend/tests/test_validation.py backend/visual-direction.json exact-reference/index.html index.html package-lock.json package.json public/favicon.svg public/fonts/OFL.txt public/fonts/Onest.ttf public/planet.json public/style.json scripts/backend.mjs scripts/check-integration.mjs scripts/dev.mjs scripts/generate-decisions.mjs scripts/stop-process-tree.py scripts/validate-data.mjs src/App.ai.test.tsx src/App.test.tsx src/App.tsx src/CityMap.test.tsx src/CityMap.tsx src/DistrictSilhouette.tsx src/MeasureCatalog.test.tsx src/MeasureCatalog.tsx src/ScenarioInsights.test.tsx src/ScenarioInsights.tsx src/UiIcon.tsx src/api.integration.test.ts src/api.test.ts src/api.ts src/data.ts src/data/decisions.geojson src/data/districts.geojson src/data/metrics.json src/data/projects.geojson src/exact-reference/main.js src/exact-reference/style.css src/format.ts src/main.tsx src/mapOverlays.ts src/model.test.ts src/model.ts src/modelData.ts src/previewApi.test.ts src/previewApi.ts src/scenarioGeometry.test.ts src/scenarioGeometry.ts src/scenarioTestFixtures.ts src/style.css src/test-fixtures/audit_results.json src/useSavedPlan.test.tsx src/useSavedPlan.ts src/useScenario.test.tsx src/useScenario.ts src/useTraffic.test.tsx src/useTraffic.ts src/vite-env.d.ts tsconfig.json vite.config.ts
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.