AAYMA
RU
Войти

событие.

Сервис подбора до трёх подрядчиков под мероприятие по городу, дате, формату, категории и бюджету.

Рабочий MVP
Команда «The Good The Bad and The Ugly» 7💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: The Good The Bad and The Ugly
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP

Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор

Суть

Сервис подбора до трёх подрядчиков под мероприятие по городу, дате, формату, категории и бюджету. К каждой карточке даётся объяснение с фактами из анкеты, а при пустом результате система называет причину и предлагает альтернативные запросы.

Как устроено

Сначала каталог из 70 профилей проходит детерминированные фильтры: город и категория → занятость → бюджет → формат → длительность → язык, со статистикой отсева. Оставшиеся ранжируются формулой 0.5·cosine (заранее посчитанные векторы multilingual-e5-small ONNX) + ценовой член + языковой член − штраф за синтетику. Объяснение пишет LLM (адаптер GPT-6 Sol с резервным LLM и шаблоном) в замкнутой грамматике; валидатор отклоняет чужие факты, числа и перестановки. Запросы-уточнения к подрядчикам сохраняются в SQLite и могут отправляться по SMTP.

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

  • rule-based filtering
  • multilingual-e5-small embeddings (ONNX)
  • cosine similarity ranking
  • closed-grammar LLM validation
  • alternative query suggestions (изменение одного условия)
  • SHA256 проверка артефактов

Особенности

Эмбеддинги посчитаны заранее для всех 408 комбинаций (категория × формат × язык) и проверяются по хэшу на старте, поэтому GPU и ключи не нужны. LLM-объяснение ограничено замкнутой грамматикой с валидатором. Альтернативные запросы гарантированно увеличивают число кандидатов. README честно отмечает, что живой вызов LLM и сборка Docker не подтверждены.

Стек

Python · FastAPI · Pydantic · SQLite · Docker · GitHub Actions

Модели

multilingual-e5-small · OpenAI GPT-6 Sol


Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-89a1ad55-the-good-the-bad-and-the-ugly.

README

README.md

событие.

Подрядчики для мероприятий — по условиям, с понятным объяснением

HackAlem AI · трек #79-lite · The Good The Bad and The Ugly

Задайте город, дату, формат, категорию и бюджет. Сервис проверит каталог, покажет до трёх подходящих кандидатов и объяснит выбор конкретными фактами из анкет. Если совпадений нет, он покажет причину и предложит проверенное изменение условий.

КаталогРезультатТехнологииЗапуск без ключей
70 профилей · 17 категорий · 3 города1–3 карточки или объяснение пустого результатаPython 3.12+ · FastAPI · локальные E5-векторыШаблонные объяснения · без Node и GPU

Навигация: Быстрый запуск · Что умеет продукт · Как устроен подбор · Демо · Проверка и ограничения

[!IMPORTANT] Подбор и шаблонные объяснения работают локально без секретов. Адаптер OpenAI GPT-6 Sol и SMTP реализованы, но живой вызов модели и получение письма командой пока не подтверждены. Для Docker подготовлены файлы и проверка в CI; успешная сборка контейнера пока не подтверждена.

Разработка ведётся в demoVer-1. HANDOFF.md содержит передачу команде, конфигурацию и порядок проверки; AGENTS.md — контекст для ИИ-участников. Отчёты ранних шагов в docs/ сохранены как история, а текущую реализацию описывает этот README.

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

Windows: один файл

  1. Запустите start-demo.cmd из корня проекта.
  2. Откройте http://127.0.0.1:8000 — скрипт также попробует открыть страницу сам.
  3. Остановите сервер сочетанием Ctrl+C в его окне.

Скрипт найдёт Python 3.12+ и подготовит окружение. Первый запуск может занять больше времени из-за загрузки зависимостей. Если приложение уже работает, скрипт проверит его готовность; чужой процесс на порту он не завершит. Для проверки без открытия браузера: start-demo.cmd --check --no-browser. Подробности запуска.

Ручной запуск на Python 3.12+

Из корня репозитория создайте окружение и установите зависимости. В Windows используйте .venv\Scripts\python.exe вместо .venv/bin/python.

python3 -m venv .venv
.venv/bin/python -m pip install -r requirements-dev.txt
.venv/bin/python -m uvicorn contractor_matching.api:app --app-dir app --host 127.0.0.1 --port 8000

Проверка: .venv/bin/python -m pytest -q (Windows: .venv\Scripts\python.exe -m pytest -q).

Docker Compose

Нужен Docker с Compose ≥2.24. Из корня проекта:

docker compose up --build -d

