Noo88ear's picture
πŸš€ Initial deployment of Multi-Agent Job Application Assistant
7498f2c
"""
Enhanced UI Components for Job Application Assistant
Integrates multi-format support, LinkedIn extraction, and job matching
"""
import gradio as gr
import logging
from typing import Dict, Any, List, Optional, Tuple
import json
import os
from pathlib import Path
# Import our new services
try:
from services.document_processor import document_processor
DOC_PROCESSOR_AVAILABLE = True
except ImportError:
DOC_PROCESSOR_AVAILABLE = False
try:
from services.linkedin_profile_extractor import linkedin_extractor
LINKEDIN_EXTRACTOR_AVAILABLE = True
except ImportError:
LINKEDIN_EXTRACTOR_AVAILABLE = False
try:
from services.job_matcher import job_matcher
JOB_MATCHER_AVAILABLE = True
except ImportError:
JOB_MATCHER_AVAILABLE = False
logger = logging.getLogger(__name__)
def create_enhanced_ui_components():
"""Create enhanced UI components for the application"""
components = {}
# Multi-format Resume Upload Section
with gr.Accordion("πŸ“„ Resume Upload & Management", open=True) as resume_section:
gr.Markdown("""
### Upload your resume in any format
Supported formats: Word (.docx), PDF, Text (.txt), PowerPoint (.pptx)
""")
with gr.Row():
resume_upload = gr.File(
label="Upload Resume",
file_types=[".docx", ".pdf", ".txt", ".pptx"],
type="filepath"
)
resume_format_output = gr.Dropdown(
label="Export Format",
choices=["Word", "PDF", "Text", "PowerPoint"],
value="Word"
)
with gr.Row():
extract_btn = gr.Button("πŸ“Š Extract Resume Data", variant="primary")
linkedin_import_btn = gr.Button("πŸ”— Import from LinkedIn", variant="secondary")
# LinkedIn Profile Import
with gr.Row():
linkedin_url = gr.Textbox(
label="LinkedIn Profile URL",
placeholder="https://www.linkedin.com/in/yourprofile"
)
linkedin_auto_fill = gr.Button("πŸ”„ Auto-Fill from LinkedIn")
# Extracted Data Display
with gr.Tabs():
with gr.TabItem("Contact Info"):
contact_name = gr.Textbox(label="Full Name")
contact_email = gr.Textbox(label="Email")
contact_phone = gr.Textbox(label="Phone")
contact_linkedin = gr.Textbox(label="LinkedIn URL")
contact_location = gr.Textbox(label="Location")
with gr.TabItem("Professional Summary"):
summary_text = gr.Textbox(
label="Summary",
lines=5,
placeholder="Your professional summary..."
)
with gr.TabItem("Experience"):
experience_data = gr.JSON(label="Experience Data")
with gr.TabItem("Skills"):
skills_list = gr.Textbox(
label="Skills (comma-separated)",
placeholder="Python, JavaScript, Project Management..."
)
with gr.TabItem("Education"):
education_data = gr.JSON(label="Education Data")
components['resume_section'] = resume_section
components['resume_upload'] = resume_upload
components['resume_format_output'] = resume_format_output
components['extract_btn'] = extract_btn
components['linkedin_import_btn'] = linkedin_import_btn
components['linkedin_url'] = linkedin_url
components['linkedin_auto_fill'] = linkedin_auto_fill
components['contact_name'] = contact_name
components['contact_email'] = contact_email
components['contact_phone'] = contact_phone
components['contact_linkedin'] = contact_linkedin
components['contact_location'] = contact_location
components['summary_text'] = summary_text
components['experience_data'] = experience_data
components['skills_list'] = skills_list
components['education_data'] = education_data
# Job Matching Section
with gr.Accordion("🎯 Smart Job Matching", open=True) as job_matching_section:
gr.Markdown("""
### AI-Powered Job Matching
Automatically match your profile with the best-fit jobs from LinkedIn, Adzuna, and other sources
""")
with gr.Row():
job_search_keywords = gr.Textbox(
label="Job Keywords",
placeholder="e.g., Python Developer, Data Scientist"
)
job_location = gr.Textbox(
label="Preferred Location",
placeholder="e.g., San Francisco, Remote"
)
with gr.Row():
desired_salary = gr.Number(
label="Desired Salary ($)",
value=0
)
job_type_pref = gr.Dropdown(
label="Job Type",
choices=["Full-time", "Part-time", "Contract", "Remote", "Hybrid"],
value="Full-time"
)
match_jobs_btn = gr.Button("πŸ” Find Matching Jobs", variant="primary")
# Job Matches Display
job_matches_output = gr.Dataframe(
headers=["Job Title", "Company", "Match %", "Location", "Salary", "Source"],
label="Matched Jobs"
)
# Detailed Match Analysis
with gr.Tabs():
with gr.TabItem("Match Details"):
match_details = gr.JSON(label="Detailed Match Analysis")
with gr.TabItem("Recommendations"):
recommendations = gr.Markdown(label="Personalized Recommendations")
with gr.TabItem("Skills Gap"):
skills_gap = gr.Markdown(label="Skills Gap Analysis")
components['job_matching_section'] = job_matching_section
components['job_search_keywords'] = job_search_keywords
components['job_location'] = job_location
components['desired_salary'] = desired_salary
components['job_type_pref'] = job_type_pref
components['match_jobs_btn'] = match_jobs_btn
components['job_matches_output'] = job_matches_output
components['match_details'] = match_details
components['recommendations'] = recommendations
components['skills_gap'] = skills_gap
# Export Options Section
with gr.Accordion("πŸ“€ Export Options", open=False) as export_section:
gr.Markdown("""
### Export your documents in multiple formats
Choose your preferred format and template
""")
with gr.Row():
export_format = gr.Dropdown(
label="Export Format",
choices=["Word (.docx)", "PDF", "Text (.txt)", "PowerPoint (.pptx)"],
value="Word (.docx)"
)
template_choice = gr.Dropdown(
label="Template",
choices=["Professional", "Modern", "Creative", "ATS-Optimized", "Executive"],
value="Professional"
)
with gr.Row():
include_cover_letter = gr.Checkbox(label="Include Cover Letter", value=True)
include_references = gr.Checkbox(label="Include References", value=False)
export_btn = gr.Button("πŸ“₯ Generate Documents", variant="primary")
with gr.Row():
resume_download = gr.File(label="Download Resume")
cover_letter_download = gr.File(label="Download Cover Letter")
components['export_section'] = export_section
components['export_format'] = export_format
components['template_choice'] = template_choice
components['include_cover_letter'] = include_cover_letter
components['include_references'] = include_references
components['export_btn'] = export_btn
components['resume_download'] = resume_download
components['cover_letter_download'] = cover_letter_download
return components
def handle_resume_upload(file_path: str) -> Dict[str, Any]:
"""Handle resume file upload and extraction"""
if not file_path:
return {
'error': 'No file uploaded',
'data': {}
}
if not DOC_PROCESSOR_AVAILABLE:
return {
'error': 'Document processor not available',
'data': {}
}
try:
# Extract data from uploaded file
extracted_data = document_processor.extract_from_file(file_path)
return {
'success': True,
'data': extracted_data,
'message': f'Successfully extracted data from {Path(file_path).name}'
}
except Exception as e:
logger.error(f"Error processing resume: {e}")
return {
'error': str(e),
'data': {}
}
def handle_linkedin_import(linkedin_url: str, access_token: Optional[str] = None) -> Dict[str, Any]:
"""Handle LinkedIn profile import"""
if not LINKEDIN_EXTRACTOR_AVAILABLE:
return {
'error': 'LinkedIn extractor not available',
'data': {}
}
try:
if access_token:
linkedin_extractor.set_access_token(access_token)
# Extract profile data
profile_data = linkedin_extractor.auto_populate_from_linkedin(linkedin_url)
return {
'success': True,
'data': profile_data,
'message': 'Successfully imported LinkedIn profile'
}
except Exception as e:
logger.error(f"Error importing LinkedIn profile: {e}")
return {
'error': str(e),
'data': {}
}
def handle_job_matching(
candidate_data: Dict[str, Any],
keywords: str,
location: str,
salary: float,
job_type: str
) -> Dict[str, Any]:
"""Handle job matching"""
if not JOB_MATCHER_AVAILABLE:
return {
'error': 'Job matcher not available',
'matches': [],
'recommendations': []
}
try:
# Get job listings from various sources
# This would integrate with job_aggregator.py
from services.job_aggregator import search_all_sources
job_listings = search_all_sources(keywords, location)
# Add LinkedIn jobs if available
if LINKEDIN_EXTRACTOR_AVAILABLE:
linkedin_jobs = linkedin_extractor.search_jobs(keywords, location)
job_listings.extend(linkedin_jobs)
# Set preferences
preferences = {
'desired_salary': salary,
'job_type': job_type,
'location': location
}
# Match candidate to jobs
matches = job_matcher.match_candidate_to_jobs(
candidate_data,
job_listings,
preferences
)
# Get recommendations
recommendations = job_matcher.get_recommendations(matches, top_n=5)
return {
'success': True,
'matches': matches,
'recommendations': recommendations,
'total_jobs': len(job_listings),
'message': f'Found {len(matches)} matching jobs'
}
except Exception as e:
logger.error(f"Error matching jobs: {e}")
return {
'error': str(e),
'matches': [],
'recommendations': []
}
def handle_document_export(
data: Dict[str, Any],
format: str,
template: str,
include_cover_letter: bool
) -> Tuple[Optional[bytes], Optional[bytes]]:
"""Handle document export in multiple formats"""
if not DOC_PROCESSOR_AVAILABLE:
return None, None
try:
# Clean format string
format_map = {
'Word (.docx)': 'docx',
'PDF': 'pdf',
'Text (.txt)': 'txt',
'PowerPoint (.pptx)': 'pptx'
}
clean_format = format_map.get(format, 'docx')
# Export resume
resume_bytes = document_processor.export_to_format(data, clean_format, template)
# Export cover letter if requested
cover_letter_bytes = None
if include_cover_letter:
# Generate cover letter data (would integrate with cover_letter_agent)
cover_letter_data = {
'contact': data.get('contact', {}),
'body': 'Generated cover letter content...'
}
cover_letter_bytes = document_processor.export_to_format(
cover_letter_data,
clean_format,
template
)
return resume_bytes, cover_letter_bytes
except Exception as e:
logger.error(f"Error exporting documents: {e}")
return None, None
def populate_ui_from_data(data: Dict[str, Any]) -> Tuple:
"""Populate UI fields from extracted data"""
# Handle None or empty data
if not data:
logger.warning("No data provided to populate_ui_from_data")
return ('', '', '', '', '', '', [], '', [])
contact = data.get('contact', {})
return (
contact.get('name', ''),
contact.get('email', ''),
contact.get('phone', ''),
contact.get('linkedin', ''),
contact.get('location', ''),
data.get('summary', ''),
data.get('experience', []),
', '.join(data.get('skills', [])) if isinstance(data.get('skills'), list) else data.get('skills', ''),
data.get('education', [])
)
def format_job_matches_for_display(matches: List[Dict[str, Any]]) -> List[List]:
"""Format job matches for dataframe display"""
formatted = []
for match in matches[:20]: # Limit to top 20
job = match['job']
formatted.append([
job.get('title', 'N/A'),
job.get('company', 'N/A'),
f"{match['match_percentage']}%",
job.get('location', 'N/A'),
job.get('salary', 'N/A'),
job.get('source', 'N/A')
])
return formatted
def generate_recommendations_markdown(recommendations: List[Dict[str, Any]]) -> str:
"""Generate markdown for job recommendations"""
if not recommendations:
return "No recommendations available yet. Upload your resume and search for jobs to get started!"
md_lines = ["## 🎯 Top Job Recommendations\n"]
for i, rec in enumerate(recommendations, 1):
job = rec['job']
md_lines.append(f"### {i}. {job.get('title', 'N/A')} at {job.get('company', 'N/A')}")
md_lines.append(f"**Match Level:** {rec['match_level']} ({rec['match_score']*100:.1f}%)\n")
if rec['why_good_fit']:
md_lines.append("**Why you're a good fit:**")
for reason in rec['why_good_fit']:
md_lines.append(f"- {reason}")
if rec['action_items']:
md_lines.append("\n**Recommended actions:**")
for action in rec['action_items']:
md_lines.append(f"- {action}")
md_lines.append("\n---\n")
return '\n'.join(md_lines)
def generate_skills_gap_analysis(matches: List[Dict[str, Any]]) -> str:
"""Generate skills gap analysis markdown"""
if not matches:
return "No job matches to analyze. Search for jobs to see skills gap analysis."
md_lines = ["## πŸ“Š Skills Gap Analysis\n"]
# Aggregate missing skills across top matches
all_missing_skills = {}
for match in matches[:10]:
for skill in match['match_details'].get('missing_skills', []):
all_missing_skills[skill] = all_missing_skills.get(skill, 0) + 1
if all_missing_skills:
# Sort by frequency
sorted_skills = sorted(all_missing_skills.items(), key=lambda x: x[1], reverse=True)
md_lines.append("### Most In-Demand Skills You Should Consider Learning:\n")
for skill, count in sorted_skills[:10]:
md_lines.append(f"- **{skill}** (required by {count} jobs)")
else:
md_lines.append("Great news! Your skills align well with your target jobs.")
return '\n'.join(md_lines)