from __future__ import annotations import html from typing import Any, Callable, Dict, Optional from utils.cv_parser import parse_cv from utils.cv_project_matcher import match_cv_to_projects from utils.skill_extractor import extract_skills_from_cv_text from .render_cv_html import render_cv_analysis_html from .render_cv_matching import render_cv_matching_html class CVInterface: """ Handles all CV-related functionality including upload, parsing, skill extraction, and project matching. """ def __init__(self, main_interface): """ Initialize CV interface with reference to main interface for shared functionality. Args: main_interface: Reference to EastSyncInterface instance for accessing shared methods like register_agent_action, start_processing, etc. """ self._main_interface = main_interface self._cv_skills_data: Optional[Dict[str, Any]] = None self._cv_filename: str = "Unknown" def render_cv_upload_interface(self) -> str: """Render the CV upload interface instructions for the right panel.""" return """
📄

CV ANALYSIS OPTIONS

Upload a candidate's CV (PDF or DOCX format) to extract skills and optionally match against projects.

TWO ANALYSIS MODES:
📊 EXTRACT SKILLS ONLY
Parse CV to identify technical skills, experience, certifications, and education.
🎯 EXTRACT + MATCH PROJECTS
Parse CV, rank matching projects by skill compatibility, and identify skill gaps.
⚠️ Instructions: Use the file upload on the left panel to select a CV, then choose your desired analysis mode.
""" def process_cv_upload(self, file_obj) -> str: """Process uploaded CV file and extract skills. Returns main output HTML.""" if file_obj is None: return '
⚠️ No file uploaded. Please select a CV file (PDF or DOCX).
' try: import os file_name = os.path.basename(file_obj.name) if hasattr(file_obj, 'name') else "unknown" self._cv_filename = file_name # Terminal logging print("\n" + "="*80) print(f"[CV UPLOAD] Processing CV: {file_name}") print("="*80) self._main_interface.register_agent_action("📤 CV Upload Started", {"file": file_name}) # Read file content if hasattr(file_obj, 'name'): # Gradio file object file_path = file_obj.name with open(file_path, 'rb') as f: file_content = f.read() else: # Direct file path file_path = str(file_obj) with open(file_path, 'rb') as f: file_content = f.read() file_size_kb = len(file_content) / 1024 self._main_interface.register_agent_action("📄 Parsing Document", { "size": f"{file_size_kb:.1f}KB", "format": os.path.splitext(file_name)[1].upper() }) # Extract text from CV cv_text = parse_cv(file_path=file_path, file_content=file_content, log_callback=self._main_interface.register_agent_action) if not cv_text or len(cv_text.strip()) < 50: self._main_interface.register_agent_action("⚠️ Text Extraction Failed", {"extracted_chars": len(cv_text) if cv_text else 0}) print(f"[CV UPLOAD] ❌ ERROR: Text extraction failed - only {len(cv_text) if cv_text else 0} chars extracted") return '
⚠️ Could not extract meaningful text from the CV. Please ensure the file is not corrupted.
' word_count = len(cv_text.split()) char_count = len(cv_text) self._main_interface.register_agent_action("✅ Text Extracted", { "words": word_count, "characters": char_count, "pages_est": max(1, word_count // 300) # Rough estimate }) self._main_interface.register_agent_action("🤖 AI Analysis Starting", {"status": "Initializing AI-powered skill extraction..."}) # Extract skills using LLM with logging callback skills_data = extract_skills_from_cv_text(cv_text, log_callback=self._main_interface.register_agent_action) self._cv_skills_data = skills_data if "error" not in skills_data: total_skills = len(skills_data.get("technical_skills", [])) + len(skills_data.get("soft_skills", [])) self._main_interface.register_agent_action("🎯 Skills Extracted", { "technical_skills": len(skills_data.get("technical_skills", [])), "soft_skills": len(skills_data.get("soft_skills", [])), "total": total_skills }) print(f"[CV UPLOAD] ✅ SUCCESS: Extracted {total_skills} total skills") else: print(f"[CV UPLOAD] ⚠️ WARNING: Skills extraction completed with errors") print("="*80 + "\n") # Render CV analysis HTML for main display main_output = render_cv_analysis_html(skills_data, file_name) return main_output except Exception as e: error_msg = str(e) self._main_interface.register_agent_action("CV Processing Error", {"error": error_msg}) print(f"[CV UPLOAD] ❌ EXCEPTION: {type(e).__name__} - {error_msg}") print("="*80 + "\n") return f'
⚠️ Error processing CV: {html.escape(error_msg)}
' def get_extracted_skills(self) -> Optional[Dict[str, Any]]: """Get the most recently extracted skills data.""" return self._cv_skills_data def process_cv_with_matching(self, file_obj) -> str: """Process CV and match against projects. Returns main output HTML.""" if file_obj is None: return '
⚠️ No file uploaded. Please select a CV file (PDF or DOCX).
' try: import os file_name = os.path.basename(file_obj.name) if hasattr(file_obj, 'name') else "unknown" self._cv_filename = file_name # Terminal logging print("\n" + "="*80) print(f"[CV MATCHING] Processing CV with Project Matching: {file_name}") print("="*80) self._main_interface.register_agent_action("📤 CV Upload + Matching Started", {"file": file_name}) # Read file content if hasattr(file_obj, 'name'): file_path = file_obj.name with open(file_path, 'rb') as f: file_content = f.read() else: file_path = str(file_obj) with open(file_path, 'rb') as f: file_content = f.read() file_size_kb = len(file_content) / 1024 self._main_interface.register_agent_action("📄 Parsing Document", { "size": f"{file_size_kb:.1f}KB", "format": os.path.splitext(file_name)[1].upper() }) # Extract text from CV cv_text = parse_cv(file_path=file_path, file_content=file_content, log_callback=self._main_interface.register_agent_action) if not cv_text or len(cv_text.strip()) < 50: self._main_interface.register_agent_action("⚠️ Text Extraction Failed", {"extracted_chars": len(cv_text) if cv_text else 0}) print(f"[CV MATCHING] ❌ ERROR: Text extraction failed") return '
⚠️ Could not extract meaningful text from the CV.
' word_count = len(cv_text.split()) char_count = len(cv_text) self._main_interface.register_agent_action("✅ Text Extracted", { "words": word_count, "characters": char_count, "pages_est": max(1, word_count // 300) }) self._main_interface.register_agent_action("🤖 AI Analysis Starting", {"status": "Extracting skills from CV..."}) # Extract skills using LLM skills_data = extract_skills_from_cv_text(cv_text, log_callback=self._main_interface.register_agent_action) self._cv_skills_data = skills_data if "error" in skills_data: print(f"[CV MATCHING] ⚠️ WARNING: Skills extraction completed with errors") return '
⚠️ Error extracting skills from CV.
' total_skills = len(skills_data.get("technical_skills", [])) + len(skills_data.get("soft_skills", [])) self._main_interface.register_agent_action("🎯 Skills Extracted", { "technical_skills": len(skills_data.get("technical_skills", [])), "soft_skills": len(skills_data.get("soft_skills", [])), "total": total_skills }) print(f"[CV MATCHING] ✅ Skills extracted: {total_skills} total skills") # Match against projects self._main_interface.register_agent_action("🔍 Starting Project Matching", {"status": "Comparing skills with project requirements..."}) matched_projects = match_cv_to_projects(skills_data, log_callback=self._main_interface.register_agent_action) if not matched_projects: print(f"[CV MATCHING] ⚠️ No projects found for matching") self._main_interface.register_agent_action("⚠️ No Projects Found", {"status": "No projects available in database"}) # Still show CV analysis main_output = render_cv_analysis_html(skills_data, file_name) return main_output # Skip training costs - just show matching results # Set empty training plans for all projects for project in matched_projects: project['training_plans'] = [] print(f"[CV MATCHING] ✅ SUCCESS: Matched {len(matched_projects)} projects") self._main_interface.register_agent_action("✅ Matching Complete", { "total_matches": len(matched_projects), "best_match": f"{matched_projects[0]['project_name']} ({matched_projects[0]['match_percentage']}%)" }) print("="*80 + "\n") # Render CV matching results for main display main_output = render_cv_matching_html(skills_data, matched_projects, file_name) return main_output except Exception as e: error_msg = str(e) self._main_interface.register_agent_action("CV Matching Error", {"error": error_msg}) print(f"[CV MATCHING] ❌ EXCEPTION: {type(e).__name__} - {error_msg}") print("="*80 + "\n") return f'
⚠️ Error processing CV: {html.escape(error_msg)}
'