Откройте http://localhost:8000; проверка готовности — http://localhost:8000/health/ready. Остановка: docker compose down. Кеш хранится в Docker volume: сохраните его, если проверяете повторяемость объяснений. Сборка впервые скачивает зависимости и базовый образ. После неё ключи API и локальные веса модели для запуска не нужны.

Dockerfile и сценарий контейнерной проверки добавлены в CI. Локальный Docker в среде разработки был недоступен; успешная сборка и запуск контейнера пока не подтверждены.

[!TIP] .env необязателен: готовые E5-векторы и проверенные текстовые шаблоны работают без API-ключа, Node, GPU и установленного ONNX. Приложение читает .env в корне проекта; переменные процесса имеют приоритет. Секреты не входят в Git и Docker-образ.

Что умеет продукт

  1. Задайте условия: город, дата, формат, категория, бюджет; язык и длительность необязательны.
  2. Изучите карточки: начальная цена «от», объяснение с конкретным фактом, дата по календарю, источник текста и пометки синтетических или восстановленных полей. Раскройте исходную цитату и ограничения профиля.
  3. Проверьте альтернативы: сравните две даты, откройте статистику исключений и компоненты рейтинга или выберите предложенное изменение одного условия.
  4. Подготовьте обращение: скачайте условия события; если канал команды настроен и профиль не синтетический, отправьте запрос на уточнение.
Исход поискаЧто увидит пользователь
SUCCESS1–3 карточки; при неполной тройке — объяснение причины
NO_CATEGORY_IN_CITYСообщение, что в выбранном городе нет категории
ALL_FILTERED_OUTПричины и числа исключённых профилей

Когда вариантов меньше трёх, ассистент предлагает до трёх проверенных альтернативных запросов. Каждый меняет ровно одно условие, повторно проходит все фильтры и увеличивает число подходящих кандидатов. Город сохраняется; другая категория предлагается только при отсутствии исходной и названа явно. Изменение применяется только после выбора пользователя.

Обращение команде доступно после выбора карточки. Синтетический профиль позволяет только скачать пример; реальная заявка к нему запрещена в UI и API. Если канал команды не настроен, форма не собирает контакты и не отправляет обращение. Для остальных профилей заявка сохраняется в SQLite с защитой от повторов и приватной квитанцией. Это запрос на уточнение, а не бронирование: анонимизированный каталог не содержит подтверждённых контактов исполнителей, и команда должна установить соответствие вручную. Сверка с ТЗ.

Доступность означает лишь отсутствие даты в предоставленном busy_dates; реальность подрядчика, финальная цена и возможность заказа требуют подтверждения.

Как устроен подбор

flowchart LR
  A[Запрос · Pydantic] --> B[Город + категория]
  B -->|пусто| N[NO_CATEGORY_IN_CITY]
  B --> C[Занятость → бюджет → формат → длительность → язык]
  C -->|пусто| F[ALL_FILTERED_OUT + статистика]
  C --> D[Замороженные E5-векторы + формула + tie-break]
  D --> E[Топ-3 · разные подтверждённые факты]
  E --> G[Кеш → GPT-6 Sol → резервный LLM → шаблон]
  G --> H[Проверка текста · сохранение принятого ответа]
  H --> I[Карточки + диагностика]

LLM получает уже выбранные карточки и разрешённые формулировки. Он не меняет состав, порядок или ограничения. Повторяемость решения не зависит от вероятностного текста и доступности сети.

Фильтры и контракты

ЭтапГарантия
ПулТочный город и принадлежность категории; Зарубежье не подмешивается. Профили сортируются по ID.
ОтсевПервая причина учитывается один раз в порядке занятость → бюджет → формат → длительность → язык.
ДлительностьПустой max_hours проходит фильтр; это не обещание работы без ограничений. Площадки проходят те же правила.
ПолнотаФильтр не обрезает список; stats.total() == pool_count - eligible_count проверяется явно.

Запрос принимает даты строго YYYY-MM-DD в диапазоне 2026-09-23–2026-12-31, конечный положительный бюджет и положительную целую длительность. CSV читается через DictReader: строки True/False явно преобразуются в булевы значения, массивы разделяются |, пустая длительность становится None.

Рейтинг и готовые E5-векторы

score = 0.5*cosine + 0.3*(price_from_kzt/budget) + language_term - 0.05*synthetic
language_term = 0.2 при совпадении; 0.1 если язык не задан; 0 при несовпадении
sort_key = (-round(score, 4), price_from_kzt, id)

