AAYMA
RU
Войти

Career Quest

Полноценное веб-приложение: сотрудник видит траекторию до следующего грейда и 1–3 рекомендованных мероприятия с объяснением по факторам, HR видит…

Доведённый продукт
Команда «AI-TungTungSolution» 2💬 0 комментариевс 23 сентября 2026 г.

О проекте

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

Career Quest

AI-навигатор развития сотрудника. Кейс Halyk Bank, трек «AI для корпоративных продуктов», HackAlem AI.

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

Ключевое решение: LLM не считает числа и не придумывает мероприятия. Детерминированное ядро формирует список допустимых кандидатов с точными оценками, LLM выбирает из них 1–3 и обязана процитировать факты; каждый факт сверяется с ядром перед показом. Не сошлось — ответ отклоняется и показывается разбор по правилам. Поэтому объяснение нельзя «сгаллюцинировать».


Содержание


Быстрый старт

Требуется 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), поэтому перезапуск контейнера безопасен.


Основной сценарий

  1. Вход. Выбрать сотрудника из списка либо войти как HR. Роль определяет, что доступно.
  2. Профиль и траектория. Роль, грейд, стаж, навыки 0–5, готовность к следующему грейду в процентах, разрывы по навыкам с пометкой критичных, история участия.
  3. Рекомендация. Кнопка запускает AI-агента. Возвращаются 1–3 мероприятия, у каждого — объяснение по факторам: какие навыки сдвинутся и насколько, как изменится готовность, сколько похожих активностей сотрудник завершил и сколько пропустил, насколько формат совместим с его режимом работы.
  4. Выполнение. Отметить активность выполненной — навыки пересчитываются по правилу из датасета (gain, max_level), готовность и траектория сдвигаются, кэш сбрасывается. Рядом есть «Не интересно» — отказ убирает мероприятие из будущих подборок.
  5. 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/profileemployeeпрофиль, навыки, траектория, история
POST/api/employee/recommendationsemployeeзапуск AI-агента
POST/api/employee/events/{event_id}/completeemployeeотметить выполненной
POST/api/employee/events/{event_id}/dismissemployeeотказаться от мероприятия
GET/api/hr/overviewhrсрез по организации и проседающим навыкам
POST/api/hr/datasethrзагрузка датасета жюри
GET/api/hr/agent-runshrжурнал запусков AI

Типы фронтенда генерируются из OpenAPI и не правятся руками:

cd frontend && npm run gen:api   # бэкенд должен быть запущен

Данные

Стартовый кит лежит в backend/data/, дата снимка — 2026-10-01.

ФайлОбъёмСодержимое
employees.json200 профилейроль, грейд, стаж, режим работы, язык, навыки 0–5, дата аттестации
events.json40 мероприятийтип, формат, аудитория, развиваемые навыки с gain и max_level, пререквизиты, ближайшие даты
skills.json60 навыков, 32 профиля роль×грейдhard/soft, категории, требования по грейдам с пометкой критичных
activity_history.csv2743 записи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-viewGET /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.

  1. Railway → New Project → Deploy from GitHub repo.
  2. Добавить в проект PostgreSQL.
  3. Переменные сервиса приложения:
    • DATABASE_URL=${{Postgres.DATABASE_URL}}
    • OPENAI_API_KEY
    • OPENAI_MODEL
    • SESSION_SECRET — минимум 32 символа
    • SESSION_COOKIE_SECURE=true
  4. Settings → Deploy → Pre-deploy command: alembic upgrade head.
  5. Settings → Networking → Generate Domain.
  6. Проверить <домен>/api/health.

Railway выдаёт обычный postgresql://; app/db.py подставляет драйвер postgresql+asyncpg, а migrations/env.py переиспользует тот же URL — приложение и Alembic работают без правок.


Как мы использовали AI

  • Codex писал код приложения по AGENTS.md.
  • Чат-ассистент — оценка идей, черновики плана и текст питча.
  • Люди — постановка задачи, проектирование схемы, ревью каждой миграции, проверка каждой функции и разметка для оценки качества рекомендаций.
Звёзды
⭐ 0
Форки
0
Язык
Python
Последний push
в прошлом месяце
Коммиты
5
Авторы
2
Файлы
103
Ветки
1

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

daniil 3didar 2

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

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

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

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