Alem Events
Локальный веб-сервис, который подбирает до 3 подрядчиков под мероприятие и объясняет выбор.
О проекте
Команда: Scourge
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP
Задача кейса: «#79-lite»: подобрать до 3 подрядчиков для мероприятия и объяснить выбор
Суть
Локальный веб-сервис, который подбирает до 3 подрядчиков под мероприятие и объясняет выбор. Есть AI-поиск по свободному описанию, альтернативы при пустой выдаче и сравнение двух дат.
Как устроено
ingest.py сверяет CSV и HTML по id и нормализует их в profiles.json. matching.py фильтрует по цепочке: город → категория → дата → бюджет → формат → язык → часы. Ранжирование по цене, объяснения строятся из фактов и фрагментов описаний. AI-режим через OpenAI Responses API со Structured Outputs извлекает условия и пожелания из текста и оценивает пожелания цитатами. Сервер проверяет, что id допустимы и цитаты есть в профиле. recovery.py предлагает изменить одно условие, если выдача пустая.
Ключевые решения
- жёсткие фильтры
- ранжирование по цене
- Structured Outputs
- проверка цитат в профиле
- recovery-подсказки (одно условие)
- сравнение дат
Особенности
Защита от галлюцинаций на уровне сервера: каждое предложенное моделью id и каждая цитата проверяются по допустимому набору и тексту профиля. AI-поиск не подтягивает незаданные условия из формы.
Стек
Python (stdlib http.server) · OpenAI Responses API · HTML/CSS/JS
Модели
gpt-4.1-mini
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-c63e3b74-scourge.
README
README.mdAlem Events — объяснимый подбор подрядчиков
Локальный веб-сервис для команды и жюри HackAlem: задайте город, дату, мероприятие, категорию и бюджет; при необходимости — часы и язык. Сервис вернёт до трёх карточек с объяснениями и причины малого или пустого результата. Опциональный AI-помощник за одно действие ищет по описанию события своими словами. Он учитывает только условия из текста и подтверждает пожелания к стилю цитатами из профилей. Если точных совпадений нет, отдельно предлагает близкие варианты с объяснением отличий, в том числе другие даты.
Запуск
Python 3.9+, без сторонних зависимостей. Обычный подбор работает без API-ключей:
python3 server.py
Откройте http://127.0.0.1:8000. Завершение — Ctrl+C.
Другой порт: python3 server.py --port 8080.
Для демонстрации другим устройствам в локальной сети:
python3 server.py --host 0.0.0.0 --port 8000, затем откройте адрес компьютера
в этой сети. Это демо-сервер стандартной библиотеки, не публичный production-хостинг.
Развёрнутой внешней версии нет.
Подключение AI
Скопируйте шаблон и заполните ключ в локальном файле:
cp .env.example .env
OPENAI_API_KEY=ваш_ключ
OPENAI_MODEL=gpt-4.1-mini
Если сервер ещё не запущен, выполните python3 server.py. .env читается при
запросе, поэтому для изменения ключа перезапуск не требуется.
Переменные окружения также поддерживаются.
.env исключён из Git; ключ используется только сервером и не передаётся браузеру.
AI вызывает OpenAI Responses API со структурированным JSON-ответом через
стандартную библиотеку Python (urllib), отдельный SDK не требуется.
Модель можно изменить через OPENAI_MODEL; она должна поддерживать Responses API
и Structured Outputs. Документация:
Structured Outputs,
GPT-4.1 mini.
При разборе текста внешний сервис получает описание события и варианты полей
каталога. При ранжировании — пожелания и описания рассматриваемых кандидатов,
включая возможные альтернативы.
Без ключа или при ошибке API доступен обычный подбор; интерфейс сообщает,
когда AI не использовался. Для прежнего сценария с заполнением формы проверены
настоящие запросы: извлечение полей, уточнения, ранжирование по двум пожеланиям.
Результат того AI-ранжирования через HTTP сохранён в data/ai_live_check.json.
Это снимок одного запуска прежнего /api/ai/recommend, а не проверка нового
поиска по тексту и не гарантия качества на любых новых запросах.
Что реализовано
- Форма с опциями из реального каталога, необязательными часами и языком.
- Кнопка «Найти по описанию»: AI разбирает текст и сразу запускает подбор. Заполненные поля обычной формы не влияют на этот поиск. Неуказанные город, категория, дата, бюджет, формат, язык и часы не ограничивают выдачу. Уточнение нужно при неоднозначности написанного, например дате без года.
- До пяти пожеланий к стилю события с цитатами из описаний. В точной выдаче соблюдаются все явно указанные условия, а пожелания имеют подтверждение в источнике. При отсутствии подтверждения показывается «нужно уточнить».
- Если точных совпадений нет, до трёх альтернатив отдельным списком: близкие профили и свободные даты в известном календаре. У каждой карточки перечислены отличия от запроса; заданные город и категория сохраняются.
- Карточки с ценами «от», фактами и отметками происхождения данных.
- Причины отсева, сводка для пустой и неполной выдачи.
- Сравнение двух дат с одинаковыми условиями рядом и поимённым объяснением изменений: занят, нет данных календаря или подходит, но ниже первой тройки.
- В обычной форме — проверенные подсказки при пустой выдаче: ближайшая дата в пределах недели, минимальный бюджет по указанным ценам, снятие ограничения по языку или сокращение длительности. Каждая меняет только одно условие и применяется кнопкой.
- Кнопки проверенных демо: плотная категория, редкая категория, пустая выдача, отсутствие категории, сравнение дат, банкетный зал и декабрьский сезон.
- Адаптивная вёрстка, состояния загрузки и ошибок.
- HTTP API
GET /api/meta,POST /api/recommend,POST /api/compare. - AI API
POST /api/ai/searchдля поиска по тексту; прежниеPOST /api/ai/parseиPOST /api/ai/recommendтакже доступны. - Серверная валидация, ограничение размера запроса, выдача только разрешённых статических файлов. Данные карточек вставляются как текст, не HTML.
Пайплайн
CSV + HTML → ingest.py → data/profiles.json → matching.py → explanations.py → server.py → web/
- Источники сверяются по
id, без слепого объединения. - Полные записи берутся из CSV, числа, списки и даты нормализуются.
- Фильтры: город → категория → дата → бюджет → формат → язык → часы.
- В обычном режиме прошедшие кандидаты ранжируются по меньшей цене,
затем строковому
id. В поиске по тексту применяются только явно указанные условия. AI оценивает пожелания по описаниям; в точную выдачу попадают максимум три профиля, прошедших условия и имеющих основания для пожеланий. - Базовые объяснения формируются из фактов и фрагментов описаний.
AI дополняет их оценкой пожеланий; сервер проверяет принадлежность каждого
предложенного
idдопустимому набору и наличие цитат в описании профиля. - При отсутствии точных совпадений поиск по тексту отдельно показывает
альтернативы с минимальными отклонениями. Это могут быть другая проверенная
дата, большая цена «от», другой формат, язык, длительность или неподтверждённые
пожелания. Отличия перечисляются в карточке, исходный запрос не меняется.
В обычной форме
recovery.pyпродолжает проверять изменения одного условия; эти подсказки применяются пользователем.
AI-пайплайн: текст → условия + пожелания → фильтрация → AI-оценка → карточки.
Разбор выполняет ai_brief.py, проверку условий — matching.py, сбор точной
выдачи и альтернатив — assistant_search.py; запросы к модели — ai_client.py.
При неоднозначности написанного помощник задаёт вопрос; дополните текст и снова
нажмите «Найти по описанию». Пропущенные условия не заимствуются из формы.
Например, «Нужен ведущий в Алматы» ищет ведущих без ограничения по дате и бюджету;
доступность на конкретный день при таком запросе не проверяется.
Сравнение двух дат использует обычный порядок по цене; пожелания к стилю
в этом режиме не учитываются.
Подробнее об объяснениях: EXPLANATIONS.md. Сценарий выступления: DEMO.md.
Данные и происхождение профилей
Предоставлены CSV и HTML: data/source/. По 66 профилей, одинаковые id,
расхождений в сопоставимых полях нет — см. data/comparison.json.
HTML содержит сокращённые описания и количества занятых дней, поэтому
конкретные даты берутся из CSV. Календарь: 23.09–31.12.2026.
Если запрошена дата за пределами периода, доступность считается неизвестной:
профиль не попадает в точную выдачу на эту дату.
Синтетические записи исходного датасета получают отметку «Синтетический профиль».
Добавленные командой записи нужно помещать отдельно в data/team_profiles.json
в нормализованной схеме data/profiles.json, с уникальными id.
Сервер назначает им team_added: true и отметку «Добавлен командой».
Если запись искусственная, также укажите synthetic: true — появятся обе отметки.
Сейчас файл пуст, добавленных командой профилей нет. После изменения данных
перезапустите сервер. Дубликаты id блокируют загрузку.
Пересоздать нормализованный датасет:
python3 ingest.py data/source/profiles.csv data/source/preview.html
При конфликтах создаётся отчёт, но новый profiles.json не записывается.
Старый файл остаётся и не должен считаться результатом неуспешного импорта.
API и проверки
curl http://127.0.0.1:8000/api/recommend \
-H 'Content-Type: application/json' \
--data-binary @examples/dataset_request.json
python3 -m unittest discover -s tests -v
python3 quality_demo.py
Для обычного POST /api/recommend обязательные поля JSON:
city, date, format, category, budget.
Необязательные: language, duration_hours (можно null), languages (массив).
При передаче обоих языковых полей требуется владение всеми перечисленными языками.
Ответ содержит cards, message, status, exclusion_counts, а также
исходные candidates, rejected, eligible_count, quality.
Поля suggestions и recovery_message описывают проверенные изменения условий.
В каждой подсказке есть полный request для повторного запроса и eligible_count.
POST /api/compare принимает те же поля и дополнительную дату compare_date:
{"city":"Алматы","category":"Ведущий","date":"2026-10-06","compare_date":"2026-10-01","format":"свадьба","budget":3000000,"language":"русский","duration_hours":5}
Ответ: results — две обычные выдачи, changes — профили, появившиеся или
исчезнувшие из тройки, с причинами для каждой даты, message — итог сравнения.
Некорректный запрос возвращает HTTP 400; слишком большой — 413.
Для поиска по свободному тексту без заполнения формы:
curl http://127.0.0.1:8000/api/ai/search \
-H 'Content-Type: application/json' \
--data-binary '{"text":"Нужен ведущий в Алматы"}'
Тело содержит только text. Ответ содержит:
parsed: извлечённыеrequest,preferences, уточненияquestions, предупрежденияwarningsи признакready;result: точныеcardsили отдельныеalternatives, сводкуmessage,status,total,eligible_count,hard_match_countи сведения об AI.
При требующем ответа уточнении result равен null. Если дата не указана,
request_date равен null; это поиск без проверки конкретного дня, а не обещание
свободного календаря. Отсутствие бюджета также не означает нулевой бюджет.
hard_match_count отражает прохождение структурированных условий;
eligible_count дополнительно учитывает подтверждение пожеланий.
Альтернативы не входят в точную выдачу и явно показывают отклонения от запроса.
status различает matched, category_absent и no_matches, как обычный подбор.
Сохранился отдельный API разбора текста для интеграций с ручной проверкой формы:
curl http://127.0.0.1:8000/api/ai/parse \
-H 'Content-Type: application/json' \
--data-binary '{"text":"Нужен ведущий в Алматы на свадьбу 6 октября 2026, до 3 млн тенге, на 5 часов, русский язык. Хочу современные интерактивы."}'
Ответ прежнего /api/ai/parse содержит извлечённые поля request, пожелания
preferences и уточняющие вопросы questions. В этом отдельном сценарии
перед вызовом /api/ai/recommend нужно заполнить обязательные поля формы.
В /api/ai/search такого требования нет. Несколько обязательных языков
в обоих сценариях означают, что исполнитель должен владеть каждым из них.
POST /api/ai/recommend принимает обычные поля запроса, массив preferences
и опциональный массив languages для нескольких обязательных языков:
curl http://127.0.0.1:8000/api/ai/recommend \
-H 'Content-Type: application/json' \
--data-binary '{"city":"Алматы","category":"Ведущий","date":"2026-10-06","format":"свадьба","budget":3000000,"duration_hours":5,"languages":["русский"],"preferences":["современные интерактивы"]}'
Тесты покрывают алгоритм, объяснения, сверку файлов, HTTP-запросы всех демо,
некорректный ввод, маркировку профилей команды, применимость подсказок и причины
изменений между датами. quality_demo.py проверяет семь фиксированных запросов,
повторяемость, различимость объяснений и результат применения подсказок.
Дополнительно интерфейс проверен в Chromium: все семь демо-кнопок,
применение подсказки бюджета, сравнение дат и мобильная ширина 390 px.
Проверки интерфейса запускаются отдельно: node tests/test_web.cjs.
После проверки поиска по тексту прошли 91 Python-тест и проверки JavaScript.
Живой запрос Нужен ведущий в Алматы через /api/ai/search вернул три карточки
из десяти подходящих профилей без ограничений по дате, бюджету, формату и языку.
В Chromium также проверены экраны нового AI-поиска: точная выдача, альтернативы
и уточняющий вопрос, включая ширину 390 px. Для проверки отображения
воспроизводились сохранённые ответы API без повторной оплаты запросов.
Тесты стандартной команды не отправляют запросы модели и не расходуют API-баланс.
Живая проверка трёх AI-сценариев через временный локальный HTTP-сервер:
python3 ai_live_check.py
Эта отдельная команда использует настроенный ключ и расходует API-баланс. Она проверяет поиск без даты и бюджета, полный запрос со свадьбой и пожеланием, а также уточнение года: явно названные условия не должны теряться, неизвестный год не должен подставляться. Проверяются обязательные условия карточек, происхождение цитат и доступность предлагаемых дат. При текущем запуске все три сценария прошли, время ответа составило 3,0–7,5 секунды. Это проверка конкретных запросов, а не гарантия безошибочного разбора любого текста.
Ограничения
Цена «от» не гарантирует итоговую стоимость. Отсутствие даты в списке занятых
не подтверждает бронь. max_hours: null означает отсутствие ограничения по
присутствию. Флаги восстановленных города и цены показаны в карточках.
Описания принадлежат источнику и не являются проверенными рекомендациями.
Нет бронирования, авторизации, почасовых тарифов, конвертации валют и обработки
синонимов в обычном фильтре. AI может сопоставлять формулировки с вариантами
каталога, но разбор может ошибаться: извлечённые условия показываются рядом
с результатом, а исправляются изменением текста. Бюджет в KZT.
XLSX не читается, поскольку предоставлен CSV.
HTML-парсер предназначен для структуры данного превью.
Внешний OpenAI API нужен только AI-функциям. Семантическая оценка не является
независимой проверкой обещаний из описания; точная цитата подтверждает источник,
но не гарантирует верность интерпретации модели. AI-ответы могут меняться между
вызовами, а при отсутствии подтверждений порядок не обязан измениться.
В точной AI-выдаче подтверждение пожелания означает наличие основания в описании,
а не гарантию услуги. Если профиль подходит по полям, но пожелание не подтверждено,
он может появиться только среди явно обозначенных альтернатив.
Альтернативы поиска по тексту сохраняют заданные город и категорию; для другой даты
проверяется весь известный календарь. При отсутствии нужной категории в заданном
городе смена даты не создаёт новых профилей.
Подсказки обычной формы по-прежнему не перебирают сочетания нескольких изменений
и не меняют город, категорию или формат мероприятия. Они проверяют другие даты
только в пределах семи дней; отсутствие такой подсказки не означает отсутствия
вариантов на любых других датах.
Авторы коммитов
История коммитов
- 2026-09-23 17:56Fix AI search explanations and extraction; add live checkssaloverni
- 2026-09-23 17:38trained helperhailrxke
- 2026-09-23 17:26add ai helperhailrxke
- 2026-09-23 16:33Add verified recovery suggestions and explain date comparisonssaloverni
- 2026-09-23 15:48Добавить веб-интерфейс подбора и демо для защитыhailrxke
- 2026-09-23 14:52Добавить объяснения подбора и проверки качестваmanyuaaaaaa
- 2026-09-23 14:18module_1hailrxke
- 2026-09-23 14:07module_1hailrxke
Структура репозитория (42 файлов)
.env.example .gitignore DEMO.md EXPLANATIONS.md README.md ai_brief.py ai_client.py ai_live_check.py ai_ranking.py assistant_search.py data/ai_live_check.json data/comparison.json data/example_result.json data/profiles.json data/quality_demo_result.json data/source/preview.html data/source/profiles.csv data/team_profiles.json examples/dataset_request.json examples/profiles.json examples/quality_requests.json examples/request.json explanations.py ingest.py matching.py quality_demo.py recovery.py server.py tests/test_ai_brief.py tests/test_ai_client.py tests/test_ai_ranking.py tests/test_assistant_search.py tests/test_comparison.py tests/test_explanations.py tests/test_ingest.py tests/test_matching.py tests/test_recovery.py tests/test_server.py tests/test_web.cjs web/app.js web/index.html web/style.css
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.