AAYMA
RU
Войти

QalaQadam

Симулятор бюджета Астаны: пользователь выбирает 5 мер из каталога из 14, получает Score, изменения по районам, heatmap, траекторию по кварталам и…

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

О проекте

Команда: Melnik
Кейс HackAlem.ai: Спецтрек — Astana Innovations
Стадия: Доведённый продукт

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

Суть

Симулятор бюджета Астаны: пользователь выбирает 5 мер из каталога из 14, получает Score, изменения по районам, heatmap, траекторию по кварталам и AI-объяснение. Добавлены советник акима и голосование жителей за предложения.

Как устроено

simulator.py детерминированно проверяет бюджет 100, направления и несовместимости и считает эффекты с лагами, синергиями и клиппингом, итог — Astana QoL Score. optimizer.py перебирает все допустимые наборы и размещения. advisor.py даёт LLM инструменты валидации, симуляции и поиска через tool calling, после чего все кандидаты пересчитываются кодом. explainer.py передаёт проверенный JSON в OpenAI (gpt-4o-mini, Structured Outputs), при сбое срабатывает fallback. resident.py сопоставляет жалобу жителя с мерой каталога, proposals/digest собирают голоса и народный сценарий.

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

  • exhaustive optimizer
  • tool calling
  • Structured Outputs
  • quarterly trajectory Q1–Q8
  • heatmap 5×10
  • radar chart
  • deterministic fallback

Особенности

Цепочка «жалоба жителя → мера каталога → проверенный эффект → голосование → допустимый народный сценарий» с разбором аналогов (Balancing Act, Decidim, vTaiwan). Публичный /api/search и /llms.txt для внешних ИИ-агентов. Главный принцип: «LLM не считает числа».

Стек

Python · FastAPI · Pydantic v2 · vanilla JS · Chart.js · Docker · pytest · Ruff

Модели

OpenAI gpt-4o-mini


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

README

README.md

QalaQadam — «Аким на 5 часов»

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

От жалобы к решению. Житель приходит с проблемой и уходит с предложением, сопоставленным с мерой каталога и проверенным расчётом.

Deployed-версия: нет, локальный запуск.

Для кого и какую проблему решает

QalaQadam предназначен для жителей, городских аналитиков и управленцев. При ограниченном бюджете трудно заранее сравнить инициативы по нескольким направлениям и понять, где появятся эффекты и какие районы останутся позади. В прототипе все команды получают одинаковый синтетический набор данных и бюджет; человек выбирает приоритеты, код проверяет ограничения и считает последствия, а ИИ помогает интерпретировать результат.

Аналоги и чем мы отличаемся

СемействоПримерЧто даётЧего не хватает
Бюджетные симуляторыBalancing Act (Polco), с 2015 г.Житель сам сводит бюджет и видит компромиссы.Нет модели последствий по районам; результат совещательный.
Бюджет народного участияБарселона, Decidim 2020–2023: €30 млн, 1 982 предложения → 823 прошли техпроверку → 76 реализованы; Париж: 104 проекта-победителя в 2025 г.; Астана — онлайн-голосование БНУ.Реальные деньги, решения исполняются.Проверка ручная; жители голосуют за описание, эффект для района в цифрах не показан.
ИИ для обсужденияvTaiwan / Pol.is: около 200 тыс. участников и 26 законов в 2015–2018 гг.; Talk to the City группирует мнения для власти с помощью LLM.ИИ понимает и группирует свободный текст.Не связано с бюджетом и расчётом; решения необязательные.
Цифровые двойникиVirtual Singapore, MIT CityScope.Глубокое моделирование города.Экспертный инструмент, нужны большие данные или оборудование.

QalaQadam соединяет три звена в одном потоке: житель пишет обычным языком → ИИ сопоставляет текст с мерой из каталога → код показывает эффект для района → голосование → из поддержанных предложений код собирает допустимый «Народный сценарий». Это инструмент для существующего «Бюджета народного участия» и акимата: проверка и расчёт эффекта до голосования, а не замена решений людей. Мы не обещаем, что «ИИ решит за город».

