fix(analysis): refresh stale job summaries
This commit is contained in:
@@ -833,6 +833,9 @@ Canonical profile:
|
||||
var oldStatus = job.Status;
|
||||
var oldResponseReceived = job.ResponseReceived;
|
||||
var oldResponseDate = job.ResponseDate;
|
||||
var previousJobTitle = job.JobTitle;
|
||||
var previousDescription = job.Description;
|
||||
var previousTranslatedDescription = job.TranslatedDescription;
|
||||
|
||||
var title = (request.JobTitle ?? "").Trim();
|
||||
if (title.Length == 0) return BadRequest("Job title is required.");
|
||||
@@ -868,6 +871,36 @@ Canonical profile:
|
||||
// Status may have changed above; keep DateApplied consistent with the stage.
|
||||
SyncAppliedDateWithHistory(job);
|
||||
|
||||
// The persisted short analysis is generated on create. Refresh it only when the source
|
||||
// job content materially changes; opening the workspace or saving an unrelated field
|
||||
// must never spend another AI call. Clear the old summary first so a failed refresh
|
||||
// cannot present analysis for an advert the user has replaced.
|
||||
if (HasSubstantialAnalysisChange(
|
||||
previousJobTitle, previousDescription, previousTranslatedDescription,
|
||||
job.JobTitle, job.Description, job.TranslatedDescription))
|
||||
{
|
||||
job.ShortSummary = null;
|
||||
try
|
||||
{
|
||||
if (await CanCurrentUserUseAiAsync(cancellationToken))
|
||||
{
|
||||
job.ShortSummary = await _summarizer.SummarizeAsync(BuildSummarySource(job), 160, 60);
|
||||
_db.JobEvents.Add(new JobEvent
|
||||
{
|
||||
JobApplicationId = job.Id,
|
||||
Type = "AiRefreshed",
|
||||
Note = "Job analysis automatically refreshed after the advert changed.",
|
||||
At = DateTime.Now,
|
||||
});
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Editing the application must still succeed if the optional analysis provider
|
||||
// is unavailable. The manual refresh action remains available.
|
||||
}
|
||||
}
|
||||
|
||||
if (oldResponseReceived != job.ResponseReceived || oldResponseDate != job.ResponseDate)
|
||||
{
|
||||
_db.JobEvents.Add(new JobEvent
|
||||
@@ -887,6 +920,43 @@ Canonical profile:
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static bool HasSubstantialAnalysisChange(
|
||||
string? previousTitle,
|
||||
string? previousDescription,
|
||||
string? previousTranslatedDescription,
|
||||
string? currentTitle,
|
||||
string? currentDescription,
|
||||
string? currentTranslatedDescription)
|
||||
{
|
||||
if (!string.Equals(NormalizeAnalysisText(previousTitle), NormalizeAnalysisText(currentTitle), StringComparison.Ordinal))
|
||||
return true;
|
||||
|
||||
var before = NormalizeAnalysisText($"{previousDescription} {previousTranslatedDescription}");
|
||||
var after = NormalizeAnalysisText($"{currentDescription} {currentTranslatedDescription}");
|
||||
if (before == after) return false;
|
||||
if (before.Length == 0 || after.Length == 0) return true;
|
||||
|
||||
var lengthChange = Math.Abs(before.Length - after.Length);
|
||||
if (lengthChange >= Math.Max(80, before.Length / 10)) return true;
|
||||
|
||||
var beforeTokens = before.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToHashSet(StringComparer.Ordinal);
|
||||
var afterTokens = after.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToHashSet(StringComparer.Ordinal);
|
||||
var union = beforeTokens.Union(afterTokens).Count();
|
||||
if (union == 0) return false;
|
||||
var overlap = beforeTokens.Intersect(afterTokens).Count();
|
||||
return 1d - overlap / (double)union >= 0.12d;
|
||||
}
|
||||
|
||||
private static string NormalizeAnalysisText(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return string.Empty;
|
||||
var words = value.ToLowerInvariant()
|
||||
.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(word => word.Trim().Trim(',', '.', ':', ';', '!', '?', '-', '–', '—', '(', ')', '[', ']', '"', '\''))
|
||||
.Where(word => word.Length > 0);
|
||||
return string.Join(' ', words);
|
||||
}
|
||||
|
||||
/// <summary>Canonical ordered pipeline stages so the UI renders one source of truth.</summary>
|
||||
[HttpGet("pipeline")]
|
||||
public ActionResult<IEnumerable<PipelineStageDto>> GetPipeline()
|
||||
|
||||
Reference in New Issue
Block a user