feat: AI categorisation fallback, unsubscribe confidence scoring, one-line summaries

Heuristic classifier falls back to AI only when it can't determine a category;
unsubscribe confidence blends method reliability with an AI safety opinion and
surfaces it in the Unsubscribe Manager; emails get an on-demand AI one-line
summary in the detail pane. All AI calls degrade gracefully when AI is disabled
or the provider errors.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
cesnimda
2026-06-30 22:21:10 +02:00
parent 0163165beb
commit 64f835f719
14 changed files with 949 additions and 10 deletions
@@ -24,14 +24,16 @@ public class SyncService : ISyncService
private readonly ILogger<SyncService> _logger;
private readonly GmailSyncOptions _options;
private readonly ISyncQueue _queue;
private readonly IAiService _ai;
public SyncService(AppDbContext db, IGmailService gmail, ILogger<SyncService> logger, IOptions<GmailSyncOptions> options, ISyncQueue queue)
public SyncService(AppDbContext db, IGmailService gmail, ILogger<SyncService> logger, IOptions<GmailSyncOptions> options, ISyncQueue queue, IAiService ai)
{
_db = db;
_gmail = gmail;
_logger = logger;
_options = options.Value;
_queue = queue;
_ai = ai;
}
public async Task QueueSyncAsync(Guid userId, bool fullSync, CancellationToken ct = default)
@@ -232,6 +234,12 @@ public class SyncService : ISyncService
var sender = await ResolveSenderAsync(userId, d.FromAddress, d.FromDisplayName, ct);
var thread = await ResolveThreadAsync(userId, d.GmailThreadId, d.Subject, d.Snippet, d.SentAtUtc, ct);
var category = HeuristicClassifier.Classify(d);
// The heuristic rule set falls back to Personal when nothing more specific
// matches; if AI is enabled, give it a shot at a better category.
if (category == EmailCategory.Personal && _ai.IsEnabled)
category = await _ai.ClassifyFallbackAsync(d.Subject, d.FromAddress, d.Snippet, ct);
var email = new Email
{
UserId = userId,
@@ -252,7 +260,7 @@ public class SyncService : ISyncService
HasListUnsubscribe = d.HasListUnsubscribe,
ListUnsubscribeRaw = Trunc(d.ListUnsubscribeRaw, 2048),
SupportsOneClickUnsubscribe = d.SupportsOneClickUnsubscribe,
Category = HeuristicClassifier.Classify(d)
Category = category
};
_db.Emails.Add(email);