Spaces:
Paused
Paused
Commit
·
194e7a7
1
Parent(s):
aba3be2
updated
Browse files- backend/services/report_generator.py +175 -128
backend/services/report_generator.py
CHANGED
|
@@ -139,6 +139,11 @@ def generate_llm_interview_report(application) -> str:
|
|
| 139 |
for idx, entry in enumerate(qa_log, 1):
|
| 140 |
q = entry.get("question", "N/A")
|
| 141 |
a = entry.get("answer", "N/A")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
eval_score = entry.get("evaluation", {}).get("score", "N/A")
|
| 143 |
eval_feedback = entry.get("evaluation", {}).get("feedback", "N/A")
|
| 144 |
|
|
@@ -542,151 +547,190 @@ def _add_section_header(ax, x: float, y: float, title: str, width: float):
|
|
| 542 |
ax.add_line(line)
|
| 543 |
|
| 544 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
def _create_transcript_pages(pdf, qa_log: List[Dict], page_width: float, page_height: float,
|
| 546 |
left_margin: float, right_margin: float,
|
| 547 |
top_margin: float, bottom_margin: float):
|
| 548 |
-
"""Create professional pages for interview transcript."""
|
| 549 |
content_width = page_width - left_margin - right_margin
|
| 550 |
wrapper = textwrap.TextWrapper(width=75)
|
| 551 |
|
| 552 |
-
#
|
| 553 |
-
|
| 554 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
)
|
| 574 |
-
ax.add_patch(
|
| 575 |
|
| 576 |
-
|
| 577 |
-
|
|
|
|
|
|
|
| 578 |
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
|
| 583 |
-
|
|
|
|
|
|
|
| 584 |
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
|
|
|
| 588 |
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
facecolor='#eff6ff',
|
| 597 |
-
edgecolor='#3b82f6',
|
| 598 |
-
linewidth=2
|
| 599 |
-
)
|
| 600 |
-
ax.add_patch(q_box)
|
| 601 |
-
|
| 602 |
-
# Question number badge
|
| 603 |
-
q_badge = Circle((left_margin + 0.4, y_pos - 0.5), 0.2,
|
| 604 |
-
facecolor='#3b82f6', edgecolor='white', linewidth=2)
|
| 605 |
-
ax.add_patch(q_badge)
|
| 606 |
-
|
| 607 |
-
ax.text(left_margin + 0.4, y_pos - 0.5, f'{i+1}',
|
| 608 |
-
fontsize=12, fontweight='bold', color='white',
|
| 609 |
-
horizontalalignment='center', verticalalignment='center')
|
| 610 |
-
|
| 611 |
-
# Question text
|
| 612 |
-
ax.text(left_margin + 0.8, y_pos - 0.3, 'QUESTION',
|
| 613 |
-
fontsize=9, fontweight='bold', color='#1e40af')
|
| 614 |
-
|
| 615 |
-
q_wrapped = wrapper.wrap(qa['question'])
|
| 616 |
-
for j, line in enumerate(q_wrapped[:3]): # Max 3 lines
|
| 617 |
-
ax.text(left_margin + 0.8, y_pos - 0.5 - (j * 0.15), line,
|
| 618 |
-
fontsize=11, fontweight='bold', color='#1e293b')
|
| 619 |
-
|
| 620 |
-
y_pos -= 1.4
|
| 621 |
-
|
| 622 |
-
# Answer section
|
| 623 |
-
answer_box = FancyBboxPatch(
|
| 624 |
-
(left_margin + 0.2, y_pos - 1.2), content_width - 0.4, 1.2,
|
| 625 |
-
boxstyle="round,pad=0.05",
|
| 626 |
-
facecolor='#f9fafb',
|
| 627 |
-
edgecolor='#d1d5db',
|
| 628 |
-
linewidth=1
|
| 629 |
-
)
|
| 630 |
-
ax.add_patch(answer_box)
|
| 631 |
-
|
| 632 |
-
ax.text(left_margin + 0.4, y_pos - 0.2, 'CANDIDATE RESPONSE',
|
| 633 |
-
fontsize=9, fontweight='bold', color='#6b7280')
|
| 634 |
|
| 635 |
-
|
| 636 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 637 |
ax.text(left_margin + 0.4, y_pos - 0.4 - (j * 0.15), line,
|
| 638 |
fontsize=10, color='#374151')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
(left_margin + 0.4, y_pos - 0.8), content_width - 0.8, 0.8,
|
| 645 |
-
boxstyle="round,pad=0.05",
|
| 646 |
-
facecolor='#fefefe',
|
| 647 |
-
edgecolor='#e5e7eb',
|
| 648 |
-
linewidth=1
|
| 649 |
-
)
|
| 650 |
-
ax.add_patch(eval_box)
|
| 651 |
-
|
| 652 |
-
# Score badge
|
| 653 |
-
score_color = _get_score_color(qa['score'])
|
| 654 |
-
score_badge = FancyBboxPatch(
|
| 655 |
-
(left_margin + 0.6, y_pos - 0.35), 1.2, 0.25,
|
| 656 |
-
boxstyle="round,pad=0.02",
|
| 657 |
-
facecolor=score_color,
|
| 658 |
-
alpha=0.2,
|
| 659 |
-
edgecolor=score_color,
|
| 660 |
-
linewidth=1
|
| 661 |
-
)
|
| 662 |
-
ax.add_patch(score_badge)
|
| 663 |
-
|
| 664 |
-
ax.text(left_margin + 1.2, y_pos - 0.225, qa['score'],
|
| 665 |
-
fontsize=10, fontweight='bold', color=score_color,
|
| 666 |
-
horizontalalignment='center', verticalalignment='center')
|
| 667 |
-
|
| 668 |
-
# Feedback
|
| 669 |
-
if qa['feedback'] and qa['feedback'] != 'N/A':
|
| 670 |
-
ax.text(left_margin + 2.2, y_pos - 0.15, 'Feedback:',
|
| 671 |
-
fontsize=9, fontweight='bold', color='#6b7280')
|
| 672 |
-
|
| 673 |
-
f_wrapped = wrapper.wrap(qa['feedback'])
|
| 674 |
-
for j, line in enumerate(f_wrapped[:2]): # Max 2 lines
|
| 675 |
-
ax.text(left_margin + 2.2, y_pos - 0.35 - (j * 0.15), line,
|
| 676 |
-
fontsize=9, color='#6b7280', style='italic')
|
| 677 |
-
|
| 678 |
-
y_pos -= 1.2
|
| 679 |
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 690 |
|
| 691 |
|
| 692 |
# Keep the original advanced version as fallback
|
|
@@ -800,6 +844,9 @@ def create_pdf_report_advanced(report_text: str) -> BytesIO:
|
|
| 800 |
elif stripped.startswith('Question'):
|
| 801 |
story.append(Paragraph(stripped, question_style))
|
| 802 |
elif stripped.startswith('Answer:'):
|
|
|
|
|
|
|
|
|
|
| 803 |
story.append(Paragraph(stripped, answer_style))
|
| 804 |
elif stripped.startswith('Score:'):
|
| 805 |
story.append(Paragraph(stripped, score_style))
|
|
@@ -830,4 +877,4 @@ def create_pdf_report_advanced(report_text: str) -> BytesIO:
|
|
| 830 |
return create_pdf_report(report_text)
|
| 831 |
|
| 832 |
|
| 833 |
-
__all__ = ['generate_llm_interview_report', 'create_pdf_report']
|
|
|
|
| 139 |
for idx, entry in enumerate(qa_log, 1):
|
| 140 |
q = entry.get("question", "N/A")
|
| 141 |
a = entry.get("answer", "N/A")
|
| 142 |
+
|
| 143 |
+
# Handle salary question specifically
|
| 144 |
+
if "salary" in q.lower() and (a == "0$" or a == "0" or a == "$0"):
|
| 145 |
+
a = "Prefer not to disclose"
|
| 146 |
+
|
| 147 |
eval_score = entry.get("evaluation", {}).get("score", "N/A")
|
| 148 |
eval_feedback = entry.get("evaluation", {}).get("feedback", "N/A")
|
| 149 |
|
|
|
|
| 547 |
ax.add_line(line)
|
| 548 |
|
| 549 |
|
| 550 |
+
def _calculate_dynamic_box_height(text: str, width_chars: int = 75, base_height: float = 0.8) -> float:
|
| 551 |
+
"""Calculate dynamic height for text box based on content."""
|
| 552 |
+
wrapped_lines = textwrap.wrap(text, width=width_chars)
|
| 553 |
+
num_lines = len(wrapped_lines)
|
| 554 |
+
# Base height + additional height per line
|
| 555 |
+
return base_height + (max(0, num_lines - 2) * 0.15)
|
| 556 |
+
|
| 557 |
+
|
| 558 |
def _create_transcript_pages(pdf, qa_log: List[Dict], page_width: float, page_height: float,
|
| 559 |
left_margin: float, right_margin: float,
|
| 560 |
top_margin: float, bottom_margin: float):
|
| 561 |
+
"""Create professional pages for interview transcript with dynamic text wrapping."""
|
| 562 |
content_width = page_width - left_margin - right_margin
|
| 563 |
wrapper = textwrap.TextWrapper(width=75)
|
| 564 |
|
| 565 |
+
# Create pages dynamically based on content
|
| 566 |
+
fig = plt.figure(figsize=(page_width, page_height))
|
| 567 |
+
fig.patch.set_facecolor('white')
|
| 568 |
+
ax = fig.add_subplot(111)
|
| 569 |
+
ax.set_xlim(0, page_width)
|
| 570 |
+
ax.set_ylim(0, page_height)
|
| 571 |
+
ax.axis('off')
|
| 572 |
|
| 573 |
+
# Initialize page
|
| 574 |
+
y_pos = page_height - top_margin
|
| 575 |
+
page_num = 0
|
| 576 |
+
|
| 577 |
+
# Header for first transcript page
|
| 578 |
+
_add_transcript_header(ax, left_margin, y_pos, content_width, page_num + 2)
|
| 579 |
+
y_pos -= 1.0
|
| 580 |
+
|
| 581 |
+
for idx, qa in enumerate(qa_log):
|
| 582 |
+
# Calculate space needed for this Q&A
|
| 583 |
+
q_height = _calculate_dynamic_box_height(qa['question'])
|
| 584 |
+
a_height = _calculate_dynamic_box_height(qa['answer'], width_chars=70)
|
| 585 |
+
total_height = q_height + a_height + 2.5 # Include spacing
|
| 586 |
+
|
| 587 |
+
# Check if we need a new page
|
| 588 |
+
if y_pos - total_height < bottom_margin + 1.0:
|
| 589 |
+
# Save current page
|
| 590 |
+
pdf.savefig(fig, bbox_inches='tight', pad_inches=0)
|
| 591 |
+
plt.close(fig)
|
| 592 |
+
|
| 593 |
+
# Create new page
|
| 594 |
+
fig = plt.figure(figsize=(page_width, page_height))
|
| 595 |
+
fig.patch.set_facecolor('white')
|
| 596 |
+
ax = fig.add_subplot(111)
|
| 597 |
+
ax.set_xlim(0, page_width)
|
| 598 |
+
ax.set_ylim(0, page_height)
|
| 599 |
+
ax.axis('off')
|
| 600 |
+
|
| 601 |
+
y_pos = page_height - top_margin
|
| 602 |
+
page_num += 1
|
| 603 |
+
_add_transcript_header(ax, left_margin, y_pos, content_width, page_num + 2)
|
| 604 |
+
y_pos -= 1.0
|
| 605 |
+
|
| 606 |
+
# Question section with dynamic height
|
| 607 |
+
q_box = FancyBboxPatch(
|
| 608 |
+
(left_margin, y_pos - q_height), content_width, q_height,
|
| 609 |
+
boxstyle="round,pad=0.05",
|
| 610 |
+
facecolor='#eff6ff',
|
| 611 |
+
edgecolor='#3b82f6',
|
| 612 |
+
linewidth=2
|
| 613 |
)
|
| 614 |
+
ax.add_patch(q_box)
|
| 615 |
|
| 616 |
+
# Question number badge
|
| 617 |
+
q_badge = Circle((left_margin + 0.4, y_pos - q_height/2), 0.2,
|
| 618 |
+
facecolor='#3b82f6', edgecolor='white', linewidth=2)
|
| 619 |
+
ax.add_patch(q_badge)
|
| 620 |
|
| 621 |
+
ax.text(left_margin + 0.4, y_pos - q_height/2, f'{idx+1}',
|
| 622 |
+
fontsize=12, fontweight='bold', color='white',
|
| 623 |
+
horizontalalignment='center', verticalalignment='center')
|
| 624 |
|
| 625 |
+
# Question text with proper wrapping
|
| 626 |
+
ax.text(left_margin + 0.8, y_pos - 0.2, 'QUESTION',
|
| 627 |
+
fontsize=9, fontweight='bold', color='#1e40af')
|
| 628 |
|
| 629 |
+
q_wrapped = wrapper.wrap(qa['question'])
|
| 630 |
+
for j, line in enumerate(q_wrapped):
|
| 631 |
+
ax.text(left_margin + 0.8, y_pos - 0.4 - (j * 0.15), line,
|
| 632 |
+
fontsize=11, fontweight='bold', color='#1e293b')
|
| 633 |
|
| 634 |
+
y_pos -= (q_height + 0.3)
|
| 635 |
+
|
| 636 |
+
# Answer section with dynamic height
|
| 637 |
+
answer_text = qa['answer']
|
| 638 |
+
# Handle salary question replacement
|
| 639 |
+
if "salary" in qa['question'].lower() and (answer_text == "0$" or answer_text == "0" or answer_text == "$0"):
|
| 640 |
+
answer_text = "Prefer not to disclose"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 641 |
|
| 642 |
+
answer_box = FancyBboxPatch(
|
| 643 |
+
(left_margin + 0.2, y_pos - a_height), content_width - 0.4, a_height,
|
| 644 |
+
boxstyle="round,pad=0.05",
|
| 645 |
+
facecolor='#f9fafb',
|
| 646 |
+
edgecolor='#d1d5db',
|
| 647 |
+
linewidth=1
|
| 648 |
+
)
|
| 649 |
+
ax.add_patch(answer_box)
|
| 650 |
+
|
| 651 |
+
ax.text(left_margin + 0.4, y_pos - 0.2, 'CANDIDATE RESPONSE',
|
| 652 |
+
fontsize=9, fontweight='bold', color='#6b7280')
|
| 653 |
+
|
| 654 |
+
a_wrapped = wrapper.wrap(answer_text)
|
| 655 |
+
for j, line in enumerate(a_wrapped):
|
| 656 |
+
if j * 0.15 < a_height - 0.4: # Ensure text fits in box
|
| 657 |
ax.text(left_margin + 0.4, y_pos - 0.4 - (j * 0.15), line,
|
| 658 |
fontsize=10, color='#374151')
|
| 659 |
+
|
| 660 |
+
y_pos -= (a_height + 0.4)
|
| 661 |
+
|
| 662 |
+
# Evaluation section
|
| 663 |
+
eval_box = FancyBboxPatch(
|
| 664 |
+
(left_margin + 0.4, y_pos - 0.8), content_width - 0.8, 0.8,
|
| 665 |
+
boxstyle="round,pad=0.05",
|
| 666 |
+
facecolor='#fefefe',
|
| 667 |
+
edgecolor='#e5e7eb',
|
| 668 |
+
linewidth=1
|
| 669 |
+
)
|
| 670 |
+
ax.add_patch(eval_box)
|
| 671 |
+
|
| 672 |
+
# Score badge
|
| 673 |
+
score_color = _get_score_color(qa['score'])
|
| 674 |
+
score_badge = FancyBboxPatch(
|
| 675 |
+
(left_margin + 0.6, y_pos - 0.35), 1.2, 0.25,
|
| 676 |
+
boxstyle="round,pad=0.02",
|
| 677 |
+
facecolor=score_color,
|
| 678 |
+
alpha=0.2,
|
| 679 |
+
edgecolor=score_color,
|
| 680 |
+
linewidth=1
|
| 681 |
+
)
|
| 682 |
+
ax.add_patch(score_badge)
|
| 683 |
+
|
| 684 |
+
ax.text(left_margin + 1.2, y_pos - 0.225, qa['score'],
|
| 685 |
+
fontsize=10, fontweight='bold', color=score_color,
|
| 686 |
+
horizontalalignment='center', verticalalignment='center')
|
| 687 |
+
|
| 688 |
+
# Feedback with proper wrapping
|
| 689 |
+
if qa['feedback'] and qa['feedback'] != 'N/A':
|
| 690 |
+
ax.text(left_margin + 2.2, y_pos - 0.15, 'Feedback:',
|
| 691 |
+
fontsize=9, fontweight='bold', color='#6b7280')
|
| 692 |
|
| 693 |
+
# Calculate feedback area width
|
| 694 |
+
feedback_width = content_width - 2.6
|
| 695 |
+
feedback_wrapper = textwrap.TextWrapper(width=int(feedback_width * 10))
|
| 696 |
+
f_wrapped = feedback_wrapper.wrap(qa['feedback'])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 697 |
|
| 698 |
+
for j, line in enumerate(f_wrapped[:2]): # Max 2 lines
|
| 699 |
+
ax.text(left_margin + 2.2, y_pos - 0.35 - (j * 0.15), line,
|
| 700 |
+
fontsize=9, color='#6b7280', style='italic')
|
| 701 |
+
|
| 702 |
+
y_pos -= 1.2
|
| 703 |
+
|
| 704 |
+
# Add separator between questions (except last)
|
| 705 |
+
if idx < len(qa_log) - 1:
|
| 706 |
+
separator = plt.Line2D([left_margin + 1, left_margin + content_width - 1],
|
| 707 |
+
[y_pos + 0.3, y_pos + 0.3],
|
| 708 |
+
color='#e5e7eb', linewidth=1, linestyle='--')
|
| 709 |
+
ax.add_line(separator)
|
| 710 |
+
y_pos -= 0.3
|
| 711 |
+
|
| 712 |
+
# Save last page
|
| 713 |
+
pdf.savefig(fig, bbox_inches='tight', pad_inches=0)
|
| 714 |
+
plt.close(fig)
|
| 715 |
+
|
| 716 |
+
|
| 717 |
+
def _add_transcript_header(ax, left_margin: float, y_pos: float, content_width: float, page_num: int):
|
| 718 |
+
"""Add header for transcript pages."""
|
| 719 |
+
# Header background
|
| 720 |
+
header_rect = FancyBboxPatch(
|
| 721 |
+
(left_margin, y_pos - 0.6), content_width, 0.6,
|
| 722 |
+
boxstyle="round,pad=0.02",
|
| 723 |
+
facecolor='#1e40af',
|
| 724 |
+
edgecolor='none'
|
| 725 |
+
)
|
| 726 |
+
ax.add_patch(header_rect)
|
| 727 |
+
|
| 728 |
+
ax.text(left_margin + 0.2, y_pos - 0.3, 'INTERVIEW TRANSCRIPT',
|
| 729 |
+
fontsize=14, fontweight='bold', color='white')
|
| 730 |
+
|
| 731 |
+
# Page number
|
| 732 |
+
ax.text(left_margin + content_width - 0.2, y_pos - 0.3, f'Page {page_num}',
|
| 733 |
+
fontsize=10, color='white', horizontalalignment='right')
|
| 734 |
|
| 735 |
|
| 736 |
# Keep the original advanced version as fallback
|
|
|
|
| 844 |
elif stripped.startswith('Question'):
|
| 845 |
story.append(Paragraph(stripped, question_style))
|
| 846 |
elif stripped.startswith('Answer:'):
|
| 847 |
+
# Handle salary replacement
|
| 848 |
+
if "0$" in stripped or " 0 " in stripped or "$0" in stripped:
|
| 849 |
+
stripped = stripped.replace("0$", "Prefer not to disclose").replace(" 0 ", " Prefer not to disclose ").replace("$0", "Prefer not to disclose")
|
| 850 |
story.append(Paragraph(stripped, answer_style))
|
| 851 |
elif stripped.startswith('Score:'):
|
| 852 |
story.append(Paragraph(stripped, score_style))
|
|
|
|
| 877 |
return create_pdf_report(report_text)
|
| 878 |
|
| 879 |
|
| 880 |
+
__all__ = ['generate_llm_interview_report', 'create_pdf_report']
|