Выдаётся min(3, eligible_count). Формула сохранена из задания: ценовой член поощряет более дорогой вариант внутри бюджета. После языкового фильтра языковой член одинаков для всех оставшихся профилей. Score не является вероятностью качества или оценкой репутации.

Используется готовая multilingual-e5-small, 384 измерения, ONNX-версия Xenova, фиксированная ревизия 761b726dd34fb83930e26aab4e9ac3899aa1fa78. Это локальный провайдер эмбеддингов без API-ключей. Модель не дообучалась.

В data/embeddings.json сохранены векторы 70 профилей и всех 408 комбинаций: 17 категорий × 6 форматов × 4 варианта языка, включая отсутствие. Дата, город и бюджет обрабатываются правилами/формулой. Произвольного текстового запроса нет.

Использованы префиксы query:/passage:, attention-mask mean pooling и L2-нормализация. Длинные описания разбиваются до 512 токенов; в текущем каталоге разбиение не понадобилось. SHA256 описаний и семантического текста, ревизия модели и хеш артефакта проверяются на старте. Устаревшие векторы блокируют запуск до явной пересборки; скрытого переключения модели нет. Веса не включены в репозиторий.

Пересборка только при изменении каталога/шаблона:

python -m pip install -r requirements-embeddings.txt
python scripts/build_embeddings.py --model-dir model-cache --download
python -m pytest

Потребуется ~136 МБ загрузки. После проверки зафиксируйте оба embeddings*.json. Для обычного запуска это не требуется. Манифест содержит версии сборки, ревизию и SHA256 файлов модели.

Факты, кеш и LLM

data/facts.json содержит вручную отобранные утверждения, точные цитаты и SHA256 описаний всех 70 профилей. Факты проверяются на старте и назначаются после ранжирования. Для бедных описаний добавлены конкретные часы/цена и предупреждение, без выдумывания достижений.

LLM составляет объяснение в ограниченной грамматике: закреплённое утверждение из описания и самостоятельно скомпонованное предложение из истинных условий (цена, дата, формат, язык, часы). Готовые полные ответы модели не передаются. Валидатор принимает только заданный факт и разрешённые сочетания частей предложения; это проверка замкнутой грамматики, а не доказательство истинности произвольного текста. Чужие факты, добавленные услуги, числа, отрицания, перестановка карточек и клише отклоняются. Выход: 1–2 предложения, ≤45 слов.

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

По актуальному выбору команды используется GPT-6 Sol от OpenAI через Chat Completions. Живой вызов пока не проверен: нужен новый ключ в локальном .env и доступ команды к модели. Ключ, опубликованный в переписке, следует отозвать. ASTRA_* — сохранённый для совместимости исторический префикс основного провайдера, а не имя активной модели:

ASTRA_API_PROTOCOL=openai-chat-completions
ASTRA_CHAT_COMPLETIONS_URL=https://api.openai.com/v1/chat/completions
ASTRA_MODEL=gpt-6-sol
ASTRA_API_KEY_1=<новый ключ только в локальном .env>

Опционально ASTRA_API_KEY_2/3. Общий предел попыток основного LLM — 2.5 с, второго провайдера — 1.5 с, всего запроса — 8 с. Резервный адаптер: FALLBACK_LLM_API_PROTOCOL, FALLBACK_LLM_CHAT_COMPLETIONS_URL, FALLBACK_LLM_MODEL, FALLBACK_LLM_API_KEY. Последний рубеж — конкретный детерминированный шаблон.

Для Sol на официальном endpoint передаются reasoning_effort=none, temperature=0, max_completion_tokens=1000; это короткая композиция проверенных фраз. Совместимость Astra сохранена отдельно с low, без temperature. Ограниченные таймауты могут приводить к шаблону даже при работающем API, поэтому в живой проверке необходимо проверить explanation_source=openai, а не только HTTP 200. В .env.example резервом указан GPT-4.1 mini, но он включается только при отдельном заполнении ключа.

SQLite сохраняет принятый набор объяснений и их происхождение, включая шаблон. Ключ включает запрос, профили, факты, версии объяснителя и конфигурацию модели. Повтор получает сохранённые тексты; конкурирующие записи выбирают первый принятый ответ. API-ключи не входят в кеш, логи и фронтенд. Изменение конфигурации создаёт новый ключ кеша.

Данные

НаборПрофилейСинтетическихПроисхождение
data/original.csv6613Исходный CSV без изменения байтов
data/team_synthetic.csv443 декоратора + 1 инструменталист в Астане
Всего7017Отдельное поле origin

