from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Dict, Optional, Union
import pandas as pd
import numpy as np
import json
import logging
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity
from fastapi.middleware.cors import CORSMiddleware
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain.schema import HumanMessage, SystemMessage
import re
from collections import defaultdict
import os
import asyncio
from urllib.parse import urlparse
import time
from functools import lru_cache
import gradio as gr
from datetime import datetime, timedelta
import hashlib
import urllib.request
import urllib.error
from html.parser import HTMLParser
import re as _re_for_url_extract
app = FastAPI(
title="Advanced Job Skill & Certification Matcher",
description="Comprehensive certification analysis with reliability scoring, skill matching, and career path recommendations",
version="10.0"
)
logger = logging.getLogger("uvicorn")
logging.basicConfig(level=logging.INFO)
# Configuration - Using only skil.xlsx file
SKILL_FILE = "skil.xlsx"
GOOGLE_API_KEY = "AIzaSyBZNWhMXa9SG0WDKbK5uhLc5ewxFmOyH_Y"
# Reliability scoring factors
CERT_RELIABILITY_FACTORS = {
"provider_reputation": {
"weight": 0.25,
"providers": {
"microsoft": 0.9, "aws": 0.95, "google": 0.9, "cisco": 0.85,
"comptia": 0.8, "pmi": 0.85, "isc2": 0.9, "oracle": 0.85,
"ibm": 0.8, "salesforce": 0.85, "apple": 0.8, "linux": 0.8
}
},
"industry_demand": {"weight": 0.2},
"exam_rigor": {
"weight": 0.15,
"indicators": ["proctored", "practical", "hands-on", "lab", "performance-based"]
},
"validity_period": {
"weight": 0.1,
"scale": {"lifetime": 1.0, "3_years": 0.8, "2_years": 0.7, "1_year": 0.5}
},
"renewal_requirements": {
"weight": 0.1,
"scale": {"none": 0.3, "exam": 0.8, "continuing_education": 0.7, "both": 0.9}
},
"market_recognition": {"weight": 0.2}
}
# Models
class InputText(BaseModel):
text: str
min_relevance: float = 0.4
max_internal_results: int = 20
max_external_results: int = 10
analyze: bool = True
class CertificateURL(BaseModel):
url: str
extract_skills: bool = True
analyze_reliability: bool = True
class CertificationComparison(BaseModel):
cert_names: List[str]
compare_by: str = "reliability"
class LearningPathRequest(BaseModel):
target_role: str
current_skills: List[str]
timeframe: str = "6_months"
budget: str = "moderate"
class SkillGapAnalysis(BaseModel):
current_skills: List[str]
target_skills: List[str]
current_role: Optional[str] = None
target_role: Optional[str] = None
class CertificationTracker(BaseModel):
cert_name: str
achieved_date: str
expiration_date: Optional[str] = None
renewal_requirements: Optional[List[str]] = None
class AnalyzeCertificateURL(BaseModel):
url: str
min_relevance: float = 0.4
max_internal_results: int = 20
max_external_results: int = 10
analyze: bool = True
# New model for job ontology extraction
class JobOntologyRequest(BaseModel):
job_description: str
include_certificates: bool = True
min_relevance: float = 0.4
# Initialize LLM
llm = ChatGoogleGenerativeAI(
model="gemini-2.0-flash",
temperature=0.2,
google_api_key=GOOGLE_API_KEY
)
# Global cached models and throttling
_embedding_model_cached = None
LLM_MAX_CONCURRENCY = 2
_llm_semaphore = asyncio.Semaphore(LLM_MAX_CONCURRENCY)
def get_embedding_model():
"""Return a singleton embedding model instance."""
global _embedding_model_cached
if _embedding_model_cached is None:
_embedding_model_cached = SentenceTransformer('BAAI/bge-small-en-v1.5')
return _embedding_model_cached
async def ainvoke_llm(messages, retries: int = 2, delay: float = 0.8):
"""LLM call with concurrency guard and simple retries."""
attempt = 0
while True:
try:
async with _llm_semaphore:
return await llm.ainvoke(messages)
except Exception as e:
attempt += 1
if attempt > retries:
raise
await asyncio.sleep(delay * attempt)
# Global variables
certs_df = None
cert_embeddings = None
skills_df = None
skill_embeddings = None
id_col = None
skill_col = None
# --------------------------
# ENHANCED FUNCTIONS
# --------------------------
# Utilities to cope with duplicate column names and non-scalar cells
def get_column_series(df: pd.DataFrame, column_name: str) -> Optional[pd.Series]:
"""Return a single Series for a column name even if duplicates exist."""
if df is None or column_name not in df.columns:
return None
col = df[column_name]
if isinstance(col, pd.DataFrame):
# Pick the first occurrence
return col.iloc[:, 0]
return col
def get_row_scalar(row: pd.Series, key: str) -> str:
"""Safely extract a scalar string from a row that may have duplicate column names."""
if row is None:
return ""
try:
value = row.get(key, "")
except Exception:
value = ""
if isinstance(value, pd.Series):
# Choose first non-empty string among duplicates
for v in value.values:
s = str(v).strip()
if s and s.lower() != 'nan':
return s
return ""
return str(value)
# Robust getter to fetch a field using multiple candidate column names
def get_field_with_fallback(row: pd.Series, candidate_keys: List[str]) -> str:
"""Try multiple column names and return the first non-empty scalar string."""
for key in candidate_keys:
if key in row.index:
val = get_row_scalar(row, key)
s = str(val).strip()
if s and s.lower() != 'nan' and s.lower() != 'none':
return s
return ""
async def calculate_certificate_reliability(cert_data: Dict) -> Dict:
"""Calculate comprehensive reliability score for a certification"""
scores = {}
total_score = 0.0
# 1. Provider reputation scoring
provider = str(cert_data.get("provider", "")).lower()
provider_score = 0.5 # Default for unknown providers
for known_provider, score in CERT_RELIABILITY_FACTORS["provider_reputation"]["providers"].items():
if known_provider in provider:
provider_score = score
break
provider_weight = CERT_RELIABILITY_FACTORS["provider_reputation"]["weight"]
scores["provider_reputation"] = provider_score
total_score += provider_score * provider_weight
# 2. Exam rigor scoring
description = str(cert_data.get("description", "")).lower()
exam_rigor_score = 0.3 # Default
for indicator in CERT_RELIABILITY_FACTORS["exam_rigor"]["indicators"]:
if indicator in description:
exam_rigor_score = min(1.0, exam_rigor_score + 0.2)
exam_rigor_weight = CERT_RELIABILITY_FACTORS["exam_rigor"]["weight"]
scores["exam_rigor"] = exam_rigor_score
total_score += exam_rigor_score * exam_rigor_weight
# 3. Validity period scoring (if available)
validity_score = 0.5
validity_weight = CERT_RELIABILITY_FACTORS["validity_period"]["weight"]
scores["validity_period"] = validity_score
total_score += validity_score * validity_weight
# 4. Renewal requirements scoring (if available)
renewal_score = 0.5
renewal_weight = CERT_RELIABILITY_FACTORS["renewal_requirements"]["weight"]
scores["renewal_requirements"] = renewal_score
total_score += renewal_score * renewal_weight
# 5. Industry demand (would require external data source)
demand_score = 0.7 # Placeholder
demand_weight = CERT_RELIABILITY_FACTORS["industry_demand"]["weight"]
scores["industry_demand"] = demand_score
total_score += demand_score * demand_weight
# 6. Market recognition (would require external data source)
recognition_score = 0.7 # Placeholder
recognition_weight = CERT_RELIABILITY_FACTORS["market_recognition"]["weight"]
scores["market_recognition"] = recognition_score
total_score += recognition_score * recognition_weight
return {
"overall_score": round(total_score, 2),
"component_scores": scores,
"confidence": "medium"
}
class _TextExtractor(HTMLParser):
"""Simple HTML text extractor using stdlib only."""
def __init__(self):
super().__init__()
self._texts = []
self._in_script_style = False
def handle_starttag(self, tag, attrs):
if tag in ("script", "style", "noscript"):
self._in_script_style = True
def handle_endtag(self, tag):
if tag in ("script", "style", "noscript"):
self._in_script_style = False
def handle_data(self, data):
if not self._in_script_style:
text = data.strip()
if text:
self._texts.append(text)
def get_text(self) -> str:
return " ".join(self._texts)
def fetch_url_content(url: str, timeout: int = 12) -> Dict:
"""Fetch URL content using stdlib with a browser-like User-Agent.
Returns: {"html": str, "text": str, "title": str, "meta": Dict}
"""
try:
req = urllib.request.Request(
url,
headers={
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0 Safari/537.36"
)
},
)
with urllib.request.urlopen(req, timeout=timeout) as resp:
raw = resp.read()
# Try to decode; fall back to utf-8 with errors ignored
try:
charset = resp.headers.get_content_charset() or "utf-8"
except Exception:
charset = "utf-8"
html = raw.decode(charset, errors="ignore")
# Extract title and simple meta tags
title = ""
meta: Dict[str, str] = {}
# crude title extraction
lower = html.lower()
start = lower.find("
")
end = lower.find("")
if start != -1 and end != -1 and end > start:
title = html[start + 7:end].strip()
# meta description
for needle in (
'name="description"',
"name='description'",
'property="og:description"',
"property='og:description'",
):
idx = lower.find("", idx)
if close == -1:
break
chunk = html[idx:close + 1]
if needle in chunk.lower():
# extract content="..."
c_start = chunk.lower().find('content="')
if c_start != -1:
c_start += len('content="')
c_end = chunk.find('"', c_start)
if c_end != -1:
meta["description"] = chunk[c_start:c_end].strip()
break
c_start = chunk.lower().find("content='")
if c_start != -1:
c_start += len("content='")
c_end = chunk.find("'", c_start)
if c_end != -1:
meta["description"] = chunk[c_start:c_end].strip()
break
search_from = close + 1
idx = lower.find(" Dict:
"""Extract information from a certificate URL"""
try:
parsed_url = urlparse(url)
domain = parsed_url.netloc.lower()
# Check if URL is from our internal database
internal_match = None
if certs_df is not None and 'hyperlink' in certs_df.columns:
try:
hyperlink_series = get_column_series(certs_df, 'hyperlink')
if hyperlink_series is not None:
match_mask = hyperlink_series.astype(str).str.contains(parsed_url.path, na=False, case=False, regex=False)
match = certs_df[match_mask]
if not match.empty:
internal_match = match.iloc[0].to_dict()
except Exception as _:
internal_match = None
# Try to fetch page content up-front for external URLs
fetched = fetch_url_content(url)
fetched_summary = {
"title": fetched.get("title", ""),
"description": fetched.get("meta", {}).get("description", ""),
"text_preview": (fetched.get("text", "")[:1200] + "...") if len(fetched.get("text", "")) > 1200 else fetched.get("text", ""),
"domain": domain,
}
# Use Gemini to extract information from the page if not found internally
if internal_match is None:
prompt = f"""Analyze this certification page and extract information.
URL: {url}
Page Title: {fetched_summary.get('title','')}
Meta Description: {fetched_summary.get('description','')}
Text Preview (truncated): {fetched_summary.get('text_preview','')}
Extract the following information:
- name: Certification name
- provider: Organization offering the certification
- description: Brief description of the certification
- skills: List of skills covered (comma-separated or array)
- requirements: Any prerequisites or requirements
- exam_details: Information about the exam format
- validity: How long the certification is valid
- renewal: Renewal policy if available
Return strict JSON only.
"""
messages = [SystemMessage(content="Return valid JSON with certification information."),
HumanMessage(content=prompt)]
response = await ainvoke_llm(messages)
response_text = response.content.strip()
cert_info: Dict = {"name": "Unknown", "provider": domain}
if '{' in response_text and '}' in response_text:
start_idx = response_text.find('{')
end_idx = response_text.rfind('}') + 1
json_str = response_text[start_idx:end_idx]
try:
cert_info = json.loads(json_str)
except Exception:
pass
# Fallbacks using fetched content
if not cert_info.get("name"):
cert_info["name"] = fetched_summary.get("title") or domain
if not cert_info.get("description"):
desc = fetched_summary.get("description") or fetched_summary.get("text_preview", "")[:280]
cert_info["description"] = desc
if not cert_info.get("provider"):
cert_info["provider"] = domain
else:
cert_info = internal_match
# Add URL and source information
cert_info["url"] = url
cert_info["source"] = "internal" if internal_match else "external"
# Normalize skills field to list of strings
if "skills" in cert_info:
if isinstance(cert_info["skills"], str):
# split by comma or pipe
if "," in cert_info["skills"]:
cert_info["skills"] = [s.strip() for s in cert_info["skills"].split(",") if s.strip()]
elif "|" in cert_info["skills"]:
cert_info["skills"] = [s.strip() for s in cert_info["skills"].split("|") if s.strip()]
else:
cert_info["skills"] = [cert_info["skills"].strip()] if cert_info["skills"].strip() else []
elif isinstance(cert_info["skills"], list):
cert_info["skills"] = [str(s).strip() for s in cert_info["skills"] if str(s).strip()]
else:
cert_info["skills"] = []
return cert_info
except Exception as e:
logger.error(f"URL processing failed: {e}")
return {"error": str(e), "url": url}
def extract_first_url_from_row(row: pd.Series) -> str:
"""Scan all string fields in a row and return the first http(s) URL if found."""
url_pattern = _re_for_url_extract.compile(r"https?://[^\s]+", flags=_re_for_url_extract.IGNORECASE)
try:
for value in row.values:
s = str(value)
if not s or s.lower() == 'nan':
continue
m = url_pattern.search(s)
if m:
return m.group(0)
except Exception:
return ""
return ""
async def search_internal_certificates(query: str, min_score: float, max_results: int) -> List[Dict]:
"""Search internal certificate database with enhanced filtering for unique skills"""
try:
embedding_model = get_embedding_model()
query_embedding = embedding_model.encode([query])
scores = cosine_similarity(query_embedding, cert_embeddings)[0]
# Get more results for filtering
top_indices = np.argsort(scores)[-max_results * 5:][::-1]
results = []
used_skills = set()
for idx in top_indices:
if scores[idx] >= min_score:
row = certs_df.iloc[idx]
# Get skills from this certificate (handle possible duplicate 'skills' columns)
cert_skills = set()
try:
raw_skills_str = ""
# Try multiple possible skills columns
possible_skill_cols = [
'skills', 'skill', 'skill_list', 'competencies', 'capabilities'
]
for key in possible_skill_cols:
if key in row.index:
val = row[key]
if isinstance(val, pd.Series):
parts = [str(v) for v in val.values if pd.notna(v) and str(v).strip()]
if parts:
raw_skills_str = '|'.join(parts)
break
else:
s = str(val)
if s and s.strip() and s.lower() != 'nan':
raw_skills_str = s
break
if raw_skills_str:
# Support both '|' and ',' separated lists
tokens = []
for sep in ['|', ',']:
if sep in raw_skills_str:
tokens = [t.strip() for t in raw_skills_str.split(sep)]
break
if not tokens:
tokens = [raw_skills_str.strip()]
# Normalize each token to a canonical internal skill name if possible
normalized = set()
for tok in tokens:
if not tok:
continue
if skills_df is not None and skill_col is not None:
matched = await strict_internal_skill_match(tok)
if matched and matched.get('name'):
normalized.add(str(matched['name']))
else:
normalized.add(tok)
else:
normalized.add(tok)
cert_skills = {s for s in normalized if s and s.lower() != 'nan'}
except Exception:
# Fallback: no skills extracted
cert_skills = set()
# Check if this certificate adds new skills
if cert_skills and not cert_skills.issubset(used_skills):
# Calculate reliability score
name_val = get_field_with_fallback(row, ['name', 'title'])
provider_val = get_field_with_fallback(row, ['provider', 'vendor', 'organization', 'issuer'])
description_val = get_field_with_fallback(row, ['description', 'desc', 'details', 'summary'])
hyperlink_val = get_field_with_fallback(row, ['hyperlink', 'link', 'url', 'website'])
if not hyperlink_val:
# Try to parse any URL from the row values
hyperlink_val = extract_first_url_from_row(row)
if not hyperlink_val and name_val:
# Last resort: provide a search link so user can navigate
q = urllib.parse.quote_plus(f"{name_val} {provider_val} certification")
hyperlink_val = f"https://www.google.com/search?q={q}"
cert_data = {
"name": name_val,
"provider": provider_val,
"description": description_val,
"skills": list(cert_skills)
}
reliability = await calculate_certificate_reliability(cert_data)
result_data = {
"type": "certification",
"name": name_val,
"provider": provider_val,
"score": float(scores[idx]),
"hyperlink": hyperlink_val,
"description": (lambda d: (d[:200] + "...") if len(d) > 200 else d)(description_val),
"skills": list(cert_skills),
"new_skills_count": len(cert_skills - used_skills),
"bucket": "internal",
"reliability_score": reliability
}
results.append(result_data)
used_skills.update(cert_skills)
if len(results) >= max_results:
break
return results
except Exception as e:
logger.error(f"Internal certificate search failed: {e}")
return []
async def search_external_certificates(query: str, max_results: int) -> List[Dict]:
"""Search for external certificates using Gemini API"""
try:
prompt = f"""Find {max_results} relevant professional certifications for the skill: "{query}".
For each certification, provide:
- name: The full name of the certification
- provider: The organization that offers it
- description: A brief description of what it covers
- skills: A list of specific skills covered (comma-separated)
- url: The official URL for more information
- relevance_score: A score from 0-1 indicating relevance to the query
Return the results in JSON format with this structure:
{{
"certifications": [
{{
"name": "Certification Name",
"provider": "Provider Name",
"description": "Brief description",
"skills": ["skill1", "skill2", "skill3"],
"url": "https://example.com",
"relevance_score": 0.85
}}
]
}}
"""
messages = [SystemMessage(content="Return valid JSON with certification information."),
HumanMessage(content=prompt)]
response = await ainvoke_llm(messages)
response_text = response.content.strip()
# Extract JSON from response
if '{' in response_text and '}' in response_text:
start_idx = response_text.find('{')
end_idx = response_text.rfind('}') + 1
json_str = response_text[start_idx:end_idx]
result = json.loads(json_str)
certifications = result.get("certifications", [])
# Format the results
formatted_results = []
used_skills = set() # Track skills to calculate new_skills_count
for cert in certifications[:max_results]:
# Calculate reliability score
# Ensure provider/description are strings in case of Series types
safe_cert = dict(cert)
safe_cert['provider'] = str(safe_cert.get('provider', ''))
safe_cert['description'] = str(safe_cert.get('description', ''))
reliability = await calculate_certificate_reliability(safe_cert)
# Get skills for this certificate
cert_skills = set()
if "skills" in cert and cert["skills"]:
if isinstance(cert["skills"], str):
cert_skills = set(s.strip() for s in cert["skills"].split(",") if s.strip())
elif isinstance(cert["skills"], list):
cert_skills = set(str(s).strip() for s in cert["skills"] if str(s).strip())
# Calculate new skills count
new_skills_count = len(cert_skills - used_skills) if cert_skills else 0
formatted_results.append({
"type": "certification",
"name": cert.get("name", ""),
"provider": cert.get("provider", ""),
"score": float(cert.get("relevance_score", 0.5)),
"hyperlink": cert.get("url", ""),
"description": (lambda d: (d[:200] + "...") if len(d) > 200 else d)(str(cert.get("description", ""))),
"skills": list(cert_skills),
"new_skills_count": new_skills_count,
"bucket": "external",
"source": "gemini_api",
"reliability_score": reliability
})
# Update used skills for next iteration
used_skills.update(cert_skills)
return formatted_results
return []
except Exception as e:
logger.error(f"External certificate search failed: {e}")
return []
async def get_certificate_recommendations(query: str, min_score: float,
max_internal: int, max_external: int) -> Dict:
"""Get certificate recommendations from both internal and external sources"""
# Get internal recommendations
internal_certs = await search_internal_certificates(query, min_score, max_internal)
# Get external recommendations
external_certs = await search_external_certificates(query, max_external)
# Remove duplicates between internal and external
internal_names = {cert["name"].lower() for cert in internal_certs}
external_certs = [cert for cert in external_certs
if cert["name"].lower() not in internal_names]
return {
"internal_bucket": internal_certs,
"external_bucket": external_certs
}
async def determine_input_type(text: str) -> str:
"""Determine if the input is a job description or a skill"""
try:
prompt = f"Analyze: {text}\nReturn ONLY 'job_description' or 'skill'"
messages = [SystemMessage(content="Return only 'job_description' or 'skill'"), HumanMessage(content=prompt)]
response = await ainvoke_llm(messages)
return "job_description" if "job" in response.content.lower() else "skill"
except Exception as e:
logger.error(f"Input type determination failed: {e}")
return "job_description"
def skills_to_json(df: pd.DataFrame, max_items: int = 1000) -> str:
"""Convert skills DataFrame to JSON string"""
if df.empty:
return "[]"
records = []
for _, row in df.iterrows():
try:
rec = {"id": str(row[id_col]), "skill": str(row[skill_col])}
for c in df.columns:
if c not in [id_col, skill_col] and pd.notna(row[c]):
rec[c] = str(row[c])
records.append(rec)
if len(records) >= max_items: break
except Exception as e:
continue
return json.dumps(records, ensure_ascii=False, indent=2)
async def extract_skills_with_gemini(job_description: str) -> List[Dict]:
"""Extract skills using Gemini (only for job description analysis)"""
try:
skills_json = skills_to_json(skills_df)
prompt = f"""Analyze this job description and extract relevant skills from the provided list.
Available skills: {skills_json}
Job Description: {job_description}
Return JSON format: {{"skills": [{{"id": "id", "skill": "name", "relevance_score": 1-10}}]}}
"""
messages = [SystemMessage(content="Return valid JSON with skills from the provided list."),
HumanMessage(content=prompt)]
response = await ainvoke_llm(messages)
response_text = response.content.strip()
# Extract JSON from response
if '{' in response_text and '}' in response_text:
start_idx = response_text.find('{')
end_idx = response_text.rfind('}') + 1
json_str = response_text[start_idx:end_idx]
result = json.loads(json_str)
return [
{
"type": "skill",
"name": s["skill"],
"id": s["id"],
"score": float(s.get("relevance_score", 5)) / 10
}
for s in result.get("skills", [])
if "id" in s and "skill" in s
]
return []
except Exception as e:
logger.error(f"Skill extraction failed: {e}")
return []
async def analyze_with_gemini(job_desc: str, skills: List[Dict], certs: Dict) -> Dict:
"""Get comprehensive analysis from Gemini"""
try:
prompt = f"""Analyze this job matching result:
Job Description: {job_desc}
Matched Skills: {json.dumps(skills, indent=2)}
Recommended Certifications: {json.dumps(certs, indent=2)}
Provide analysis including skill gaps, certification relevance, and recommendations.
Return in JSON format with analysis of both internal and external certification buckets.
"""
messages = [SystemMessage(content="Return comprehensive analysis in JSON format."),
HumanMessage(content=prompt)]
response = await ainvoke_llm(messages)
response_text = response.content.strip()
if '{' in response_text and '}' in response_text:
start_idx = response_text.find('{')
end_idx = response_text.rfind('}') + 1
return json.loads(response_text[start_idx:end_idx])
return {"error": "Failed to parse analysis"}
except Exception as e:
logger.error(f"Analysis failed: {e}")
return {"error": str(e)}
# --------------------------
# JOB ONTOLOGY FUNCTIONS
# --------------------------
async def extract_job_ontology(job_description: str, include_certificates: bool = True,
min_relevance: float = 0.4) -> Dict:
"""Extract job ontology with categorized skills from internal database only"""
try:
# Convert internal skills to JSON for the prompt
skills_json = skills_to_json(skills_df)
# Use Gemini to extract and categorize skills (only from internal database)
prompt = f"""Analyze this job description and extract skills categorized into:
1. Must to Have (essential technical skills)
2. Good to Have (nice-to-have technical skills)
3. Behavioral Skills (soft skills, personality traits)
IMPORTANT: Only use skills from this internal skills list: {skills_json}
If a skill in the job description is not in this list, DO NOT include it.
Job Description: {job_description}
Return in JSON format:
{{
"job_title": "extracted job title",
"must_have_skills": ["skill_name1", "skill_name2", ...],
"good_to_have_skills": ["skill_name1", "skill_name2", ...],
"behavioral_skills": ["skill_name1", "skill_name2", ...],
"overall_analysis": "brief analysis of the role"
}}
"""
messages = [SystemMessage(
content="Return valid JSON with categorized skills using ONLY skills from the provided internal list."),
HumanMessage(content=prompt)]
response = await ainvoke_llm(messages)
response_text = response.content.strip()
# Extract JSON from response
ontology = {
"job_title": "Unknown Role",
"must_have_skills": [],
"good_to_have_skills": [],
"behavioral_skills": [],
"overall_analysis": "Failed to extract detailed ontology"
}
if '{' in response_text and '}' in response_text:
start_idx = response_text.find('{')
end_idx = response_text.rfind('}') + 1
json_str = response_text[start_idx:end_idx]
try:
extracted_ontology = json.loads(json_str)
# Validate and use the extracted ontology
if isinstance(extracted_ontology, dict):
ontology = extracted_ontology
except json.JSONDecodeError:
logger.warning("Failed to parse LLM response as JSON")
# Enhance ontology with skill IDs (only internal skills)
ontology = await enhance_ontology_with_skill_ids(ontology)
# Get certificates for must-have skills if requested
certificates = {}
if include_certificates and ontology.get("must_have_skills"):
must_have_certs = {}
for skill_obj in ontology["must_have_skills"][:5]: # Limit to top 5 skills
skill_name = skill_obj.get("name", "")
if skill_name:
cert_recommendations = await get_certificate_recommendations(
skill_name, min_relevance, 3, 2 # Get 3 internal and 2 external certs per skill
)
# Combine internal and external certs, remove duplicates
all_certs = cert_recommendations["internal_bucket"] + cert_recommendations["external_bucket"]
unique_certs = []
seen_names = set()
for cert in all_certs:
if cert["name"] not in seen_names:
unique_certs.append(cert)
seen_names.add(cert["name"])
must_have_certs[skill_name] = unique_certs[:3] # Limit to 3 certs per skill
certificates["must_have_certificates"] = must_have_certs
return {
"status": "success",
"ontology": ontology,
"certificates": certificates if include_certificates else {}
}
except Exception as e:
logger.error(f"Job ontology extraction failed: {e}")
return {
"status": "error",
"message": f"Failed to extract job ontology: {str(e)}"
}
async def match_skills_with_ids(skills_list: List[str]) -> List[Dict]:
"""Match skill names with their IDs from the internal database only"""
matched_skills = []
for skill_name in skills_list:
# Try to find the skill in the internal database using strict matching
matched_skill = await strict_internal_skill_match(skill_name)
if matched_skill:
matched_skills.append(matched_skill)
else:
# Log skipped external skills for debugging
logger.debug(f"Skipped external skill: {skill_name}")
return matched_skills
# Add this function to enhance the ontology with better skill matching
async def enhance_ontology_with_skill_ids(ontology: Dict) -> Dict:
"""Enhance ontology with proper skill IDs from internal database only"""
try:
# Process must-have skills
if ontology.get("must_have_skills"):
if isinstance(ontology["must_have_skills"][0], str):
# Convert string list to object list with IDs (internal only)
must_have_skills = await match_skills_with_ids(ontology["must_have_skills"])
ontology["must_have_skills"] = must_have_skills
elif isinstance(ontology["must_have_skills"][0], dict) and "id" not in ontology["must_have_skills"][0]:
# Convert dict list without IDs to include IDs (internal only)
skill_names = [skill.get("name", "") for skill in ontology["must_have_skills"]]
must_have_skills = await match_skills_with_ids(skill_names)
ontology["must_have_skills"] = must_have_skills
# Process good-to-have skills
if ontology.get("good_to_have_skills"):
if isinstance(ontology["good_to_have_skills"][0], str):
good_to_have_skills = await match_skills_with_ids(ontology["good_to_have_skills"])
ontology["good_to_have_skills"] = good_to_have_skills
elif isinstance(ontology["good_to_have_skills"][0], dict) and "id" not in ontology["good_to_have_skills"][
0]:
skill_names = [skill.get("name", "") for skill in ontology["good_to_have_skills"]]
good_to_have_skills = await match_skills_with_ids(skill_names)
ontology["good_to_have_skills"] = good_to_have_skills
# Process behavioral skills
if ontology.get("behavioral_skills"):
if isinstance(ontology["behavioral_skills"][0], str):
behavioral_skills = await match_skills_with_ids(ontology["behavioral_skills"])
ontology["behavioral_skills"] = behavioral_skills
elif isinstance(ontology["behavioral_skills"][0], dict) and "id" not in ontology["behavioral_skills"][0]:
skill_names = [skill.get("name", "") for skill in ontology["behavioral_skills"]]
behavioral_skills = await match_skills_with_ids(skill_names)
ontology["behavioral_skills"] = behavioral_skills
return ontology
except Exception as e:
logger.error(f"Skill ID enhancement failed: {e}")
return ontology
async def strict_internal_skill_match(skill_name: str) -> Optional[Dict]:
"""Enhanced matching of skill name against internal database with semantic similarity"""
if skills_df is None or skill_embeddings is None:
return None
# Try exact match first
exact_matches = skills_df[skills_df[skill_col].str.lower() == skill_name.lower()]
if not exact_matches.empty:
row = exact_matches.iloc[0]
return {
"id": str(row[id_col]),
"name": str(row[skill_col]),
"source": "internal",
"match_type": "exact"
}
# Try partial match with word boundaries
word_boundary_matches = skills_df[
skills_df[skill_col].str.contains(r'\b' + re.escape(skill_name.lower()) + r'\b',
case=False, na=False, regex=True)
]
if not word_boundary_matches.empty:
row = word_boundary_matches.iloc[0]
return {
"id": str(row[id_col]),
"name": str(row[skill_col]),
"source": "internal",
"match_type": "word_boundary"
}
# Try contains match
contains_matches = skills_df[
skills_df[skill_col].str.contains(skill_name.lower(), case=False, na=False, regex=False)
]
if not contains_matches.empty:
row = contains_matches.iloc[0]
return {
"id": str(row[id_col]),
"name": str(row[skill_col]),
"source": "internal",
"match_type": "contains"
}
# Try semantic/related match using embeddings
try:
embedding_model = get_embedding_model()
query_embedding = embedding_model.encode([skill_name])
scores = cosine_similarity(query_embedding, skill_embeddings)[0]
# Find the best semantic match with a minimum similarity threshold
best_match_idx = np.argmax(scores)
best_score = scores[best_match_idx]
# Only return semantic match if similarity is above threshold (0.6 = 60% similar)
if best_score >= 0.6:
row = skills_df.iloc[best_match_idx]
return {
"id": str(row[id_col]),
"name": str(row[skill_col]),
"source": "internal",
"match_type": "semantic_related",
"similarity_score": float(best_score)
}
except Exception as e:
logger.warning(f"Semantic matching failed for '{skill_name}': {e}")
return None
async def process_job_description_url(url: str) -> str:
"""Extract job description text from a URL"""
try:
# Use Gemini to extract job description from the URL
prompt = f"""Extract the job description text from this URL: {url}
Return ONLY the job description text, nothing else.
If this is not a job description URL, return "NOT_A_JOB_DESCRIPTION".
"""
messages = [SystemMessage(content="Return only the job description text."),
HumanMessage(content=prompt)]
response = await ainvoke_llm(messages)
job_description = response.content.strip()
if job_description == "NOT_A_JOB_DESCRIPTION":
raise ValueError("The provided URL does not contain a job description")
return job_description
except Exception as e:
logger.error(f"Job description URL processing failed: {e}")
raise HTTPException(status_code=400, detail=f"Failed to extract job description from URL: {e}")
# --------------------------
# NEW FUNCTIONS FOR CERTIFICATE URL ANALYSIS
# --------------------------
async def analyze_certificate_with_gemini(cert_info: Dict, skills: List[str], recommendations: Dict) -> Dict:
"""Get comprehensive analysis of a certificate from Gemini"""
try:
prompt = f"""Analyze this certification and provide insights:
Certification: {json.dumps(cert_info, indent=2)}
Skills Covered: {skills}
Related Certifications: {json.dumps(recommendations, indent=2)}
Provide analysis including:
1. Career value and ROI
2. Skill development opportunities
3. Market demand and trends
4. Comparison with related certifications
5. Recommended learning path
Return in JSON format.
"""
messages = [SystemMessage(content="Return comprehensive analysis in JSON format."),
HumanMessage(content=prompt)]
response = await ainvoke_llm(messages)
response_text = response.content.strip()
if '{' in response_text and '}' in response_text:
start_idx = response_text.find('{')
end_idx = response_text.rfind('}') + 1
return json.loads(response_text[start_idx:end_idx])
return {"error": "Failed to parse analysis"}
except Exception as e:
logger.error(f"Certificate analysis failed: {e}")
return {"error": str(e)}
async def generate_radar_data(cert_info: Dict, recommendations: Dict) -> Dict:
"""Generate radar chart data points for visualization"""
try:
# Calculate reliability score for the source certificate
reliability_score = await calculate_certificate_reliability(cert_info)
# Calculate average scores for recommendations
internal_scores = [c.get("score", 0.5) for c in recommendations["internal_bucket"]]
external_scores = [c.get("score", 0.5) for c in recommendations["external_bucket"]]
avg_internal_score = sum(internal_scores) / len(internal_scores) if internal_scores else 0
avg_external_score = sum(external_scores) / len(external_scores) if external_scores else 0
# Count skills coverage
all_skills = set()
for cert in recommendations["internal_bucket"] + recommendations["external_bucket"]:
if "skills" in cert and cert["skills"]:
all_skills.update(cert["skills"])
# Calculate market alignment (placeholder - would use real data)
market_alignment = await calculate_job_market_alignment(cert_info)
# Prepare radar points
radar_points = {
"reliability": reliability_score["overall_score"],
"internal_relevance": avg_internal_score,
"external_relevance": avg_external_score,
"skills_coverage": min(1.0, len(all_skills) / 20), # Normalized to 0-1 scale
"market_alignment": market_alignment,
"cost_effectiveness": 0.7, # Placeholder - would calculate based on actual cost data
"learning_curve": 0.6 # Placeholder - would calculate based on complexity
}
return radar_points
except Exception as e:
logger.error(f"Radar data generation failed: {e}")
return {
"reliability": 0.5,
"internal_relevance": 0.5,
"external_relevance": 0.5,
"skills_coverage": 0.5,
"market_alignment": 0.5,
"cost_effectiveness": 0.5,
"learning_curve": 0.5
}
# --------------------------
# DATA LOADING
# --------------------------
def load_skills():
"""Load skills data from skil.xlsx and create embeddings for semantic matching"""
try:
df = pd.read_excel(SKILL_FILE).fillna('')
original_columns = df.columns.tolist()
df.columns = [str(col).strip().lower().replace(' ', '_') for col in df.columns]
# Better column detection logic
# Look for ID column - prioritize 'skill_id' or columns with 'id' in them
id_col = None
for col in df.columns:
if 'skill_id' in col.lower():
id_col = col
break
if id_col is None:
id_col = next((c for c in df.columns if 'id' in c.lower() and 'skill' in c.lower()), None)
if id_col is None:
id_col = next((c for c in df.columns if 'id' in c.lower()), df.columns[0])
# Look for skill name column - prioritize 'skill_name'
skill_col = None
for col in df.columns:
if 'skill_name' in col.lower():
skill_col = col
break
if skill_col is None:
skill_col = next((c for c in df.columns if 'name' in c.lower() and 'skill' in c.lower()), None)
if skill_col is None:
skill_col = next((c for c in df.columns if 'name' in c.lower()), None)
if skill_col is None:
skill_col = df.columns[1] if len(df.columns) > 1 else df.columns[0]
logger.info(f"Using ID column: '{id_col}' and Skill column: '{skill_col}'")
logger.info(f"Available columns: {original_columns}")
# Create embeddings for semantic matching
logger.info("Creating skill embeddings for semantic matching...")
embedding_model = get_embedding_model()
# Create search text combining skill name and definition if available
search_texts = []
for _, row in df.iterrows():
skill_name = str(row[skill_col])
skill_def = str(row.get('skill_definition', ''))
if skill_def and skill_def != 'nan':
search_text = f"{skill_name} {skill_def}"
else:
search_text = skill_name
search_texts.append(search_text)
skill_embeddings = embedding_model.encode(search_texts)
logger.info(f"Created embeddings for {len(skill_embeddings)} skills")
return df, id_col, skill_col, skill_embeddings
except Exception as e:
logger.error(f"Failed to load skills: {e}")
raise
async def load_certifications():
"""Load certification data from skil.xlsx"""
try:
certs_df = pd.read_excel(SKILL_FILE).fillna('')
original_columns = certs_df.columns.tolist()
logger.info(f"Original columns in skil.xlsx: {original_columns}")
# Map to expected columns - ADJUST THIS BASED ON ACTUAL COLUMNS
column_mapping = {}
for col in certs_df.columns:
col_lower = col.lower()
if 'title' in col_lower or 'name' in col_lower:
column_mapping[col] = 'name'
elif 'provider' in col_lower or 'vendor' in col_lower:
column_mapping[col] = 'provider'
elif 'link' in col_lower or 'url' in col_lower or 'hyperlink' in col_lower:
column_mapping[col] = 'hyperlink'
elif 'desc' in col_lower:
column_mapping[col] = 'description'
elif 'skill' in col_lower and 'name' not in col_lower:
column_mapping[col] = 'skills'
certs_df.rename(columns=column_mapping, inplace=True)
# Ensure required columns exist
for col in ['name', 'hyperlink']:
if col not in certs_df.columns:
certs_df[col] = ''
logger.info(f"Mapped columns: {certs_df.columns.tolist()}")
# Create search text from available columns
search_parts = []
for col in ['name', 'provider', 'skills', 'description']:
if col in certs_df.columns:
search_parts.append(col)
# If there are duplicate columns with same name, select first occurrence for search text
parts_for_search = []
for col in search_parts:
series = get_column_series(certs_df, col)
if series is not None:
parts_for_search.append(series.astype(str))
if parts_for_search:
certs_df['search_text'] = pd.concat(parts_for_search, axis=1).agg('|'.join, axis=1)
else:
certs_df['search_text'] = ''
# Initialize embeddings
embedding_model = get_embedding_model()
cert_embeddings = embedding_model.encode(certs_df['search_text'].tolist())
logger.info(f"Loaded {len(certs_df)} certifications with embeddings")
return certs_df, cert_embeddings
except Exception as e:
logger.error(f"Failed to load certification data: {e}")
# Return empty dataframe instead of raising error
return pd.DataFrame(), None
# --------------------------
# API ENDPOINTS
# --------------------------
@app.post("/extract-job-ontology")
async def extract_job_ontology_endpoint(request: JobOntologyRequest):
"""Extract job ontology with categorized skills and relevant certificates"""
try:
result = await extract_job_ontology(
request.job_description,
request.include_certificates,
request.min_relevance
)
return result
except Exception as e:
logger.error(f"Job ontology endpoint failed: {e}")
raise HTTPException(status_code=500, detail=f"Job ontology extraction failed: {e}")
@app.post("/analyze-job-url")
async def analyze_job_url_endpoint(url: str, include_certificates: bool = True, min_relevance: float = 0.4):
"""Analyze job description from a URL and extract ontology"""
try:
# Extract job description from URL
job_description = await process_job_description_url(url)
# Extract ontology
result = await extract_job_ontology(job_description, include_certificates, min_relevance)
# Add URL information to the result
result["source_url"] = url
result["extracted_description"] = job_description[:500] + "..." if len(
job_description) > 500 else job_description
return result
except Exception as e:
logger.error(f"Job URL analysis failed: {e}")
raise HTTPException(status_code=500, detail=f"Job URL analysis failed: {e}")
@app.post("/analyze-certificate")
async def analyze_certificate_endpoint(cert_data: AnalyzeCertificateURL):
"""Analyze a certificate URL and extract skills from internal database and external sources"""
try:
# Process the certificate URL
cert_info = await process_certificate_url(cert_data.url)
if "error" in cert_info:
raise HTTPException(status_code=400, detail=f"Failed to process URL: {cert_info['error']}")
# Extract skills from the certificate
cert_skills = []
if "skills" in cert_info:
if isinstance(cert_info["skills"], str):
cert_skills = [s.strip() for s in cert_info["skills"].split(",")]
elif isinstance(cert_info["skills"], list):
cert_skills = cert_info["skills"]
# If no skills found in certificate, try to extract them from description
if not cert_skills and "description" in cert_info:
# Use Gemini to extract skills from description
prompt = f"""Extract technical skills from this certification description: {cert_info['description']}
Return a JSON array of skills: ["skill1", "skill2", "skill3"]
"""
messages = [SystemMessage(content="Return valid JSON array of skills."),
HumanMessage(content=prompt)]
response = await ainvoke_llm(messages)
response_text = response.content.strip()
if response_text.startswith('[') and response_text.endswith(']'):
try:
cert_skills = json.loads(response_text)
except:
# Fallback: simple keyword extraction
description = cert_info['description'].lower()
tech_keywords = ["python", "java", "cloud", "aws", "azure", "security",
"network", "database", "linux", "windows", "docker", "kubernetes"]
cert_skills = [kw for kw in tech_keywords if kw in description]
# Match extracted skills with internal database (skil.xlsx) to get IDs
matched_internal_skills = []
external_skills = []
for skill in cert_skills:
# Try to match with internal skills database
matched_skill = await strict_internal_skill_match(skill)
if matched_skill:
matched_internal_skills.append(matched_skill)
else:
# Keep as external skill if not found in internal database
external_skills.append({
"name": skill,
"source": "external",
"match_type": "extracted_from_certificate"
})
# Calculate reliability score for the certificate itself
reliability_score = await calculate_certificate_reliability(cert_info)
response = {
"status": "success",
"source_certificate": {
"name": cert_info.get("name", "Unknown Certification"),
"provider": cert_info.get("provider", "Unknown Provider"),
"url": cert_data.url,
"description": cert_info.get("description", ""),
"reliability_score": reliability_score
},
"skills_analysis": {
"total_skills_extracted": len(cert_skills),
"internal_skills_matched": len(matched_internal_skills),
"external_skills_found": len(external_skills),
"internal_skills": matched_internal_skills,
"external_skills": external_skills
},
"analysis_method": "skill_extraction_only"
}
# Optional detailed analysis using Gemini
if cert_data.analyze:
analysis_prompt = f"""Analyze the skills extracted from this certification:
Certificate: {cert_info.get('name', 'Unknown')}
Provider: {cert_info.get('provider', 'Unknown')}
Internal Skills (from database): {json.dumps(matched_internal_skills, indent=2)}
External Skills: {json.dumps(external_skills, indent=2)}
Provide insights about:
1. Skill relevance and market demand
2. Career opportunities these skills enable
3. Skill categories and technical domains covered
4. Learning progression and prerequisites
Return in JSON format.
"""
messages = [SystemMessage(content="Return comprehensive skill analysis in JSON format."),
HumanMessage(content=analysis_prompt)]
analysis_response = await ainvoke_llm(messages)
analysis_text = analysis_response.content.strip()
if '{' in analysis_text and '}' in analysis_text:
start_idx = analysis_text.find('{')
end_idx = analysis_text.rfind('}') + 1
try:
response["detailed_analysis"] = json.loads(analysis_text[start_idx:end_idx])
except:
response["detailed_analysis"] = {"error": "Failed to parse analysis"}
return response
except Exception as e:
logger.error(f"Certificate analysis failed: {e}")
raise HTTPException(status_code=500, detail=f"Certificate analysis failed: {e}")
@app.get("/certificate-reliability/{cert_name}")
async def get_certificate_reliability(cert_name: str):
"""Get reliability score for a specific certificate"""
try:
# Search for certificate in internal database
cert_data = {}
if certs_df is not None and 'name' in certs_df.columns:
matches = certs_df[certs_df['name'].str.contains(cert_name, na=False, case=False, regex=False)]
if not matches.empty:
cert_data = matches.iloc[0].to_dict()
# If not found internally, try to find information
if not cert_data:
# Use Gemini to get certificate information
prompt = f"""Get information about this certification: {cert_name}
Return JSON with: name, provider, description, typical skills covered.
"""
messages = [SystemMessage(content="Return valid JSON with certification information."),
HumanMessage(content=prompt)]
response = await ainvoke_llm(messages)
response_text = response.content.strip()
if '{' in response_text and '}' in response_text:
start_idx = response_text.find('{')
end_idx = response_text.rfind('}') + 1
json_str = response_text[start_idx:end_idx]
cert_data = json.loads(json_str)
# Calculate reliability score
reliability_score = await calculate_certificate_reliability(cert_data)
return {
"status": "success",
"certificate": cert_name,
"reliability_score": reliability_score,
"factors": CERT_RELIABILITY_FACTORS,
"certificate_data": cert_data
}
except Exception as e:
logger.error(f"Reliability check failed: {e}")
raise HTTPException(status_code=500, detail=f"Reliability check failed: {e}")
@app.post("/compare-certifications")
async def compare_certifications(comparison: CertificationComparison):
"""Compare multiple certifications across various dimensions"""
try:
comparison_results = {}
for cert_name in comparison.cert_names:
# Get certificate information
cert_info = await get_certificate_info(cert_name)
reliability = await calculate_certificate_reliability(cert_info)
comparison_results[cert_name] = {
"reliability": reliability,
"skills": cert_info.get("skills", []),
"provider": cert_info.get("provider", ""),
"estimated_cost": await estimate_certification_cost(cert_info),
"duration": await estimate_preparation_time(cert_info),
"job_market_alignment": await calculate_job_market_alignment(cert_info)
}
# Generate comparative analysis
analysis = await generate_comparative_analysis(comparison_results, comparison.compare_by)
return {
"status": "success",
"comparison": comparison_results,
"analysis": analysis,
"recommendation": await generate_recommendation(comparison_results)
}
except Exception as e:
logger.error(f"Certification comparison failed: {e}")
raise HTTPException(status_code=500, detail=f"Comparison failed: {e}")
@app.post("/skill-gap-analysis")
async def skill_gap_analysis(analysis: SkillGapAnalysis):
"""Analyze skill gaps between current and target skills/roles"""
try:
# Get missing skills
missing_skills = list(set(analysis.target_skills) - set(analysis.current_skills))
# Get overlapping skills
overlapping_skills = list(set(analysis.target_skills) & set(analysis.current_skills))
# Find certifications to bridge the gap
cert_recommendations = await recommend_certifications_for_skills(missing_skills)
# Generate learning recommendations
learning_recommendations = await generate_learning_recommendations(missing_skills)
return {
"status": "success",
"missing_skills": missing_skills,
"overlapping_skills": overlapping_skills,
"skill_gap_percentage": len(missing_skills) / len(
analysis.target_skills) * 100 if analysis.target_skills else 0,
"certification_recommendations": cert_recommendations,
"learning_recommendations": learning_recommendations,
"timeline_estimation": await estimate_timeline_for_skills(missing_skills)
}
except Exception as e:
logger.error(f"Skill gap analysis failed: {e}")
raise HTTPException(status_code=500, detail=f"Skill gap analysis failed: {e}")
@app.post("/track-certification")
async def track_certification(tracker: CertificationTracker):
"""Track certification status and renewal requirements"""
try:
cert_info = await get_certificate_info(tracker.cert_name)
# Calculate days until expiration
days_until_expiration = None
if tracker.expiration_date:
expiration = datetime.strptime(tracker.expiration_date, "%Y-%m-%d")
days_until_expiration = (expiration - datetime.now()).days
# Get renewal requirements if not provided
if not tracker.renewal_requirements:
tracker.renewal_requirements = await extract_renewal_requirements(cert_info)
return {
"status": "success",
"certification": tracker.cert_name,
"days_until_expiration": days_until_expiration,
"renewal_requirements": tracker.renewal_requirements,
"renewal_cost_estimate": await estimate_renewal_cost(tracker.cert_name),
"recommended_renewal_timeline": await generate_renewal_timeline(tracker),
"alternative_renewal_options": await find_alternative_renewal_options(tracker.cert_name)
}
except Exception as e:
logger.error(f"Certification tracking failed: {e}")
raise HTTPException(status_code=500, detail=f"Certification tracking failed: {e}")
@app.post("/analyze")
async def analyze_input(input_data: Union[InputText, AnalyzeCertificateURL, JobOntologyRequest]):
# Check if it's a job ontology request
if isinstance(input_data, JobOntologyRequest):
return await extract_job_ontology_endpoint(input_data)
# Check if it's a URL (existing functionality)
elif isinstance(input_data, AnalyzeCertificateURL) or (hasattr(input_data, 'text') and
(input_data.text.startswith('http://') or
input_data.text.startswith('https://'))):
# Handle as certificate URL or job URL
url_to_check = input_data.url if isinstance(input_data, AnalyzeCertificateURL) else input_data.text
if "linkedin.com" in url_to_check or "indeed.com" in url_to_check or "job" in url_to_check.lower():
# Likely a job description URL
try:
return await analyze_job_url_endpoint(
url_to_check,
include_certificates=True,
min_relevance=input_data.min_relevance if hasattr(input_data, 'min_relevance') else 0.4
)
except:
# Fall back to certificate analysis
pass
# Handle as certificate URL (existing code)
if isinstance(input_data, InputText):
cert_data = AnalyzeCertificateURL(
url=input_data.text,
min_relevance=input_data.min_relevance,
max_internal_results=input_data.max_internal_results,
max_external_results=input_data.max_external_results,
analyze=input_data.analyze
)
else:
cert_data = input_data
return await analyze_certificate_endpoint(cert_data)
else:
# Handle as text input (existing functionality with job description detection)
try:
input_type = await determine_input_type(input_data.text)
if input_type == "job_description":
# Use the new ontology extraction for job descriptions
return await extract_job_ontology(
input_data.text,
include_certificates=True,
min_relevance=input_data.min_relevance
)
else:
# Existing skill-based processing
cert_recommendations = await get_certificate_recommendations(
input_data.text,
input_data.min_relevance,
input_data.max_internal_results,
input_data.max_external_results
)
response = {
"status": "success",
"input_type": "skill",
"matches": {
"certifications": cert_recommendations
},
"analysis_method": "internal_external_buckets"
}
if input_data.analyze:
try:
analysis_prompt = f"Analyze these certification recommendations for skill '{input_data.text}': {json.dumps(cert_recommendations)}"
messages = [SystemMessage(content="Return valid JSON analysis."),
HumanMessage(content=analysis_prompt)]
response_content = (await ainvoke_llm(messages)).content
if '{' in response_content and '}' in response_content:
start_idx = response_content.find('{')
end_idx = response_content.rfind('}') + 1
response["analysis"] = json.loads(response_content[start_idx:end_idx])
except Exception as e:
response["analysis"] = {"error": str(e)}
return response
except Exception as e:
logger.error(f"Analysis failed: {e}")
raise HTTPException(status_code=500, detail=f"Analysis failed: {e}")
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {
"status": "healthy",
"skills_loaded": len(skills_df) if skills_df is not None else 0,
"certifications_loaded": len(certs_df) if certs_df is not None else 0
}
# --------------------------
# HELPER FUNCTIONS
# --------------------------
async def get_certificate_info(cert_name: str) -> Dict:
"""Get certificate information from internal database or external source"""
cert_data = {}
if certs_df is not None and 'name' in certs_df.columns:
try:
name_series = get_column_series(certs_df, 'name')
if name_series is not None:
mask = name_series.astype(str).str.contains(cert_name, na=False, case=False, regex=False)
matches = certs_df[mask]
else:
matches = pd.DataFrame()
except Exception:
matches = pd.DataFrame()
if not matches.empty:
cert_data = matches.iloc[0].to_dict()
# If not found internally, use Gemini to get information
if not cert_data:
prompt = f"""Get information about this certification: {cert_name}
Return JSON with: name, provider, description, skills covered.
"""
messages = [SystemMessage(content="Return valid JSON with certification information."),
HumanMessage(content=prompt)]
response = await ainvoke_llm(messages)
response_text = response.content.strip()
if '{' in response_text and '}' in response_text:
start_idx = response_text.find('{')
end_idx = response_text.rfind('}') + 1
json_str = response_text[start_idx:end_idx]
cert_data = json.loads(json_str)
return cert_data
async def estimate_certification_cost(cert_info: Dict) -> Dict:
"""Estimate total cost of certification"""
# Placeholder implementation - would integrate with actual cost data
return {
"exam_fee": 150,
"study_materials": 100,
"training_course": 300,
"total": 550,
"currency": "USD"
}
async def estimate_preparation_time(cert_info: Dict) -> Dict:
"""Estimate preparation time for certification"""
# Placeholder implementation
return {
"hours_required": 40,
"weeks_recommended": 8,
"intensity": "moderate"
}
async def calculate_job_market_alignment(cert_info: Dict) -> float:
"""Calculate how well the certification aligns with job market demands"""
# Placeholder implementation
return 0.75
async def recommend_certifications_for_skills(skills: List[str], max_results: int = 5) -> List[Dict]:
"""Recommend certifications for a set of skills"""
recommendations = []
for skill in skills[:3]: # Limit to top 3 skills
certs = await get_certificate_recommendations(skill, 0.3, 2, 2)
for cert in certs["internal_bucket"] + certs["external_bucket"]:
if cert["name"] not in [r["name"] for r in recommendations]:
recommendations.append(cert)
if len(recommendations) >= max_results:
break
if len(recommendations) >= max_results:
break
return recommendations
async def generate_learning_recommendations(skills: List[str]) -> List[Dict]:
"""Generate learning recommendations for skills"""
# Placeholder implementation
return [
{
"skill": skill,
"resources": ["Online course", "Practice labs", "Documentation"],
"estimated_time": "2-4 weeks"
}
for skill in skills[:3] # Limit to top 3 skills
]
async def estimate_timeline_for_skills(skills: List[str]) -> Dict:
"""Estimate timeline for acquiring skills"""
# Placeholder implementation
return {
"total_weeks": len(skills) * 3,
"total_hours": len(skills) * 20,
"recommended_schedule": "2-3 hours per week per skill"
}
async def extract_renewal_requirements(cert_info: Dict) -> List[str]:
"""Extract renewal requirements from certificate information"""
# Placeholder implementation
return ["Continuing education units", "Annual fee", "Periodic exam"]
async def estimate_renewal_cost(cert_name: str) -> Dict:
"""Estimate renewal cost for certification"""
# Placeholder implementation
return {
"fee": 100,
"continuing_education": 200,
"total": 300,
"currency": "USD"
}
async def generate_renewal_timeline(tracker: CertificationTracker) -> Dict:
"""Generate renewal timeline for certification"""
# Placeholder implementation
return {
"recommended_start": "90 days before expiration",
"steps": [
"Complete continuing education",
"Submit renewal application",
"Pay renewal fee"
]
}
async def find_alternative_renewal_options(cert_name: str) -> List[Dict]:
"""Find alternative renewal options for certification"""
# Placeholder implementation
return [
{
"option": "Higher-level certification",
"description": "Earn a more advanced certification instead of renewing",
"benefits": "Demonstrates continued growth and expertise"
}
]
async def generate_comparative_analysis(comparison_data: Dict, compare_by: str) -> Dict:
"""Generate comparative analysis of certifications"""
# Placeholder implementation
return {
"summary": "Comparative analysis based on " + compare_by,
"key_findings": ["Certification A has higher reliability", "Certification B covers more skills"],
"recommendation": "Consider your specific career goals when choosing"
}
async def generate_recommendation(comparison_data: Dict) -> str:
"""Generate recommendation based on comparison data"""
# Placeholder implementation
return "Based on the analysis, Certification A is recommended for most users due to its higher reliability score."
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# --------------------------
# ENHANCED GRADIO UI
# --------------------------
def create_gradio_interface():
"""Create Gradio interface for the application"""
with gr.Blocks(title="Advanced Certification Analyzer", theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🎯 Advanced Certification Analyzer")
gr.Markdown(
"Comprehensive certification analysis with reliability scoring, skill matching, and career path recommendations")
with gr.Tab("Job Ontology Analysis"):
with gr.Row():
with gr.Column(scale=2):
job_desc_input = gr.Textbox(
label="Job Description",
placeholder="Paste a job description here...",
lines=5,
max_lines=10
)
with gr.Row():
min_relevance_ontology = gr.Slider(
minimum=0.1,
maximum=1.0,
value=0.4,
step=0.1,
label="Minimum Relevance Score"
)
include_certs_check = gr.Checkbox(
value=True,
label="Include Certificates"
)
analyze_job_btn = gr.Button("Extract Job Ontology", variant="primary")
clear_job_btn = gr.Button("Clear")
with gr.Column(scale=3):
job_title_output = gr.Textbox(label="Job Title")
with gr.Tab("Must Have Skills"):
must_have_skills = gr.JSON(label="Must Have Skills")
with gr.Tab("Good to Have Skills"):
good_to_have_skills = gr.JSON(label="Good to Have Skills")
with gr.Tab("Behavioral Skills"):
behavioral_skills = gr.JSON(label="Behavioral Skills")
with gr.Tab("Certificates"):
cert_recommendations = gr.JSON(label="Certificate Recommendations")
with gr.Tab("Analysis"):
job_analysis = gr.JSON(label="Job Analysis")
with gr.Tab("Main Analysis"):
with gr.Row():
with gr.Column(scale=2):
input_text = gr.Textbox(
label="Input Text",
placeholder="Paste a job description or enter a specific skill...",
lines=5,
max_lines=10
)
with gr.Row():
min_relevance = gr.Slider(
minimum=0.1,
maximum=1.0,
value=0.4,
step=0.1,
label="Minimum Relevance Score"
)
analyze_checkbox = gr.Checkbox(
value=True,
label="Enable Detailed Analysis"
)
with gr.Row():
max_internal = gr.Slider(
minimum=1,
maximum=50,
value=20,
step=1,
label="Max Internal Results"
)
max_external = gr.Slider(
minimum=1,
maximum=20,
value=10,
step=1,
label="Max External Results"
)
submit_btn = gr.Button("Analyze", variant="primary")
clear_btn = gr.Button("Clear")
with gr.Column(scale=3):
output_type = gr.Textbox(label="Input Type Detected")
status = gr.Textbox(label="Status")
with gr.Tab("Internal Certifications"):
internal_certs = gr.JSON(label="Internal Certification Recommendations")
with gr.Tab("External Certifications"):
external_certs = gr.JSON(label="External Certification Recommendations")
with gr.Tab("Skills"):
skills_output = gr.JSON(label="Extracted Skills")
with gr.Tab("Analysis"):
analysis_output = gr.JSON(label="Detailed Analysis")
with gr.Tab("Raw Output"):
raw_output = gr.JSON(label="Complete Response")
with gr.Tab("Certificate URL Analysis"):
with gr.Row():
with gr.Column(scale=1):
cert_url_input = gr.Textbox(label="Certificate URL", placeholder="Enter certificate URL...")
extract_skills_check = gr.Checkbox(value=True, label="Extract Skills")
analyze_reliability_check = gr.Checkbox(value=True, label="Analyze Reliability")
analyze_url_btn = gr.Button("Analyze URL", variant="primary")
with gr.Column(scale=2):
cert_info_output = gr.JSON(label="Certificate Information")
cert_skills_output = gr.JSON(label="Skills from Certificate")
cert_reliability_output = gr.JSON(label="Reliability Analysis")
with gr.Tab("Reliability Check"):
with gr.Row():
with gr.Column(scale=1):
cert_name_input = gr.Textbox(label="Certificate Name", placeholder="Enter certificate name...")
check_reliability_btn = gr.Button("Check Reliability", variant="primary")
with gr.Column(scale=2):
reliability_output = gr.JSON(label="Reliability Score")
with gr.Tab("Skill Gap Analysis"):
with gr.Row():
with gr.Column(scale=1):
current_skills_input = gr.Textbox(label="Current Skills",
placeholder="Enter your current skills (comma-separated)...")
target_skills_input = gr.Textbox(label="Target Skills",
placeholder="Enter target skills (comma-separated)...")
analyze_gap_btn = gr.Button("Analyze Gap", variant="primary")
with gr.Column(scale=2):
gap_analysis_output = gr.JSON(label="Gap Analysis Results")
# Examples
gr.Examples(
examples=[
["Python programming and data analysis with machine learning experience"],
["Cloud computing and AWS infrastructure management"],
["Cybersecurity and network security protocols"],
["Project management with agile methodology experience"],
["Frontend development with React and JavaScript"]
],
inputs=input_text
)
def analyze_job_ontology(job_desc, min_rel, include_certs):
"""Wrapper function for Gradio to call the job ontology API"""
if not job_desc.strip():
return "Please enter a job description", {}, {}, {}, {}, {}
try:
# Call the internal function
result = asyncio.run(extract_job_ontology(job_desc, include_certs, min_rel))
if result["status"] == "success":
ontology = result["ontology"]
certs = result.get("certificates", {})
return (
ontology.get("job_title", "Unknown"),
ontology.get("must_have_skills", []),
ontology.get("good_to_have_skills", []),
ontology.get("behavioral_skills", []),
certs.get("must_have_certificates", {}),
ontology.get("overall_analysis", "")
)
else:
return "Error", {}, {}, {}, {}, result.get("message", "Unknown error")
except Exception as e:
return f"Error: {str(e)}", {}, {}, {}, {}, {}
def analyze_text(text, min_rel, max_int, max_ext, analyze_flag):
"""Wrapper function for Gradio to call the API"""
if not text.strip():
return "Please enter some text", "Error", {}, {}, {}, {}, {}
try:
# Prepare the request
payload = {
"text": text,
"min_relevance": min_rel,
"max_internal_results": int(max_int),
"max_external_results": int(max_ext),
"analyze": analyze_flag
}
# For Gradio, we'll call the function directly instead of HTTP
result = asyncio.run(analyze_input_internal(payload))
if result["status"] == "success":
input_type = result["input_type"]
matches = result["matches"]
# Extract different components
internal_bucket = matches.get("certifications", {}).get("internal_bucket", [])
external_bucket = matches.get("certifications", {}).get("external_bucket", [])
skills_list = matches.get("skills", [])
analysis = result.get("analysis", {})
return (
input_type,
"Analysis completed successfully",
internal_bucket,
external_bucket,
skills_list,
analysis,
result
)
else:
return "error", "Analysis failed", {}, {}, {}, {}, {}
except Exception as e:
logger.error(f"Gradio analysis error: {e}")
return "error", f"Error: {str(e)}", {}, {}, {}, {}, {}
def analyze_cert_url(url, extract_skills, analyze_reliability):
"""Analyze certificate URL"""
if not url.strip():
return {}, {}, {}
try:
# Call the internal function
result = asyncio.run(process_certificate_url(url))
# Extract skills
skills_from_cert = []
if extract_skills and "skills" in result:
cert_skills = result["skills"]
if isinstance(cert_skills, str):
cert_skills = [s.strip() for s in cert_skills.split(",")]
for skill in cert_skills:
skills_from_cert.append({
"skill": skill,
"source": "certificate"
})
# Calculate reliability
reliability_score = {}
if analyze_reliability:
reliability_score = asyncio.run(calculate_certificate_reliability(result))
return result, skills_from_cert, reliability_score
except Exception as e:
return {"error": str(e)}, {}, {}
def check_reliability(cert_name):
"""Check reliability of a certificate"""
if not cert_name.strip():
return {}
try:
# Search for certificate in internal database
cert_data = {}
if certs_df is not None and 'name' in certs_df.columns:
matches = certs_df[certs_df['name'].str.contains(cert_name, na=False, case=False, regex=False)]
if not matches.empty:
cert_data = matches.iloc[0].to_dict()
# Calculate reliability score
reliability_score = asyncio.run(calculate_certificate_reliability(cert_data))
return {
"certificate": cert_name,
"reliability_score": reliability_score,
"factors": CERT_RELIABILITY_FACTORS
}
except Exception as e:
return {"error": str(e)}
def analyze_skill_gap(current_skills, target_skills):
"""Analyze skill gap"""
if not current_skills.strip() or not target_skills.strip():
return {}
try:
current_skills_list = [s.strip() for s in current_skills.split(",")]
target_skills_list = [s.strip() for s in target_skills.split(",")]
# Get missing skills
missing_skills = list(set(target_skills_list) - set(current_skills_list))
# Get overlapping skills
overlapping_skills = list(set(target_skills_list) & set(current_skills_list))
return {
"missing_skills": missing_skills,
"overlapping_skills": overlapping_skills,
"gap_percentage": f"{(len(missing_skills) / len(target_skills_list) * 100):.1f}%"
}
except Exception as e:
return {"error": str(e)}
# Connect the interface
analyze_job_btn.click(
fn=analyze_job_ontology,
inputs=[job_desc_input, min_relevance_ontology, include_certs_check],
outputs=[job_title_output, must_have_skills, good_to_have_skills, behavioral_skills, cert_recommendations,
job_analysis]
)
clear_job_btn.click(
fn=lambda: ["", {}, {}, {}, {}, {}],
outputs=[job_desc_input, job_title_output, must_have_skills, good_to_have_skills, behavioral_skills,
job_analysis]
)
submit_btn.click(
fn=analyze_text,
inputs=[input_text, min_relevance, max_internal, max_external, analyze_checkbox],
outputs=[output_type, status, internal_certs, external_certs, skills_output, analysis_output, raw_output]
)
clear_btn.click(
fn=lambda: ["", "", "", {}, {}, {}, {}, {}],
outputs=[input_text, output_type, status, internal_certs, external_certs, skills_output, analysis_output,
raw_output]
)
analyze_url_btn.click(
fn=analyze_cert_url,
inputs=[cert_url_input, extract_skills_check, analyze_reliability_check],
outputs=[cert_info_output, cert_skills_output, cert_reliability_output]
)
check_reliability_btn.click(
fn=check_reliability,
inputs=cert_name_input,
outputs=reliability_output
)
analyze_gap_btn.click(
fn=analyze_skill_gap,
inputs=[current_skills_input, target_skills_input],
outputs=gap_analysis_output
)
return demo
async def analyze_input_internal(payload):
"""Internal analysis function for Gradio"""
class InputTextInternal:
def __init__(self, text, min_relevance=0.4, max_internal_results=20, max_external_results=10, analyze=True):
self.text = text
self.min_relevance = min_relevance
self.max_internal_results = max_internal_results
self.max_external_results = max_external_results
self.analyze = analyze
input_data = InputTextInternal(**payload)
return await analyze_input(input_data)
# Mount Gradio app to FastAPI
gradio_app = create_gradio_interface()
app = gr.mount_gradio_app(app, gradio_app, path="/")
@app.on_event("startup")
async def startup_event():
"""Initialize data on startup"""
try:
logger.info("Loading data...")
global certs_df, cert_embeddings, skills_df, skill_embeddings, id_col, skill_col
# Load skills with embeddings
skills_df, id_col, skill_col, skill_embeddings = load_skills()
logger.info(f"Loaded {len(skills_df)} skills with embeddings")
# Load certifications from the same file
certs_df, cert_embeddings = await load_certifications()
logger.info(f"Loaded {len(certs_df)} certifications")
except Exception as e:
logger.error(f"Data loading failed: {e}")
raise RuntimeError(f"Service initialization failed: {e}")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860)
async def match_certificate_skills_with_internal(cert_skills: List[str]) -> List[Dict]:
"""Match certificate skills with internal skills database and return only matching skills"""
matched_skills = []
if not cert_skills or not skills_df is not None:
return matched_skills
for cert_skill in cert_skills:
if not cert_skill or cert_skill.strip() == '':
continue
# Try to find matching skill in internal database
matched_skill = await strict_internal_skill_match(cert_skill.strip())
if matched_skill:
matched_skills.append({
"id": matched_skill["id"],
"name": matched_skill["name"],
"source": "internal",
"match_type": matched_skill.get("match_type", "unknown"),
"similarity_score": matched_skill.get("similarity_score", 1.0),
"original_cert_skill": cert_skill.strip()
})
else:
# Log skipped external/unmatched skills
logger.debug(f"Skipped unmatched certificate skill: {cert_skill}")
return matched_skills
async def filter_relevant_certificates_only(cert_recommendations: Dict, extracted_skills: List[str]) -> Dict:
"""Filter certificate recommendations to only include those with skills matching extracted skills"""
if not extracted_skills:
return {"internal_bucket": [], "external_bucket": []}
# Create set of extracted skill names for comparison
extracted_skill_names = {skill.lower().strip() for skill in extracted_skills if skill and skill.strip()}
filtered_recommendations = {"internal_bucket": [], "external_bucket": []}
for bucket_name in ["internal_bucket", "external_bucket"]:
for cert in cert_recommendations.get(bucket_name, []):
cert_skills = cert.get("skills", [])
if not cert_skills:
continue
# Check if any certificate skills match our extracted skills
cert_skill_names = {skill.lower().strip() for skill in cert_skills if skill and skill.strip()}
# Calculate overlap
overlap = extracted_skill_names.intersection(cert_skill_names)
overlap_ratio = len(overlap) / len(extracted_skill_names) if extracted_skill_names else 0
# Only include certificates with meaningful skill overlap
if overlap_ratio >= 0.3: # At least 30% overlap
cert["skill_overlap_ratio"] = overlap_ratio
cert["matching_skills"] = list(overlap)
filtered_recommendations[bucket_name].append(cert)
return filtered_recommendations