|
import axios from 'axios';
|
|
import { ChatbotService } from './ChatbotService.js';
|
|
|
|
export interface ChatMessage {
|
|
role: 'system' | 'user' | 'assistant';
|
|
content: string;
|
|
}
|
|
|
|
export interface ChatRequest {
|
|
model: string;
|
|
messages: ChatMessage[];
|
|
temperature: number;
|
|
}
|
|
|
|
export interface ChatChoice {
|
|
message: ChatMessage;
|
|
}
|
|
|
|
export interface ChatResponse {
|
|
choices: ChatChoice[];
|
|
}
|
|
|
|
export class AIService {
|
|
private apiKey: string;
|
|
private model: string;
|
|
private baseUrl: string;
|
|
private relPath: string;
|
|
private temperature: number;
|
|
private chatbotService: ChatbotService;
|
|
|
|
// Predefined models from your C# code
|
|
private static readonly PREDEFINED_MODELS: Record<string, string> = {
|
|
'dobby': 'sentientagi/dobby-mini-unhinged-plus-llama-3.1-8b',
|
|
'dolphin': 'cognitivecomputations/dolphin-mixtral-8x22b',
|
|
'dolphin_free': 'cognitivecomputations/dolphin3.0-mistral-24b:free',
|
|
'gemma': 'google/gemma-3-12b-it',
|
|
'gpt-4o-mini': 'openai/gpt-4o-mini',
|
|
'gpt-4.1-nano': 'openai/gpt-4.1-nano',
|
|
'qwen': 'qwen/qwen3-30b-a3b',
|
|
'unslop': 'thedrummer/unslopnemo-12b',
|
|
'euryale': 'sao10k/l3.3-euryale-70b',
|
|
'wizard': 'microsoft/wizardlm-2-8x22b',
|
|
'deepseek': 'deepseek/deepseek-chat-v3-0324'
|
|
};
|
|
|
|
constructor() {
|
|
this.apiKey = process.env.OPENROUTER_API_KEY || 'sk-or-REPLACE_ME';
|
|
this.model = process.env.OPENROUTER_MODEL || 'gemma';
|
|
this.baseUrl = process.env.OPENROUTER_BASE_URL || 'openrouter.ai';
|
|
this.relPath = process.env.OPENROUTER_REL_PATH || '/api';
|
|
this.temperature = parseFloat(process.env.OPENROUTER_TEMPERATURE || '0.7');
|
|
this.chatbotService = new ChatbotService();
|
|
|
|
// Map predefined model names to full model names
|
|
if (AIService.PREDEFINED_MODELS[this.model]) {
|
|
this.model = AIService.PREDEFINED_MODELS[this.model];
|
|
}
|
|
|
|
console.log(`[DEBUG] AIService initialized:`);
|
|
console.log(`[DEBUG] - API Key: ${this.apiKey.substring(0, 10)}...`);
|
|
console.log(`[DEBUG] - Model: ${this.model}`);
|
|
console.log(`[DEBUG] - Base URL: ${this.baseUrl}`);
|
|
console.log(`[DEBUG] - Rel Path: ${this.relPath}`);
|
|
console.log(`[DEBUG] - Temperature: ${this.temperature}`);
|
|
console.log(`[DEBUG] - Chatbot Service: ${this.chatbotService ? 'Enabled' : 'Disabled'}`);
|
|
}
|
|
|
|
async generateResponse(prompt: string, systemMessage?: string): Promise<string | null> {
|
|
try {
|
|
const messages: ChatMessage[] = [];
|
|
|
|
if (systemMessage) {
|
|
messages.push({ role: 'system', content: systemMessage });
|
|
}
|
|
|
|
messages.push({ role: 'user', content: prompt });
|
|
|
|
const payload: ChatRequest = {
|
|
model: this.model,
|
|
messages: messages,
|
|
temperature: this.temperature
|
|
};
|
|
|
|
const url = `https://${this.baseUrl}${this.relPath}/v1/chat/completions`;
|
|
|
|
console.log(`[DEBUG] Sending to OpenRouter - Model: ${this.model}, URL: ${url}`);
|
|
console.log(`[DEBUG] Prompt length: ${prompt.length} characters`);
|
|
|
|
const response = await axios.post(url, payload, {
|
|
headers: {
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
const data = response.data as ChatResponse;
|
|
const aiResponse = data.choices?.[0]?.message?.content || null;
|
|
|
|
console.log(`[DEBUG] OpenRouter Response: ${aiResponse}`);
|
|
|
|
return aiResponse;
|
|
} catch (error) {
|
|
console.error('Error calling OpenRouter:', error);
|
|
if (error.response) {
|
|
console.error('Response status:', error.response.status);
|
|
console.error('Response data:', error.response.data);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async generateResponseWithHistory(
|
|
userMessage: string,
|
|
conversationHistory: any[],
|
|
systemMessage?: string
|
|
): Promise<string | null> {
|
|
try {
|
|
const messages: ChatMessage[] = [];
|
|
|
|
if (systemMessage) {
|
|
messages.push({ role: 'system', content: systemMessage });
|
|
}
|
|
|
|
// Add conversation history
|
|
conversationHistory.forEach(msg => {
|
|
if (msg.sender === 'candidate' || msg.sender === 'user') {
|
|
messages.push({ role: 'user', content: msg.message });
|
|
} else if (msg.sender === 'ai' || msg.sender === 'assistant') {
|
|
messages.push({ role: 'assistant', content: msg.message });
|
|
}
|
|
});
|
|
|
|
// Add current user message
|
|
messages.push({ role: 'user', content: userMessage });
|
|
|
|
const payload: ChatRequest = {
|
|
model: this.model,
|
|
messages: messages,
|
|
temperature: this.temperature
|
|
};
|
|
|
|
const url = `https://${this.baseUrl}${this.relPath}/v1/chat/completions`;
|
|
|
|
console.log(`[DEBUG] Sending to OpenRouter with history - Model: ${this.model}`);
|
|
console.log(`[DEBUG] Messages count: ${messages.length}`);
|
|
|
|
const response = await axios.post(url, payload, {
|
|
headers: {
|
|
'Authorization': `Bearer ${this.apiKey}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
const data = response.data as ChatResponse;
|
|
const aiResponse = data.choices?.[0]?.message?.content || null;
|
|
|
|
console.log(`[DEBUG] OpenRouter Response: ${aiResponse}`);
|
|
|
|
return aiResponse;
|
|
} catch (error) {
|
|
console.error('Error calling OpenRouter with history:', error);
|
|
if (error.response) {
|
|
console.error('Response status:', error.response.status);
|
|
console.error('Response data:', error.response.data);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate response using chatbot service with fallback to direct OpenRouter
|
|
*/
|
|
async generateResponseWithChatbot(
|
|
userMessage: string,
|
|
conversationHistory: any[],
|
|
systemMessage?: string,
|
|
job?: any,
|
|
candidateName?: string,
|
|
linkId?: string
|
|
): Promise<string | null> {
|
|
// Try chatbot service first
|
|
try {
|
|
const isHealthy = await this.chatbotService.isHealthy();
|
|
if (isHealthy) {
|
|
console.log(`[DEBUG] Using chatbot service for response generation`);
|
|
|
|
const response = await this.chatbotService.sendMessage({
|
|
message: userMessage,
|
|
conversationHistory,
|
|
systemMessage,
|
|
job,
|
|
candidateName,
|
|
linkId
|
|
});
|
|
|
|
if (response) {
|
|
return response;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('[ERROR] Chatbot service failed, falling back to direct OpenRouter:', error);
|
|
}
|
|
|
|
// Fallback to direct OpenRouter
|
|
if (this.chatbotService.shouldUseFallback()) {
|
|
console.log(`[DEBUG] Falling back to direct OpenRouter`);
|
|
return await this.generateResponseWithHistory(userMessage, conversationHistory, systemMessage);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Initialize interview using chatbot service
|
|
*/
|
|
async initializeInterviewWithChatbot(
|
|
job: any,
|
|
candidateName: string,
|
|
linkId: string,
|
|
conversationHistory: any[] = []
|
|
): Promise<string | null> {
|
|
try {
|
|
const isHealthy = await this.chatbotService.isHealthy();
|
|
if (isHealthy) {
|
|
console.log(`[DEBUG] Using chatbot service for interview initialization`);
|
|
return await this.chatbotService.initializeInterview(job, candidateName, linkId, conversationHistory);
|
|
}
|
|
} catch (error) {
|
|
console.error('[ERROR] Chatbot service failed for interview initialization:', error);
|
|
}
|
|
|
|
// Fallback to direct OpenRouter
|
|
if (this.chatbotService.shouldUseFallback()) {
|
|
console.log(`[DEBUG] Falling back to direct OpenRouter for interview initialization`);
|
|
const systemMessage = this.buildInterviewSystemMessage(job, candidateName, conversationHistory);
|
|
return await this.generateResponse(`The candidate's name is ${candidateName}. Please start the interview.`, systemMessage);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* End interview using chatbot service
|
|
*/
|
|
async endInterviewWithChatbot(linkId: string): Promise<boolean> {
|
|
try {
|
|
const isHealthy = await this.chatbotService.isHealthy();
|
|
if (isHealthy) {
|
|
console.log(`[DEBUG] Using chatbot service for interview end`);
|
|
return await this.chatbotService.endInterview(linkId);
|
|
}
|
|
} catch (error) {
|
|
console.error('[ERROR] Chatbot service failed for interview end:', error);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Build interview system message
|
|
*/
|
|
private buildInterviewSystemMessage(job: any, candidateName: string, conversationHistory: any[] = []): string {
|
|
const skills = job.skills_required ? job.skills_required.join(', ') : 'various technical skills';
|
|
const experience = job.experience_level.replace('_', ' ');
|
|
|
|
// Build context from conversation history (mandatory question answers)
|
|
const conversationContext = conversationHistory
|
|
.map(msg => `${msg.sender === 'candidate' ? 'Candidate' : 'Interviewer'}: ${msg.message}`)
|
|
.join('\n');
|
|
|
|
return `You are an AI interview agent conducting an interview for the position: ${job.title}
|
|
|
|
Job Description: ${job.description}
|
|
Requirements: ${job.requirements}
|
|
Required Skills: ${skills}
|
|
Experience Level: ${experience}
|
|
Location: ${job.location || 'Remote'}
|
|
|
|
${conversationContext ? `Previous conversation (mandatory questions answered):
|
|
${conversationContext}
|
|
|
|
Based on the candidate's answers to the mandatory questions above, you should now conduct a deeper interview.` : ''}
|
|
|
|
Your task is to:
|
|
1. Greet the candidate warmly and professionally
|
|
2. Introduce yourself as their evaluation agent
|
|
3. ${conversationContext ? 'Acknowledge their previous answers and build upon them' : 'Explain that you\'ll be conducting a comprehensive interview'}
|
|
4. Ask them to tell you about themselves and their interest in this role
|
|
5. Keep your response conversational and engaging
|
|
6. Don't ask multiple questions at once - start with one open-ended question
|
|
|
|
Respond in a friendly, professional tone. Keep it concise but welcoming.`;
|
|
}
|
|
}
|