Use structured CV sections in tailoring and test profile parsing
This commit is contained in:
@@ -56,6 +56,47 @@ namespace JobTrackerApi.Controllers
|
||||
return "Hi there,";
|
||||
}
|
||||
|
||||
private sealed record CvSectionRecord(string? Name, string? Content, int? WordCount);
|
||||
|
||||
private static string BuildStructuredCvContext(ApplicationUser? user)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(user?.ProfileCvStructureJson)) return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var sections = JsonSerializer.Deserialize<List<CvSectionRecord>>(user.ProfileCvStructureJson);
|
||||
if (sections is null || sections.Count == 0) return string.Empty;
|
||||
|
||||
var preferredOrder = new[]
|
||||
{
|
||||
"Professional Summary",
|
||||
"Core Skills",
|
||||
"Experience Highlights",
|
||||
"Selected Achievements",
|
||||
"Projects",
|
||||
"Education",
|
||||
"Certifications",
|
||||
};
|
||||
|
||||
var ordered = preferredOrder
|
||||
.Select(name => sections.FirstOrDefault(section => string.Equals(section.Name?.Trim(), name, StringComparison.OrdinalIgnoreCase)))
|
||||
.Where(section => section is not null)
|
||||
.Concat(sections.Where(section => !preferredOrder.Contains(section.Name ?? string.Empty, StringComparer.OrdinalIgnoreCase)))
|
||||
.Where(section => !string.IsNullOrWhiteSpace(section?.Content))
|
||||
.Take(6)
|
||||
.Select(section => $"{section!.Name}:\n{section.Content!.Trim()}")
|
||||
.ToList();
|
||||
|
||||
return ordered.Count > 0
|
||||
? $"Structured CV sections:\n{string.Join("\n\n", ordered)}"
|
||||
: string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<string>> BuildListFromAiAsync(string instruction, string context, CancellationToken cancellationToken, string fallbackPrefix)
|
||||
{
|
||||
var raw = await _summarizer.SummarizeSectionAsync(instruction, context, 220, 70);
|
||||
@@ -1525,6 +1566,7 @@ namespace JobTrackerApi.Controllers
|
||||
var jobTags = SkillTagger.Detect(jobText).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
var strengths = jobTags.Where(tag => normalizedCv.Contains(tag.ToLowerInvariant())).Take(8).ToList();
|
||||
var gaps = jobTags.Where(tag => !normalizedCv.Contains(tag.ToLowerInvariant())).Take(8).ToList();
|
||||
var structuredCvContext = BuildStructuredCvContext(user);
|
||||
|
||||
var attachmentContext = await BuildAttachmentContextAsync(id, cancellationToken, attachmentIds);
|
||||
var jobContext = $@"Job title: {job.JobTitle}
|
||||
@@ -1535,7 +1577,7 @@ Job description and notes:
|
||||
{jobText}
|
||||
|
||||
Candidate CV/profile:
|
||||
{cvText}{(attachmentContext is not null ? $"\n\n{attachmentContext.Context}" : string.Empty)}";
|
||||
{cvText}{(!string.IsNullOrWhiteSpace(structuredCvContext) ? $"\n\n{structuredCvContext}" : string.Empty)}{(attachmentContext is not null ? $"\n\n{attachmentContext.Context}" : string.Empty)}";
|
||||
|
||||
var matchSummary = await _summarizer.SummarizeSectionAsync(
|
||||
"Write a concise candidate-fit assessment. Explain overall alignment, strongest evidence, biggest risks, and how competitive the candidate appears.",
|
||||
@@ -1648,6 +1690,7 @@ Candidate CV/profile:
|
||||
var normalizedCv = cvText.ToLowerInvariant();
|
||||
var matchedTags = jobTags.Where(tag => normalizedCv.Contains(tag.ToLowerInvariant())).Take(5).ToList();
|
||||
var missingTags = jobTags.Where(tag => !normalizedCv.Contains(tag.ToLowerInvariant())).Take(5).ToList();
|
||||
var structuredCvContext = BuildStructuredCvContext(user);
|
||||
|
||||
var attachmentContext = await BuildAttachmentContextAsync(id, cancellationToken, attachmentIds);
|
||||
var context = $@"Job title: {job.JobTitle}
|
||||
@@ -1657,7 +1700,7 @@ Job description and notes:
|
||||
{jobText}
|
||||
|
||||
Candidate master CV:
|
||||
{cvText}{(attachmentContext is not null ? $"\n\n{attachmentContext.Context}" : string.Empty)}";
|
||||
{cvText}{(!string.IsNullOrWhiteSpace(structuredCvContext) ? $"\n\n{structuredCvContext}" : string.Empty)}{(attachmentContext is not null ? $"\n\n{attachmentContext.Context}" : string.Empty)}";
|
||||
|
||||
var strategicSummary = await _summarizer.SummarizeSectionAsync(
|
||||
"Write a concise strategy summary for how the candidate should approach this role. Focus on what matters most in the posting, what evidence to lead with, and where to be careful.",
|
||||
@@ -1824,6 +1867,7 @@ Candidate master CV:
|
||||
|
||||
var packageModeInstruction = BuildPackageModeInstruction(mode);
|
||||
var coverLetterStyleInstruction = BuildCoverLetterStyleInstruction(coverLetterStyle);
|
||||
var structuredCvContext = BuildStructuredCvContext(user);
|
||||
var attachmentContext = await BuildAttachmentContextAsync(id, cancellationToken, attachmentIds);
|
||||
|
||||
var packageContext = $@"Job title: {job.JobTitle}
|
||||
@@ -1836,7 +1880,7 @@ Job context:
|
||||
{jobText}
|
||||
|
||||
Candidate master CV:
|
||||
{cvText}{(attachmentContext is not null ? $"\n\n{attachmentContext.Context}" : string.Empty)}";
|
||||
{cvText}{(!string.IsNullOrWhiteSpace(structuredCvContext) ? $"\n\n{structuredCvContext}" : string.Empty)}{(attachmentContext is not null ? $"\n\n{attachmentContext.Context}" : string.Empty)}";
|
||||
|
||||
var tailoredCvText = await _summarizer.SummarizeSectionAsync(
|
||||
$"Rewrite the candidate CV into a tailored role-specific resume draft. Keep it credible, structured, and focused on the strongest overlaps with this job. {packageModeInstruction}",
|
||||
|
||||
Reference in New Issue
Block a user