SHA256 исходника: 6a724b6b7dfb5973343e68ba18dadb60fc807d87e3d78f03ee86fb26cb089f7d. Командные профили вымышлены и явно помечены. Цены ориентированы на категории исходника, календари воспроизводимо созданы один раз скриптом scripts/generate_synthetic.py. Флористы и сувениры дополнительно не добавлялись.

Восстановленные города/цены и слабые описания показаны в UI. Для HK-77838 (язык) и HK-90009 (минимум аренды) есть предупреждения; расхождения не превращены в придуманные правила. Рейтинги и достижения атрибутируются описанию, а не независимой проверке.

Демо для жюри

Все даты ниже относятся к 2026 году. Начните с ведущих в Алматы, затем сравните 4 и 5 октября; для пустого результата покажите проверенное изменение бюджета и раскрытие цитаты. Пошаговый сценарий.

ПресетУсловияОжидаемый результат
Ведущие · Алматы04.10, корпоратив, 2 млн ₸, 6 ч, русскийПул 10; заняты 5; подходят 5; топ-3
Сравнение датТе же условия, 04.10 → 05.10Подходят 5 → 4; другая тройка из-за занятости
Декораторы · Астана05.10, свадьба, 3 млн ₸, 10 ч, русский3 командных синтетических профиля
Редкая категорияАлматы, Флорист, 04.10, свадьба, 300 тыс. ₸, 10 ч, русский2 карточки; None длительность проходит
Категории нетЗарубежье, ВедущийNO_CATEGORY_IN_CITY; пул 0
Все вне бюджетаТе же флористы, 100 тыс. ₸ALL_FILTERED_OUT; бюджет: 2
Все занятыТе же флористы, 01.10, 300 тыс. ₸ALL_FILTERED_OUT; заняты: 2
Банкетные залыАлматы, 01.10, свадьба, 4 млн ₸, 8 ч, русскийПул 7; подходят 4; заняты 1, бюджет 1, формат 1

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

Проверка и ограничения

КритерийПодтверждённое состояние
Ответ ≤10 сСквозные локальные тесты и ограниченные таймауты; холодная сборка образа отдельно
До 3 конкретных невзаимозаменяемых объясненийВсе 70 профилей, лимит слов, различительные факты; реализовано
Одинаковый запрос → одинаковый порядок20 повторов, tie-break, перезапуск кеша; реализовано
Две даты → другая выдача и объяснение занятости04/05 октября + причины исключения; реализовано
Плотная, редкая и пустая категорииПроверенные пресеты и API-тесты; реализовано
Пустой результат словамиРазные исходы, первая причина и статистика; реализовано
Команда объясняет пайплайнДиагностика, формула, Mermaid и цитаты; реализовано
Готовые эмбеддинги без дообученияЗакреплённая E5 + хеши; реализовано
LLM только для текстаАдаптер и строгая проверка; живой вызов требует локального ключа и проверки доступа к модели
Docker и воспроизводимостьDockerfile, Compose, healthcheck и сценарий CI подготовлены; успешная контейнерная проверка не подтверждена

Тесты покрывают причины отсева, инвариант суммы, города, пустой max_hours, булевы строки, неверные даты и типы, формулу, устаревшие эмбеддинги, факты, кеш, таймауты, враждебные ответы LLM и API. Платные внешние API в тестах не вызываются. Фактическую границу проверок описывает docs/implementation-validation.md.

При Windows checkout с CRLF один исходный тест байтовой воспроизводимости team_synthetic.csv может не пройти: генератор пишет LF, а Git выдаёт CSV с CRLF. Этот случай не затрагивает работающий подбор; остальные тесты в текущем прогоне прошли.

API и файлы

UI и API работают на одном origin. Основные маршруты:

МаршрутНазначение
GET /api/metaСправочники, пресеты, режимы и состояние канала обращений
POST /api/searchПодбор и диагностическая статистика
POST /api/inquiriesОбращение к команде при настроенном канале и явном согласии
GET /api/inquiries/{id}Приватная квитанция; токен передаётся в заголовке Authorization: Bearer, не в URL
GET /health/readyГотовность каталога и сервиса
GET /openapi.jsonМашинный контракт API

Пример тела запроса к POST /api/search:

{"city":"Алматы","date":"2026-10-04","event_format":"корпоратив","category":"Ведущий","budget":2000000,"duration":6,"language":"русский"}
app/contractor_matching/  модели, фильтры, рейтинг, объяснения, API
app/static/              UI без сборки и CDN
data/                    CSV, факты, готовые векторы
scripts/                 подготовка данных и эмбеддингов
tests/                   ядро, API, отказы, детерминизм
docs/                    аудит, архитектура, демо

