AAYMA
RU
Войти

Ai Job Hunter

Backend-сервис (FastAPI, без фронтенда), возвращающий до трёх подрядчиков для мероприятия из каталога 66 профилей с объяснением выбора.

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

О проекте

Команда: Ai Job Hunter
Кейс HackAlem.ai: Креативные индустрии — Firebird
Стадия: Рабочий MVP

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

Суть

Backend-сервис (FastAPI, без фронтенда), возвращающий до трёх подрядчиков для мероприятия из каталога 66 профилей с объяснением выбора.

Как устроено

Запрос (город, дата, формат, категория, бюджет, опционально длительность, язык, пожелания) проходит жёсткие Python-фильтры по формату, бюджету, занятости и длительности. Оставшиеся ранжируются по баллу, где текстовые пожелания сопоставляются с описаниями через OpenAI text-embedding-3-small (с кэшем) или TF-IDF-фолбэк без ключа. Ответ содержит статус success/category_not_found/no_matches, статистику отказов и шаблонное объяснение (цена vs бюджет, дата, язык, цитата описания).

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

  • жёсткая фильтрация
  • OpenAI embeddings semantic ranking
  • TF-IDF fallback
  • rejection stats

Особенности

Различение бизнес-исходов «категории нет в городе» и «никто не прошёл условия» со счётчиками причин отказа; язык сделан мягким предпочтением, а не фильтром.

Стек

Python · FastAPI · Pydantic · pandas · scikit-learn · uv · pytest

Модели

text-embedding-3-small


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

README

README.md

HackAlem AI — умный подбор подрядчиков для мероприятий

Backend-сервис, который помогает выбрать исполнителя или площадку из готового каталога: пользователь задаёт параметры мероприятия, а система возвращает до трёх подходящих подрядчиков с объяснением выбора.

