feat(cv-import): add source-grounded review and explicit corrections
This commit is contained in:
@@ -411,9 +411,19 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
? await _db.CvExtractionRuns.IgnoreQueryFilters().FirstOrDefaultAsync(x => x.Id == runId, cancellationToken)
|
||||
: await _db.CvExtractionRuns.FirstOrDefaultAsync(x => x.Id == runId && x.OwnerUserId == ownerUserId, cancellationToken);
|
||||
if (run is null) return null;
|
||||
var guardedDocument = run.ParserVersion == DocumentParserVersion && operationContext is not null;
|
||||
if (guardedDocument)
|
||||
{
|
||||
var lease = operationContext!.Lease;
|
||||
var claimed = await _db.CvExtractionRuns.Where(r => r.Id == run.Id && r.OwnerUserId == run.OwnerUserId && (r.Status == "queued" || r.Status == "running") && _db.UserOperations.Any(o => o.Id == lease.OperationId && o.LeaseToken == lease.LeaseToken && o.Status == OperationStatuses.Running && o.CancellationRequestedAtUtc == null && o.LeaseExpiresAtUtc > DateTime.UtcNow))
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(r => r.Status, "running").SetProperty(r => r.ErrorMessage, (string?)null), cancellationToken);
|
||||
if (claimed != 1) { _db.Entry(run).State = EntityState.Detached; throw new OperationCanceledException("The import worker lease is no longer active."); }
|
||||
await _db.Entry(run).ReloadAsync(cancellationToken);
|
||||
}
|
||||
var user = await _users.FindByIdAsync(run.OwnerUserId);
|
||||
if (user is null)
|
||||
{
|
||||
if (guardedDocument) throw new AiOperationFailure("cv_user_not_found", "The import account is no longer available.", false);
|
||||
run.Status = "failed";
|
||||
run.ErrorMessage = "CV processing user was not found.";
|
||||
run.CompletedAtUtc = DateTimeOffset.UtcNow;
|
||||
@@ -423,6 +433,7 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
|
||||
if (!user.AiEnabled || !AccountPlans.ForRoles(await _users.GetRolesAsync(user)).Ai)
|
||||
{
|
||||
if (guardedDocument) throw new AiOperationFailure("entitlement_changed", "CV import access changed before processing began.", false);
|
||||
run.Status = "failed";
|
||||
run.ErrorMessage = user.AiEnabled
|
||||
? "This AI feature requires Pro."
|
||||
@@ -432,9 +443,12 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
return new CvProcessingOutcome(false, "entitlement_changed", run.ErrorMessage);
|
||||
}
|
||||
|
||||
run.Status = "running";
|
||||
run.ErrorMessage = null;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
if (!guardedDocument)
|
||||
{
|
||||
run.Status = "running";
|
||||
run.ErrorMessage = null;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
@@ -535,20 +549,36 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
}
|
||||
var generationFailure = ex as AiGenerationException;
|
||||
var retryable = generationFailure?.Retryable == true;
|
||||
var failureMessage = generationFailure?.Message
|
||||
var failureMessage = run.ParserVersion == DocumentParserVersion
|
||||
? "The document could not be processed safely. Retry or upload a supported PDF or DOCX CV."
|
||||
: generationFailure?.Message
|
||||
?? (ex is InvalidOperationException
|
||||
? ex.Message
|
||||
: "CV processing failed unexpectedly. Please try again.");
|
||||
run.Status = retryable ? "queued" : "failed";
|
||||
run.ErrorMessage = failureMessage;
|
||||
run.CompletedAtUtc = retryable ? null : DateTimeOffset.UtcNow;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
if (guardedDocument)
|
||||
{
|
||||
var lease = operationContext!.Lease;
|
||||
var failedAt = retryable ? (DateTimeOffset?)null : DateTimeOffset.UtcNow;
|
||||
var failedStatus = retryable ? "queued" : "failed";
|
||||
var updated = await _db.CvExtractionRuns.Where(r => r.Id == run.Id && (r.Status == "queued" || r.Status == "running") && _db.UserOperations.Any(o => o.Id == lease.OperationId && o.LeaseToken == lease.LeaseToken && o.Status == OperationStatuses.Running && o.CancellationRequestedAtUtc == null && o.LeaseExpiresAtUtc > DateTime.UtcNow))
|
||||
.ExecuteUpdateAsync(s => s.SetProperty(r => r.Status, failedStatus).SetProperty(r => r.ErrorMessage, failureMessage).SetProperty(r => r.CompletedAtUtc, failedAt), cancellationToken);
|
||||
_db.Entry(run).State = EntityState.Detached;
|
||||
if (updated != 1) throw new OperationCanceledException("The import worker lease is no longer active.");
|
||||
}
|
||||
else
|
||||
{
|
||||
run.Status = retryable ? "queued" : "failed";
|
||||
run.ErrorMessage = failureMessage;
|
||||
run.CompletedAtUtc = retryable ? null : DateTimeOffset.UtcNow;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
if (!retryable)
|
||||
{
|
||||
await PruneExtractionRunsAsync(user.Id, cancellationToken);
|
||||
await SendRunCompletionEmailAsync(user, run, false, cancellationToken);
|
||||
}
|
||||
_logger.LogWarning(ex, "CV processing run {RunId} failed for user {UserId}", run.Id, user.Id);
|
||||
if (run.ParserVersion == DocumentParserVersion) _logger.LogWarning("CV document processing run {RunId} failed ({Category}).", run.Id, generationFailure?.Category ?? "cv_processing_failed");
|
||||
else _logger.LogWarning(ex, "CV processing run {RunId} failed for user {UserId}", run.Id, user.Id);
|
||||
return new CvProcessingOutcome(
|
||||
false,
|
||||
generationFailure?.Category ?? "cv_processing_failed",
|
||||
@@ -564,7 +594,7 @@ public sealed partial class ProfileCvController : ControllerBase
|
||||
{
|
||||
run.RawExtractedText = rawText;
|
||||
run.NormalizedText = normalizedText;
|
||||
run.StructuredProfileJson = StructuredCvProfileJson.Serialize(structuredCv);
|
||||
run.StructuredProfileJson = run.ParserVersion == DocumentParserVersion ? StructuredCvProfileJson.SerializePersisted(structuredCv) : StructuredCvProfileJson.Serialize(structuredCv);
|
||||
run.Status = "pending_review";
|
||||
run.CompletedAtUtc = DateTimeOffset.UtcNow;
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
Reference in New Issue
Block a user