Настройки доступа и хранения

MVP рассчитан на небольшой каталог и один worker; кеш хранится на диске. Для доступа по публичному домену задайте APP_ALLOWED_HOSTS списком точных имён хостов через запятую — без схемы, порта и *. По умолчанию разрешены только локальные адреса и тестовый клиент. INQUIRY_DB_PATH должен находиться вне app/static; иначе приложение не запустится. Порядок настройки email и секретов.

Семантическая полезность весов на реальных пользовательских выборах пока не оценивалась. Публичный многопользовательский запуск требует отдельной эксплуатационной подготовки.

Границы продукта

Официальное ТЗ ограничено рекомендацией: бронирование, заявки и уведомления находятся вне обязательного объёма. Путь обращения добавлен по отдельному запросу команды, без оплаты и обещания брони. Аккаунты и конкретный движок БД в PDF не требуются: поиск работает без регистрации; SQLite хранит кеш и обращения.

Кабинеты исполнителей, живые календари, договоры и оплата требуют отдельного этапа и настоящего каталога с контактами. Для публичного запуска нужны HTTPS, защита от спама и лимиты для нескольких работников/прокси, приватное постоянное хранилище, резервные копии и согласованная командой политика хранения контактов. Локальное демо не подтверждает выполнение этих эксплуатационных требований.

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

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

Aokoshi Eidjii 6Yerassyl Irangait 4Kuman 2

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

  • 2026-09-23 17:50Merge critical inquiry protections and READMEYerassyl Irangait
  • 2026-09-23 17:47fix: protect inquiry storage and trusted hostsYerassyl Irangait
  • 2026-09-23 17:07Merge completed HackAlem contractor assistant into mainAokoshi Eidjii
  • 2026-09-23 17:06Merge demoVer-1 product history with mainAokoshi Eidjii
  • 2026-09-23 17:02feat: finish contractor assistant, email inquiries and team handoffAokoshi Eidjii
  • 2026-09-23 16:13feat: deliver explainable contractor matching product with frozen semantic rankingAokoshi Eidjii
  • 2026-09-23 16:05chore: absolute clean startYerassyl Irangait
  • 2026-09-23 15:38docs: collect audits, MVP architecture and verified demos on demoVer-1Aokoshi Eidjii
Все коммиты (12)
  • 2026-09-23 15:32models.py commitYerassyl Irangait
  • 2026-09-23 15:20docs: add HackAlem step 1 dataset and requirements auditAokoshi Eidjii
  • 2026-09-23 14:52feat: rank eligible contractors with audit reasonsKuman
  • 2026-09-23 14:51feat: add deterministic contractor eligibility filtersKuman
Структура репозитория (64 файлов)
.dockerignore
.env.example
.github/workflows/test.yml
.gitignore
AGENTS.md
Dockerfile
HANDOFF.md
README.md
app/contractor_matching/__init__.py
app/contractor_matching/api.py
app/contractor_matching/cache.py
app/contractor_matching/config.py
app/contractor_matching/data_loader.py
app/contractor_matching/embeddings.py
app/contractor_matching/explainer.py
app/contractor_matching/facts.py
app/contractor_matching/filters.py
app/contractor_matching/guidance.py
app/contractor_matching/inquiries.py
app/contractor_matching/inquiry_delivery.py
app/contractor_matching/models.py
app/contractor_matching/presets.py
app/contractor_matching/providers.py
app/contractor_matching/ranking.py
app/contractor_matching/service.py
app/static/app.js
app/static/index.html
app/static/styles.css
compose.yaml
data/embeddings.json
data/embeddings_manifest.json
data/facts.json
data/original.csv
data/team_synthetic.csv
docs/demo-script.md
docs/hackalem_demo_scenarios.md
docs/hackalem_step1_audit.md
docs/hackalem_step2_architecture.md
docs/implementation-validation.md
docs/launch.md
docs/readiness.md
pyproject.toml
requirements-dev.txt
requirements-embeddings.txt
requirements.txt
scripts/build_embeddings.py
scripts/build_facts.py
scripts/generate_synthetic.py
scripts/run_demo.py
start-demo.cmd
tests/conftest.py
tests/test_api.py
tests/test_api_inquiries.py
tests/test_config.py
tests/test_data.py
tests/test_embeddings.py
tests/test_explainer.py
tests/test_filters.py
tests/test_guidance.py
tests/test_inquiries.py
tests/test_inquiry_delivery.py
tests/test_models.py
tests/test_providers.py
tests/test_ranking.py
Ветки (3)

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

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

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