Ключевой принцип: LLM не считает числа. Формула, ограничения, лаги, синергии и штрафы реализованы детерминированно в Python. OpenAI получает только уже проверенный результат и объясняет его.

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

  • каталог из 14 мероприятий по пяти направлениям;
  • стартовый экран с матрицей состояния города и автоматически найденной точкой роста;
  • выбор ровно пяти решений и района для районных мер;
  • живая клиентская и обязательная серверная валидация;
  • контроль бюджета 100 единиц, повторов, направлений и несовместимостей;
  • лаг-скорректированные эффекты, три синергии и единый клиппинг 0–100;
  • Astana Quality of Life Score с городской средней, минимальным районом и штрафом за показатели ниже 40;
  • разбивка «до → после» по пяти районам и десяти показателям;
  • график районных оценок и подсветка критических значений;
  • heatmap 5 × 10 в режимах «до», «после» и «изменение»;
  • radar-профиль выбранного района по всем десяти показателям;
  • единая айдентика Астаны: navy/sky/gold для бренда и отдельная шкала данных;
  • структурированное объяснение через OpenAI;
  • «Советник акима»: перевод естественно-языковых пожеланий в формальные ограничения;
  • точный полный поиск лучших допустимых сценариев по Score или стоимости;
  • три AI-инструмента: валидация, симуляция и поиск сценариев;
  • поквартальная траектория Score и районного разрыва на горизонте 8 кварталов;
  • дорожная карта пяти выбранных мер по кварталам Q1–Q8 с задержкой эффекта, долей реализации к концу горизонта и стоимостью из каталога;
  • сравнение выбранной политики, математического максимума и альтернативы с ограничениями;
  • «Голос жителя»: перевод предложения в существующую меру, проверенный план и черновик обращения;
  • «Голосование жителей»: пустая доска, проверка каталогом, голоса и сборка допустимого народного сценария;
  • статусы предложений «Подано», «Проверено кодом», «В народном сценарии», «Не вошло» с причиной исключения;
  • «Сводка для акимата»: группировка предложений по темам, проверенные счётчики голосов и выгрузка .txt;
  • квартальный прогноз Q1/Q2/Q4/Q8 и переключаемая матрица показателей;
  • открытый детерминированный поиск /api/search и памятка /llms.txt для внешних ИИ;
  • детерминированное резервное объяснение, если API-ключ отсутствует или OpenAI недоступен;
  • автоматические тесты, включая эталонные числа организаторов.

Пользовательский сценарий

  1. Пользователь выбирает пять карточек мероприятий.
  2. Для районных мероприятий указывает район.
  3. Интерфейс сразу показывает использованный бюджет и нарушения правил.
  4. Сервер независимо валидирует набор и рассчитывает все значения.
  5. Экран результата показывает Score, изменения районов, heatmap показателей, radar выбранного района, эффекты, синергии и траекторию на 8 кварталов.
  6. Панель сравнивает сценарий с максимумом формулы и альтернативой «без ЛРТ, с экологией».
  7. LLM объясняет сильные стороны, риски и компромиссы, не меняя рассчитанные числа.

Архитектура

backend/app/simulator.py — источник итогового расчёта сценария. Он не обращается к сети или БД: принимает пять решений, проверяет правила и возвращает полную разбивку. Исходные синтетические данные из брифа находятся в backend/app/data.py.

backend/app/main.py публикует каталог и API симуляции, объяснения, траектории и советника. Перед объяснением сервер не доверяет числам из браузера и повторно запускает симуляцию по selections. backend/app/explainer.py передаёт модели компактный проверенный JSON через Structured Outputs; при сбое внешнего API работает честный deterministic fallback.

backend/app/optimizer.py исчерпывающе перебирает допустимые наборы и размещения районных мер. До раскрытия районов он отбрасывает наборы, нарушающие бюджет, направления, обязательные/исключённые меры и глобальные несовместимости. backend/app/advisor.py даёт LLM инструменты поверх валидатора, симулятора и оптимизатора. Все кандидаты после ответа модели повторно пересчитываются кодом, а числовая часть рекомендации строится только из проверенного результата.

backend/app/trajectory.py переиспользует ту же формулу на шагах Q1–Q8. Финальная точка строго совпадает с /api/simulate; районный разрыв max(D_d) − min(D_d) показан как «Индекс городской справедливости», где меньше — лучше.

