Spaces:
Sleeping
Sleeping
Commit
·
b39667b
1
Parent(s):
d3528b2
Add experience management and skill categorization to profile model
Browse files- agents/profile_extractor.py +85 -116
- app.py +126 -13
- models.py +18 -3
- services/storage_service.py +24 -5
agents/profile_extractor.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
Agent for extracting profile information from resumes
|
| 3 |
"""
|
| 4 |
import groq
|
| 5 |
-
from models import Profile, SocialMedia, Project, Skill, Education
|
| 6 |
from typing import List, Dict, Any, Optional
|
| 7 |
from langchain.output_parsers import PydanticOutputParser
|
| 8 |
from langchain.prompts import PromptTemplate
|
|
@@ -61,7 +61,7 @@ class ProfileExtractor:
|
|
| 61 |
logger.error(f"LangChain extraction failed: {e}")
|
| 62 |
if settings.DEBUG:
|
| 63 |
print(f"LangChain extraction failed: {e}")
|
| 64 |
-
return
|
| 65 |
|
| 66 |
def _extract_with_langchain(self, pdf_text: str) -> Profile:
|
| 67 |
"""Extract profile with structured LangChain approach"""
|
|
@@ -75,8 +75,9 @@ class ProfileExtractor:
|
|
| 75 |
5. Tagline (a short 5-10 word catchy phrase summarizing professional identity)
|
| 76 |
6. Social media links (LinkedIn, GitHub, Instagram)
|
| 77 |
7. Projects (with title, description, and tech stack)
|
| 78 |
-
8. Skills
|
| 79 |
9. Education history (with school, degree, field of study, start date and end date)
|
|
|
|
| 80 |
|
| 81 |
Return the information in the following JSON format:
|
| 82 |
{
|
|
@@ -98,8 +99,9 @@ class ProfileExtractor:
|
|
| 98 |
}
|
| 99 |
],
|
| 100 |
"skills": [
|
| 101 |
-
{"name": "Skill 1"},
|
| 102 |
-
{"name": "Skill 2"}
|
|
|
|
| 103 |
],
|
| 104 |
"educations": [
|
| 105 |
{
|
|
@@ -109,6 +111,15 @@ class ProfileExtractor:
|
|
| 109 |
"startDate": "Start Year",
|
| 110 |
"endDate": "End Year or Present"
|
| 111 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
]
|
| 113 |
}
|
| 114 |
|
|
@@ -136,7 +147,7 @@ class ProfileExtractor:
|
|
| 136 |
json_start = response_text.find('{')
|
| 137 |
json_end = response_text.rfind('}') + 1
|
| 138 |
|
| 139 |
-
if json_start >= 0 and json_end > json_start:
|
| 140 |
json_str = response_text[json_start:json_end]
|
| 141 |
profile_dict = json.loads(json_str)
|
| 142 |
profile = Profile.model_validate(profile_dict)
|
|
@@ -217,122 +228,80 @@ class ProfileExtractor:
|
|
| 217 |
logger.debug(f"Added education: {education}")
|
| 218 |
except Exception as e:
|
| 219 |
logger.error(f"Error extracting education: {e}")
|
| 220 |
-
|
|
|
|
| 221 |
try:
|
| 222 |
-
|
| 223 |
-
skills
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
except Exception as e:
|
| 229 |
-
logger.error(f"Error extracting skills: {e}")
|
| 230 |
-
|
| 231 |
-
return profile
|
| 232 |
-
|
| 233 |
-
def _extract_with_fallback(self, pdf_text: str) -> Profile:
|
| 234 |
-
"""Fallback method for profile extraction using direct API calls"""
|
| 235 |
-
logger.debug("Extracting profile with fallback method")
|
| 236 |
-
client = groq.Groq(api_key=self.groq_api_key)
|
| 237 |
|
| 238 |
-
|
| 239 |
-
"""Helper function to get a response from the LLM."""
|
| 240 |
try:
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
)
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
except Exception as e:
|
| 249 |
-
logger.error(f"Error
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
name = get_llm_response(f"Extract the full name from the following text. If no name is present, respond with 'N/A'. Only respond with the name: {pdf_text}").strip()
|
| 253 |
-
title = get_llm_response(f"Extract the professional title from the following text. If no title is present, respond with 'N/A'. Only respond with the title: {pdf_text}").strip()
|
| 254 |
-
email = get_llm_response(f"Extract the email address from the following text. If no email is present, respond with 'N/A'. Only respond with the email: {pdf_text}").strip()
|
| 255 |
-
bio = get_llm_response(f"Create a short professional biography (around 50-100 words) based on the following text. Focus on skills and experience. If no bio is possible, respond with 'N/A'. Provide only the biography itself: {pdf_text}").strip()
|
| 256 |
-
tagline = get_llm_response(f"Create a short and catchy tagline (around 5-10 words) that summarizes the person's professional identity from the following text. If no tagline is possible, respond with 'N/A'. Provide only the tagline: {pdf_text}").strip()
|
| 257 |
-
|
| 258 |
-
linkedin = get_llm_response(f"Extract the LinkedIn profile URL from the following text. If no LinkedIn URL is present, respond with 'N/A'. Only respond with the LinkedIn URL: {pdf_text}").strip()
|
| 259 |
-
github = get_llm_response(f"Extract the GitHub profile URL from the following text. If no GitHub URL is present, respond with 'N/A'. Only respond with the GitHub URL: {pdf_text}").strip()
|
| 260 |
-
instagram = get_llm_response(f"Extract the Instagram profile URL from the following text. If no Instagram URL is present, respond with 'N/A'. Only respond with the Instagram URL: {pdf_text}").strip()
|
| 261 |
-
|
| 262 |
-
project_info = get_llm_response(f"Extract information about projects from the following text in this format Project Title: Project Description: Tech Stack:. If no projects are present, respond with 'N/A': {pdf_text}").strip()
|
| 263 |
-
skills_info = get_llm_response(f"Extract a list of skills from the following text, separated by commas. If no skills are present, respond with 'N/A'. Only respond with the skills: {pdf_text}").strip()
|
| 264 |
-
|
| 265 |
-
education_info = get_llm_response(f"Extract education history from the following resume. For each education entry, provide the school name, degree type, field of study, start date, and end date. Format as 'School: Degree: Field: StartDate: EndDate' with each education on a new line. If no education is found, respond with 'N/A': {pdf_text}").strip()
|
| 266 |
-
|
| 267 |
-
social_media = SocialMedia(
|
| 268 |
-
linkedin=linkedin if linkedin != 'N/A' else None,
|
| 269 |
-
github=github if github != 'N/A' else None,
|
| 270 |
-
instagram=instagram if instagram != 'N/A' else None
|
| 271 |
-
)
|
| 272 |
-
|
| 273 |
-
projects = []
|
| 274 |
-
if project_info != "N/A":
|
| 275 |
-
project_lines = project_info.split("\n")
|
| 276 |
-
for line in project_lines:
|
| 277 |
-
if ":" in line:
|
| 278 |
-
try:
|
| 279 |
-
project_title, project_description_techstack = line.split(":", 1)
|
| 280 |
-
project_description, tech_stack = project_description_techstack.split("Tech Stack:", 1)
|
| 281 |
-
|
| 282 |
-
projects.append(Project(
|
| 283 |
-
title=project_title.strip(),
|
| 284 |
-
description=project_description.strip(),
|
| 285 |
-
techStack=tech_stack.strip()
|
| 286 |
-
))
|
| 287 |
-
logger.debug(f"Added project: {project_title.strip()}")
|
| 288 |
-
except ValueError as e:
|
| 289 |
-
logger.error(f"Error parsing project: {line}. Error: {e}")
|
| 290 |
-
|
| 291 |
-
skills = []
|
| 292 |
-
if skills_info != "N/A":
|
| 293 |
-
skill_list = [skill.strip() for skill in skills_info.split(",")]
|
| 294 |
-
for skill_name in skill_list:
|
| 295 |
-
if skill_name:
|
| 296 |
-
skills.append(Skill(name=skill_name))
|
| 297 |
-
logger.debug(f"Added skill: {skill_name}")
|
| 298 |
-
|
| 299 |
-
educations = []
|
| 300 |
-
if education_info != "N/A":
|
| 301 |
-
education_lines = education_info.split("\n")
|
| 302 |
-
for line in education_lines:
|
| 303 |
-
if ":" in line:
|
| 304 |
-
try:
|
| 305 |
-
parts = line.split(":")
|
| 306 |
-
if len(parts) >= 5:
|
| 307 |
-
educations.append(Education(
|
| 308 |
-
school=parts[0].strip(),
|
| 309 |
-
degree=parts[1].strip(),
|
| 310 |
-
fieldOfStudy=parts[2].strip(),
|
| 311 |
-
startDate=parts[3].strip(),
|
| 312 |
-
endDate=parts[4].strip()
|
| 313 |
-
))
|
| 314 |
-
logger.debug(f"Added education: {parts[0].strip()}")
|
| 315 |
-
except Exception as e:
|
| 316 |
-
logger.error(f"Error parsing education: {line}. Error: {e}")
|
| 317 |
-
|
| 318 |
-
profile = Profile(
|
| 319 |
-
name=name if name != 'N/A' else "N/A",
|
| 320 |
-
title=title if title != 'N/A' else "N/A",
|
| 321 |
-
email=email if email != 'N/A' else "N/A",
|
| 322 |
-
bio=bio if bio != 'N/A' else "N/A",
|
| 323 |
-
tagline=tagline if tagline != 'N/A' else None,
|
| 324 |
-
social=social_media if (social_media.github or social_media.instagram or social_media.linkedin) else None,
|
| 325 |
-
chatbot=None,
|
| 326 |
-
profileImg=None,
|
| 327 |
-
heroImg=None,
|
| 328 |
-
projects=projects,
|
| 329 |
-
skills=skills,
|
| 330 |
-
educations=educations
|
| 331 |
-
)
|
| 332 |
-
|
| 333 |
-
logger.info("Profile extracted successfully with fallback method")
|
| 334 |
return profile
|
| 335 |
-
|
| 336 |
|
| 337 |
# Create module-level instance for easier imports
|
| 338 |
profile_extractor = ProfileExtractor()
|
|
|
|
| 2 |
Agent for extracting profile information from resumes
|
| 3 |
"""
|
| 4 |
import groq
|
| 5 |
+
from models import Profile, SocialMedia, Project, Skill, Education, Experience, Category
|
| 6 |
from typing import List, Dict, Any, Optional
|
| 7 |
from langchain.output_parsers import PydanticOutputParser
|
| 8 |
from langchain.prompts import PromptTemplate
|
|
|
|
| 61 |
logger.error(f"LangChain extraction failed: {e}")
|
| 62 |
if settings.DEBUG:
|
| 63 |
print(f"LangChain extraction failed: {e}")
|
| 64 |
+
return Profile(name="N/A", title="N/A", email="N/A", bio="N/A")
|
| 65 |
|
| 66 |
def _extract_with_langchain(self, pdf_text: str) -> Profile:
|
| 67 |
"""Extract profile with structured LangChain approach"""
|
|
|
|
| 75 |
5. Tagline (a short 5-10 word catchy phrase summarizing professional identity)
|
| 76 |
6. Social media links (LinkedIn, GitHub, Instagram)
|
| 77 |
7. Projects (with title, description, and tech stack)
|
| 78 |
+
8. Skills (with category only one of : Technical, Soft Skills, or Domain Knowledge)
|
| 79 |
9. Education history (with school, degree, field of study, start date and end date)
|
| 80 |
+
10. Work experience (with company, position, start date, end date, and description)
|
| 81 |
|
| 82 |
Return the information in the following JSON format:
|
| 83 |
{
|
|
|
|
| 99 |
}
|
| 100 |
],
|
| 101 |
"skills": [
|
| 102 |
+
{"name": "Skill 1", "category": "Technical"},
|
| 103 |
+
{"name": "Skill 2", "category": "Soft Skills"},
|
| 104 |
+
{"name": "Skill 3", "category": "Domain Knowledge"}
|
| 105 |
],
|
| 106 |
"educations": [
|
| 107 |
{
|
|
|
|
| 111 |
"startDate": "Start Year",
|
| 112 |
"endDate": "End Year or Present"
|
| 113 |
}
|
| 114 |
+
],
|
| 115 |
+
"experiences": [
|
| 116 |
+
{
|
| 117 |
+
"company": "Company Name",
|
| 118 |
+
"position": "Job Title",
|
| 119 |
+
"startDate": "Start Date",
|
| 120 |
+
"endDate": "End Date or Present",
|
| 121 |
+
"description": "Job Description"
|
| 122 |
+
}
|
| 123 |
]
|
| 124 |
}
|
| 125 |
|
|
|
|
| 147 |
json_start = response_text.find('{')
|
| 148 |
json_end = response_text.rfind('}') + 1
|
| 149 |
|
| 150 |
+
if (json_start >= 0 and json_end > json_start):
|
| 151 |
json_str = response_text[json_start:json_end]
|
| 152 |
profile_dict = json.loads(json_str)
|
| 153 |
profile = Profile.model_validate(profile_dict)
|
|
|
|
| 228 |
logger.debug(f"Added education: {education}")
|
| 229 |
except Exception as e:
|
| 230 |
logger.error(f"Error extracting education: {e}")
|
| 231 |
+
|
| 232 |
+
if not profile.skills:
|
| 233 |
try:
|
| 234 |
+
skills_prompt = """
|
| 235 |
+
Extract skills from this resume text and categorize them.
|
| 236 |
+
For each skill, determine if it's a Technical skill, Soft Skill, or Domain Knowledge.
|
| 237 |
+
Format the response as a JSON array of objects with 'name' and 'category' fields.
|
| 238 |
+
Example: [{"name": "Python", "category": "Technical"}, {"name": "Communication", "category": "Soft Skills"}]
|
| 239 |
+
"""
|
| 240 |
+
response = self.llm.invoke(skills_prompt + "\n\n" + pdf_text)
|
| 241 |
+
skills_text = response.content.strip()
|
| 242 |
+
|
| 243 |
+
json_start = skills_text.find('[')
|
| 244 |
+
json_end = skills_text.rfind(']') + 1
|
| 245 |
+
|
| 246 |
+
if json_start >= 0 and json_end > json_start:
|
| 247 |
+
skills_json = skills_text[json_start:json_end]
|
| 248 |
+
skills_list = json.loads(skills_json)
|
| 249 |
+
|
| 250 |
+
for skill_data in skills_list:
|
| 251 |
+
category = None
|
| 252 |
+
skill_name = skill_data.get("name", "").strip()
|
| 253 |
+
category_str = skill_data.get("category", "").strip()
|
| 254 |
+
|
| 255 |
+
# Map the category string to our Category enum
|
| 256 |
+
if category_str.lower() == "technical":
|
| 257 |
+
category = Category.TECHNICAL
|
| 258 |
+
elif category_str.lower() in ["soft skills", "soft skill"]:
|
| 259 |
+
category = Category.SOFT_SKILLS
|
| 260 |
+
elif category_str.lower() in ["domain knowledge", "domain"]:
|
| 261 |
+
category = Category.DOMAIN_KNOWLEDGE
|
| 262 |
+
|
| 263 |
+
if skill_name:
|
| 264 |
+
profile.skills.append(Skill(name=skill_name, category=category))
|
| 265 |
+
logger.debug(f"Added categorized skill: {skill_name} ({category})")
|
| 266 |
except Exception as e:
|
| 267 |
+
logger.error(f"Error extracting categorized skills: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
+
if not profile.experiences:
|
|
|
|
| 270 |
try:
|
| 271 |
+
experience_prompt = """
|
| 272 |
+
Extract work experience from this resume. For each position, provide:
|
| 273 |
+
- Company name
|
| 274 |
+
- Position/job title
|
| 275 |
+
- Start date
|
| 276 |
+
- End date (or "Present" if current)
|
| 277 |
+
- Job description (summarize responsibilities and achievements)
|
| 278 |
+
Format the response as a list of JSON objects.
|
| 279 |
+
"""
|
| 280 |
+
response = self.llm.invoke(experience_prompt + "\n\n" + pdf_text)
|
| 281 |
+
exp_text = response.content.strip()
|
| 282 |
+
|
| 283 |
+
json_start = exp_text.find('[')
|
| 284 |
+
json_end = exp_text.rfind(']') + 1
|
| 285 |
+
|
| 286 |
+
if json_start >= 0 and json_end > json_start:
|
| 287 |
+
exp_json = exp_text[json_start:json_end]
|
| 288 |
+
experiences = json.loads(exp_json)
|
| 289 |
+
|
| 290 |
+
for exp in experiences:
|
| 291 |
+
experience = Experience(
|
| 292 |
+
company=exp.get("company", "Unknown"),
|
| 293 |
+
position=exp.get("position", ""),
|
| 294 |
+
startDate=exp.get("startDate", ""),
|
| 295 |
+
endDate=exp.get("endDate", ""),
|
| 296 |
+
description=exp.get("description", "")
|
| 297 |
+
)
|
| 298 |
+
profile.experiences.append(experience)
|
| 299 |
+
logger.debug(f"Added experience: {experience.company} - {experience.position}")
|
| 300 |
except Exception as e:
|
| 301 |
+
logger.error(f"Error extracting work experience: {e}")
|
| 302 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
return profile
|
| 304 |
+
|
| 305 |
|
| 306 |
# Create module-level instance for easier imports
|
| 307 |
profile_extractor = ProfileExtractor()
|
app.py
CHANGED
|
@@ -13,7 +13,7 @@ from typing import Dict, Any
|
|
| 13 |
from agents import profile_extractor as pe, grammar_corrector as gc
|
| 14 |
from utils import extract_text_from_pdf, save_temp_pdf
|
| 15 |
from services import storage_service
|
| 16 |
-
from models import Skill, Project, Education, SocialMedia
|
| 17 |
from config import get_settings
|
| 18 |
|
| 19 |
# Get settings
|
|
@@ -182,22 +182,103 @@ def collect_missing_data(profile):
|
|
| 182 |
if proj_data["title"] and proj_data["description"]: # Only add if title and description are provided
|
| 183 |
profile.projects.append(Project(**proj_data))
|
| 184 |
|
| 185 |
-
# Skills
|
| 186 |
st.subheader("Skills")
|
| 187 |
|
| 188 |
-
#
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
| 190 |
|
| 191 |
-
# Allow editing of the skills
|
| 192 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
|
| 194 |
# Update profile with skills data
|
| 195 |
profile.skills = []
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
|
| 202 |
return profile
|
| 203 |
|
|
@@ -264,10 +345,42 @@ def display_profile(profile):
|
|
| 264 |
st.subheader("Projects")
|
| 265 |
st.table(projects_data)
|
| 266 |
|
| 267 |
-
# Display
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
if profile.skills:
|
| 269 |
st.subheader("Skills")
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
def main():
|
| 273 |
"""Main application function"""
|
|
|
|
| 13 |
from agents import profile_extractor as pe, grammar_corrector as gc
|
| 14 |
from utils import extract_text_from_pdf, save_temp_pdf
|
| 15 |
from services import storage_service
|
| 16 |
+
from models import Skill, Project, Education, SocialMedia, Experience, Category
|
| 17 |
from config import get_settings
|
| 18 |
|
| 19 |
# Get settings
|
|
|
|
| 182 |
if proj_data["title"] and proj_data["description"]: # Only add if title and description are provided
|
| 183 |
profile.projects.append(Project(**proj_data))
|
| 184 |
|
| 185 |
+
# Skills with categories
|
| 186 |
st.subheader("Skills")
|
| 187 |
|
| 188 |
+
# Group existing skills by category
|
| 189 |
+
technical_skills = [skill.name for skill in profile.skills if skill.category == Category.TECHNICAL]
|
| 190 |
+
soft_skills = [skill.name for skill in profile.skills if skill.category == Category.SOFT_SKILLS]
|
| 191 |
+
domain_skills = [skill.name for skill in profile.skills if skill.category == Category.DOMAIN_KNOWLEDGE]
|
| 192 |
+
uncategorized = [skill.name for skill in profile.skills if skill.category is None]
|
| 193 |
|
| 194 |
+
# Allow editing of the skills by category
|
| 195 |
+
st.write("Technical Skills")
|
| 196 |
+
tech_skills_input = st.text_area("Enter your technical skills (comma separated):",
|
| 197 |
+
value=", ".join(technical_skills) if technical_skills else "")
|
| 198 |
+
|
| 199 |
+
st.write("Soft Skills")
|
| 200 |
+
soft_skills_input = st.text_area("Enter your soft skills (comma separated):",
|
| 201 |
+
value=", ".join(soft_skills) if soft_skills else "")
|
| 202 |
+
|
| 203 |
+
st.write("Domain Knowledge")
|
| 204 |
+
domain_skills_input = st.text_area("Enter your domain knowledge skills (comma separated):",
|
| 205 |
+
value=", ".join(domain_skills) if domain_skills else "")
|
| 206 |
+
|
| 207 |
+
st.write("Uncategorized Skills")
|
| 208 |
+
uncategorized_input = st.text_area("Enter any other skills (comma separated):",
|
| 209 |
+
value=", ".join(uncategorized) if uncategorized else "")
|
| 210 |
|
| 211 |
# Update profile with skills data
|
| 212 |
profile.skills = []
|
| 213 |
+
|
| 214 |
+
def add_skills_by_category(skills_input, category):
|
| 215 |
+
if skills_input:
|
| 216 |
+
skills_list = [skill.strip() for skill in skills_input.split(",")]
|
| 217 |
+
for skill_name in skills_list:
|
| 218 |
+
if skill_name:
|
| 219 |
+
profile.skills.append(Skill(name=skill_name, category=category))
|
| 220 |
+
|
| 221 |
+
add_skills_by_category(tech_skills_input, Category.TECHNICAL)
|
| 222 |
+
add_skills_by_category(soft_skills_input, Category.SOFT_SKILLS)
|
| 223 |
+
add_skills_by_category(domain_skills_input, Category.DOMAIN_KNOWLEDGE)
|
| 224 |
+
add_skills_by_category(uncategorized_input, None)
|
| 225 |
+
|
| 226 |
+
# Work Experience
|
| 227 |
+
st.subheader("Work Experience")
|
| 228 |
+
experience_data = []
|
| 229 |
+
|
| 230 |
+
# Display existing experience entries with edit options
|
| 231 |
+
if profile.experiences:
|
| 232 |
+
for i, exp in enumerate(profile.experiences):
|
| 233 |
+
st.write(f"Experience #{i+1}")
|
| 234 |
+
company = st.text_input(f"Company {i+1}:", value=exp.company, key=f"company_{i}")
|
| 235 |
+
position = st.text_input(f"Position {i+1}:", value=exp.position, key=f"position_{i}")
|
| 236 |
+
start = st.text_input(f"Start Date {i+1}:", value=exp.startDate, key=f"exp_start_{i}")
|
| 237 |
+
end = st.text_input(f"End Date {i+1}:", value=exp.endDate if exp.endDate else "", key=f"exp_end_{i}")
|
| 238 |
+
description = st.text_area(f"Description {i+1}:", value=exp.description if exp.description else "", key=f"exp_desc_{i}")
|
| 239 |
+
|
| 240 |
+
if company and position: # Only add if company and position are provided
|
| 241 |
+
experience_data.append({
|
| 242 |
+
"company": company,
|
| 243 |
+
"position": position,
|
| 244 |
+
"startDate": start,
|
| 245 |
+
"endDate": end if end else None,
|
| 246 |
+
"description": description if description else None
|
| 247 |
+
})
|
| 248 |
+
|
| 249 |
+
# Option to add new experience entries
|
| 250 |
+
add_experience = st.checkbox("Add more work experience", key="add_exp")
|
| 251 |
+
if add_experience:
|
| 252 |
+
num_new_exp = st.number_input("Number of additional experiences:", min_value=1, max_value=10, value=1)
|
| 253 |
+
offset = len(profile.experiences) if profile.experiences else 0
|
| 254 |
+
|
| 255 |
+
for i in range(int(num_new_exp)):
|
| 256 |
+
st.write(f"Additional Experience #{i+1}")
|
| 257 |
+
company = st.text_input(f"Company:", key=f"new_company_{offset+i}")
|
| 258 |
+
position = st.text_input(f"Position:", key=f"new_position_{offset+i}")
|
| 259 |
+
start = st.text_input(f"Start Date:", key=f"new_exp_start_{offset+i}")
|
| 260 |
+
end = st.text_input(f"End Date:", key=f"new_exp_end_{offset+i}")
|
| 261 |
+
description = st.text_area(f"Description:", key=f"new_exp_desc_{offset+i}")
|
| 262 |
+
|
| 263 |
+
if company and position: # Only add if company and position are provided
|
| 264 |
+
correct_grammar_btn = st.button(f"Correct Grammar for Experience #{i+1}")
|
| 265 |
+
if correct_grammar_btn and description:
|
| 266 |
+
description = grammar_corrector.correct_grammar(description)
|
| 267 |
+
st.success("Grammar corrected!")
|
| 268 |
+
|
| 269 |
+
experience_data.append({
|
| 270 |
+
"company": company,
|
| 271 |
+
"position": position,
|
| 272 |
+
"startDate": start,
|
| 273 |
+
"endDate": end if end else None,
|
| 274 |
+
"description": description if description else None
|
| 275 |
+
})
|
| 276 |
+
|
| 277 |
+
# Update profile with experience data
|
| 278 |
+
profile.experiences = []
|
| 279 |
+
for exp_data in experience_data:
|
| 280 |
+
if exp_data["company"] and exp_data["position"]: # Only add if company and position are provided
|
| 281 |
+
profile.experiences.append(Experience(**exp_data))
|
| 282 |
|
| 283 |
return profile
|
| 284 |
|
|
|
|
| 345 |
st.subheader("Projects")
|
| 346 |
st.table(projects_data)
|
| 347 |
|
| 348 |
+
# Display work experience in a table if available
|
| 349 |
+
if profile.experiences:
|
| 350 |
+
experiences_data = {
|
| 351 |
+
"Company": [exp.company for exp in profile.experiences],
|
| 352 |
+
"Position": [exp.position for exp in profile.experiences],
|
| 353 |
+
"Start Date": [exp.startDate for exp in profile.experiences],
|
| 354 |
+
"End Date": [exp.endDate if exp.endDate else "Present" for exp in profile.experiences],
|
| 355 |
+
"Description": [exp.description if exp.description else "" for exp in profile.experiences]
|
| 356 |
+
}
|
| 357 |
+
st.subheader("Work Experience")
|
| 358 |
+
st.table(experiences_data)
|
| 359 |
+
|
| 360 |
+
# Display skills categorized by type if available
|
| 361 |
if profile.skills:
|
| 362 |
st.subheader("Skills")
|
| 363 |
+
|
| 364 |
+
technical = [skill.name for skill in profile.skills if skill.category == Category.TECHNICAL]
|
| 365 |
+
soft = [skill.name for skill in profile.skills if skill.category == Category.SOFT_SKILLS]
|
| 366 |
+
domain = [skill.name for skill in profile.skills if skill.category == Category.DOMAIN_KNOWLEDGE]
|
| 367 |
+
other = [skill.name for skill in profile.skills if skill.category is None]
|
| 368 |
+
|
| 369 |
+
if technical:
|
| 370 |
+
st.write("**Technical Skills:**")
|
| 371 |
+
st.write(", ".join(technical))
|
| 372 |
+
|
| 373 |
+
if soft:
|
| 374 |
+
st.write("**Soft Skills:**")
|
| 375 |
+
st.write(", ".join(soft))
|
| 376 |
+
|
| 377 |
+
if domain:
|
| 378 |
+
st.write("**Domain Knowledge:**")
|
| 379 |
+
st.write(", ".join(domain))
|
| 380 |
+
|
| 381 |
+
if other:
|
| 382 |
+
st.write("**Other Skills:**")
|
| 383 |
+
st.write(", ".join(other))
|
| 384 |
|
| 385 |
def main():
|
| 386 |
"""Main application function"""
|
models.py
CHANGED
|
@@ -1,9 +1,16 @@
|
|
| 1 |
from typing import List, Optional
|
| 2 |
from pydantic import BaseModel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
class Skill(BaseModel):
|
| 5 |
img: Optional[str] = None # Allow null values
|
| 6 |
name: str
|
|
|
|
| 7 |
|
| 8 |
class Project(BaseModel):
|
| 9 |
img: Optional[str] = None
|
|
@@ -21,13 +28,20 @@ class SocialMedia(BaseModel):
|
|
| 21 |
class Chatbot(BaseModel):
|
| 22 |
token: str
|
| 23 |
apiBaseURL: str
|
| 24 |
-
|
| 25 |
class Education(BaseModel):
|
| 26 |
school: str
|
| 27 |
degree: str
|
| 28 |
fieldOfStudy: str
|
| 29 |
startDate: str
|
| 30 |
-
endDate: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
class Profile(BaseModel):
|
| 33 |
name: str
|
|
@@ -42,4 +56,5 @@ class Profile(BaseModel):
|
|
| 42 |
projects: List[Project] = []
|
| 43 |
skills: List[Skill] = []
|
| 44 |
topSkills: List[str] = []
|
| 45 |
-
educations: List[Education] = []
|
|
|
|
|
|
| 1 |
from typing import List, Optional
|
| 2 |
from pydantic import BaseModel
|
| 3 |
+
from enum import Enum
|
| 4 |
+
|
| 5 |
+
class Category(str, Enum):
|
| 6 |
+
TECHNICAL = "Technical"
|
| 7 |
+
SOFT_SKILLS = "Soft Skills"
|
| 8 |
+
DOMAIN_KNOWLEDGE = "Domain Knowledge"
|
| 9 |
|
| 10 |
class Skill(BaseModel):
|
| 11 |
img: Optional[str] = None # Allow null values
|
| 12 |
name: str
|
| 13 |
+
category: Optional[Category] = None
|
| 14 |
|
| 15 |
class Project(BaseModel):
|
| 16 |
img: Optional[str] = None
|
|
|
|
| 28 |
class Chatbot(BaseModel):
|
| 29 |
token: str
|
| 30 |
apiBaseURL: str
|
| 31 |
+
|
| 32 |
class Education(BaseModel):
|
| 33 |
school: str
|
| 34 |
degree: str
|
| 35 |
fieldOfStudy: str
|
| 36 |
startDate: str
|
| 37 |
+
endDate: str
|
| 38 |
+
|
| 39 |
+
class Experience(BaseModel):
|
| 40 |
+
company: str
|
| 41 |
+
position: str
|
| 42 |
+
startDate: str
|
| 43 |
+
endDate: Optional[str] = None
|
| 44 |
+
description: Optional[str] = None
|
| 45 |
|
| 46 |
class Profile(BaseModel):
|
| 47 |
name: str
|
|
|
|
| 56 |
projects: List[Project] = []
|
| 57 |
skills: List[Skill] = []
|
| 58 |
topSkills: List[str] = []
|
| 59 |
+
educations: List[Education] = []
|
| 60 |
+
experiences: List[Experience] = []
|
services/storage_service.py
CHANGED
|
@@ -38,6 +38,7 @@ class StorageService:
|
|
| 38 |
projects TEXT,
|
| 39 |
skills TEXT,
|
| 40 |
educations TEXT
|
|
|
|
| 41 |
)
|
| 42 |
""")
|
| 43 |
conn.commit()
|
|
@@ -70,7 +71,22 @@ class StorageService:
|
|
| 70 |
"demoUrl": project.demoUrl
|
| 71 |
} for project in profile.projects
|
| 72 |
]),
|
| 73 |
-
"skills": json.dumps([
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
"educations": json.dumps([
|
| 75 |
{
|
| 76 |
"school": edu.school,
|
|
@@ -147,8 +163,8 @@ class StorageService:
|
|
| 147 |
conn = sqlite3.connect(self.db_path)
|
| 148 |
cursor = conn.cursor()
|
| 149 |
cursor.execute("""
|
| 150 |
-
INSERT INTO profiles (name, title, email, bio, tagline, social, profileImg, projects, skills, educations)
|
| 151 |
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 152 |
""", (
|
| 153 |
profile_dict["name"],
|
| 154 |
profile_dict["title"],
|
|
@@ -159,7 +175,8 @@ class StorageService:
|
|
| 159 |
profile_dict["profileImg"],
|
| 160 |
profile_dict["projects"],
|
| 161 |
profile_dict["skills"],
|
| 162 |
-
profile_dict["educations"]
|
|
|
|
| 163 |
))
|
| 164 |
conn.commit()
|
| 165 |
profile_id = str(cursor.lastrowid)
|
|
@@ -211,7 +228,9 @@ class StorageService:
|
|
| 211 |
"profileImg": row[7],
|
| 212 |
"projects": json.loads(row[8]) if row[8] else [],
|
| 213 |
"skills": json.loads(row[9]) if row[9] else [],
|
| 214 |
-
"educations": json.loads(row[10]) if row[10] else []
|
|
|
|
|
|
|
| 215 |
}
|
| 216 |
logger.debug(f"Retrieved profile: {profile_id}")
|
| 217 |
return profile
|
|
|
|
| 38 |
projects TEXT,
|
| 39 |
skills TEXT,
|
| 40 |
educations TEXT
|
| 41 |
+
experiences TEXT
|
| 42 |
)
|
| 43 |
""")
|
| 44 |
conn.commit()
|
|
|
|
| 71 |
"demoUrl": project.demoUrl
|
| 72 |
} for project in profile.projects
|
| 73 |
]),
|
| 74 |
+
"skills": json.dumps([
|
| 75 |
+
{
|
| 76 |
+
"name": skill.name,
|
| 77 |
+
"category": skill.category.value if skill.category else None,
|
| 78 |
+
"img": skill.img
|
| 79 |
+
} for skill in profile.skills
|
| 80 |
+
]),
|
| 81 |
+
"experiences": json.dumps([
|
| 82 |
+
{
|
| 83 |
+
"company": exp.company,
|
| 84 |
+
"position": exp.position,
|
| 85 |
+
"startDate": exp.startDate,
|
| 86 |
+
"endDate": exp.endDate,
|
| 87 |
+
"description": exp.description
|
| 88 |
+
} for exp in profile.experiences
|
| 89 |
+
]),
|
| 90 |
"educations": json.dumps([
|
| 91 |
{
|
| 92 |
"school": edu.school,
|
|
|
|
| 163 |
conn = sqlite3.connect(self.db_path)
|
| 164 |
cursor = conn.cursor()
|
| 165 |
cursor.execute("""
|
| 166 |
+
INSERT INTO profiles (name, title, email, bio, tagline, social, profileImg, projects, skills, educations, experiences)
|
| 167 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 168 |
""", (
|
| 169 |
profile_dict["name"],
|
| 170 |
profile_dict["title"],
|
|
|
|
| 175 |
profile_dict["profileImg"],
|
| 176 |
profile_dict["projects"],
|
| 177 |
profile_dict["skills"],
|
| 178 |
+
profile_dict["educations"],
|
| 179 |
+
profile_dict["experiences"]
|
| 180 |
))
|
| 181 |
conn.commit()
|
| 182 |
profile_id = str(cursor.lastrowid)
|
|
|
|
| 228 |
"profileImg": row[7],
|
| 229 |
"projects": json.loads(row[8]) if row[8] else [],
|
| 230 |
"skills": json.loads(row[9]) if row[9] else [],
|
| 231 |
+
"educations": json.loads(row[10]) if row[10] else [],
|
| 232 |
+
"experiences": json.loads(row[11]) if row[11] else []
|
| 233 |
+
|
| 234 |
}
|
| 235 |
logger.debug(f"Retrieved profile: {profile_id}")
|
| 236 |
return profile
|