using System.Text.Json; using JobTrackerApi.Data; using JobTrackerApi.Services.JobImport; using JobTrackerApi.Models; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace JobTrackerApi.Services; public sealed class JobEnrichmentHostedService( BackgroundTenantRunner tenants, IConfiguration configuration, ILogger logger, IStartupReadiness startupReadiness) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await startupReadiness.WaitUntilReadyAsync(stoppingToken); if (!configuration.GetValue("Workers:JobEnrichmentEnabled", false)) { logger.LogInformation("Job enrichment worker disabled (Workers:JobEnrichmentEnabled=false)."); return; } await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); while (!stoppingToken.IsCancellationRequested) { await RunOnceAsync(stoppingToken); await Task.Delay(TimeSpan.FromMinutes(10), stoppingToken); } } public Task RunOnceAsync(CancellationToken cancellationToken) { if (!configuration.GetValue("Workers:JobEnrichmentEnabled", false)) return Task.FromResult(BackgroundWorkerRunResult.Disabled); return tenants.RunForJobOwnersAsync("job-enrichment", ProcessOwnerAsync, cancellationToken); } private static async Task ProcessOwnerAsync(IServiceProvider services, CancellationToken cancellationToken) { var db = services.GetRequiredService(); var userId = db.CurrentUserId; var users = services.GetRequiredService>(); var user = string.IsNullOrWhiteSpace(userId) ? null : await users.FindByIdAsync(userId); var canUseAi = user is not null && user.AiEnabled && AccountPlans.ForRoles(await users.GetRolesAsync(user)).Ai; var summarizer = services.GetRequiredService(); var jobs = await db.JobApplications .Where(job => !job.IsDeleted) .Where(job => string.IsNullOrWhiteSpace(job.ShortSummary) || string.IsNullOrWhiteSpace(job.Tags)) .OrderByDescending(job => job.DateApplied) .Take(20) .ToListAsync(cancellationToken); foreach (var job in jobs) { var sourceText = string.IsNullOrWhiteSpace(job.Description) ? job.Notes : job.Description; if (string.IsNullOrWhiteSpace(job.Tags) && !string.IsNullOrWhiteSpace(sourceText)) { var tags = SkillTagger.Detect(sourceText) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(tag => tag, StringComparer.OrdinalIgnoreCase) .ToList(); if (tags.Count > 0) job.Tags = JsonSerializer.Serialize(tags); } if (canUseAi && string.IsNullOrWhiteSpace(job.ShortSummary) && !string.IsNullOrWhiteSpace(sourceText)) { var summary = await summarizer.SummarizeAsync(sourceText, 160, 60); if (!string.IsNullOrWhiteSpace(summary)) job.ShortSummary = summary; } } await db.SaveChangesAsync(cancellationToken); } }