| import os |
| |
| os.environ["HF_HOME"] = "/app/cache" |
| os.environ['TRANSFORMERS_CACHE'] = '/app/cache' |
| os.environ['HF_HUB_CACHE'] = '/app/cache' |
|
|
| from fastapi import FastAPI, File, UploadFile, Request |
| from fastapi.responses import HTMLResponse |
| from fastapi.templating import Jinja2Templates |
| from fastapi.middleware.cors import CORSMiddleware |
| from pydantic import BaseModel |
| from typing import List |
| import json |
| import pandas as pd |
| from transformers import AutoTokenizer, AutoModelForSequenceClassification |
| from scipy.special import softmax |
| import torch |
|
|
| app = FastAPI( |
| title="Candidate Ranking API", |
| description="API for ranking candidates based on resume and cover letter sentiment analysis", |
| version="1.0.0" |
| ) |
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| |
| @app.get("/") |
| async def root(): |
| return { |
| "message": "Candidate Ranking API is running!", |
| "endpoints": { |
| "upload": "POST /upload - Upload Excel file with candidate data", |
| "predict": "GET /predict - Get ranked candidates", |
| "docs": "GET /docs - API documentation", |
| "upload_form": "GET /upload-form - Simple upload form", |
| "test_interface": "GET /test-interface - Complete testing interface" |
| }, |
| "links": { |
| "api_docs": "/docs", |
| "upload_form": "/upload-form", |
| "test_interface": "/test-interface" |
| } |
| } |
|
|
| |
| tokenizer = AutoTokenizer.from_pretrained("mrm8488/distilroberta-finetuned-financial-news-sentiment-analysis") |
| model = AutoModelForSequenceClassification.from_pretrained("mrm8488/distilroberta-finetuned-financial-news-sentiment-analysis") |
|
|
| class Candidate(BaseModel): |
| name: str |
| surname: str |
| resume_link: str |
| cover_letter_link: str |
| priority: float = 0.0 |
|
|
| def rank_candidate(name, surname, resume, cover_letter): |
| text = f"{name} {surname} {resume} {cover_letter}" |
| inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True) |
| with torch.no_grad(): |
| logits = model(**inputs).logits |
| scores = softmax(logits.numpy()[0]) |
| return round(float(scores[2]) * 100, 2) |
|
|
| |
| @app.get("/test-interface", response_class=HTMLResponse) |
| async def test_interface(): |
| return """ |
| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>API Testing Interface</title> |
| <style> |
| body { font-family: Arial, sans-serif; max-width: 1000px; margin: 20px auto; padding: 20px; background: #f5f5f5; } |
| .container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); } |
| h1 { color: #333; text-align: center; margin-bottom: 30px; } |
| .section { background: #f8f9ff; padding: 20px; margin: 20px 0; border-radius: 8px; border-left: 4px solid #007bff; } |
| .section h2 { color: #007bff; margin-bottom: 15px; } |
| .form-group { margin: 15px 0; } |
| label { display: block; margin-bottom: 5px; font-weight: bold; } |
| input[type="file"] { width: 100%; padding: 10px; border: 2px solid #ddd; border-radius: 5px; } |
| button { background: #007bff; color: white; padding: 12px 20px; border: none; border-radius: 5px; cursor: pointer; margin: 5px; } |
| button:hover { background: #0056b3; } |
| button:disabled { background: #ccc; cursor: not-allowed; } |
| .btn-secondary { background: #28a745; } |
| .btn-danger { background: #dc3545; } |
| .response { background: #1e1e1e; color: #00ff00; padding: 15px; border-radius: 5px; font-family: monospace; white-space: pre-wrap; max-height: 300px; overflow-y: auto; margin: 15px 0; } |
| .loading { display: none; text-align: center; padding: 20px; } |
| .spinner { border: 4px solid #f3f3f3; border-top: 4px solid #007bff; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 0 auto; } |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } |
| .candidates-table { width: 100%; border-collapse: collapse; margin: 20px 0; background: white; border-radius: 5px; overflow: hidden; } |
| .candidates-table th, .candidates-table td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; } |
| .candidates-table th { background: #007bff; color: white; } |
| .candidates-table tr:hover { background: #f8f9ff; } |
| .priority-high { background: #28a745; color: white; padding: 4px 8px; border-radius: 15px; } |
| .priority-medium { background: #ffc107; color: black; padding: 4px 8px; border-radius: 15px; } |
| .priority-low { background: #dc3545; color: white; padding: 4px 8px; border-radius: 15px; } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>🧪 Candidate Ranking API Tester</h1> |
| |
| <div class="section"> |
| <h2>📋 API Endpoints</h2> |
| <button onclick="testRoot()">Test Root (/)</button> |
| <button onclick="getCandidates()">Get Candidates (/predict)</button> |
| <button onclick="window.open('/docs', '_blank')" class="btn-secondary">API Docs</button> |
| </div> |
| |
| <div class="section"> |
| <h2>📁 File Upload Test</h2> |
| <form id="uploadForm" enctype="multipart/form-data"> |
| <div class="form-group"> |
| <label for="file">Select Excel File:</label> |
| <input type="file" id="file" name="file" accept=".xlsx,.xls" required> |
| <small>Required columns: Name, Surname, Resume Link, Cover Letter Link</small> |
| </div> |
| <button type="submit">Upload & Process File</button> |
| <button type="button" onclick="generateSample()" class="btn-secondary">Download Sample CSV</button> |
| <button type="button" onclick="clearLog()" class="btn-danger">Clear Log</button> |
| </form> |
| </div> |
| |
| <div class="loading" id="loading"> |
| <div class="spinner"></div> |
| <p>Processing...</p> |
| </div> |
| |
| <div class="section"> |
| <h2>📊 Response Log</h2> |
| <div class="response" id="responseLog">Ready to test API...\n</div> |
| </div> |
| |
| <div class="section" id="resultsSection" style="display: none;"> |
| <h2>🏆 Ranked Candidates</h2> |
| <div id="candidatesTable"></div> |
| </div> |
| </div> |
| |
| <script> |
| function log(message, type = 'info') { |
| const logArea = document.getElementById('responseLog'); |
| const timestamp = new Date().toLocaleTimeString(); |
| const prefix = type === 'error' ? '❌' : type === 'success' ? '✅' : 'ℹ️'; |
| logArea.textContent += `[${timestamp}] ${prefix} ${message}\n`; |
| logArea.scrollTop = logArea.scrollHeight; |
| } |
| |
| function showLoading() { |
| document.getElementById('loading').style.display = 'block'; |
| } |
| |
| function hideLoading() { |
| document.getElementById('loading').style.display = 'none'; |
| } |
| |
| function clearLog() { |
| document.getElementById('responseLog').textContent = 'Log cleared...\n'; |
| document.getElementById('resultsSection').style.display = 'none'; |
| } |
| |
| async function testRoot() { |
| showLoading(); |
| log('Testing root endpoint...'); |
| try { |
| const response = await fetch('/'); |
| const data = await response.json(); |
| log(`Status: ${response.status}`, response.ok ? 'success' : 'error'); |
| log(`Response: ${JSON.stringify(data, null, 2)}`); |
| } catch (error) { |
| log(`Error: ${error.message}`, 'error'); |
| } |
| hideLoading(); |
| } |
| |
| async function getCandidates() { |
| showLoading(); |
| log('Getting ranked candidates...'); |
| try { |
| const response = await fetch('/predict'); |
| const data = await response.json(); |
| log(`Status: ${response.status}`, response.ok ? 'success' : 'error'); |
| |
| if (response.ok && Array.isArray(data) && data.length > 0) { |
| log(`Found ${data.length} candidates`, 'success'); |
| displayCandidates(data); |
| } else if (Array.isArray(data) && data.length === 0) { |
| log('No candidates found. Upload a file first.'); |
| } else { |
| log(`Response: ${JSON.stringify(data, null, 2)}`); |
| } |
| } catch (error) { |
| log(`Error: ${error.message}`, 'error'); |
| } |
| hideLoading(); |
| } |
| |
| function displayCandidates(candidates) { |
| const sorted = candidates.sort((a, b) => (b.priority || 0) - (a.priority || 0)); |
| |
| let tableHTML = ` |
| <table class="candidates-table"> |
| <thead> |
| <tr> |
| <th>Rank</th> |
| <th>Name</th> |
| <th>Surname</th> |
| <th>Priority Score</th> |
| <th>Resume</th> |
| <th>Cover Letter</th> |
| </tr> |
| </thead> |
| <tbody> |
| `; |
| |
| sorted.forEach((candidate, index) => { |
| const priority = candidate.priority || 0; |
| let scoreClass = 'priority-low'; |
| if (priority >= 70) scoreClass = 'priority-high'; |
| else if (priority >= 40) scoreClass = 'priority-medium'; |
| |
| tableHTML += ` |
| <tr> |
| <td><strong>#${index + 1}</strong></td> |
| <td>${candidate.name || 'N/A'}</td> |
| <td>${candidate.surname || 'N/A'}</td> |
| <td><span class="${scoreClass}">${priority.toFixed(1)}%</span></td> |
| <td><a href="${candidate.resume_link || '#'}" target="_blank">Link</a></td> |
| <td><a href="${candidate.cover_letter_link || '#'}" target="_blank">Link</a></td> |
| </tr> |
| `; |
| }); |
| |
| tableHTML += '</tbody></table>'; |
| document.getElementById('candidatesTable').innerHTML = tableHTML; |
| document.getElementById('resultsSection').style.display = 'block'; |
| } |
| |
| function generateSample() { |
| const sampleData = [ |
| ['Name', 'Surname', 'Resume Link', 'Cover Letter Link'], |
| ['John', 'Smith', 'https://example.com/john-resume.pdf', 'https://example.com/john-cover.pdf'], |
| ['Sarah', 'Johnson', 'https://example.com/sarah-resume.pdf', 'https://example.com/sarah-cover.pdf'], |
| ['Michael', 'Brown', 'https://example.com/michael-resume.pdf', 'https://example.com/michael-cover.pdf'], |
| ['Emily', 'Davis', 'https://example.com/emily-resume.pdf', 'https://example.com/emily-cover.pdf'], |
| ['David', 'Wilson', 'https://example.com/david-resume.pdf', 'https://example.com/david-cover.pdf'] |
| ]; |
| |
| const csvContent = sampleData.map(row => row.join(',')).join('\\n'); |
| const blob = new Blob([csvContent], { type: 'text/csv' }); |
| const url = window.URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| a.download = 'sample_candidates.csv'; |
| a.click(); |
| window.URL.revokeObjectURL(url); |
| log('Sample CSV downloaded! Convert to Excel and upload.', 'success'); |
| } |
| |
| // Handle form submission |
| document.getElementById('uploadForm').addEventListener('submit', async function(e) { |
| e.preventDefault(); |
| |
| const fileInput = document.getElementById('file'); |
| const file = fileInput.files[0]; |
| |
| if (!file) { |
| log('Please select a file', 'error'); |
| return; |
| } |
| |
| showLoading(); |
| log(`Uploading: ${file.name} (${(file.size / 1024).toFixed(1)} KB)`); |
| |
| const formData = new FormData(); |
| formData.append('file', file); |
| |
| try { |
| const response = await fetch('/upload', { |
| method: 'POST', |
| body: formData |
| }); |
| |
| const data = await response.json(); |
| log(`Upload status: ${response.status}`, response.ok ? 'success' : 'error'); |
| log(`Response: ${JSON.stringify(data, null, 2)}`); |
| |
| if (response.ok) { |
| log('File uploaded successfully!', 'success'); |
| setTimeout(() => getCandidates(), 1500); |
| } |
| } catch (error) { |
| log(`Upload error: ${error.message}`, 'error'); |
| } |
| |
| hideLoading(); |
| }); |
| |
| // Initialize |
| log('API Testing Interface ready!', 'success'); |
| log('1. Test endpoints first'); |
| log('2. Upload Excel file or download sample'); |
| log('3. View ranked results'); |
| </script> |
| </body> |
| </html> |
| """ |
|
|
| @app.get("/upload-form", response_class=HTMLResponse) |
| async def upload_form(): |
| return """ |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>Candidate Ranking System</title> |
| <style> |
| body { font-family: Arial, sans-serif; max-width: 800px; margin: 50px auto; padding: 20px; } |
| .container { background: #f5f5f5; padding: 30px; border-radius: 10px; } |
| h1 { color: #333; text-align: center; } |
| .form-group { margin: 20px 0; } |
| label { display: block; margin-bottom: 5px; font-weight: bold; } |
| input[type="file"] { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; } |
| button { background: #007bff; color: white; padding: 10px 20px; border: none; border-radius: 5px; cursor: pointer; } |
| button:hover { background: #0056b3; } |
| .info { background: #e7f3ff; padding: 15px; border-radius: 5px; margin: 20px 0; } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>Candidate Ranking System</h1> |
| <div class="info"> |
| <p><strong>Upload an Excel file with the following columns:</strong></p> |
| <ul> |
| <li>Name</li> |
| <li>Surname</li> |
| <li>Resume Link</li> |
| <li>Cover Letter Link</li> |
| </ul> |
| </div> |
| <form action="/upload" method="post" enctype="multipart/form-data"> |
| <div class="form-group"> |
| <label for="file">Select Excel File:</label> |
| <input type="file" id="file" name="file" accept=".xlsx,.xls" required> |
| </div> |
| <button type="submit">Upload and Rank Candidates</button> |
| </form> |
| <div style="margin-top: 30px;"> |
| <a href="/predict" target="_blank">View Ranked Candidates (JSON)</a> | |
| <a href="/docs" target="_blank">API Documentation</a> |
| </div> |
| </div> |
| </body> |
| </html> |
| """ |
|
|
| @app.post("/upload") |
| async def upload_excel(file: UploadFile = File(...)): |
| df = pd.read_excel(file.file) |
| candidates = [] |
| |
| for _, row in df.iterrows(): |
| name = str(row.get("Name", "")) |
| surname = str(row.get("Surname", "")) |
| resume_link = str(row.get("Resume Link", "")) |
| cover_letter_link = str(row.get("Cover Letter Link", "")) |
| |
| |
| resume = "This is a dummy summary of resume from link" |
| cover_letter = "This is a dummy summary of cover letter from link" |
| |
| priority = rank_candidate(name, surname, resume, cover_letter) |
| |
| candidate = { |
| "name": name, |
| "surname": surname, |
| "resume_link": resume_link, |
| "cover_letter_link": cover_letter_link, |
| "priority": priority |
| } |
| candidates.append(candidate) |
| |
| |
| with open("candidates.json", "w") as f: |
| json.dump(candidates, f, indent=4) |
| |
| return {"message": "File processed and candidates.json created successfully!"} |
|
|
| @app.get("/predict", response_model=List[Candidate]) |
| def get_ranked_candidates(): |
| if not os.path.exists("candidates.json"): |
| return [] |
| |
| with open("candidates.json", "r") as f: |
| return json.load(f) |