ty-produktovyy-dizayner-i-se/aiAdapter.js

408 lines
16 KiB
JavaScript
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.

/**
* AI Adapter - демонстрационный режим
* Детерминированная логика на словарях и шаблонах без реальной ML-модели
*/
const AISKILLS = {
frontend: ['javascript', 'js', 'react', 'vue', 'angular', 'typescript', 'ts', 'html', 'css', 'sass', 'less', 'webpack', 'vite', 'gulp', 'npm', 'yarn', 'git', 'redux', 'mobx', 'next.js', 'nuxt', 'tailwind', 'bootstrap', 'jquery', 'ajax', 'rest', 'graphql', 'api', 'dom', 'es6', 'ecmascript'],
backend: ['node.js', 'nodejs', 'python', 'java', 'php', 'go', 'golang', 'rust', 'ruby', 'c#', 'csharp', '.net', 'dotnet', 'django', 'flask', 'fastapi', 'spring', 'laravel', 'express', 'nestjs', 'fastify', 'postgresql', 'mysql', 'mongodb', 'redis', 'elasticsearch', 'kafka', 'rabbitmq', 'docker', 'kubernetes', 'aws', 'azure', 'gcp', 'nginx', 'apache', 'linux', 'bash', 'shell', 'ci/cd', 'gitlab ci', 'github actions', 'terraform', 'ansible'],
database: ['sql', 'postgresql', 'postgres', 'mysql', 'mariadb', 'mongodb', 'mongo', 'redis', 'elasticsearch', 'cassandra', 'oracle', 'mssql', 'sqlite', 'typeorm', 'sequelize', 'prisma', 'mongoose', 'sqlalchemy', 'hibernate', 'entity framework'],
devops: ['docker', 'kubernetes', 'k8s', 'jenkins', 'gitlab ci', 'github actions', 'circleci', 'travis', 'terraform', 'ansible', 'puppet', 'chef', 'aws', 'azure', 'gcp', 'linux', 'bash', 'shell', 'nginx', 'apache', 'prometheus', 'grafana', 'elk', 'splunk', 'helm', 'istio', 'vault', 'consul'],
mobile: ['ios', 'android', 'swift', 'kotlin', 'react native', 'flutter', 'dart', 'xamarin', 'cordova', 'ionic', 'mobile ui', 'app store', 'google play'],
qa: ['testing', 'test', 'qa', 'automation', 'selenium', 'cypress', 'playwright', 'jest', 'mocha', 'chai', 'pytest', 'unittest', 'junit', 'testng', 'postman', 'swagger', 'api testing', 'load testing', 'jmeter', 'manual testing', 'regression', 'smoke', 'integration testing'],
soft: ['communication', 'teamwork', 'leadership', 'management', 'agile', 'scrum', 'kanban', 'jira', 'confluence', 'mentoring', 'code review', 'documentation', 'english', 'russian', 'kazakh', 'problem solving', 'analytical thinking', 'time management']
};
const SYNONYMS = {
'javascript': ['js', 'es6', 'ecmascript', 'vanilla js'],
'typescript': ['ts', 'typed javascript'],
'react': ['react.js', 'reactjs', 'react jsx'],
'vue': ['vue.js', 'vuejs', 'vue 3', 'vue 2'],
'angular': ['angular 2+', 'angularjs'],
'node.js': ['nodejs', 'node', 'backend javascript'],
'python': ['python 3', 'django', 'flask', 'fastapi'],
'postgresql': ['postgres', 'psql'],
'mongodb': ['mongo', 'nosql'],
'docker': ['containers', 'containerization'],
'kubernetes': ['k8s', 'orchestration'],
'git': ['version control', 'vcs', 'github', 'gitlab', 'bitbucket'],
'css': ['styles', 'cascading style sheets', 'scss', 'sass', 'less'],
'html': ['markup', 'semantic html'],
'rest': ['rest api', 'http api', 'web api'],
'graphql': ['gql', 'graphql api'],
'aws': ['amazon web services', 'ec2', 's3', 'lambda'],
'linux': ['unix', 'ubuntu', 'centos', 'debian'],
'agile': ['scrum', 'kanban', 'sprint', 'iterative'],
'communication': ['коммуникабельность', 'общение', 'презентация'],
'teamwork': ['командная работа', 'коллаборация'],
'leadership': ['лидерство', 'управление', 'менторство']
};
const PATTERNS = {
experience: [
/опыт[\s\S]*?(\d+)\s*(?:лет|года|год)/gi,
/(\d+)\s*(?:лет|года|год)\s*опыта/gi,
/стаж[\s\S]*?(\d+)\s*(?:лет|года|год)/gi,
/working[\s\S]*?(\d+)\s*(?:years?|year)/gi,
/(\d+)\s*(?:years?|year)\s*of\s*experience/gi
],
skill: [
/знание\s+([а-яёa-z0-9+\-.#]+)/gi,
/владе[ею]\s+([а-яёa-z0-9+\-.#]+)/gi,
/опыт\s+работы\s+с\s+([а-яёa-z0-9+\-.#]+)/gi,
/уверенное\s+владение\s+([а-яёa-z0-9+\-.#]+)/gi,
/профессиональное\s+владение\s+([а-яёa-z0-9+\-.#]+)/gi,
/хорошее\s+знание\s+([а-яёa-z0-9+\-.#]+)/gi,
/знаком[а-я]*\s+с\s+([а-яёa-z0-9+\-.#]+)/gi,
/(?:skill|proficient|familiar)\s+(?:in|with)\s+([а-яёa-z0-9+\-.#]+)/gi,
/experience\s+(?:with|in)\s+([а-яёa-z0-9+\-.#]+)/gi
],
education: [
/(?:высшее|бакалавр|магистр|кандидат|доктор)[\s\S]*?(?:образование|ученая степень)/gi,
/(?:bachelor|master|phd|doctor)[\s\S]*?(?:degree|education)/gi,
/окончил[\s\S]*?(?:университет|институт|академия|колледж)/gi
]
};
const NEGATION_PATTERNS = [
/без\s+опыта/gi,
/не\s+(?:имею|имеет|владе[ею]|зна[ею]ю)/gi,
/отсутствие\s+(?:опыта|знаний)/gi,
/не\s+работал/gi,
/never\s+(?:used|worked)/gi,
/no\s+experience/gi,
/without\s+experience/gi
];
function generate(kind, payload) {
switch (kind) {
case 'extractCriteria':
return extractCriteria(payload.vacancyText);
case 'matchResume':
return matchResume(payload.criteria, payload.resumeText);
case 'generateQuestions':
return generateQuestions(payload.unmatchedCriteria);
default:
return { error: 'Unknown operation' };
}
}
function extractCriteria(text) {
if (!text || typeof text !== 'string') {
return { criteria: [], error: 'Пустой текст вакансии' };
}
const criteria = [];
const lowerText = text.toLowerCase();
for (const [category, skills] of Object.entries(AISKILLS)) {
for (const skill of skills) {
const regex = new RegExp(`\\b${escapeRegex(skill)}\\b`, 'gi');
const matches = lowerText.match(regex);
if (matches && matches.length > 0) {
const existing = criteria.find(c => c.label.toLowerCase() === skill);
if (!existing) {
const isRequired = checkIfRequired(text, skill);
criteria.push({
id: generateId(skill),
label: capitalizeFirst(skill),
required: isRequired,
weight: isRequired ? 5 : 3,
synonyms: SYNONYMS[skill.toLowerCase()] || [],
category
});
}
}
}
}
const expMatches = extractExperience(text);
if (expMatches.length > 0) {
criteria.push({
id: 'experience-years',
label: 'Опыт работы (лет)',
required: true,
weight: 5,
synonyms: ['стаж', 'years of experience', 'working experience'],
category: 'experience',
minYears: expMatches[0]
});
}
const eduMatches = lowerText.match(PATTERNS.education[0]);
if (eduMatches && eduMatches.length > 0) {
criteria.push({
id: 'education',
label: 'Образование',
required: true,
weight: 4,
synonyms: ['высшее образование', 'degree', 'bachelor', 'master'],
category: 'education'
});
}
if (criteria.length === 0) {
criteria.push({
id: 'custom-1',
label: 'Профессиональные навыки',
required: true,
weight: 5,
synonyms: [],
category: 'general'
});
}
return { criteria, vacancyText: text };
}
function matchResume(criteria, resumeText) {
if (!resumeText || typeof resumeText !== 'string') {
return { results: [], coverage: 0 };
}
const results = criteria.map(criterion => {
const match = findSkillInResume(criterion, resumeText);
return {
criterionId: criterion.id,
label: criterion.label,
required: criterion.required,
weight: criterion.weight,
status: match.status,
evidence: match.evidence,
note: ''
};
});
const totalWeight = results.reduce((sum, r) => sum + r.weight, 0);
const confirmedWeight = results
.filter(r => r.status === 'confirmed')
.reduce((sum, r) => sum + r.weight, 0);
const coverage = totalWeight > 0 ? Math.round((confirmedWeight / totalWeight) * 100) : 0;
return { results, coverage };
}
function generateQuestions(unmatchedCriteria) {
if (!unmatchedCriteria || unmatchedCriteria.length === 0) {
return { questions: [] };
}
const questions = unmatchedCriteria.map(criterion => {
const questionTemplates = getQuestionTemplates(criterion.category);
const template = questionTemplates[Math.floor(Math.random() * questionTemplates.length)];
return {
criterionId: criterion.criterionId,
label: criterion.label,
status: criterion.status,
question: template.replace('{skill}', criterion.label.toLowerCase()),
followUp: getFollowUpQuestions(criterion.status)
};
});
return { questions };
}
function findSkillInResume(criterion, text) {
const lowerText = text.toLowerCase();
const searchTerms = [criterion.label.toLowerCase(), ...(criterion.synonyms || [])];
for (const term of searchTerms) {
const regex = new RegExp(`\\b${escapeRegex(term)}\\b`, 'gi');
const matches = lowerText.match(regex);
if (matches && matches.length > 0) {
if (isNegated(text, term)) {
continue;
}
const evidence = extractEvidence(text, term, matches.length);
return {
status: 'confirmed',
evidence: [evidence]
};
}
}
if (criterion.required) {
return {
status: 'clarify',
evidence: []
};
}
return {
status: 'notfound',
evidence: []
};
}
function extractEvidence(text, searchTerm, matchCount) {
const lowerText = text.toLowerCase();
const index = lowerText.indexOf(searchTerm.toLowerCase());
if (index === -1) return '';
const start = Math.max(0, index - 50);
const end = Math.min(text.length, index + searchTerm.length + 50);
let snippet = text.substring(start, end);
if (start > 0) snippet = '...' + snippet;
if (end < text.length) snippet = snippet + '...';
return snippet.trim();
}
function isNegated(text, searchTerm) {
const lowerText = text.toLowerCase();
const index = lowerText.indexOf(searchTerm.toLowerCase());
if (index === -1) return false;
const contextStart = Math.max(0, index - 30);
const context = text.substring(contextStart, index + searchTerm.length);
for (const pattern of NEGATION_PATTERNS) {
pattern.lastIndex = 0;
if (pattern.test(context)) {
return true;
}
}
return false;
}
function checkIfRequired(text, skill) {
const lowerText = text.toLowerCase();
const requiredPatterns = [
new RegExp(`обязательно.*${escapeRegex(skill)}`, 'gi'),
new RegExp(`требуется.*${escapeRegex(skill)}`, 'gi'),
new RegExp(`необходимо.*${escapeRegex(skill)}`, 'gi'),
new RegExp(`must have.*${escapeRegex(skill)}`, 'gi'),
new RegExp(`required.*${escapeRegex(skill)}`, 'gi')
];
for (const pattern of requiredPatterns) {
if (pattern.test(lowerText)) {
return true;
}
}
const niceToHavePatterns = [
new RegExp(`будет\\s*(\\+|плюсом|преимуществом).*${escapeRegex(skill)}`, 'gi'),
new RegExp(`nice\\s*to\\s*have.*${escapeRegex(skill)}`, 'gi'),
new RegExp(`preferred.*${escapeRegex(skill)}`, 'gi')
];
for (const pattern of niceToHavePatterns) {
if (pattern.test(lowerText)) {
return false;
}
}
return true;
}
function extractExperience(text) {
for (const pattern of PATTERNS.experience) {
const matches = text.match(pattern);
if (matches && matches.length > 0) {
const numberMatch = matches[0].match(/(\d+)/);
if (numberMatch) {
return [parseInt(numberMatch[1], 10)];
}
}
}
return [];
}
function getQuestionTemplates(category) {
const templates = {
frontend: [
'Расскажите о проекте, где вы использовали {skill}. С какими сложностями столкнулись?',
'Как вы применяете {skill} в реальной разработке? Приведите пример.',
'Опишите ваш подход к решению задач с использованием {skill}.'
],
backend: [
'Как вы проектировали архитектуру backend с использованием {skill}?',
'Расскажите о производительности и масштабируемости при работе с {skill}.',
'С какими проблемами вы сталкивались при использовании {skill} в production?'
],
database: [
'Как вы оптимизировали запросы в {skill}?',
'Опишите схему данных, которую вы проектировали с использованием {skill}.',
'Как вы обеспечивали целостность данных в {skill}?'
],
devops: [
'Опишите ваш CI/CD пайплайн с использованием {skill}.',
'Как вы мониторили и поддерживали инфраструктуру с {skill}?',
'Расскажите о развёртывании проектов с помощью {skill}.'
],
mobile: [
'Расскажите о опубликованном приложении, где вы использовали {skill}.',
'Как вы оптимизировали производительность мобильного приложения на {skill}?',
'С какими особенностями платформ вы сталкивались при работе с {skill}?'
],
qa: [
'Опишите стратегию тестирования, которую вы применяли с {skill}.',
'Как вы автоматизировали тестирование с помощью {skill}?',
'Расскажите о найденных критических багах благодаря {skill}.'
],
soft: [
'Приведите пример успешной командной работы, где вы проявили {skill}.',
'Как вы применяете {skill} в ежедневной работе?',
'Опишите ситуацию, где {skill} помог решить сложную задачу.'
],
experience: [
'Расскажите о вашем опыте работы с {skill}. Какие проекты вы реализовали?',
'Как развивались ваши навыки в {skill} за последние годы?',
'Что нового вы изучили в области {skill} за последний год?'
],
education: [
'Как ваше образование связано с этой позицией?',
'Какие курсы или сертификации вы проходили по направлению {skill}?',
'Как вы продолжаете развиваться в профессиональной области?'
],
general: [
'Расскажите подробнее о вашем опыте работы с {skill}.',
'Как вы применяли {skill} в предыдущих проектах?',
'Какие задачи вы решали с помощью {skill}?'
]
};
return templates[category] || templates.general;
}
function getFollowUpQuestions(status) {
if (status === 'clarify') {
return [
'Уточните уровень владения этим навыком.',
'Когда вы в последний раз использовали этот навык на практике?'
];
}
return [
'Можете привести конкретный пример использования?',
'Какой результат был достигнут?'
];
}
function generateId(text) {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
function capitalizeFirst(text) {
return text.charAt(0).toUpperCase() + text.slice(1);
}
function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = { generate, AISKILLS, SYNONYMS };
}