Ai Job Hunter
Backend-сервис (FastAPI, без фронтенда), возвращающий до трёх подрядчиков для мероприятия из каталога 66 профилей с объяснением выбора.
О проекте
Команда: 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.mdHackAlem 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_score | 20 × (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.py | RecommendationRequest, ContractorRecommendation, RejectionStats, RecommendationResponse; проверка параметров. |
data_loader.py | Чтение CSV и преобразование списковых полей. |
filters.py | Проверка наличия категории и обязательные фильтры. |
ranking.py | Расчёт компонентов рейтинга и сортировка. |
semantic.py | OpenAI 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_contractors | API возвращает не больше трёх карточек. |
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-режим. Если подходящих профилей нет, сервис различает отсутствие категории и отказ по условиям, а шесть тестов проверяют ключевые сценарии отбора и выдачи.
Авторы коммитов
История коммитов
- 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 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.