backend/app/resident.py сопоставляет текст жителя только с одной из существующих мер каталога. Оптимизатор получает обязательную пару «мера + район», а найденный план повторно проходит симуляцию. Стоимость, эффекты, районные оценки и Score в черновике обращения подставляются кодом; при отсутствии подходящей меры сервис честно возвращает ближайшие варианты без числовых обещаний.

backend/app/public_api.py валидирует ограничения открытого поиска и независимо повторно симулирует каждый возвращаемый кандидат. /llms.txt объясняет внешним агентам главный контракт: не вычислять Score самостоятельно, а получать числа через API.

backend/app/proposals.py хранит предложения и голоса в var/proposals.json с атомарной записью. Клиентские числа не принимаются как доказательство: сценарий формируется оптимизатором и проверяется симулятором. Голосование — демонстрационное и не подтверждает личность участника.

backend/app/digest.py группирует тексты предложений через OpenAI Structured Outputs. Модель получает только идентификаторы и тексты; сервер проверяет принадлежность каждого предложения группе и сам считает голоса, районы, меры и участие в народном сценарии. Без ключа работает группировка по направлениям каталога.

Frontend — один HTML, vanilla JavaScript и CSS без сборщика. theme.js читает единые CSS-токены и передаёт их графикам. Chart.js 4.4.7 хранится локально в frontend/vendor, поэтому графики не зависят от доступа к CDN. Google Fonts используются как улучшение; системные fallback-шрифты сохраняют интерфейс работоспособным без сети.

frontend/roadmap.js строит дорожную карту непосредственно из пяти selections результата /api/simulate и каталога /api/catalog. Она не меняет расчёт: Q1 — старт, первый квартал эффекта следует за лагом, а доля реализации к Q8 равна (8 − lag) / 8.

frontend → POST /api/simulate → validator + simulator
                                  ↓
                проверенный SimulationResult
                                  ↓
frontend → POST /api/explain → повторный simulate → OpenAI/fallback
frontend → POST /api/trajectory → поквартальный детерминированный расчёт
frontend → POST /api/advise → LLM-ограничения → optimizer → повторный simulate
frontend → POST /api/proposals → проверка меры → голоса → допустимый народный сценарий
frontend → POST /api/proposals/digest → темы → проверенные агрегаты

Технологии

  • Python 3.10+, FastAPI, Pydantic v2;
  • OpenAI Structured Outputs и tool calling; модель задаётся через OPENAI_MODEL (по умолчанию gpt-4o-mini);
  • Chart.js 4.4.7 хранится локально;
  • pytest и Ruff;
  • Docker и Docker Compose.

Потенциал развития

  • заменить синтетические данные брифа официальной городской статистикой после проверки качества и лицензий;
  • интеграция с «Бюджетом народного участия» и eOtinish;
  • подтверждение личности жителей через eGov вместо демонстрационного localStorage-токена.

Внедрение

Это оценка команды, не часть расчёта симулятора и не подтверждённый план государственных организаций.

Целевая аудитория: управления экономики и бюджетного планирования акимата, жители — участники «Бюджета народного участия», депутаты маслихата и общественные советы. Astana Innovations рассматривается как возможная площадка пилота при согласовании с организацией.

Для пилота потребуются продакт со стороны акимата, два backend-разработчика, один frontend-разработчик, аналитик или городской экономист для калибровки эффектов на реальной статистике, дизайнер на неполной занятости, а также специалисты по интеграции с eGov/eOtinish и безопасности.

Последовательность внедрения: калибровать данные и эффекты одного района → провести пилот вместе с «Бюджетом народного участия» в одном районе → после оценки результатов расширить на город. До работы с реальными обращениями необходимо отдельно решить вопросы доступа, хранения и проверки данных.

Структура проекта

