Spaces:
Running
Running
| 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 """ | |
| <div style="display: flex; align-items: center; justify-content: center; min-height: 60vh; padding: 40px;"> | |
| <div style="max-width: 900px; width: 100%;"> | |
| <div style="background: var(--bg-panel); border: 2px solid var(--arc-orange); border-radius: 4px; padding: 40px;"> | |
| <div style="display: flex; align-items: center; gap: 16px; margin-bottom: 24px;"> | |
| <div style="font-size: 48px;">π</div> | |
| <div> | |
| <h2 style="color: var(--arc-orange); margin: 0; font-size: 28px; font-weight: 700;">CV ANALYSIS OPTIONS</h2> | |
| <p style="color: var(--text-dim); margin: 8px 0 0 0; font-size: 15px;"> | |
| Upload a candidate's CV (PDF or DOCX format) to extract skills and optionally match against projects. | |
| </p> | |
| </div> | |
| </div> | |
| <div style="margin-top: 32px; padding: 24px; background: rgba(0,0,0,0.4); border-radius: 4px; border: 1px solid var(--border-dim);"> | |
| <div style="color: var(--arc-cyan); font-size: 14px; margin-bottom: 20px; text-transform: uppercase; letter-spacing: 1px;"> | |
| <strong>TWO ANALYSIS MODES:</strong> | |
| </div> | |
| <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 24px;"> | |
| <div style="padding: 20px; background: rgba(255,255,255,0.05); border-radius: 4px; border-left: 4px solid var(--arc-cyan);"> | |
| <div style="color: var(--arc-cyan); font-weight: 700; margin-bottom: 10px; font-size: 15px;">π EXTRACT SKILLS ONLY</div> | |
| <div style="color: var(--text-dim); font-size: 13px; line-height: 1.5;"> | |
| Parse CV to identify technical skills, experience, certifications, and education. | |
| </div> | |
| </div> | |
| <div style="padding: 20px; background: rgba(255,255,255,0.05); border-radius: 4px; border-left: 4px solid var(--arc-orange);"> | |
| <div style="color: var(--arc-orange); font-weight: 700; margin-bottom: 10px; font-size: 15px;">π― EXTRACT + MATCH PROJECTS</div> | |
| <div style="color: var(--text-dim); font-size: 13px; line-height: 1.5;"> | |
| Parse CV, rank matching projects by skill compatibility, and identify skill gaps. | |
| </div> | |
| </div> | |
| </div> | |
| <div style="color: var(--arc-yellow); font-size: 13px; padding: 16px; background: rgba(255,185,0,0.1); border-radius: 4px; border: 1px solid rgba(255,185,0,0.3);"> | |
| <strong>β οΈ Instructions:</strong> Use the file upload on the left panel to select a CV, then choose your desired analysis mode. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| 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 '<div style="color: var(--arc-red); padding: 40px; text-align: center;">β οΈ No file uploaded. Please select a CV file (PDF or DOCX).</div>' | |
| 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 '<div style="color: var(--arc-red); padding: 40px; text-align: center;">β οΈ Could not extract meaningful text from the CV. Please ensure the file is not corrupted.</div>' | |
| 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'<div style="color: var(--arc-red); padding: 40px; text-align: center;">β οΈ Error processing CV: {html.escape(error_msg)}</div>' | |
| 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 '<div style="color: var(--arc-red); padding: 40px; text-align: center;">β οΈ No file uploaded. Please select a CV file (PDF or DOCX).</div>' | |
| 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 '<div style="color: var(--arc-red); padding: 40px; text-align: center;">β οΈ Could not extract meaningful text from the CV.</div>' | |
| 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 '<div style="color: var(--arc-red); padding: 40px; text-align: center;">β οΈ Error extracting skills from CV.</div>' | |
| 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'<div style="color: var(--arc-red); padding: 40px; text-align: center;">β οΈ Error processing CV: {html.escape(error_msg)}</div>' | |