/** * 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 }; }