backend/
  app/
    data.py          # районы, показатели, меры и правила из брифа
    schemas.py       # Pydantic v2 модели
    simulator.py     # детерминированный расчёт
    trajectory.py    # траектория Score и районного разрыва Q1–Q8
    optimizer.py     # точный поиск сценариев с ограничениями
    advisor.py       # tool-calling «Советник акима»
    resident.py      # предложение жителя → мера → проверенный план
    proposals.py     # файловая доска предложений, голосование и народный сценарий
    digest.py        # тематическая сводка предложений
    public_api.py    # валидация открытого детерминированного поиска
    explainer.py     # Structured Outputs и резервное объяснение
    main.py          # FastAPI API и раздача frontend
  tests/             # эталонные и API-тесты
frontend/
  index.html
  app.js
  theme.js
  advisor.js
  resident.js
  votes.js
  digest.js
  llms.txt           # краткий контракт для внешних ИИ
  trajectory.js
  roadmap.js
  matrix.js
  radar.js
  style.css
  advisor.css
  resident.css
  votes.css
  digest.css
  trajectory.css
  roadmap.css
  insights.css
  vendor/
    chart.umd.min.js # локальный Chart.js 4.4.7
Dockerfile
docker-compose.yml

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

Требуется Python 3.10+. Команды выполняются из корня репозитория.

python -m venv .venv
source .venv/bin/activate       # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env            # Windows: copy .env.example .env
uvicorn backend.app.main:app --reload

Откройте http://localhost:8000. Документация API: http://localhost:8000/docs.

Frontend и его локальные статические ресурсы раздаются FastAPI по /static; открывайте приложение через адрес backend, а не как file://.

Docker

cp .env.example .env
docker compose up --build

OpenAI необязателен для расчёта. Без действующего OPENAI_API_KEY приложение покажет резервное объяснение и явно отметит его источник.

Проверка корректности

pip install -r requirements-dev.txt
pytest -q
ruff check backend/app backend/tests

Проверяются контрольные значения из брифа:

  • базовая городская средняя: 56.86;
  • минимальный район: Нура, 49.18;
  • два критических показателя: S1=38, S2=35;
  • базовый Score: 52.56;
  • официальный сценарий стоимостью 95: 56.54307;
  • синергия M10 + M12 применяется в Нуре.

Для демонстрации нажмите «Загрузить пример из брифа», затем «Рассчитать сценарий».

Для проверки «Голоса жителя» откройте результат сценария и введите «В Нуре не хватает поликлиник». Сервис должен сопоставить текст с M8 в Нуре и показать проверенный план. Запрос без меры каталога должен завершиться сообщением об отсутствии соответствия, без рассчитанных обещаний.

Скриншоты проверочного прогона: прогноз «3 месяца», доска после сборки народного сценария. Для доски использованы четыре тестовых предложения: три с голосами, одно без. После съёмки демо-доска очищена.

API

POST /api/simulate

{
  "selections": [
    {"measure_id":"M7","district_id":"nura"},
    {"measure_id":"M8","district_id":"nura"},
    {"measure_id":"M10","district_id":"nura"},
    {"measure_id":"M12","district_id":null},
    {"measure_id":"M5","district_id":"saryarka"}
  ]
}

Невалидный сценарий возвращает HTTP 200 и { "valid": false, "reason": "..." }, чтобы интерфейс мог показать точную причину пользователю.

POST /api/explain

Принимает результат /api/simulate. Сервер извлекает только selections, заново рассчитывает сценарий и передаёт модели проверенную компактную версию результата.

POST /api/advise

{
  "current_scenario": [
    {"measure_id":"M7","district_id":"nura"},
    {"measure_id":"M8","district_id":"nura"},
    {"measure_id":"M10","district_id":"nura"},
    {"measure_id":"M12","district_id":null},
    {"measure_id":"M5","district_id":"saryarka"}
  ],
  "message": "без ЛРТ и с экологией"
}

LLM интерпретирует запрос как ограничения, а код выполняет точный поиск и повторную проверку top-3. Без API-ключа поддерживается прозрачный fallback для типовых ограничений (без M3/ЛРТ, обязательное направление, минимальная стоимость).

GET /api/trajectory и POST /api/trajectory

GET возвращает восемь кварталов базового сценария без мер. POST принимает тот же объект selections, что /api/simulate, и возвращает для каждого квартала score, D_avg, минимальную и максимальную районную оценку и balance.

POST /api/resident/propose

