ai-hackaton-backend/app/services/ai_interviewer_service.py
2025-09-03 14:36:27 +05:00

294 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.

import asyncio
import json
import logging
from typing import Dict, Optional, List
from datetime import datetime
from livekit import api, rtc
from rag.settings import settings
from app.services.interview_service import InterviewRoomService
logger = logging.getLogger(__name__)
class AIInterviewerService:
"""Сервис AI интервьюера, который подключается к LiveKit комнате как участник"""
def __init__(self, interview_session_id: int, resume_data: Dict):
self.interview_session_id = interview_session_id
self.resume_data = resume_data
self.room: Optional[rtc.Room] = None
self.audio_source: Optional[rtc.AudioSource] = None
self.conversation_history: List[Dict] = []
self.current_question_index = 0
self.interview_questions = []
async def connect_to_room(self, room_name: str, token: str):
"""Подключение AI агента к LiveKit комнате"""
try:
self.room = rtc.Room()
# Настройка обработчиков событий
self.room.on("participant_connected", self.on_participant_connected)
self.room.on("track_subscribed", self.on_track_subscribed)
self.room.on("data_received", self.on_data_received)
# Подключение к комнате
await self.room.connect(settings.livekit_url, token)
logger.info(f"AI agent connected to room: {room_name}")
# Создание аудио источника для TTS
self.audio_source = rtc.AudioSource(sample_rate=16000, num_channels=1)
track = rtc.LocalAudioTrack.create_audio_track("ai_voice", self.audio_source)
# Публикация аудио трека
await self.room.local_participant.publish_track(track, rtc.TrackPublishOptions())
# Генерация первого вопроса
await self.generate_interview_questions()
await self.start_interview()
except Exception as e:
logger.error(f"Error connecting to room: {str(e)}")
raise
async def on_participant_connected(self, participant: rtc.RemoteParticipant):
"""Обработка подключения пользователя"""
logger.info(f"Participant connected: {participant.identity}")
# Можем отправить приветственное сообщение
await self.send_message({
"type": "ai_speaking_start"
})
async def on_track_subscribed(
self,
track: rtc.Track,
publication: rtc.RemoteTrackPublication,
participant: rtc.RemoteParticipant
):
"""Обработка получения аудио трека от пользователя"""
if track.kind == rtc.TrackKind.KIND_AUDIO:
logger.info("Subscribed to user audio track")
# Настройка обработки аудио для STT
audio_stream = rtc.AudioStream(track)
asyncio.create_task(self.process_user_audio(audio_stream))
async def on_data_received(self, data: bytes, participant: rtc.RemoteParticipant):
"""Обработка сообщений от фронтенда"""
try:
message = json.loads(data.decode())
await self.handle_frontend_message(message)
except Exception as e:
logger.error(f"Error processing data message: {str(e)}")
async def handle_frontend_message(self, message: Dict):
"""Обработка сообщений от фронтенда"""
msg_type = message.get("type")
if msg_type == "start_interview":
await self.start_interview()
elif msg_type == "end_interview":
await self.end_interview()
elif msg_type == "user_finished_speaking":
# Пользователь закончил говорить, можем обрабатывать его ответ
pass
async def process_user_audio(self, audio_stream: rtc.AudioStream):
"""Обработка аудио от пользователя через STT"""
try:
# Здесь будет интеграция с STT сервисом
# Пока заглушка
async for audio_frame in audio_stream:
# TODO: Отправить аудио в STT (Whisper API)
# user_text = await self.speech_to_text(audio_frame)
# if user_text:
# await self.process_user_response(user_text)
pass
except Exception as e:
logger.error(f"Error processing user audio: {str(e)}")
async def speech_to_text(self, audio_data: bytes) -> Optional[str]:
"""Конвертация речи в текст через Whisper API"""
# TODO: Интеграция с OpenAI Whisper или другим STT сервисом
pass
async def text_to_speech(self, text: str) -> bytes:
"""Конвертация текста в речь через TTS сервис"""
# TODO: Интеграция с ElevenLabs, OpenAI TTS или другим сервисом
pass
async def generate_interview_questions(self):
"""Генерация вопросов для интервью на основе резюме"""
try:
from rag.registry import registry
chat_model = registry.get_chat_model()
# Используем существующую логику генерации вопросов
questions_prompt = f"""
Сгенерируй 8 вопросов для голосового собеседования кандидата.
РЕЗЮМЕ КАНДИДАТА:
Имя: {self.resume_data.get('name', 'Не указано')}
Навыки: {', '.join(self.resume_data.get('skills', []))}
Опыт работы: {self.resume_data.get('total_years', 0)} лет
Образование: {self.resume_data.get('education', 'Не указано')}
ВАЖНО:
1. Вопросы должны быть короткими и ясными для голосового формата
2. Начни с простого приветствия и представления
3. Каждый вопрос должен занимать не более 2-3 предложений
4. Используй естественную разговорную речь
Верни только JSON массив строк с вопросами:
["Привет! Расскажи немного о себе", "Какой у тебя опыт в...", ...]
"""
from langchain.schema import HumanMessage, SystemMessage
messages = [
SystemMessage(content="Ты HR интервьюер. Говори естественно и дружелюбно."),
HumanMessage(content=questions_prompt)
]
response = chat_model.get_llm().invoke(messages)
response_text = response.content.strip()
# Парсим JSON ответ
if response_text.startswith('[') and response_text.endswith(']'):
self.interview_questions = json.loads(response_text)
else:
# Fallback вопросы
self.interview_questions = [
"Привет! Расскажи немного о себе и своем опыте",
"Что тебя привлекает в этой позиции?",
"Расскажи о своем самом значимом проекте",
"Какие технологии ты используешь в работе?",
"Как ты решаешь сложные задачи?",
"Есть ли у тебя вопросы ко мне?"
]
logger.info(f"Generated {len(self.interview_questions)} interview questions")
except Exception as e:
logger.error(f"Error generating questions: {str(e)}")
# Используем базовые вопросы
self.interview_questions = [
"Привет! Расскажи о своем опыте",
"Что тебя интересует в этой позиции?",
"Есть ли у тебя вопросы?"
]
async def start_interview(self):
"""Начало интервью"""
if not self.interview_questions:
await self.generate_interview_questions()
# Отправляем первый вопрос
await self.ask_next_question()
async def ask_next_question(self):
"""Задать следующий вопрос"""
if self.current_question_index >= len(self.interview_questions):
await self.end_interview()
return
question = self.interview_questions[self.current_question_index]
# Отправляем сообщение фронтенду
await self.send_message({
"type": "question",
"text": question,
"questionNumber": self.current_question_index + 1
})
# Конвертируем в речь и воспроизводим
# TODO: Реализовать TTS
# audio_data = await self.text_to_speech(question)
# await self.play_audio(audio_data)
self.current_question_index += 1
logger.info(f"Asked question {self.current_question_index}: {question}")
async def process_user_response(self, user_text: str):
"""Обработка ответа пользователя"""
# Сохраняем ответ в историю
self.conversation_history.append({
"type": "user_response",
"text": user_text,
"timestamp": datetime.utcnow().isoformat(),
"question_index": self.current_question_index - 1
})
# Можем добавить анализ ответа через LLM
# И решить - задать уточняющий вопрос или перейти к следующему
# Пока просто переходим к следующему вопросу
await asyncio.sleep(1) # Небольшая пауза
await self.ask_next_question()
async def send_message(self, message: Dict):
"""Отправка сообщения фронтенду"""
if self.room:
data = json.dumps(message).encode()
await self.room.local_participant.publish_data(data)
async def play_audio(self, audio_data: bytes):
"""Воспроизведение аудио через LiveKit"""
if self.audio_source:
# TODO: Конвертировать audio_data в нужный формат и отправить
pass
async def end_interview(self):
"""Завершение интервью"""
await self.send_message({
"type": "interview_complete",
"summary": f"Interview completed with {len(self.conversation_history)} responses"
})
# Сохраняем транскрипт в базу данных
transcript = json.dumps(self.conversation_history, ensure_ascii=False, indent=2)
# TODO: Обновить interview_session в БД с транскриптом
logger.info("Interview completed")
# Отключение от комнаты
if self.room:
await self.room.disconnect()
class AIInterviewerManager:
"""Менеджер для управления AI интервьюерами"""
def __init__(self):
self.active_sessions: Dict[int, AIInterviewerService] = {}
async def start_interview_session(self, interview_session_id: int, room_name: str, resume_data: Dict):
"""Запуск AI интервьюера для сессии"""
try:
# Создаем токен для AI агента
from app.services.interview_service import InterviewRoomService
# Нужно создать специальный токен для AI агента
ai_interviewer = AIInterviewerService(interview_session_id, resume_data)
# TODO: Генерировать токен для AI агента
# ai_token = generate_ai_agent_token(room_name)
# await ai_interviewer.connect_to_room(room_name, ai_token)
self.active_sessions[interview_session_id] = ai_interviewer
logger.info(f"Started AI interviewer for session: {interview_session_id}")
except Exception as e:
logger.error(f"Error starting AI interviewer: {str(e)}")
raise
async def stop_interview_session(self, interview_session_id: int):
"""Остановка AI интервьюера"""
if interview_session_id in self.active_sessions:
ai_interviewer = self.active_sessions[interview_session_id]
await ai_interviewer.end_interview()
del self.active_sessions[interview_session_id]
logger.info(f"Stopped AI interviewer for session: {interview_session_id}")
# Глобальный менеджер
ai_interviewer_manager = AIInterviewerManager()