Improve CV rewrite flow and parser accuracy
This commit is contained in:
@@ -74,7 +74,7 @@ public sealed class ProfileCvController : ControllerBase
|
||||
_paths = paths;
|
||||
}
|
||||
|
||||
public sealed record RewriteSectionRequest(string SectionName, string? Style, string? TargetRole);
|
||||
public sealed record RewriteSectionRequest(string? SectionName, string? Style, string? TargetRole, int? JobApplicationId, string? TemplateId);
|
||||
public sealed record ParseCvRequest(string? Text);
|
||||
|
||||
private sealed record ExtractionPipelineResult(string RawText, string NormalizedText, StructuredCvProfile StructuredCv);
|
||||
@@ -274,24 +274,59 @@ public sealed class ProfileCvController : ControllerBase
|
||||
{
|
||||
var user = await _users.GetUserAsync(User);
|
||||
if (user is null) return Unauthorized();
|
||||
if (string.IsNullOrWhiteSpace(user.ProfileCvText)) return BadRequest("Add or import CV text before rewriting a section.");
|
||||
|
||||
var sectionName = string.IsNullOrWhiteSpace(request.SectionName) ? "Professional Summary" : request.SectionName.Trim();
|
||||
var style = string.IsNullOrWhiteSpace(request.Style) ? "balanced" : request.Style.Trim();
|
||||
var sourceText = string.IsNullOrWhiteSpace(user.ProfileCvText) ? null : user.ProfileCvText.Trim();
|
||||
var structuredCv = StructuredCvProfileJson.Deserialize(user.ProfileCvStructureJson);
|
||||
if (string.IsNullOrWhiteSpace(sourceText) && structuredCv.Sections.Count == 0)
|
||||
{
|
||||
return BadRequest("Add or import CV text before rewriting your CV.");
|
||||
}
|
||||
|
||||
var sectionName = string.IsNullOrWhiteSpace(request.SectionName) ? null : request.SectionName.Trim();
|
||||
var style = string.IsNullOrWhiteSpace(request.Style) ? "ats-minimal" : request.Style.Trim();
|
||||
var templateId = string.IsNullOrWhiteSpace(request.TemplateId) ? "ats-minimal" : request.TemplateId.Trim();
|
||||
var targetRole = string.IsNullOrWhiteSpace(request.TargetRole) ? null : request.TargetRole.Trim();
|
||||
var jobContext = request.JobApplicationId.HasValue
|
||||
? await _db.JobApplications.AsNoTracking().Where(job => job.Id == request.JobApplicationId.Value && job.OwnerUserId == user.Id).Select(job => new
|
||||
{
|
||||
job.Id,
|
||||
job.JobTitle,
|
||||
job.Description,
|
||||
job.ShortSummary,
|
||||
CompanyName = job.Company != null ? job.Company.Name : null
|
||||
}).FirstOrDefaultAsync(HttpContext.RequestAborted)
|
||||
: null;
|
||||
|
||||
var effectiveTargetRole = targetRole ?? jobContext?.JobTitle;
|
||||
var rewriteSource = BuildRewriteSourceText(sectionName, sourceText, structuredCv);
|
||||
var templateGuidance = DescribeRewriteTemplate(templateId);
|
||||
var roleGuidance = jobContext is not null
|
||||
? $"Target this toward the saved job '{jobContext.JobTitle}' at '{jobContext.CompanyName ?? "Unknown company"}'. Use the job context below to sharpen wording without inventing facts.\nJob summary: {jobContext.ShortSummary ?? "-"}\nJob description: {jobContext.Description ?? "-"}"
|
||||
: effectiveTargetRole is not null
|
||||
? $"Target role: {effectiveTargetRole}. Keep it broadly reusable but clearly aligned to that role family."
|
||||
: "Keep it broadly reusable for future tailoring.";
|
||||
|
||||
var subject = sectionName is null ? "this CV" : $"the '{sectionName}' section of this CV";
|
||||
var rewritten = await _aiService.SummarizeSectionAsync(
|
||||
$"Rewrite only the '{sectionName}' section of this CV. Preserve facts, avoid inventing employers or metrics, and output only the rewritten section text. Style: {style}. {(targetRole is not null ? $"Target role: {targetRole}." : "Make it broadly reusable for future tailoring.")}",
|
||||
user.ProfileCvText,
|
||||
900,
|
||||
180);
|
||||
$"Rewrite only {subject}. Preserve facts, avoid inventing employers, titles, qualifications, dates, locations, or metrics. Style guidance: {style}. Template direction: {templateGuidance}. {roleGuidance} Return only the rewritten text with clean headings and bullets when useful.",
|
||||
rewriteSource,
|
||||
sectionName is null ? 1800 : 900,
|
||||
sectionName is null ? 400 : 180);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rewritten))
|
||||
{
|
||||
return BadRequest("The AI service could not rewrite that CV section right now.");
|
||||
return StatusCode(StatusCodes.Status502BadGateway, "The AI service could not rewrite your CV right now.");
|
||||
}
|
||||
|
||||
return Ok(new { sectionName, style, targetRole, text = rewritten.Trim() });
|
||||
return Ok(new
|
||||
{
|
||||
sectionName,
|
||||
style,
|
||||
templateId,
|
||||
targetRole = effectiveTargetRole,
|
||||
jobApplicationId = jobContext?.Id,
|
||||
text = rewritten.Trim()
|
||||
});
|
||||
}
|
||||
|
||||
[HttpPost("parse")]
|
||||
@@ -338,6 +373,37 @@ public sealed class ProfileCvController : ControllerBase
|
||||
return Ok(new { improved = true, characters = user.ProfileCvText.Length, text = user.ProfileCvText, structuredCv, sections = structuredCv.Sections, extractionRunId = user.CurrentCvExtractionRunId, profileVersion = user.CurrentCvProfileVersion });
|
||||
}
|
||||
|
||||
private static string BuildRewriteSourceText(string? sectionName, string? sourceText, StructuredCvProfile structuredCv)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(sectionName))
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(sourceText)
|
||||
? sourceText.Trim()
|
||||
: string.Join("\n\n", structuredCv.Sections.Select(section => $"## {section.Name}\n{section.Content}"));
|
||||
}
|
||||
|
||||
var matchingSection = structuredCv.Sections.FirstOrDefault(section => string.Equals(section.Name, sectionName, StringComparison.OrdinalIgnoreCase));
|
||||
if (matchingSection is not null && !string.IsNullOrWhiteSpace(matchingSection.Content))
|
||||
{
|
||||
return $"## {matchingSection.Name}\n{matchingSection.Content}";
|
||||
}
|
||||
|
||||
return !string.IsNullOrWhiteSpace(sourceText)
|
||||
? sourceText.Trim()
|
||||
: string.Join("\n\n", structuredCv.Sections.Select(section => $"## {section.Name}\n{section.Content}"));
|
||||
}
|
||||
|
||||
private static string DescribeRewriteTemplate(string templateId)
|
||||
{
|
||||
return templateId.ToLowerInvariant() switch
|
||||
{
|
||||
"harvard" => "Harvard template: refined, traditional, strong hierarchy, restrained and credible.",
|
||||
"auckland" => "Auckland template: modern sidebar layout, crisp highlights, confident but readable.",
|
||||
"edinburgh" => "Edinburgh template: polished editorial layout with stronger visual personality and premium spacing.",
|
||||
_ => "ATS Minimal template: clean, compact, scanner-friendly, and easy to tailor.",
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<StructuredCvProfile> BuildStructuredCvAsync(string text, CancellationToken cancellationToken)
|
||||
{
|
||||
var parseSource = NormalizeTextForStructuredParsing(text);
|
||||
|
||||
Reference in New Issue
Block a user