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)}
'