ibuilder / app.py
Soufianesejjari's picture
Add experience management and skill categorization to profile model
b39667b
raw
history blame
24.7 kB
"""
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()