Career Quest
Полноценное веб-приложение: сотрудник видит траекторию до следующего грейда и 1–3 рекомендованных мероприятия с объяснением по факторам, HR видит…
О проекте
Команда: AI-TungTungSolution
Кейс HackAlem.ai: Менеджмент — Halyk Bank · кейс 1
Стадия: Доведённый продукт
Задача кейса: «Career Quest»: AI-навигатор, связывающий навыки сотрудника с шагами развития
Суть
Полноценное веб-приложение: сотрудник видит траекторию до следующего грейда и 1–3 рекомендованных мероприятия с объяснением по факторам, HR видит проседающие компетенции и журнал запусков AI.
Как устроено
Детерминированное ядро (trajectory, progress, scoring) пересчитывает эффективные навыки после аттестации, отсеивает недопустимые мероприятия по 9 причинам и ранжирует кандидатов по формуле 0.40·gap_closure + 0.25·grade_relevance + 0.20·history_affinity (сглаживание Лапласа) + 0.15·format_fit. LLM-агент на Responses API обязан вызвать 4 инструмента, привязанных к текущему сотруднику, и выбрать 1–3 кандидата с точными фактами. validate_selection сверяет каждое число, при расхождении ответ отклоняется и показывается fallback по правилам. Текст объяснений собирается кодом из проверенных фактов.
Ключевые решения
- взвешенный 4-факторный скоринг
- сглаживание Лапласа
- function calling (4 инструмента)
- структурированный вывод
- верификация фактов LLM
- rule-based fallback
- LRU-кэш
- HMAC-сессии
Особенности
LLM здесь не придумывает объяснения: она выбирает из детерминированных кандидатов, каждый процитированный факт сверяется с ядром. Тесты «ловушечных» профилей (test_trap_profiles) показывают, где правило «бери самый слабый навык» ошибается. Инструменты агента не дают доступа к чужим данным.
Стек
Python · FastAPI · PostgreSQL · Alembic · uv · React · TanStack Query · Vite · Docker · Playwright
Модели
OpenAI (Responses API)
Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-1c82c8eb-ai-tungtungsolution.
README
README.mdCareer Quest
AI-навигатор развития сотрудника. Кейс Halyk Bank, трек «AI для корпоративных продуктов», HackAlem AI.
Сотрудник видит свою траекторию до следующего грейда и получает 1–3 рекомендованных шага развития с обоснованием, опирающимся на четыре независимых фактора. HR видит, какие компетенции проседают в организации, и может проверить каждую выданную AI рекомендацию по сохранённому следу запуска.
Ключевое решение: LLM не считает числа и не придумывает мероприятия. Детерминированное ядро формирует список допустимых кандидатов с точными оценками, LLM выбирает из них 1–3 и обязана процитировать факты; каждый факт сверяется с ядром перед показом. Не сошлось — ответ отклоняется и показывается разбор по правилам. Поэтому объяснение нельзя «сгаллюцинировать».
Содержание
- Быстрый старт
- Основной сценарий
- Как загрузить проверочные профили жюри
- Архитектура
- Как считается рекомендация
- AI-слой
- API
- Данные
- Безопасность и приватность
- Тесты и проверки
- Соответствие must-have
- Стек
- Деплой
Быстрый старт
Требуется Docker, uv и Node 22.12+.
# 1. База данных
docker compose up -d
# 2. Конфигурация
cd backend
cp .env.example .env
# заполнить OPENAI_API_KEY, OPENAI_MODEL
# SESSION_SECRET — минимум 32 символа: openssl rand -hex 32
# 3. Схема и данные
uv run alembic upgrade head
uv run python -m app.seed
# 4. Бэкенд → http://127.0.0.1:8000
uv run fastapi dev app/main.py
# 5. Фронтенд (в другом терминале) → http://127.0.0.1:5173
cd frontend && npm ci && npm run dev
Проверка: curl http://127.0.0.1:8000/api/health → {"status":"ok","database":"ok"}.
Одним контейнером
Прод-образ собирает фронтенд и отдаёт его из FastAPI:
docker build -t career-quest .
docker run --rm --network host --env-file backend/.env career-quest
Открыть http://127.0.0.1:8000 — и API, и интерфейс на одном порту. Сидирование данных идемпотентно (upsert), поэтому перезапуск контейнера безопасен.
Основной сценарий
- Вход. Выбрать сотрудника из списка либо войти как HR. Роль определяет, что доступно.
- Профиль и траектория. Роль, грейд, стаж, навыки 0–5, готовность к следующему грейду в процентах, разрывы по навыкам с пометкой критичных, история участия.
- Рекомендация. Кнопка запускает AI-агента. Возвращаются 1–3 мероприятия, у каждого — объяснение по факторам: какие навыки сдвинутся и насколько, как изменится готовность, сколько похожих активностей сотрудник завершил и сколько пропустил, насколько формат совместим с его режимом работы.
- Выполнение. Отметить активность выполненной — навыки пересчитываются по правилу из
датасета (
gain,max_level), готовность и траектория сдвигаются, кэш сбрасывается. Рядом есть «Не интересно» — отказ убирает мероприятие из будущих подборок. - HR-экран. Срез по организации: сколько сотрудников имеют траекторию, средняя готовность, разрезы по департаментам, топ проседающих компетенций, журнал запусков AI с полным следом вызовов инструментов и меткой, срабатывал ли fallback.
Как загрузить проверочные профили жюри
Решение принимает дополнительные профили и историю в формате стартового кита.
Через интерфейс: войти как HR → раздел загрузки датасета → выбрать четыре файла
(employees.json, events.json, skills.json, activity_history.csv) → загрузить.
Через API:
curl -X POST http://127.0.0.1:8000/api/hr/dataset \
-b cookies.txt \
-F [email protected] \
-F [email protected] \
-F [email protected] \
-F activity_history=@activity_history.csv
Загрузка выполняется как upsert в одной транзакции: существующие записи обновляются,
новые добавляются, кэш рекомендаций очищается полностью. Файлы валидируются до записи —
при несоответствии схеме возвращается 422 с описанием проблемы, база не меняется.
В ответе приходят версия датасета, as_of_date и число записей по каждой таблице.
Архитектура
Браузер ── React SPA (TanStack Query, openapi-fetch, типы из OpenAPI)
│
└─ /api ── FastAPI
├── auth.py HMAC-подписанная сессия, роли employee / hr
├── employee_api.py профиль, рекомендации, выполнение, отказ
├── hr_api.py срез по организации, загрузка датасета, аудит
│
├── recommendation_data.py снимок входов одного сотрудника
├── trajectory.py следующий грейд, разрывы, готовность ── чистые
├── progress.py правило роста навыка, предпросмотр ── функции
├── scoring.py допуск + ранжирование по 4 факторам ── без БД и AI
│
├── agent.py инструменты, верификация, fallback
├── recommendation_service.py кэш и аудит запусков
└── ingest.py парсинг и валидация датасета
│
PostgreSQL
Модули trajectory.py, progress.py и scoring.py не зависят ни от базы, ни от конфигурации,
ни от OpenAI — их можно вызывать и тестировать изолированно. Именно поэтому расчёт
воспроизводим, а AI-слой остаётся тонкой надстройкой, а не источником истины.
Как считается рекомендация
Эффективные навыки
Уровни в employees.json зафиксированы на дату аттестации (last_review_date). Поверх них
проигрываются только завершённые активности строго после этой даты — иначе прогресс
учитывался бы дважды. Правило роста берётся из датасета: min(gain, max_level − current).
Операция идемпотентна: повторный прогон тех же входов даёт тот же результат.
Допуск
Мероприятие исключается из кандидатов, если верно хотя бы одно:
mandatory (обязательное — не предмет добровольной рекомендации) · wrong_role · wrong_grade ·
prerequisites_not_met · already_completed · in_progress · dismissed (сотрудник отказался) ·
no_effective_gain (навыки уже на потолке) · no_upcoming_session (нет предстоящей даты).
Ранжирование — четыре фактора
score = 0.40 · gap_closure
+ 0.25 · grade_relevance
+ 0.20 · history_affinity
+ 0.15 · format_fit
| Фактор | Что измеряет | Формула |
|---|---|---|
gap_closure | доля закрытого разрыва до следующего грейда | закрытые единицы разрыва / все единицы разрыва |
grade_relevance | вес критичных навыков целевого грейда | (закрытые + закрытые критичные) / (2 · весь прирост) |
history_affinity | поведение в похожих добровольных активностях | (завершено + 1) / (завершено + неуспешно + 2) |
format_fit | совместимость формата с режимом работы | удалённый: офлайн 0, иначе 1; гибрид: 1; офис: офлайн 1, иначе 0.75 |
Похожей считается активность того же типа либо развивающая общий навык. Обязательные активности
в историю аффинити не входят — участие в них не добровольно. Активности «в процессе» не считаются
неудачами: исход ещё не наступил. Сглаживание +1 / +2 (правило Лапласа) не даёт одному пропуску
обнулить фактор. format_fit — эвристика по режиму работы, а не заявленное предпочтение,
и так и подписан в интерфейсе.
Почему однофакторное правило здесь ошибается
Правило «бери самый слабый навык» на проверочных профилях промахивается, и это зафиксировано
тестами в backend/tests/test_trap_profiles.py:
- Слабый навык, который сотрудник трижды пропускал. Public Speaking ниже всех, но три
пропуска подряд роняют
history_affinity, а навык не критичен для следующего грейда —grade_relevanceтоже низкий. Наверх выходит System Design. - Слабый навык, нерелевантный грейду. Мероприятие даёт больший абсолютный прирост, но
gap_closureиgrade_relevanceу него равны нулю: этот навык не входит в требования целевого грейда. Оно не выигрывает. - Потолок навыка. Активность, упирающаяся в
max_level, отсеивается на допуске (no_effective_gain), а не занижается уже после ранжирования.
AI-слой
Агент построен на OpenAI Responses API с вызовом инструментов и структурированным выводом.
Четыре инструмента, каждый уже привязан к текущему сотруднику и принимает пустой объект — LLM не может запросить чужие данные:
| Инструмент | Возвращает |
|---|---|
get_profile | профиль и эффективные уровни навыков |
get_trajectory | требования следующего грейда, разрывы, критичные навыки |
get_history_summary | историю участия только этого сотрудника |
get_candidates | допустимых кандидатов с точными значениями факторов |
Контракт ответа. Модель обязана вызвать все четыре инструмента и вернуть 1–3 разных мероприятия, для каждого — не менее трёх различных факторов с точными значениями, полный список изменений навыков, готовность до и после, счётчики истории.
Верификация. Перед показом validate_selection сверяет каждое число с детерминированным
кандидатом. Несуществующее мероприятие, недопустимое мероприятие, дубль, расхождение хотя бы
в одном значении — ответ отклоняется целиком.
Текст для пользователя не генерируется свободно. Объяснения собираются функцией
explain_factor из уже проверенных фактов. LLM отвечает за выбор и приоритизацию,
формулировки — за детерминированным кодом.
Fallback. При таймауте, исчерпании раундов, ошибке OpenAI или непройденной проверке показываются три верхних кандидата по правилам, с явной пометкой в интерфейсе: «ИИ недоступен или ответ не прошёл проверку». Пустой выдачи и молчаливой деградации нет.
Границы. Таймаут 10 секунд, максимум 4 раунда вызовов — это верхняя граница отклика из ТЗ. Названия и описания в данных объявлены данными, а не инструкциями; запрос пользователя не может изменить инструменты, правила допуска или чужие данные.
Кэш. LRU с TTL (по умолчанию 300 с, 256 записей). Ключ — хэш всего снимка входов, текста запроса и имени модели: выполненная активность, отказ или загрузка датасета жюри меняют ключ сами по себе, поэтому устаревший ответ не может быть показан. Fallback-ответы не кэшируются — восстановившийся сервис пробуется на следующем запросе.
Аудит. Каждый запуск пишется в agent_runs: запрос, ответ, полный след вызовов
инструментов, задержка, признак fallback. HR читает журнал через GET /api/hr/agent-runs —
это и есть проверяемость: видно, на каких фактах была построена каждая рекомендация.
API
Все маршруты под /api. Интерактивная документация — /docs при запущенном бэкенде.
| Метод | Путь | Роль | Назначение |
|---|---|---|---|
GET | /api/health | — | состояние сервиса и базы |
POST | /api/session | — | вход как сотрудник или HR |
GET | /api/session | любая | текущая сессия |
DELETE | /api/session | любая | выход |
GET | /api/employees | — | список сотрудников для переключателя |
GET | /api/employee/profile | employee | профиль, навыки, траектория, история |
POST | /api/employee/recommendations | employee | запуск AI-агента |
POST | /api/employee/events/{event_id}/complete | employee | отметить выполненной |
POST | /api/employee/events/{event_id}/dismiss | employee | отказаться от мероприятия |
GET | /api/hr/overview | hr | срез по организации и проседающим навыкам |
POST | /api/hr/dataset | hr | загрузка датасета жюри |
GET | /api/hr/agent-runs | hr | журнал запусков AI |
Типы фронтенда генерируются из OpenAPI и не правятся руками:
cd frontend && npm run gen:api # бэкенд должен быть запущен
Данные
Стартовый кит лежит в backend/data/, дата снимка — 2026-10-01.
| Файл | Объём | Содержимое |
|---|---|---|
employees.json | 200 профилей | роль, грейд, стаж, режим работы, язык, навыки 0–5, дата аттестации |
events.json | 40 мероприятий | тип, формат, аудитория, развиваемые навыки с gain и max_level, пререквизиты, ближайшие даты |
skills.json | 60 навыков, 32 профиля роль×грейд | hard/soft, категории, требования по грейдам с пометкой критичных |
activity_history.csv | 2743 записи | 24 месяца участия, включая пропуски, отказы и незавершённые |
Данные синтетические. README датасета доступен на английском, казахском и русском
(backend/data/README*.md).
Безопасность и приватность
- Разграничение прав. Идентичность берётся только из подписанной сессии, никогда из тела
запроса.
require_employeeиrequire_hr— отдельные зависимости FastAPI. Сотрудник не может запросить чужой профиль:employee_idв запросе физически отсутствует. - Сессии. Cookie, подписанная HMAC-SHA256, с временем жизни;
httponly,samesite=lax,secureвключается в проде. Секрет короче 32 символов не принимается — сервис не стартует. - Приватность. Данные о вовлечённости конкретного сотрудника не видны другим сотрудникам. HR-экран показывает только агрегаты по департаментам и компетенциям.
- Без публичных рейтингов. Сравнения сотрудников по результативности в продукте нет — это прямой запрет ТЗ и известный демотиватор для большинства.
- Добровольность. Обязательные мероприятия исключены из рекомендаций на уровне допуска
(
mandatory), а отказ от мероприятия — поддержанное действие, а не обход интерфейса. - Секреты. Вся конфигурация через
app/config.pyи переменные окружения..envне в репозитории.
Тесты и проверки
62 теста, включая проверочные профили, ловушки однофакторного правила, контракт агента и сценарии fallback.
cd backend && uv run pytest # тесты
cd backend && uv run ruff check . && uv run pyright # линтер и типы
cd frontend && npm run build # сборка и проверка типов
cd frontend && npm run test:e2e # Playwright
Соответствие must-have
| Требование | Где реализовано |
|---|---|
| Профиль и карьерная траектория | GET /api/employee/profile — роль, грейд, навыки, история, доступные шаги |
| AI-рекомендация следующего шага | POST /api/employee/recommendations — 1–3 мероприятия |
| Объяснение опирается минимум на три фактора | контракт требует ≥3 различных факторов из четырёх; значения сверяются с ядром |
| Обновление прогресса | POST /api/employee/events/{id}/complete — навыки, готовность и траектория пересчитываются |
| Простой HR-view | GET /api/hr/overview — проседающие компетенции, охват траекториями, участие |
| Загрузка проверочных профилей | POST /api/hr/dataset — четыре файла в формате кита, валидация до записи |
| Отклик AI до 10 секунд | таймаут агента 10 с, максимум 4 раунда, кэш на повторных запросах |
Стек
Бэкенд: Python 3.12, FastAPI, Pydantic v2, SQLAlchemy 2.0 async + asyncpg, Alembic, PostgreSQL 18, OpenAI Python SDK (Responses API). Пакетный менеджер — uv.
Фронтенд: React 19 + TypeScript, Vite, Tailwind CSS v4, shadcn/ui, TanStack Query, openapi-fetch. Одна страница, без роутера.
Прод: один Docker-образ, FastAPI отдаёт собранный фронтенд. Railway с managed Postgres.
Деплой
Railway, из GitHub-репозитория, по корневому Dockerfile.
- Railway → New Project → Deploy from GitHub repo.
- Добавить в проект PostgreSQL.
- Переменные сервиса приложения:
DATABASE_URL=${{Postgres.DATABASE_URL}}OPENAI_API_KEYOPENAI_MODELSESSION_SECRET— минимум 32 символаSESSION_COOKIE_SECURE=true
- Settings → Deploy → Pre-deploy command:
alembic upgrade head. - Settings → Networking → Generate Domain.
- Проверить
<домен>/api/health.
Railway выдаёт обычный postgresql://; app/db.py подставляет драйвер postgresql+asyncpg,
а migrations/env.py переиспользует тот же URL — приложение и Alembic работают без правок.
Как мы использовали AI
- Codex писал код приложения по
AGENTS.md. - Чат-ассистент — оценка идей, черновики плана и текст питча.
- Люди — постановка задачи, проектирование схемы, ревью каждой миграции, проверка каждой функции и разметка для оценки качества рекомендаций.
Авторы коммитов
История коммитов
- 2026-09-23 17:34feat: connect frontend to Career Quest backenddaniil
- 2026-09-23 16:47feat: add employee sessions and HR APIsdidar
- 2026-09-23 16:28feat: add Career Quest domain logic and recommendation agentdidar
- 2026-09-23 15:39Build Career Quest data foundationdaniil
- 2026-09-23 14:17Bootstrap backend skeletondaniil
Структура репозитория (103 файлов)
.dockerignore .gitignore AGENTS.md CLAUDE.md Dockerfile README.md backend/.env.example backend/.python-version backend/alembic.ini backend/app/__init__.py backend/app/agent.py backend/app/auth.py backend/app/config.py backend/app/db.py backend/app/employee_api.py backend/app/hr_api.py backend/app/ingest.py backend/app/llm.py backend/app/main.py backend/app/models.py backend/app/progress.py backend/app/recommendation_data.py backend/app/recommendation_service.py backend/app/scoring.py backend/app/seed.py backend/app/trajectory.py backend/data/README.kz.md backend/data/README.md backend/data/README.ru.md backend/data/activity_history.csv backend/data/employees.json backend/data/events.json backend/data/skills.json backend/migrations/README backend/migrations/env.py backend/migrations/script.py.mako backend/migrations/versions/10eccd36a138_create_agent_runs.py backend/migrations/versions/eab2d772e58c_add_career_quest_schema.py backend/pyproject.toml backend/tests/agent_helpers.py backend/tests/conftest.py backend/tests/test_agent.py backend/tests/test_auth.py backend/tests/test_employee_api.py backend/tests/test_hr_api.py backend/tests/test_organizer_data.py backend/tests/test_progress.py backend/tests/test_recommendation_data.py backend/tests/test_recommendation_service.py backend/tests/test_scoring.py backend/tests/test_trajectory.py backend/tests/test_trap_profiles.py backend/uv.lock compose.yaml docs/bootstrap.md docs/case.md docs/plan.md frontend/.gitignore frontend/.oxlintrc.json frontend/INTEGRATION.md frontend/README.md frontend/components.json frontend/index.html frontend/package-lock.json frontend/package.json frontend/playwright.config.ts frontend/public/quest.svg frontend/scripts/gen-api.mjs frontend/src/App.tsx frontend/src/api/client.ts frontend/src/api/schema.d.ts frontend/src/components/ui/alert.tsx frontend/src/components/ui/badge.tsx frontend/src/components/ui/button.tsx frontend/src/components/ui/card.tsx frontend/src/components/ui/collapsible.tsx frontend/src/components/ui/command.tsx frontend/src/components/ui/dialog.tsx frontend/src/components/ui/input-group.tsx frontend/src/components/ui/input.tsx frontend/src/components/ui/label.tsx frontend/src/components/ui/popover.tsx frontend/src/components/ui/progress.tsx frontend/src/components/ui/select.tsx frontend/src/components/ui/skeleton.tsx frontend/src/components/ui/table.tsx frontend/src/components/ui/tabs.tsx frontend/src/components/ui/textarea.tsx frontend/src/features/employee-view.tsx frontend/src/features/format.ts frontend/src/features/hr-view.tsx frontend/src/features/queries.ts frontend/src/features/session-switcher.tsx frontend/src/features/shared.tsx frontend/src/features/types.ts frontend/src/index.css frontend/src/lib/utils.ts frontend/src/main.tsx frontend/tests/flows.spec.ts frontend/tsconfig.app.json frontend/tsconfig.json frontend/tsconfig.node.json frontend/vite.config.ts
Обсуждение 0 комментариев
Войдите, чтобы комментироватьПока нет комментариев — будьте первым.