Проект разработан для задачи «Умный подбор подрядчиков» (#79-lite). Вместо длинного списка пользователь получает короткую подборку: обязательные условия проверяются Python-кодом, а дополнительные пожелания сопоставляются с описаниями через OpenAI embeddings.

Стек: Python · FastAPI · Pydantic · pandas · OpenAI API · scikit-learn · pytest · uv.

Текущий формат: API без отдельного frontend. Для демонстрации используется Swagger UI. Последний локальный прогон тестов: 6 passed.

Быстрый запуск · API · Алгоритм · AI и кэш · Данные · Тесты · Демо · Ограничения


1. Что делает сервис

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

Например, пользователь ищет ведущего на свадьбу в Алматы, свободного 15 октября, с бюджетом до 1 200 000 ₸. Дополнительное пожелание: спокойная подача и ненавязчивый юмор.

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

Поддерживаются три бизнес-исхода:

СтатусЗначение
successНайден хотя бы один подходящий подрядчик.
category_not_foundВ выбранном городе нет профилей нужной категории.
no_matchesКатегория в городе есть, но никто не проходит обязательные условия.

Важно: это сервис рекомендаций, а не бронирования. Он не создаёт заявки, не резервирует дату, не принимает оплату и не отправляет сообщения подрядчикам.

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

Требования

Для запуска нужны Python, uv и исходный CSV. В окружении разработки использовался Python 3.12; ограничения на версию Python задаются полем requires-python в pyproject.toml.

Интернет нужен для первоначальной установки зависимостей и для запросов к OpenAI. После установки зависимостей базовая рекомендация и резервный TF-IDF-режим не требуют OpenAI API.

Все команды ниже выполняются из корня репозитория, где находятся pyproject.toml и uv.lock.

Шаг 1. Проверить uv

uv --version

Если команда не найдена, установите uv по официальной инструкции и откройте новый терминал.

Шаг 2. Установить зависимости

uv sync --locked

Команда синхронизирует окружение с проектом. Флаг --locked не позволяет незаметно обновить lock-файл при несовпадении с конфигурацией. uv.lock нужно хранить в репозитории вместе с pyproject.toml; подробности — в документации uv.

Вручную активировать .venv для команд через uv run не требуется. Повторно выполнять uv init в готовом репозитории также не нужно.

Шаг 3. Проверить файл данных

Ожидаемый путь:

data/hackathon-dataset-anonymized.csv

Папка data должна находиться в корне проекта, рядом с src. Если исходный файл скачан под названием hackathon dataset anonymized .csv, переименуйте его в hackathon-dataset-anonymized.csv.

Шаг 4. Настроить OpenAI — необязательно для базового запуска

Создайте .env в корне проекта:

OPENAI_API_KEY=replace_with_your_api_key

Замените значение своим ключом. Не оставляйте текст-заглушку вместо ключа: приложение воспримет его как настоящий ключ и попытается обратиться к API.

Для запуска без OpenAI оставьте значение пустым или не задавайте переменную:

OPENAI_API_KEY=

Переменная может быть задана и в окружении операционной системы. Чтобы проверить режим без ключа, убедитесь, что она отсутствует не только в .env, но и в окружении процесса. После изменения конфигурации перезапустите сервер.

Модель в текущем коде: text-embedding-3-small. Её имя задано в semantic.py, а не отдельной переменной окружения. NVIDIA API в текущую реализацию не подключён.

Шаг 5. Запустить сервер

uv run uvicorn hack_dbab4d08_ai_job_hunter.main:app --reload

Локальные адреса:

Swagger UI:       http://127.0.0.1:8000/docs
OpenAPI-схема:    http://127.0.0.1:8000/openapi.json
Проверка сервиса: http://127.0.0.1:8000/health

В Swagger откройте POST /recommend, нажмите Try it out, вставьте JSON и выполните запрос.

--reload используется для разработки. Для демонстрации без автоматических перезапусков можно убрать этот флаг:

uv run uvicorn hack_dbab4d08_ai_job_hunter.main:app

Шаг 6. Запустить тесты

В отдельном терминале, из корня проекта:

uv run pytest -v

Для существующего набора из шести тестов ожидается:

6 passed

Подробности проверок описаны в разделе Тесты.

3. API

GET /health

Проверяет, что приложение отвечает, и показывает количество загруженных записей.

Пример для исходного датасета:

{
  "status": "ok",
  "contractors_loaded": 66
}

Этот endpoint не проверяет работоспособность OpenAI, ключ, баланс или доступность внешней сети.

POST /recommend

Тело запроса передаётся как JSON с Content-Type: application/json.

ПолеОбязательноТип и ограничения
cityДаАлматы, Астана или Зарубежье.
dateДаДата YYYY-MM-DD, от 2026-09-23 до 2026-12-31 включительно.
event_formatДасвадьба, той, корпоратив, конференция, юбилей, день рождения.
categoryДаСтрока, например Ведущий, Фотограф, Флорист, Банкетный зал.
budgetДаПоложительное целое число, бюджет в тенге на одного выбираемого подрядчика.
duration_hoursНетЦелое число от 1 до 24 либо null.
languageНетрусский, казахский, английский либо null.
preferencesНетТекст длиной не более 500 символов либо null.

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

Язык — предпочтение, а не обязательный фильтр. Подрядчик с нужным языком получает дополнительные баллы. Подрядчик без него может остаться в выдаче, если проходит остальные условия.

Бюджет сравнивается с price_from_kzt — ценой «от» за мероприятие. Прохождение фильтра не гарантирует окончательную стоимость заказа с дополнительными услугами.

Пример запроса:

{
  "city": "Алматы",
  "date": "2026-10-15",
  "event_format": "свадьба",
  "category": "Ведущий",
  "budget": 1200000,
  "duration_hours": 8,
  "language": "русский",
  "preferences": "Хочу спокойного интеллигентного ведущего с ненавязчивым юмором"
}

Структура успешного ответа

Ниже — иллюстрация формата на одной карточке, а не эталонный результат для запроса выше. count всегда соответствует фактической длине contractors.

{
  "status": "success",
  "count": 1,
  "message": null,
  "rejection_stats": null,
  "contractors": [
    {
      "id": "HK-77838",
      "name": "Хаул",
      "category": "Ведущий",
      "city": "Алматы",
      "price_from_kzt": 1000000,
      "languages": ["русский", "казахский"],
      "score": 66.94,
      "semantic_score": 0.465,
      "explanation": "Стоимость 1,000,000 ₸ ниже бюджета на 200,000 ₸. Свободен на выбранную дату 15.10.2026. Работает на языке «русский». Может работать необходимые 8 часов. По описанию: Профессиональный ведущий с интеллигентной, располагающей и ненавязчивой подачей."
    }
  ]
}

В ответе anon_name из CSV переименовывается в name. Поле category показывает категорию текущего запроса, а не весь список категорий профиля.

score округляется до двух знаков после запятой, semantic_score — до трёх. Сортировка выполняется до этого округления. Эти значения служебные: они не являются процентом качества или вероятностью успешного мероприятия.

Категория отсутствует

Пример для категории «Декоратор» в Астане в исходном каталоге:

{
  "status": "category_not_found",
  "count": 0,
  "message": "В городе Астана нет подрядчиков категории «Декоратор».",
  "rejection_stats": null,
  "contractors": []
}

Подрядчики есть, но не подходят

Пример ответа для фотографов Астаны при бюджете 10 000 ₸:

{
  "status": "no_matches",
  "count": 0,
  "message": "Подходящих подрядчиков не найдено: 3 превышают указанный бюджет.",
  "rejection_stats": {
    "wrong_format": 0,
    "over_budget": 3,
    "busy": 0,
    "duration": 0
  },
  "contractors": []
}

Все три бизнес-исхода возвращаются с HTTP 200: отсутствие подходящего подрядчика само по себе не является ошибкой сервера. Невалидный запрос, например дата вне календаря или отрицательный бюджет, возвращает HTTP 422.

Модель ответа подключена через response_model=RecommendationResponse. FastAPI использует её для описания, проверки и сериализации ответа; см. документацию response models.

Запрос из PowerShell

Вариант без Swagger, с явной передачей русского текста в UTF-8:

$body = @'
{
  "city": "Алматы",
  "date": "2026-10-15",
  "event_format": "свадьба",
  "category": "Ведущий",
  "budget": 1200000,
  "duration_hours": 8,
  "language": "русский",
  "preferences": "Спокойный ведущий с ненавязчивым юмором"
}
'@

$params = @{
    Uri = "http://127.0.0.1:8000/recommend"
    Method = "Post"
    ContentType = "application/json; charset=utf-8"
    Body = [System.Text.Encoding]::UTF8.GetBytes($body)
}

Invoke-RestMethod @params | ConvertTo-Json -Depth 10

4. Алгоритм рекомендации

JSON-запрос
    ↓
Pydantic: проверка параметров
    ↓
Есть ли категория в указанном городе?
    ├── Нет → category_not_found
    └── Да
         ↓
    Обязательные фильтры
         ├── Нет кандидатов → диагностика → no_matches
         └── Есть кандидаты
              ↓
         Рейтинг: язык + бюджет + часы + текстовое соответствие
              ↓
         Сортировка: score ↓, цена ↑, id ↑
              ↓
         Первые 3 профиля
              ↓
         Объяснения → RecommendationResponse

Обязательные фильтры

Все условия объединяются в одну булеву маску. Отбор применяется через contractors.loc[mask].copy(), чтобы сохранить структуру таблицы, в том числе при пустом результате.

УсловиеПравило прохождения
Городcity соответствует городу запроса.
КатегорияНужная категория присутствует в categories.
Форматevent_format входит в event_formats.
Бюджетprice_from_kzt <= budget.
ДатаДаты запроса нет в busy_dates.
ДлительностьЕсли часы указаны: max_hours >= duration_hours либо max_hours не задан.

Пустое max_hours трактуется согласно датасету: услуга не привязана к длительности присутствия. Это не означает, что подрядчик работает ноль часов.

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

Формула рейтинга

score = language_score + budget_score + duration_score + 40 × semantic_score
КомпонентТекущая реализация
language_score+30, если язык указан и присутствует у подрядчика; иначе 0.
budget_score20 × (budget − price_from_kzt) / budget, с ограничением результата диапазоном [0, 20].
duration_scoreЧасы не указаны: 0; точное совпадение: +15; запас по часам: +10; max_hours не задан при указанной длительности: +10.
semantic_scoreКосинусное сходство пожеланий и описания; без пожеланий — 0.

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

Пример расчёта при бюджете 1 200 000 ₸, цене 1 000 000 ₸, совпадении языка, точном совпадении восьми часов и условном сходстве 0.465:

Язык:            30
Бюджет:          20 × (1 200 000 − 1 000 000) / 1 200 000 ≈ 3.33
Длительность:    15
Пожелания:       40 × 0.465 = 18.60
Итого:           ≈ 66.93

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

Сортировка и воспроизводимость

ranked.sort_values(
    by=["score", "price_from_kzt", "id"],
    ascending=[False, True, True],
)

При равных баллах выше находится более дешёвый кандидат, при равных баллах и цене порядок определяется по id. Случайная перестановка не используется.

Порядок воспроизводим при одинаковом запросе, каталоге и рассчитанных оценках. Переключение с OpenAI на TF-IDF меняет способ расчёта и может изменить выдачу; строгая неизменность между этими режимами не гарантируется.

5. Диагностика отказов

Диагностика рассматривает только профили выбранных города и категории. Для каждого кандидата фиксируется одна причина — первая не пройденная проверка:

wrong_format → over_budget → busy → duration

После найденной причины обработка этого кандидата прекращается. Это предотвращает двойной подсчёт, но означает, что статистика зависит от порядка проверок.

Например, сочетание over_budget: 3 и busy: 0 означает, что три кандидата были исключены по бюджету до проверки занятости. Нулевой счётчик busy не доказывает, что все они свободны.

Сейчас rejection_stats и подробное сообщение формируются для no_matches. При успешной выдаче из одного или двух кандидатов возвращается фактический count, но отдельное объяснение нехватки до трёх пока не добавлено.

6. AI: embeddings, кэш и резервный режим

Основной режим

В semantic.py используется OpenAI API с моделью text-embedding-3-small. Модель преобразует текст в числовой вектор; API возвращает embedding, а не готовый список подрядчиков. Способ вызова описан в официальном руководстве OpenAI.

Сервис получает вектор preferences и векторы описаний прошедших фильтрацию кандидатов. Затем локально вычисляет косинусное сходство с помощью sklearn.metrics.pairwise.cosine_similarity — документация функции.

preferences  → embedding запроса ─────┐
                                     ├→ cosine similarity → semantic_score
description → embedding подрядчика ──┘

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

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

Кэширование

Embeddings описаний сохраняются в словаре description_cache, ключом служит полный текст описания.

При наличии новых описаний сервис запрашивает их одним batch-вызовом. Embedding пользовательских пожеланий запрашивается отдельно. При повторном использовании тех же описаний они берутся из кэша, но пожелания пока вычисляются заново.

СитуацияОбращения к OpenAI при успешном выполнении, без повторных попыток SDK
Нет пожеланий или после фильтров нет кандидатов0.
Все нужные описания уже в кэше1 запрос для пожеланий.
Есть новые описания1 batch-запрос для новых описаний и 1 запрос для пожеланий.

Кэш хранится только в памяти процесса: после перезапуска или срабатывания --reload он очищается. У нескольких процессов сервера будут отдельные кэши. Файлового кэша, Redis и предварительного вычисления всех профилей при старте нет.

Резервный режим TF-IDF

Если ключ отсутствует либо вызов OpenAI завершился обработанным исключением, используется:

preferences + descriptions → TfidfVectorizer → cosine_similarity

TF-IDF сравнивает тексты по взвешенным совпадениям токенов и не равнозначен embedding-модели по смысловому сопоставлению. Векторы строятся заново для текущего набора текстов.

При ошибке OpenAI в терминал выводятся сообщения:

OpenAI embeddings failed: ...
Falling back to TF-IDF

В текущем JSON нет отдельного поля, указывающего использованный режим. Само по себе большое или ненулевое semantic_score не доказывает успешный вызов OpenAI. Для подтверждения AI-режима на демонстрации проверяйте доступность ключа, сообщения сервера и факт успешного запроса в кабинете API.

Задержка и доступность

Ориентир задачи — ответ примерно до 10 секунд, но это не измеренная гарантия текущей реализации. Холодный кэш, сеть и внешнее API влияют на время ответа.

Явные timeout и max_retries сейчас не заданы. Резервный алгоритм запускается после ошибки вызова, а не сразу при замедлении сети; SDK поддерживает настройку этих параметров — см. официальную Python-библиотеку OpenAI.

7. Как формируются объяснения

explanations.py формирует текст программно, без обращения к генеративной модели. Используются цена и разница с бюджетом, свободная дата по каталогу, совпадение языка и достаточная длительность.

Если переданы пожелания и semantic_score > 0.20, добавляется фрагмент description: первое предложение длиной не менее 20 символов, с ограничением около 160 символов.

Фрагмент сопровождается словами «По описанию». Это содержание профиля подрядчика, а не независимо подтверждённая оценка его опыта или качества.

Текущий алгоритм берёт первое подходящее по длине предложение, а не наиболее релевантный пожеланиям фрагмент. Порог 0.20 — эвристика для добавления описания, не граница доказанного совпадения с запросом.

8. Данные

Источник — предоставленный организаторами анонимизированный датасет: 66 профилей, включая 13 полностью синтетических. Описание набора приведено в задании «Умный подбор подрядчиков» (#79-lite).

Календарь охватывает 23 сентября — 31 декабря 2026 года, всего 100 дней. Отсутствие даты в busy_dates используется как признак доступности только внутри этого окна. Поэтому запросы вне диапазона отклоняются валидацией.

Поле CSVСмысл
idИдентификатор профиля.
anon_nameВымышленное имя.
categoriesОдна или несколько категорий.
cityАлматы, Астана или Зарубежье.
price_from_kztЦена «от» в тенге за мероприятие.
event_formatsПоддерживаемые форматы мероприятий.
languagesЯзыки работы.
max_hoursМаксимум часов присутствия; пустое значение — ограничение неприменимо.
busy_datesЗанятые календарные даты.
descriptionТекстовое описание профиля.
syntheticПолностью синтетический профиль.
city_imputedГород добавлен при подготовке датасета.
price_imputedЦена добавлена при подготовке датасета.

При загрузке значения categories, event_formats и languages, разделённые символом |, преобразуются в списки. busy_dates превращается в set строк формата YYYY-MM-DD; для сравнения используется request.date.isoformat().

CSV читается при импорте приложения один раз на процесс. Данные хранятся в pandas DataFrame. После изменения файла приложение нужно перезапустить.

База данных в текущем MVP не используется. API не изменяет каталог и не записывает заказы. HTML-превью и JSONL из комплекта задания не нужны для работы текущего CSV-загрузчика.

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

hack-dbab4d08-ai-job-hunter/
├── data/
│   └── hackathon-dataset-anonymized.csv
├── src/
│   └── hack_dbab4d08_ai_job_hunter/
│       ├── __init__.py
│       ├── main.py
│       ├── schemas.py
│       ├── data_loader.py
│       ├── filters.py
│       ├── ranking.py
│       ├── semantic.py
│       ├── explanations.py
│       ├── diagnostics.py
│       └── tests/
│           └── test_recommendations.py
├── .gitignore
├── .env                         # локальный файл; не коммитится
├── pyproject.toml
├── uv.lock
└── README.md
МодульОтветственность
main.pyМаршруты /health, /recommend, последовательность обработки, сборка ответа.
schemas.pyRecommendationRequest, ContractorRecommendation, RejectionStats, RecommendationResponse; проверка параметров.
data_loader.pyЧтение CSV и преобразование списковых полей.
filters.pyПроверка наличия категории и обязательные фильтры.
ranking.pyРасчёт компонентов рейтинга и сортировка.
semantic.pyOpenAI embeddings, кэш описаний и TF-IDF-fallback.
explanations.pyФормирование объяснений и извлечение фрагмента описания.
diagnostics.pyПодсчёт первых причин отказа и сообщение для пустой выдачи.
tests/test_recommendations.py внутри пакетаШесть автоматических тестов.

Настройки и числовые веса пока находятся в коде. Отдельного конфигурационного файла для рейтинга, порогов и лимита выдачи нет.

10. Тестирование

Используются pytest, FastAPI TestClient и monkeypatch. TestClient позволяет отправлять запросы к приложению без отдельного запуска Uvicorn; см. руководство FastAPI по тестированию.

uv run pytest -v
ТестЧто проверяет
test_busy_contractor_is_excludedЗанятый в указанную дату кандидат отсутствует после фильтрации.
test_over_budget_contractor_is_excludedВсе возвращённые фильтром цены не превышают бюджет.
test_api_returns_maximum_three_contractorsAPI возвращает не больше трёх карточек.
test_same_request_returns_same_orderДва одинаковых запроса дают одинаковый порядок ID.
test_category_not_foundОтсутствующая категория возвращает соответствующий статус и пустой список.
test_different_dates_change_resultsНа контролируемом наборе изменение даты меняет список результатов.

В API-тестах глобальный каталог подменяется тестовым DataFrame. В запросах нет preferences, поэтому эти проверки не обращаются к OpenAI и не требуют ключа.

При этом main.py загружает CSV при импорте — файл данных должен присутствовать и при запуске тестов, до подмены каталога.

Граница покрытия: шесть тестов не проверяют качество embeddings, работу fallback при сетевых ошибках, задержку внешнего API или повторяемость AI-оценок. Успешный результат 6 passed относится к перечисленным сценариям, а не ко всем возможным входам.

11. Демонстрационные запросы

Запросы ниже выполняются через POST /recommend. Для первой демонстрации рекомендуется последовательность: популярная категория → редкая категория → пустой результат.

A. Популярная категория и дополнительные пожелания

{
  "city": "Алматы",
  "date": "2026-10-15",
  "event_format": "свадьба",
  "category": "Ведущий",
  "budget": 1200000,
  "duration_hours": 8,
  "language": "русский",
  "preferences": "Хочу спокойного интеллигентного ведущего с ненавязчивым юмором"
}

Проверить: до трёх карточек, порядок по рейтингу, semantic_score, объяснения и выбранную дату. Затем можно изменить только пожелания на «Энергичный ведущий с импровизацией и интерактивами» и сравнить оценки. Смена пожеланий не обязана каждый раз менять порядок: на итог влияют и другие компоненты.

B. Редкая категория

{
  "city": "Алматы",
  "date": "2026-10-15",
  "event_format": "свадьба",
  "category": "Флорист",
  "budget": 300000,
  "language": "русский",
  "preferences": "Современное цветочное оформление свадьбы"
}

В исходном CSV по обязательным условиям этого запроса проходят два профиля. Сервис должен вернуть фактическое количество, а не добавлять неподходящего третьего.

C. Нет результата из-за бюджета

{
  "city": "Астана",
  "date": "2026-10-15",
  "event_format": "свадьба",
  "category": "Фотограф",
  "budget": 10000,
  "duration_hours": 8,
  "language": "русский"
}

Ожидается no_matches, пустой список и объяснение превышения бюджета. OpenAI для этого запроса не нужен.

D. Категория отсутствует в городе

{
  "city": "Астана",
  "date": "2026-10-15",
  "event_format": "свадьба",
  "category": "Декоратор",
  "budget": 3000000
}

Для исходного каталога ожидается category_not_found. Это отличается от существующей категории с неподходящими условиями.

E. Изменение даты

{
  "city": "Астана",
  "date": "2026-10-15",
  "event_format": "свадьба",
  "category": "Фотограф",
  "budget": 400000,
  "duration_hours": 8,
  "language": "русский"
}

Повторите запрос, заменив только дату на 2026-10-16. В исходном CSV для этих условий 15 октября проходят два профиля, а 16 октября подходящих свободных профилей нет. Ожидаемое изменение — с success на no_matches.

Для проверки стабильности отправьте один из этих запросов дважды без изменений и сравните порядок id.

F. Проверка валидации

В любом валидном запросе замените дату на 2027-01-01 или бюджет на -1. Ожидается HTTP 422, а не попытка рекомендовать кандидатов вне разрешённых параметров.

12. Секреты и передаваемые данные

API-ключ хранится только в .env или окружении процесса. В репозиторий можно добавить .env.example с пустым значением:

OPENAI_API_KEY=

Минимальные правила для секретов в .gitignore:

.env
.env.*
!.env.example
.venv/
__pycache__/

uv.lock, pyproject.toml, исходный CSV и тесты должны оставаться доступными для воспроизводимого запуска.

Проверка в PowerShell:

git check-ignore .env
git ls-files .env

Первая команда должна показать, что .env игнорируется; вторая — не выводить файл. Если он уже отслеживается, добавление правила не убирает его из индекса или истории. Это поведение описано в документации Git. Уже раскрытый ключ следует отозвать и заменить.

В OpenAI передаются тексты пожеланий и необходимые описания кандидатов. Код не отправляет весь CSV как файл. Не следует включать секреты и лишние персональные данные в preferences. Запросы к API могут тарифицироваться; актуальные условия относятся к используемому API-аккаунту.

13. Ограничения текущего MVP

Документация описывает реализованное поведение, а не планируемые возможности.

ОбластьТекущее ограничение
Менее трёх результатовВозвращается фактическое количество, но при success ещё нет отдельного сообщения, почему кандидатов меньше трёх.
ОбъясненияМогут быть длиннее требуемых заданием 1–2 предложений; фрагмент описания выбирается по позиции и длине, а не по релевантности.
Происхождение профилейsynthetic, city_imputed, price_imputed есть в CSV, но не отображаются в текущей карточке API.
ЯзыкУчитывается как предпочтение, а не обязательное условие. При отсутствии нужного языка отдельное предупреждение пока не формируется.
Режим AIНет поля semantic_backend; переключение на TF-IDF может изменить рейтинг.
СкоростьНет зафиксированного замера SLA, явного короткого таймаута и ограничения повторных попыток AI-вызовов.
КэшТолько память процесса; пожелания не кэшируются, общий кэш для нескольких процессов отсутствует.
Входные текстыНет исправления опечаток; пустые или нетокенизируемые тексты не покрыты текущими тестами TF-IDF.
КаталогОграничен предоставленными профилями и датами. Доступность не синхронизируется с реальным бронированием.
Продуктовая частьНет авторизации, оплаты, заявок, frontend и постоянного хранения пользовательских запросов.

Приоритет ближайших доработок: объяснять короткую выдачу, сократить и улучшить основания в карточках, показывать происхождение данных и режим AI, ограничить сетевое ожидание и расширить тесты.

14. Частые проблемы запуска

ПроблемаЧто проверить
No pyproject.toml foundКоманда должна выполняться из корня репозитория.
ModuleNotFoundError для пакетаВыполнить uv sync --locked; использовать запуск hack_dbab4d08_ai_job_hunter.main:app, а не старый путь app/main.py.
ImportError для модели схемыВ schemas.py должны сохраняться и входная RecommendationRequest, и выходная RecommendationResponse.
FileNotFoundError для CSVПуть и имя должны точно совпадать с data/hackathon-dataset-anonymized.csv.
422Сверить допустимые город, язык, формат, положительный бюджет и окно дат.
500Посмотреть traceback в терминале Uvicorn: Swagger показывает только общий ответ.
404 при открытии /Отдельная главная страница не создана; открыть /docs или /health.
Swagger показывает старую схемуПроверить response_model, остановить старый процесс, запустить текущий модуль и обновить страницу.
Переход на TF-IDFПроверить ключ, доступ к модели, сеть и ограничения API по сообщению в терминале.
/health работает, но AI не подтверждёнHealth endpoint проверяет приложение и каталог, а не OpenAI.

15. Краткое объяснение решения для демонстрации

Сначала мы проверяем обязательные ограничения обычным кодом: город, категорию, формат, бюджет, календарь и часы. Затем сравниваем дополнительные пожелания с описаниями оставшихся подрядчиков через OpenAI embeddings и добавляем сходство в общий рейтинг. Показываем максимум три карточки с основаниями выбора. Описания кэшируются, при ошибке API предусмотрен TF-IDF-режим. Если подходящих профилей нет, сервис различает отсутствие категории и отказ по условиям, а шесть тестов проверяют ключевые сценарии отбора и выдачи.

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

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

Vestcode04 5

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

  • 2026-09-23 17:25docs: document setup, API, AI ranking and testsVestcode04
  • 2026-09-23 17:18feat: last commitVestcode04
  • 2026-09-23 16:47feat: add AI semantic ranking with OpenAI embeddingsVestcode04
  • 2026-09-23 16:02second commit 16.00Vestcode04
  • 2026-09-23 15:22feat: setup project structureVestcode04
Структура репозитория (16 файлов)
.gitignore
.python-version
README.md
data/hackathon-dataset-anonymized.csv
pyproject.toml
src/hack_dbab4d08_ai_job_hunter/__init__.py
src/hack_dbab4d08_ai_job_hunter/data_loader.py
src/hack_dbab4d08_ai_job_hunter/diagnostics.py
src/hack_dbab4d08_ai_job_hunter/explanations.py
src/hack_dbab4d08_ai_job_hunter/filters.py
src/hack_dbab4d08_ai_job_hunter/main.py
src/hack_dbab4d08_ai_job_hunter/ranking.py
src/hack_dbab4d08_ai_job_hunter/schemas.py
src/hack_dbab4d08_ai_job_hunter/semantic.py
src/hack_dbab4d08_ai_job_hunter/tests/test_recommendations.py
uv.lock

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

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

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