{
  "district_id": null,
  "message": "В Нуре не хватает поликлиник"
}

Сервис выбирает только существующую меру каталога, ищет лучший допустимый план с её точным размещением и возвращает доказательства из повторной симуляции. Явно выбранный район имеет приоритет; при значении «Весь город» район может извлекаться из текста. Если выбранный район отличается от упомянутого в тексте, ответ содержит district_hint, а кнопка под результатом повторяет расчёт для района из текста. Без OPENAI_API_KEY работает детерминированное сопоставление по словам и основам. Если мера не найдена, сервис возвращает ближайшие варианты без расчётных чисел; если районная мера найдена, но район не определён, предлагает выбрать район. Черновик обращения создаётся локально и никуда не отправляется.

POST /api/search

{
  "constraints": {
    "exclude": ["M3"],
    "require_directions": ["Экология"],
    "require_placements": [{"measure_id": "M8", "district_id": "nura"}]
  },
  "objective": "max_score",
  "limit": 3
}

Поиск не вызывает LLM. Поддерживаются цели max_score и min_cost; каждый кандидат повторно проходит /api/simulate-эквивалент на сервере. Невалидные идентификаторы, районы, цели и лимиты возвращают HTTP 400 с причиной.

GET/POST/DELETE /api/proposals, POST /api/proposals/{id}/vote

Доска предложений изначально пустая. POST принимает measure_id, district_id и текст; значения эффекта, цены и голоса от клиента игнорируются. Статусы показывают, проверено ли предложение кодом и вошло ли оно в народный сценарий. Голосование учебное: защита от повторного голоса основана на браузерном токене, это не авторизация. POST /api/proposals/people-scenario собирает допустимый план только из предложений хотя бы с одним голосом и возвращает причины исключения с названиями мер; предложения без голосов сохраняют статус. DELETE очищает демо-доску.

POST /api/proposals/digest

Формирует «Сводку для акимата»: темы и краткие текстовые резюме, количество предложений и голосов, районы, меры и число включённых в народный сценарий. При доступном OpenAI модель группирует только тексты и идентификаторы, а все числовые поля вычисляет сервер; без ключа предложения группируются по направлениям. Интерфейс позволяет скачать сводку в .txt.

GET /llms.txt

Краткая plain-text документация для ИИ-агентов со списком эндпоинтов, минимальными примерами и запретом самостоятельно вычислять Score. Полная интерактивная схема доступна на /docs.

Данные и формула

Все районы, веса и мероприятия взяты из синтетического информационного пакета организаторов. Исходный датасет не содержит персональных данных, реляционная БД не используется. Тексты предложений и демонстрационные идентификаторы голосующих хранятся локально в var/proposals.json (путь можно задать через QQ_PROPOSALS_PATH); свободный текст не следует заполнять персональными данными. Внешние сервисы: OpenAI API для текстового объяснения, советника, сопоставления неочевидных запросов жителя и тематической сводки; Google Fonts — только для шрифтов. Chart.js поставляется с репозиторием.

effect_realized = effect × (8 − lag) / 8
effect_realized(t) = 0, если t ≤ lag; иначе effect × (t − lag) / 8
D_d = Σ weight_k × indicator_dk
D_avg = Σ population_share_d × D_d
Score = 0.7 × D_avg + 0.3 × min(D_d) − N_crit

N_crit — число пар «район × показатель» со значением строго меньше 40 после применения всех эффектов.

Знаменатель 8 в поквартальной формуле выбран намеренно: при t=8 траектория воспроизводит официальную лаг-реализованную долю (8 − lag) / 8 и в точности совпадает с основным симулятором. Деление на (8 − lag) означало бы полную реализацию каждой меры к Q8 и противоречило бы эталонному результату 56.54307.

