Spaces:
Sleeping
Sleeping
| """ | |
| Streamlit web application for resume profile extraction | |
| """ | |
| import streamlit as st | |
| import os | |
| import json | |
| import traceback | |
| import base64 | |
| import logging | |
| from typing import Dict, Any | |
| # Import from our refactored modules | |
| from agents import profile_extractor as pe, grammar_corrector as gc | |
| from utils import extract_text_from_pdf, save_temp_pdf | |
| from services import storage_service | |
| from models import Skill, Project, Education, SocialMedia, Experience, Category | |
| from config import get_settings | |
| # Get settings | |
| settings = get_settings() | |
| # Configure logging | |
| logging.basicConfig( | |
| level=logging.DEBUG if settings.DEBUG else logging.INFO, | |
| format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| profile_extractor=pe.ProfileExtractor() | |
| grammar_corrector=gc.GrammarCorrector() | |
| def collect_missing_data(profile): | |
| """ | |
| Collects missing data from user input when automatic extraction fails. | |
| Args: | |
| profile: The profile object with potentially missing data | |
| Returns: | |
| Updated profile object | |
| """ | |
| st.subheader("Complete Your Profile") | |
| st.write("Please fill in any missing information below:") | |
| # Profile image upload | |
| st.subheader("Profile Image") | |
| uploaded_file = st.file_uploader("Upload a profile image (optional)", type=["jpg", "jpeg", "png"]) | |
| if uploaded_file is not None: | |
| # Convert the file to base64 for storage | |
| bytes_data = uploaded_file.getvalue() | |
| encoded = base64.b64encode(bytes_data).decode() | |
| profile.profileImg = f"data:image/{uploaded_file.type.split('/')[-1]};base64,{encoded}" | |
| st.image(uploaded_file, caption="Profile Image Preview", width=150) | |
| # Essential information | |
| st.subheader("Basic Information") | |
| profile.name = st.text_input("Your Full Name:", value=profile.name if profile.name != "N/A" else "") | |
| profile.title = st.text_input("Your Professional Title:", value=profile.title if profile.title != "N/A" else "") | |
| profile.email = st.text_input("Your Email Address:", value=profile.email if profile.email != "N/A" else "") | |
| profile.bio = st.text_area("Professional Bio (50-100 words):", value=profile.bio if profile.bio != "N/A" else "") | |
| if profile.bio and profile.bio != "N/A": | |
| if st.button("Improve Bio Grammar"): | |
| profile.bio = grammar_corrector.correct_grammar(profile.bio) | |
| st.success("Grammar corrected!") | |
| # Optional information | |
| profile.tagline = st.text_input("Professional Tagline (short catchy phrase):", value=profile.tagline if profile.tagline else "") | |
| # Social media | |
| if not profile.social: | |
| profile.social = SocialMedia() | |
| st.subheader("Social Media Links") | |
| profile.social.linkedin = st.text_input("LinkedIn URL:", value=profile.social.linkedin if profile.social and profile.social.linkedin else "") | |
| profile.social.github = st.text_input("GitHub URL:", value=profile.social.github if profile.social and profile.social.github else "") | |
| profile.social.instagram = st.text_input("Instagram URL:", value=profile.social.instagram if profile.social and profile.social.instagram else "") | |
| # Education | |
| st.subheader("Education") | |
| education_data = [] | |
| # Display existing education entries with edit options | |
| if profile.educations: | |
| for i, edu in enumerate(profile.educations): | |
| st.write(f"Education #{i+1}") | |
| school = st.text_input(f"School {i+1}:", value=edu.school, key=f"school_{i}") | |
| degree = st.text_input(f"Degree {i+1}:", value=edu.degree, key=f"degree_{i}") | |
| field = st.text_input(f"Field of Study {i+1}:", value=edu.fieldOfStudy, key=f"field_{i}") | |
| start = st.text_input(f"Start Date {i+1}:", value=edu.startDate, key=f"start_{i}") | |
| end = st.text_input(f"End Date {i+1}:", value=edu.endDate, key=f"end_{i}") | |
| education_data.append({ | |
| "school": school, | |
| "degree": degree, | |
| "fieldOfStudy": field, | |
| "startDate": start, | |
| "endDate": end | |
| }) | |
| # Option to add new education entries | |
| add_education = st.checkbox("Add more education", key="add_edu") | |
| if add_education: | |
| num_new_edu = st.number_input("Number of additional education entries:", min_value=1, max_value=5, value=1) | |
| offset = len(profile.educations) if profile.educations else 0 | |
| for i in range(int(num_new_edu)): | |
| st.write(f"Additional Education #{i+1}") | |
| school = st.text_input(f"School:", key=f"new_school_{offset+i}") | |
| degree = st.text_input(f"Degree:", key=f"new_degree_{offset+i}") | |
| field = st.text_input(f"Field of Study:", key=f"new_field_{offset+i}") | |
| start = st.text_input(f"Start Date:", key=f"new_start_{offset+i}") | |
| end = st.text_input(f"End Date:", key=f"new_end_{offset+i}") | |
| if school: # Only add if at least school is provided | |
| education_data.append({ | |
| "school": school, | |
| "degree": degree, | |
| "fieldOfStudy": field, | |
| "startDate": start, | |
| "endDate": end | |
| }) | |
| # Update profile with education data | |
| profile.educations = [] | |
| for edu_data in education_data: | |
| if edu_data["school"]: # Only add if school is provided | |
| profile.educations.append(Education(**edu_data)) | |
| # Projects | |
| st.subheader("Projects") | |
| project_data = [] | |
| # Display existing projects with edit options | |
| if profile.projects: | |
| for i, proj in enumerate(profile.projects): | |
| st.write(f"Project #{i+1}") | |
| title = st.text_input(f"Title:", value=proj.title, key=f"proj_title_{i}") | |
| description = st.text_area(f"Description:", value=proj.description, key=f"proj_desc_{i}") | |
| tech_stack = st.text_input(f"Tech Stack:", value=proj.techStack if proj.techStack else "", key=f"proj_tech_{i}") | |
| github_url = st.text_input(f"GitHub URL:", value=proj.githubUrl if proj.githubUrl else "", key=f"proj_git_{i}") | |
| demo_url = st.text_input(f"Demo URL:", value=proj.demoUrl if proj.demoUrl else "", key=f"proj_demo_{i}") | |
| if title and description: # Only add if title and description are provided | |
| project_data.append({ | |
| "title": title, | |
| "description": description, | |
| "techStack": tech_stack, | |
| "githubUrl": github_url if github_url else None, | |
| "demoUrl": demo_url if demo_url else None | |
| }) | |
| # Option to add new projects | |
| add_project = st.checkbox("Add more projects", key="add_proj") | |
| if add_project: | |
| num_new_proj = st.number_input("Number of additional projects:", min_value=1, max_value=5, value=1) | |
| offset = len(profile.projects) if profile.projects else 0 | |
| for i in range(int(num_new_proj)): | |
| st.write(f"Additional Project #{i+1}") | |
| title = st.text_input(f"Title:", key=f"new_proj_title_{offset+i}") | |
| description = st.text_area(f"Description:", key=f"new_proj_desc_{offset+i}") | |
| tech_stack = st.text_input(f"Tech Stack:", key=f"new_proj_tech_{offset+i}") | |
| github_url = st.text_input(f"GitHub URL (optional):", key=f"new_proj_git_{offset+i}") | |
| demo_url = st.text_input(f"Demo URL (optional):", key=f"new_proj_demo_{offset+i}") | |
| if title and description: # Only add if title and description are provided | |
| correct_grammar_btn = st.button(f"Correct Grammar for Project #{i+1}") | |
| if correct_grammar_btn: | |
| description = grammar_corrector.correct_grammar(description) | |
| st.success("Grammar corrected!") | |
| project_data.append({ | |
| "title": title, | |
| "description": description, | |
| "techStack": tech_stack, | |
| "githubUrl": github_url if github_url else None, | |
| "demoUrl": demo_url if demo_url else None | |
| }) | |
| # Update profile with project data | |
| profile.projects = [] | |
| for proj_data in project_data: | |
| if proj_data["title"] and proj_data["description"]: # Only add if title and description are provided | |
| profile.projects.append(Project(**proj_data)) | |
| # Skills with categories | |
| st.subheader("Skills") | |
| # Group existing skills by category | |
| technical_skills = [skill.name for skill in profile.skills if skill.category == Category.TECHNICAL] | |
| soft_skills = [skill.name for skill in profile.skills if skill.category == Category.SOFT_SKILLS] | |
| domain_skills = [skill.name for skill in profile.skills if skill.category == Category.DOMAIN_KNOWLEDGE] | |
| uncategorized = [skill.name for skill in profile.skills if skill.category is None] | |
| # Allow editing of the skills by category | |
| st.write("Technical Skills") | |
| tech_skills_input = st.text_area("Enter your technical skills (comma separated):", | |
| value=", ".join(technical_skills) if technical_skills else "") | |
| st.write("Soft Skills") | |
| soft_skills_input = st.text_area("Enter your soft skills (comma separated):", | |
| value=", ".join(soft_skills) if soft_skills else "") | |
| st.write("Domain Knowledge") | |
| domain_skills_input = st.text_area("Enter your domain knowledge skills (comma separated):", | |
| value=", ".join(domain_skills) if domain_skills else "") | |
| st.write("Uncategorized Skills") | |
| uncategorized_input = st.text_area("Enter any other skills (comma separated):", | |
| value=", ".join(uncategorized) if uncategorized else "") | |
| # Update profile with skills data | |
| profile.skills = [] | |
| def add_skills_by_category(skills_input, category): | |
| if skills_input: | |
| skills_list = [skill.strip() for skill in skills_input.split(",")] | |
| for skill_name in skills_list: | |
| if skill_name: | |
| profile.skills.append(Skill(name=skill_name, category=category)) | |
| add_skills_by_category(tech_skills_input, Category.TECHNICAL) | |
| add_skills_by_category(soft_skills_input, Category.SOFT_SKILLS) | |
| add_skills_by_category(domain_skills_input, Category.DOMAIN_KNOWLEDGE) | |
| add_skills_by_category(uncategorized_input, None) | |
| # Work Experience | |
| st.subheader("Work Experience") | |
| experience_data = [] | |
| # Display existing experience entries with edit options | |
| if profile.experiences: | |
| for i, exp in enumerate(profile.experiences): | |
| st.write(f"Experience #{i+1}") | |
| company = st.text_input(f"Company {i+1}:", value=exp.company, key=f"company_{i}") | |
| position = st.text_input(f"Position {i+1}:", value=exp.position, key=f"position_{i}") | |
| start = st.text_input(f"Start Date {i+1}:", value=exp.startDate, key=f"exp_start_{i}") | |
| end = st.text_input(f"End Date {i+1}:", value=exp.endDate if exp.endDate else "", key=f"exp_end_{i}") | |
| description = st.text_area(f"Description {i+1}:", value=exp.description if exp.description else "", key=f"exp_desc_{i}") | |
| if company and position: # Only add if company and position are provided | |
| experience_data.append({ | |
| "company": company, | |
| "position": position, | |
| "startDate": start, | |
| "endDate": end if end else None, | |
| "description": description if description else None | |
| }) | |
| # Option to add new experience entries | |
| add_experience = st.checkbox("Add more work experience", key="add_exp") | |
| if add_experience: | |
| num_new_exp = st.number_input("Number of additional experiences:", min_value=1, max_value=10, value=1) | |
| offset = len(profile.experiences) if profile.experiences else 0 | |
| for i in range(int(num_new_exp)): | |
| st.write(f"Additional Experience #{i+1}") | |
| company = st.text_input(f"Company:", key=f"new_company_{offset+i}") | |
| position = st.text_input(f"Position:", key=f"new_position_{offset+i}") | |
| start = st.text_input(f"Start Date:", key=f"new_exp_start_{offset+i}") | |
| end = st.text_input(f"End Date:", key=f"new_exp_end_{offset+i}") | |
| description = st.text_area(f"Description:", key=f"new_exp_desc_{offset+i}") | |
| if company and position: # Only add if company and position are provided | |
| correct_grammar_btn = st.button(f"Correct Grammar for Experience #{i+1}") | |
| if correct_grammar_btn and description: | |
| description = grammar_corrector.correct_grammar(description) | |
| st.success("Grammar corrected!") | |
| experience_data.append({ | |
| "company": company, | |
| "position": position, | |
| "startDate": start, | |
| "endDate": end if end else None, | |
| "description": description if description else None | |
| }) | |
| # Update profile with experience data | |
| profile.experiences = [] | |
| for exp_data in experience_data: | |
| if exp_data["company"] and exp_data["position"]: # Only add if company and position are provided | |
| profile.experiences.append(Experience(**exp_data)) | |
| return profile | |
| def display_profile(profile): | |
| """ | |
| Displays a profile in the Streamlit UI | |
| Args: | |
| profile: The Profile object to display | |
| """ | |
| st.header("Your Complete Profile") | |
| # Display profile image if available | |
| if profile.profileImg: | |
| st.image(profile.profileImg, width=150) | |
| # Display basic info in a table | |
| basic_data = { | |
| "Field": ["Name", "Title", "Email", "Bio", "Tagline"], | |
| "Value": [ | |
| profile.name, | |
| profile.title, | |
| profile.email, | |
| profile.bio, | |
| profile.tagline if profile.tagline else "" | |
| ] | |
| } | |
| st.table(basic_data) | |
| # Display social media if available | |
| if profile.social: | |
| social_data = { | |
| "Platform": ["LinkedIn", "GitHub", "Instagram"], | |
| "URL": [ | |
| profile.social.linkedin if profile.social.linkedin else "", | |
| profile.social.github if profile.social.github else "", | |
| profile.social.instagram if profile.social.instagram else "" | |
| ] | |
| } | |
| st.subheader("Social Media") | |
| st.table(social_data) | |
| # Display education in a table if available | |
| if profile.educations: | |
| education_data = { | |
| "School": [edu.school for edu in profile.educations], | |
| "Degree": [edu.degree for edu in profile.educations], | |
| "Field of Study": [edu.fieldOfStudy for edu in profile.educations], | |
| "Start Date": [edu.startDate for edu in profile.educations], | |
| "End Date": [edu.endDate for edu in profile.educations] | |
| } | |
| st.subheader("Education") | |
| st.table(education_data) | |
| # Display projects in a table if available | |
| if profile.projects: | |
| projects_data = { | |
| "Title": [project.title for project in profile.projects], | |
| "Description": [project.description for project in profile.projects], | |
| "Tech Stack": [project.techStack if project.techStack else "" for project in profile.projects], | |
| "GitHub": [project.githubUrl if project.githubUrl else "" for project in profile.projects], | |
| "Demo": [project.demoUrl if project.demoUrl else "" for project in profile.projects] | |
| } | |
| st.subheader("Projects") | |
| st.table(projects_data) | |
| # Display work experience in a table if available | |
| if profile.experiences: | |
| experiences_data = { | |
| "Company": [exp.company for exp in profile.experiences], | |
| "Position": [exp.position for exp in profile.experiences], | |
| "Start Date": [exp.startDate for exp in profile.experiences], | |
| "End Date": [exp.endDate if exp.endDate else "Present" for exp in profile.experiences], | |
| "Description": [exp.description if exp.description else "" for exp in profile.experiences] | |
| } | |
| st.subheader("Work Experience") | |
| st.table(experiences_data) | |
| # Display skills categorized by type if available | |
| if profile.skills: | |
| st.subheader("Skills") | |
| technical = [skill.name for skill in profile.skills if skill.category == Category.TECHNICAL] | |
| soft = [skill.name for skill in profile.skills if skill.category == Category.SOFT_SKILLS] | |
| domain = [skill.name for skill in profile.skills if skill.category == Category.DOMAIN_KNOWLEDGE] | |
| other = [skill.name for skill in profile.skills if skill.category is None] | |
| if technical: | |
| st.write("**Technical Skills:**") | |
| st.write(", ".join(technical)) | |
| if soft: | |
| st.write("**Soft Skills:**") | |
| st.write(", ".join(soft)) | |
| if domain: | |
| st.write("**Domain Knowledge:**") | |
| st.write(", ".join(domain)) | |
| if other: | |
| st.write("**Other Skills:**") | |
| st.write(", ".join(other)) | |
| def main(): | |
| """Main application function""" | |
| st.set_page_config(page_title="Resume Profile Extractor", page_icon="📄", layout="wide") | |
| st.title("Professional Profile Extractor") | |
| st.write("Upload a resume PDF to extract professional profile information") | |
| # Initialize session state variables | |
| if 'profile' not in st.session_state: | |
| st.session_state.profile = None | |
| if 'extraction_complete' not in st.session_state: | |
| st.session_state.extraction_complete = False | |
| if 'user_input_complete' not in st.session_state: | |
| st.session_state.user_input_complete = False | |
| if 'profile_saved' not in st.session_state: | |
| st.session_state.profile_saved = False | |
| # Step 1: Upload PDF and Extract Profile | |
| if not st.session_state.extraction_complete: | |
| uploaded_file = st.file_uploader("Upload a PDF resume", type="pdf") | |
| if uploaded_file is not None: | |
| try: | |
| # Save the uploaded file to a temporary location | |
| pdf_path = save_temp_pdf(uploaded_file.getvalue()) | |
| # Extract text from the PDF | |
| pdf_text = extract_text_from_pdf(pdf_path) | |
| if not pdf_text: | |
| st.error("Could not extract text from the PDF. The file might be scanned or protected.") | |
| else: | |
| with st.spinner("Extracting profile information..."): | |
| # Extract profile information using the profile extractor agent | |
| profile = profile_extractor.extract_profile(pdf_text) | |
| st.session_state.profile = profile | |
| st.session_state.extraction_complete = True | |
| st.rerun() | |
| # Clean up temporary file | |
| if os.path.exists(pdf_path): | |
| os.remove(pdf_path) | |
| except Exception as e: | |
| logger.error(f"Error during profile extraction: {e}") | |
| st.error(f"An error occurred during profile extraction: {str(e)}") | |
| if "403" in str(e): | |
| st.error("Authorization error (403 Forbidden). Please check your API key and permissions.") | |
| with st.expander("Technical Details"): | |
| st.code(traceback.format_exc()) | |
| # Step 2: Allow User to Edit/Complete the Profile | |
| elif not st.session_state.user_input_complete: | |
| st.info("We've extracted information from your resume. Please review and complete any missing details.") | |
| # Call the function to collect and complete missing data | |
| profile = collect_missing_data(st.session_state.profile) | |
| # Add buttons for submitting or starting over | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| if st.button("Save Profile"): | |
| st.session_state.profile = profile | |
| st.session_state.user_input_complete = True | |
| st.rerun() | |
| with col2: | |
| if st.button("Start Over"): | |
| st.session_state.profile = None | |
| st.session_state.extraction_complete = False | |
| st.rerun() | |
| # Step 3: Save Profile and Display Results | |
| elif not st.session_state.profile_saved: | |
| profile = st.session_state.profile | |
| try: | |
| # Store the profile using the storage service | |
| inserted_id = storage_service.store_profile( | |
| profile, | |
| error_handler=st.error | |
| ) | |
| if inserted_id: | |
| st.success(f"Profile saved successfully with ID: {inserted_id}") | |
| # Display the API URL using the configured API URL | |
| api_url = f"{settings.PORTFOLIO_URL}/{inserted_id}" | |
| st.info(f"Access your profile via API: [Profile API]({api_url})") | |
| # Show External API endpoint if available | |
| if hasattr(st.session_state, 'external_api_result') and st.session_state.external_api_result.get('success'): | |
| external_url = st.session_state.external_api_result.get('external_url') | |
| if external_url: | |
| st.success(f"Profile sent to external API: [External Profile Link]({external_url})") | |
| elif settings.EXTERNAL_API_URL: | |
| st.warning("Profile was not successfully sent to the external API") | |
| # Show API endpoint for developers | |
| with st.expander("API Endpoints"): | |
| st.markdown(f""" | |
| ### API Endpoints | |
| - **GET** `{api_url}` | |
| Get the full profile data | |
| - **GET** `{api_url}/image` | |
| Get just the profile image | |
| """) | |
| # Add external API endpoints if available | |
| if settings.EXTERNAL_API_URL and hasattr(st.session_state, 'external_api_result') and st.session_state.external_api_result.get('success'): | |
| external_url = st.session_state.external_api_result.get('external_url') | |
| if external_url: | |
| st.markdown(f""" | |
| ### External API Endpoints | |
| - **GET** `{external_url}` | |
| Get the profile data from external API | |
| """) | |
| # Display the Portfolio URL for the frontend | |
| # If we have an external frontend URL configured | |
| frontend_url = os.environ.get("FRONTEND_URL", "") | |
| if frontend_url: | |
| st.info(f"Access your portfolio: [Portfolio URL]({frontend_url}/{inserted_id})") | |
| # Mark as saved in session state | |
| st.session_state.profile_saved = True | |
| # Display the complete profile | |
| display_profile(profile) | |
| else: | |
| st.error("Failed to save profile.") | |
| except Exception as e: | |
| logger.error(f"Error saving profile: {e}") | |
| st.error(f"Error saving profile: {str(e)}") | |
| with st.expander("Technical Details"): | |
| st.code(traceback.format_exc()) | |
| # Final state - allow extracting another profile | |
| else: | |
| st.success("Profile extraction complete!") | |
| # Show options to extract another profile or view the current one | |
| if st.button("Extract Another Profile"): | |
| # Reset session state | |
| for key in ['profile', 'extraction_complete', 'user_input_complete', 'profile_saved']: | |
| st.session_state[key] = False | |
| st.rerun() | |
| else: | |
| # Show the profile again | |
| display_profile(st.session_state.profile) | |
| if __name__ == "__main__": | |
| main() |