Handle disconnected Gmail and bound CV rewrite prompts
This commit is contained in:
@@ -498,6 +498,14 @@ public sealed class GmailControllerTests
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
var gmail = new Mock<IGmailOAuthService>();
|
var gmail = new Mock<IGmailOAuthService>();
|
||||||
|
gmail.Setup(service => service.GetConnectionAsync("user-1", It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(new GmailConnection
|
||||||
|
{
|
||||||
|
OwnerUserId = "user-1",
|
||||||
|
GmailAddress = "user@example.test",
|
||||||
|
ConnectedAt = DateTimeOffset.UtcNow.AddDays(-1),
|
||||||
|
Scope = "gmail.readonly"
|
||||||
|
});
|
||||||
gmail.Setup(service => service.ListJobCandidateMessagesAsync("user-1", It.IsAny<IEnumerable<string>>(), 6, It.IsAny<CancellationToken>()))
|
gmail.Setup(service => service.ListJobCandidateMessagesAsync("user-1", It.IsAny<IEnumerable<string>>(), 6, It.IsAny<CancellationToken>()))
|
||||||
.ReturnsAsync(new[]
|
.ReturnsAsync(new[]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -239,6 +239,10 @@ public sealed class GmailController : ControllerBase
|
|||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var ownerUserId = GetRequiredOwnerUserId();
|
var ownerUserId = GetRequiredOwnerUserId();
|
||||||
|
if (await GetOwnerGmailConnectionAsync(ownerUserId, cancellationToken) is null)
|
||||||
|
{
|
||||||
|
return GmailNotConnectedResult();
|
||||||
|
}
|
||||||
var jobs = await _db.JobApplications
|
var jobs = await _db.JobApplications
|
||||||
.Where(x => x.OwnerUserId == ownerUserId && !x.IsDeleted)
|
.Where(x => x.OwnerUserId == ownerUserId && !x.IsDeleted)
|
||||||
.Include(x => x.Company)
|
.Include(x => x.Company)
|
||||||
@@ -426,6 +430,10 @@ public sealed class GmailController : ControllerBase
|
|||||||
public async Task<ActionResult<GmailManualSyncResultDto>> ManualSync([FromBody] GmailManualSyncRequest? request, CancellationToken cancellationToken)
|
public async Task<ActionResult<GmailManualSyncResultDto>> ManualSync([FromBody] GmailManualSyncRequest? request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var ownerUserId = GetRequiredOwnerUserId();
|
var ownerUserId = GetRequiredOwnerUserId();
|
||||||
|
if (await GetOwnerGmailConnectionAsync(ownerUserId, cancellationToken) is null)
|
||||||
|
{
|
||||||
|
return GmailNotConnectedResult();
|
||||||
|
}
|
||||||
var jobs = await _db.JobApplications
|
var jobs = await _db.JobApplications
|
||||||
.Where(x => x.OwnerUserId == ownerUserId && !x.IsDeleted)
|
.Where(x => x.OwnerUserId == ownerUserId && !x.IsDeleted)
|
||||||
.Include(x => x.Company)
|
.Include(x => x.Company)
|
||||||
@@ -552,6 +560,10 @@ public sealed class GmailController : ControllerBase
|
|||||||
public async Task<ActionResult<GmailSuggestedJobsResponseDto>> SuggestedJobs(CancellationToken cancellationToken)
|
public async Task<ActionResult<GmailSuggestedJobsResponseDto>> SuggestedJobs(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var ownerUserId = GetRequiredOwnerUserId();
|
var ownerUserId = GetRequiredOwnerUserId();
|
||||||
|
if (await GetOwnerGmailConnectionAsync(ownerUserId, cancellationToken) is null)
|
||||||
|
{
|
||||||
|
return GmailNotConnectedResult();
|
||||||
|
}
|
||||||
var reviewThreads = await ReviewCandidates(null, 6, cancellationToken);
|
var reviewThreads = await ReviewCandidates(null, 6, cancellationToken);
|
||||||
if (reviewThreads.Result is not OkObjectResult ok || ok.Value is not GmailReviewQueueResponseDto payload)
|
if (reviewThreads.Result is not OkObjectResult ok || ok.Value is not GmailReviewQueueResponseDto payload)
|
||||||
{
|
{
|
||||||
@@ -1084,6 +1096,20 @@ public sealed class GmailController : ControllerBase
|
|||||||
?? throw new InvalidOperationException("Authenticated user id is missing.");
|
?? throw new InvalidOperationException("Authenticated user id is missing.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<GmailConnection?> GetOwnerGmailConnectionAsync(string ownerUserId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _gmail.GetConnectionAsync(ownerUserId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionResult GmailNotConnectedResult()
|
||||||
|
{
|
||||||
|
return Conflict(new
|
||||||
|
{
|
||||||
|
code = "gmail_not_connected",
|
||||||
|
message = "Connect Gmail before using the Gmail review queue.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private string GetRedirectUri()
|
private string GetRedirectUri()
|
||||||
{
|
{
|
||||||
var configured = (_cfg["Google:GmailRedirectUri"] ?? _cfg["Google:RedirectUri"] ?? "").Trim();
|
var configured = (_cfg["Google:GmailRedirectUri"] ?? _cfg["Google:RedirectUri"] ?? "").Trim();
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ namespace JobTrackerApi.Services
|
|||||||
|
|
||||||
public class SummarizerService : ISummarizerService
|
public class SummarizerService : ISummarizerService
|
||||||
{
|
{
|
||||||
|
private const int AiSummarizeMaxInputChars = 20000;
|
||||||
private readonly IHttpClientFactory _httpFactory;
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IMemoryCache _cache;
|
||||||
private readonly object _metricsLock = new();
|
private readonly object _metricsLock = new();
|
||||||
@@ -119,10 +120,27 @@ namespace JobTrackerApi.Services
|
|||||||
public Task<string?> SummarizeSectionAsync(string instruction, string text, int maxLength = 180, int minLength = 40)
|
public Task<string?> SummarizeSectionAsync(string instruction, string text, int maxLength = 180, int minLength = 40)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(instruction) || string.IsNullOrWhiteSpace(text)) return Task.FromResult<string?>(null);
|
if (string.IsNullOrWhiteSpace(instruction) || string.IsNullOrWhiteSpace(text)) return Task.FromResult<string?>(null);
|
||||||
var composed = $"{instruction.Trim()}\n\n{text.Trim()}";
|
var composed = ComposeBoundedPrompt(instruction.Trim(), text.Trim());
|
||||||
return SummarizeCoreAsync(composed, maxLength, minLength);
|
return SummarizeCoreAsync(composed, maxLength, minLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ComposeBoundedPrompt(string instruction, string text)
|
||||||
|
{
|
||||||
|
var prefix = $"{instruction}\n\n";
|
||||||
|
if (prefix.Length >= AiSummarizeMaxInputChars)
|
||||||
|
{
|
||||||
|
return prefix[..AiSummarizeMaxInputChars];
|
||||||
|
}
|
||||||
|
|
||||||
|
var remaining = AiSummarizeMaxInputChars - prefix.Length;
|
||||||
|
if (text.Length <= remaining)
|
||||||
|
{
|
||||||
|
return prefix + text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return prefix + text[..remaining];
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<string?> SummarizeCoreAsync(string text, int maxLength, int minLength)
|
private async Task<string?> SummarizeCoreAsync(string text, int maxLength, int minLength)
|
||||||
{
|
{
|
||||||
var key = BuildCacheKey(text, maxLength, minLength);
|
var key = BuildCacheKey(text, maxLength, minLength);
|
||||||
@@ -151,7 +169,17 @@ namespace JobTrackerApi.Services
|
|||||||
var res = await client.PostAsync("/summarize", content);
|
var res = await client.PostAsync("/summarize", content);
|
||||||
sw.Stop();
|
sw.Stop();
|
||||||
Interlocked.Add(ref _totalLatencyTicks, sw.ElapsedTicks);
|
Interlocked.Add(ref _totalLatencyTicks, sw.ElapsedTicks);
|
||||||
if (!res.IsSuccessStatusCode) return null;
|
if (!res.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var errorBody = await res.Content.ReadAsStringAsync();
|
||||||
|
Interlocked.Increment(ref _failures);
|
||||||
|
lock (_metricsLock)
|
||||||
|
{
|
||||||
|
_lastFailureAt = DateTimeOffset.UtcNow;
|
||||||
|
_lastError = $"AI summarize returned {(int)res.StatusCode}: {errorBody}";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
using var stream = await res.Content.ReadAsStreamAsync();
|
using var stream = await res.Content.ReadAsStreamAsync();
|
||||||
using var doc = await JsonDocument.ParseAsync(stream);
|
using var doc = await JsonDocument.ParseAsync(stream);
|
||||||
|
|||||||
Reference in New Issue
Block a user