AAYMA
RU
Войти

Alem Events

Локальный веб-сервис, который подбирает до 3 подрядчиков под мероприятие и объясняет выбор.

Рабочий MVP
Команда «Scourge» 3💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: 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.md

Alem 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/

  1. Источники сверяются по id, без слепого объединения.
  2. Полные записи берутся из CSV, числа, списки и даты нормализуются.
  3. Фильтры: город → категория → дата → бюджет → формат → язык → часы.
  4. В обычном режиме прошедшие кандидаты ранжируются по меньшей цене, затем строковому id. В поиске по тексту применяются только явно указанные условия. AI оценивает пожелания по описаниям; в точную выдачу попадают максимум три профиля, прошедших условия и имеющих основания для пожеланий.
  5. Базовые объяснения формируются из фактов и фрагментов описаний. AI дополняет их оценкой пожеланий; сервер проверяет принадлежность каждого предложенного id допустимому набору и наличие цитат в описании профиля.
  6. При отсутствии точных совпадений поиск по тексту отдельно показывает альтернативы с минимальными отклонениями. Это могут быть другая проверенная дата, большая цена «от», другой формат, язык, длительность или неподтверждённые пожелания. Отличия перечисляются в карточке, исходный запрос не меняется. В обычной форме 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-выдаче подтверждение пожелания означает наличие основания в описании, а не гарантию услуги. Если профиль подходит по полям, но пожелание не подтверждено, он может появиться только среди явно обозначенных альтернатив. Альтернативы поиска по тексту сохраняют заданные город и категорию; для другой даты проверяется весь известный календарь. При отсутствии нужной категории в заданном городе смена даты не создаёт новых профилей. Подсказки обычной формы по-прежнему не перебирают сочетания нескольких изменений и не меняют город, категорию или формат мероприятия. Они проверяют другие даты только в пределах семи дней; отсутствие такой подсказки не означает отсутствия вариантов на любых других датах.

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

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

hailrxke 5saloverni 2manyuaaaaaa 1

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

  • 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 комментариев

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

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