Известные ограничения

  • эффекты мероприятий синтетические и не являются прогнозом реальной городской политики;
  • географическая карта районов не добавлена: в брифе нет официальной геометрии, а выдумывать её нельзя;
  • полный поиск перебирает много комбинаций и может занимать несколько секунд в зависимости от CPU;
  • Score оптимизирует заданную формулу и не заменяет общественное обсуждение или экспертную оценку;
  • качество LLM-текста зависит от доступности OpenAI, но расчёт от неё не зависит;
  • сопоставление свободного текста с каталогом может ошибаться; перед публикацией житель должен проверить найденную меру и район;
  • доска голосования хранится в локальном JSON-файле; браузерный токен легко заменить, подтверждения личности и защиты публичного DELETE /api/proposals нет, поэтому сервис не подходит для реального голосования без доработки доступа и хранения;
  • при нескольких процессах сервера локальный файловый lock не обеспечивает межпроцессную синхронизацию записей.
Звёзды
⭐ 0
Форки
0
Язык
Python
Последний push
в прошлом месяце
Коммиты
24
Авторы
2
Файлы
55
Ветки
1

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

Codex 17MelNikVl 7

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

  • 2026-09-23 17:42Add quarterly implementation roadmapCodex
  • 2026-09-23 17:37Polish resident voting and forecast demoCodex
  • 2026-09-23 17:18Fix resident proposal matching and district guidanceCodex
  • 2026-09-23 17:08Document positioning and reproducible demoCodex
  • 2026-09-23 17:08Add trajectory and resident-facing UICodex
  • 2026-09-23 17:08Add city scenario insights and resident participationCodex
  • 2026-09-23 16:24Polish city evidence presentationCodex
  • 2026-09-23 16:22Expose verified search API for AI clientsCodex
Все коммиты (24)
  • 2026-09-23 16:19Add verified resident proposal flowCodex
  • 2026-09-23 16:12Add mayor advisor scenario applicationCodex
  • 2026-09-23 15:58Redesign QalaQadam with Astana identityCodex
  • 2026-09-23 15:35Add district indicator visualizationsCodex
  • 2026-09-23 15:07Add quarterly policy trajectoryCodex
  • 2026-09-23 14:46Add constraint-aware AI policy advisorCodex
  • 2026-09-23 14:23Build Astana city policy simulatorCodex
  • 2026-09-23 14:01Start HackAlem hackathonCodex
  • 2026-09-23 11:05Add test fileCodex
  • 2026-09-23 10:22Disable CI/CD (blocked by org billing, not needed)MelNikVl
  • 2026-09-23 10:21Add .dockerignore to speed up buildsMelNikVl
  • 2026-09-23 10:18Lazy-init OpenAI client, load .env locallyMelNikVl
  • 2026-09-23 10:17Fix lint issuesMelNikVl
  • 2026-09-22 13:57Add organizer's README generation prompt for end of hackathonMelNikVl
  • 2026-09-22 13:33Add minimal CD: build and push image to GHCR on mainMelNikVl
  • 2026-09-22 13:29Base scaffold: FastAPI + SQLite + OpenAI wrapper + CIMelNikVl
Структура репозитория (55 файлов)
.dockerignore
.env.example
.gitignore
Dockerfile
README.md
backend/__init__.py
backend/app/__init__.py
backend/app/advisor.py
backend/app/data.py
backend/app/digest.py
backend/app/explainer.py
backend/app/main.py
backend/app/optimizer.py
backend/app/proposals.py
backend/app/public_api.py
backend/app/resident.py
backend/app/schemas.py
backend/app/simulator.py
backend/app/trajectory.py
backend/tests/__init__.py
backend/tests/test_advisor.py
backend/tests/test_api.py
backend/tests/test_digest.py
backend/tests/test_optimizer.py
backend/tests/test_proposals.py
backend/tests/test_public_api.py
backend/tests/test_resident.py
backend/tests/test_simulator.py
backend/tests/test_trajectory.py
docker-compose.yml
docs/screenshots/q1-forecast.png
docs/screenshots/votes-after-build.png
frontend/advisor.css
frontend/advisor.js
frontend/app.js
frontend/digest.css
frontend/digest.js
frontend/index.html
frontend/insights.css
frontend/llms.txt
frontend/matrix.js
frontend/radar.js
frontend/resident.css
frontend/resident.js
frontend/roadmap.css
frontend/roadmap.js
frontend/style.css
frontend/theme.js
frontend/trajectory.css
frontend/trajectory.js
frontend/votes.css
frontend/votes.js
pytest.ini
requirements-dev.txt
requirements.txt

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

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

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