Spaces:
Paused
Paused
"""Utilities for assembling and exporting interview reports. | |
This module provides two primary helpers used by the recruiter dashboard: | |
``generate_llm_interview_report(application)`` | |
Given a candidate's ``Application`` record, assemble a plain‑text report | |
summarising the interview. Because the interview process currently | |
executes entirely client‑side and does not persist questions or answers | |
to the database, this report focuses on the information available on | |
the server: the candidate's profile, the job requirements and a skills | |
match score. Should future iterations store richer interview data | |
server‑side, this function can be extended to include question/answer | |
transcripts, per‑question scores and LLM‑generated feedback. | |
``create_pdf_report(report_text)`` | |
Convert a multi‑line string into a simple PDF. The implementation | |
leverages Matplotlib's PDF backend (available by default) to avoid | |
heavyweight dependencies such as ReportLab or WeasyPrint, which are | |
absent from the runtime environment. Text is wrapped and split | |
across multiple pages as necessary. | |
""" | |
from __future__ import annotations | |
import json | |
from io import BytesIO | |
import textwrap | |
from typing import List | |
import matplotlib.pyplot as plt | |
from matplotlib.backends.backend_pdf import PdfPages | |
def generate_llm_interview_report(application) -> str: | |
"""Generate a human‑readable interview report for a candidate. | |
The report includes the candidate's name and email, job details, | |
application date, a computed skills match summary and placeholder | |
sections for future enhancements. If server‑side storage of | |
question/answer pairs is added later, this function can be updated | |
to incorporate those details. | |
Parameters | |
---------- | |
application : backend.models.database.Application | |
The SQLAlchemy Application instance representing the candidate's | |
job application. Assumed to have related ``job`` and | |
``date_applied`` attributes available. | |
Returns | |
------- | |
str | |
A multi‑line string containing the report contents. | |
""" | |
# Defensive imports to avoid circular dependencies at import time | |
try: | |
from datetime import datetime # noqa: F401 | |
except Exception: | |
pass | |
# Extract candidate skills and job skills | |
try: | |
candidate_features = json.loads(application.extracted_features) if application.extracted_features else {} | |
except Exception: | |
candidate_features = {} | |
candidate_skills: List[str] = candidate_features.get('skills', []) or [] | |
job_skills: List[str] = [] | |
try: | |
job_skills = json.loads(application.job.skills) if application.job and application.job.skills else [] | |
except Exception: | |
job_skills = [] | |
# Compute skills match ratio and label. Normalise to lower case for | |
# comparison and avoid dividing by zero when ``job_skills`` is empty. | |
candidate_set = {s.strip().lower() for s in candidate_skills} | |
job_set = {s.strip().lower() for s in job_skills} | |
common = candidate_set & job_set | |
ratio = len(common) / len(job_set) if job_set else 0.0 | |
if ratio >= 0.75: | |
score_label = 'Excellent' | |
elif ratio >= 0.5: | |
score_label = 'Good' | |
elif ratio >= 0.25: | |
score_label = 'Medium' | |
else: | |
score_label = 'Poor' | |
# Assemble report lines | |
lines: List[str] = [] | |
lines.append('Interview Report') | |
lines.append('=================') | |
lines.append('') | |
lines.append(f'Candidate Name: {application.name}') | |
lines.append(f'Candidate Email: {application.email}') | |
if application.job: | |
lines.append(f'Job Applied: {application.job.role}') | |
lines.append(f'Company: {application.job.company}') | |
else: | |
lines.append('Job Applied: N/A') | |
lines.append('Company: N/A') | |
# Format date_applied if available | |
try: | |
date_str = application.date_applied.strftime('%Y-%m-%d') if application.date_applied else 'N/A' | |
except Exception: | |
date_str = 'N/A' | |
lines.append(f'Date Applied: {date_str}') | |
lines.append('') | |
lines.append('Skills Match Summary:') | |
# Represent required and candidate skills as comma‑separated lists. Use | |
# title‑case for presentation and handle empty lists gracefully. | |
formatted_job_skills = ', '.join(job_skills) if job_skills else 'N/A' | |
formatted_candidate_skills = ', '.join(candidate_skills) if candidate_skills else 'N/A' | |
formatted_common = ', '.join(sorted(common)) if common else 'None' | |
lines.append(f' Required Skills: {formatted_job_skills}') | |
lines.append(f' Candidate Skills: {formatted_candidate_skills}') | |
lines.append(f' Skills in Common: {formatted_common}') | |
lines.append(f' Match Ratio: {ratio * 100:.0f}%') | |
lines.append(f' Score: {score_label}') | |
lines.append('') | |
lines.append('Additional Notes:') | |
lines.append('This report was generated automatically based on the information provided in the application.') | |
lines.append('Interview question and answer details are not currently stored on the server.') | |
lines.append('Future versions may include a detailed breakdown of interview responses and evaluator feedback.') | |
return '\n'.join(lines) | |
def create_pdf_report(report_text: str) -> BytesIO: | |
"""Convert a plain‑text report into a PDF document. | |
This helper uses Matplotlib's ``PdfPages`` backend to compose a PDF | |
containing the supplied text. Lines are wrapped to a reasonable | |
width and paginated as needed. The returned ``BytesIO`` object can | |
be passed directly to Flask's ``send_file`` function. | |
Parameters | |
---------- | |
report_text : str | |
The full contents of the report to be included in the PDF. | |
Returns | |
------- | |
BytesIO | |
A file‑like buffer containing the PDF data. The caller is | |
responsible for rewinding the buffer (via ``seek(0)``) before | |
sending it over HTTP. | |
""" | |
buffer = BytesIO() | |
# Split the input into lines and wrap long lines for better layout | |
raw_lines = report_text.split('\n') | |
wrapper = textwrap.TextWrapper(width=90, break_long_words=True, replace_whitespace=False) | |
wrapped_lines: List[str] = [] | |
for line in raw_lines: | |
if not line: | |
wrapped_lines.append('') | |
continue | |
# Wrap each line individually; the wrapper returns a list | |
segments = wrapper.wrap(line) | |
if segments: | |
wrapped_lines.extend(segments) | |
else: | |
wrapped_lines.append(line) | |
# Determine how many lines to place on each PDF page. The value | |
# of 40 lines per page fits comfortably on an A4 sheet using the | |
# selected font size and margins. | |
lines_per_page = 40 | |
with PdfPages(buffer) as pdf: | |
for i in range(0, len(wrapped_lines), lines_per_page): | |
fig = plt.figure(figsize=(8.27, 11.69)) # A4 portrait in inches | |
fig.patch.set_facecolor('white') | |
# Compose the block of text for this page | |
page_text = '\n'.join(wrapped_lines[i:i + lines_per_page]) | |
# Place the text near the top-left corner. ``va='top'`` | |
# ensures the first line starts at the specified y | |
fig.text(0.1, 0.95, page_text, va='top', ha='left', fontsize=10, family='monospace') | |
# Save and close the figure to free memory | |
pdf.savefig(fig) | |
plt.close(fig) | |
buffer.seek(0) | |
return buffer | |
__all__ = ['generate_llm_interview_report', 'create_pdf_report'] |