ai-hackaton-backend/app/routers/vacancy_router.py

371 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, UploadFile
from pydantic import BaseModel
from app.models.vacancy import VacancyCreate, VacancyRead, VacancyUpdate
from app.services.vacancy_service import VacancyService
from app.services.vacancy_parser_service import vacancy_parser_service
router = APIRouter(prefix="/vacancies", tags=["vacancies"])
class VacancyParseResponse(BaseModel):
"""Ответ на запрос парсинга вакансии"""
message: str
parsed_data: dict | None = None
task_id: str | None = None
@router.post("/", response_model=VacancyRead)
async def create_vacancy(
vacancy: VacancyCreate, vacancy_service: VacancyService = Depends(VacancyService)
):
return await vacancy_service.create_vacancy(vacancy)
@router.get("/", response_model=list[VacancyRead])
async def get_vacancies(
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
active_only: bool = Query(False),
title: str | None = Query(None),
company_name: str | None = Query(None),
area_name: str | None = Query(None),
vacancy_service: VacancyService = Depends(VacancyService),
):
if any([title, company_name, area_name]):
return await vacancy_service.search_vacancies(
title=title,
company_name=company_name,
area_name=area_name,
skip=skip,
limit=limit,
)
if active_only:
return await vacancy_service.get_active_vacancies(skip=skip, limit=limit)
return await vacancy_service.get_all_vacancies(skip=skip, limit=limit)
@router.get("/{vacancy_id}", response_model=VacancyRead)
async def get_vacancy(
vacancy_id: int, vacancy_service: VacancyService = Depends(VacancyService)
):
vacancy = await vacancy_service.get_vacancy(vacancy_id)
if not vacancy:
raise HTTPException(status_code=404, detail="Vacancy not found")
return vacancy
@router.put("/{vacancy_id}", response_model=VacancyRead)
async def update_vacancy(
vacancy_id: int,
vacancy: VacancyUpdate,
vacancy_service: VacancyService = Depends(VacancyService),
):
updated_vacancy = await vacancy_service.update_vacancy(vacancy_id, vacancy)
if not updated_vacancy:
raise HTTPException(status_code=404, detail="Vacancy not found")
return updated_vacancy
@router.delete("/{vacancy_id}")
async def delete_vacancy(
vacancy_id: int, vacancy_service: VacancyService = Depends(VacancyService)
):
success = await vacancy_service.delete_vacancy(vacancy_id)
if not success:
raise HTTPException(status_code=404, detail="Vacancy not found")
return {"message": "Vacancy deleted successfully"}
@router.patch("/{vacancy_id}/archive", response_model=VacancyRead)
async def archive_vacancy(
vacancy_id: int, vacancy_service: VacancyService = Depends(VacancyService)
):
archived_vacancy = await vacancy_service.archive_vacancy(vacancy_id)
if not archived_vacancy:
raise HTTPException(status_code=404, detail="Vacancy not found")
return archived_vacancy
@router.post("/parse-file", response_model=VacancyParseResponse)
async def parse_vacancy_from_file(
file: UploadFile = File(...),
create_vacancy: bool = Query(False, description="Создать вакансию после парсинга"),
vacancy_service: VacancyService = Depends(VacancyService),
):
"""
Парсинг вакансии из загруженного файла (PDF, DOCX, RTF, TXT)
Args:
file: Файл вакансии
create_vacancy: Создать вакансию в БД после парсинга
Returns:
VacancyParseResponse: Результат парсинга
"""
# Проверяем формат файла
if not file.filename:
raise HTTPException(status_code=400, detail="Имя файла не указано")
file_extension = file.filename.lower().split('.')[-1]
supported_formats = ['pdf', 'docx', 'rtf', 'txt']
if file_extension not in supported_formats:
raise HTTPException(
status_code=400,
detail=f"Неподдерживаемый формат файла. Поддерживаются: {', '.join(supported_formats)}"
)
# Проверяем размер файла (максимум 10MB)
file_content = await file.read()
if len(file_content) > 10 * 1024 * 1024:
raise HTTPException(status_code=400, detail="Файл слишком большой (максимум 10MB)")
try:
# Извлекаем текст из файла
raw_text = vacancy_parser_service.extract_text_from_file(file_content, file.filename)
if not raw_text.strip():
raise HTTPException(status_code=400, detail="Не удалось извлечь текст из файла")
# Парсим с помощью AI
parsed_data = await vacancy_parser_service.parse_vacancy_with_ai(raw_text)
# Если нужно создать вакансию, создаем её
created_vacancy = None
if create_vacancy:
try:
vacancy_create = VacancyCreate(**parsed_data)
created_vacancy = await vacancy_service.create_vacancy(vacancy_create)
except Exception as e:
# Возвращаем парсинг, но предупреждаем об ошибке создания
return VacancyParseResponse(
message=f"Парсинг выполнен, но ошибка при создании вакансии: {str(e)}",
parsed_data=parsed_data
)
response_message = "Парсинг выполнен успешно"
if created_vacancy:
response_message += f". Вакансия создана с ID: {created_vacancy.id}"
return VacancyParseResponse(
message=response_message,
parsed_data=parsed_data
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при парсинге вакансии: {str(e)}")
@router.post("/parse-text", response_model=VacancyParseResponse)
async def parse_vacancy_from_text(
text: str = Query(..., description="Текст вакансии для парсинга"),
create_vacancy: bool = Query(False, description="Создать вакансию после парсинга"),
vacancy_service: VacancyService = Depends(VacancyService),
):
"""
Парсинг вакансии из текста
Args:
text: Текст вакансии
create_vacancy: Создать вакансию в БД после парсинга
Returns:
VacancyParseResponse: Результат парсинга
"""
if not text.strip():
raise HTTPException(status_code=400, detail="Текст вакансии не может быть пустым")
if len(text) > 50000: # Ограничение на длину текста
raise HTTPException(status_code=400, detail="Текст слишком длинный (максимум 50000 символов)")
try:
# Парсим с помощью AI
parsed_data = await vacancy_parser_service.parse_vacancy_with_ai(text)
# Если нужно создать вакансию, создаем её
created_vacancy = None
if create_vacancy:
try:
vacancy_create = VacancyCreate(**parsed_data)
created_vacancy = await vacancy_service.create_vacancy(vacancy_create)
except Exception as e:
return VacancyParseResponse(
message=f"Парсинг выполнен, но ошибка при создании вакансии: {str(e)}",
parsed_data=parsed_data
)
response_message = "Парсинг выполнен успешно"
if created_vacancy:
response_message += f". Вакансия создана с ID: {created_vacancy.id}"
return VacancyParseResponse(
message=response_message,
parsed_data=parsed_data
)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при парсинге вакансии: {str(e)}")
@router.get("/parse-formats")
async def get_supported_formats():
"""
Получить список поддерживаемых форматов файлов для парсинга вакансий
Returns:
dict: Информация о поддерживаемых форматах
"""
return {
"supported_formats": [
{
"extension": "pdf",
"description": "PDF документы",
"max_size_mb": 10
},
{
"extension": "docx",
"description": "Microsoft Word документы",
"max_size_mb": 10
},
{
"extension": "rtf",
"description": "Rich Text Format",
"max_size_mb": 10
},
{
"extension": "txt",
"description": "Текстовые файлы",
"max_size_mb": 10
}
],
"features": [
"Автоматическое извлечение текста из файлов",
"AI-парсинг структурированной информации",
"Создание вакансии в базе данных",
"Валидация данных"
]
}
@router.post("/parse-file-async", response_model=dict)
async def parse_vacancy_from_file_async(
file: UploadFile = File(...),
create_vacancy: str = Form("false", description="Создать вакансию после парсинга"),
):
"""
Асинхронный парсинг вакансии из загруженного файла (PDF, DOCX, RTF, TXT)
Args:
file: Файл вакансии
create_vacancy: Создать вакансию в БД после парсинга
Returns:
dict: ID задачи для отслеживания статуса
"""
import base64
from celery_worker.tasks import parse_vacancy_task
# Проверяем формат файла
if not file.filename:
raise HTTPException(status_code=400, detail="Имя файла не указано")
file_extension = file.filename.lower().split('.')[-1]
supported_formats = ['pdf', 'docx', 'rtf', 'txt']
if file_extension not in supported_formats:
raise HTTPException(
status_code=400,
detail=f"Неподдерживаемый формат файла. Поддерживаются: {', '.join(supported_formats)}"
)
# Проверяем размер файла (максимум 10MB)
file_content = await file.read()
if len(file_content) > 10 * 1024 * 1024:
raise HTTPException(status_code=400, detail="Файл слишком большой (максимум 10MB)")
try:
# Кодируем содержимое файла в base64 для передачи в Celery
file_content_base64 = base64.b64encode(file_content).decode('utf-8')
# Конвертируем строку в boolean
create_vacancy_bool = create_vacancy.lower() in ('true', '1', 'yes', 'on')
# Запускаем асинхронную задачу
task = parse_vacancy_task.delay(
file_content_base64=file_content_base64,
filename=file.filename,
create_vacancy=create_vacancy_bool
)
return {
"message": "Задача парсинга запущена",
"task_id": task.id,
"status": "pending",
"check_status_url": f"/api/v1/vacancies/parse-status/{task.id}"
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при запуске парсинга: {str(e)}")
@router.get("/parse-status/{task_id}")
async def get_parse_status(task_id: str):
"""
Получить статус асинхронной задачи парсинга вакансии
Args:
task_id: ID задачи
Returns:
dict: Статус задачи и результат (если завершена)
"""
from celery_worker.celery_app import celery_app
try:
task = celery_app.AsyncResult(task_id)
if task.state == 'PENDING':
response = {
'task_id': task_id,
'state': task.state,
'status': 'Задача ожидает выполнения...',
'progress': 0
}
elif task.state == 'PROGRESS':
response = {
'task_id': task_id,
'state': task.state,
'status': task.info.get('status', ''),
'progress': task.info.get('progress', 0)
}
elif task.state == 'SUCCESS':
response = {
'task_id': task_id,
'state': task.state,
'status': 'completed',
'progress': 100,
'result': task.result
}
else: # FAILURE
response = {
'task_id': task_id,
'state': task.state,
'status': 'failed',
'progress': 0,
'error': str(task.info)
}
return response
except Exception as e:
raise HTTPException(status_code=500, detail=f"Ошибка при получении статуса задачи: {str(e)}")