< Summary

Information
Class: GistBackend.Handlers.AIHandler.SummaryForRecap
Assembly: GistBackend
File(s): /home/runner/work/the-gist-of-it-sec/the-gist-of-it-sec/backend/GistBackend/Handlers/AIHandler/AIHandler.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 1
Coverable lines: 1
Total lines: 101
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_Title()100%210%

File(s)

/home/runner/work/the-gist-of-it-sec/the-gist-of-it-sec/backend/GistBackend/Handlers/AIHandler/AIHandler.cs

#LineLine coverage
 1using System.Net.Http.Json;
 2using GistBackend.Exceptions;
 3using GistBackend.Types;
 4using GistBackend.Utils;
 5using Microsoft.Extensions.Caching.Memory;
 6using Microsoft.Extensions.Options;
 7using SharpToken;
 8
 9namespace GistBackend.Handlers.AIHandler;
 10
 11public record SummarizeRequest(string Title, string Article, string Language);
 12
 013public record SummaryForRecap(string Title, string Summary, int Id);
 14
 15public record RecapRequest(List<SummaryForRecap> Summaries, string RecapType);
 16
 17public interface IAIHandler
 18{
 19    public Task<float[]> GenerateEmbeddingAsync(string input, CancellationToken ct);
 20    public Task<SummaryAIResponse> GenerateSummaryAIResponseAsync(Language feedLanguage, string title, string article,
 21        CancellationToken ct);
 22    public Task<RecapAIResponse> GenerateDailyRecapAsync(IEnumerable<ConstructedGist> gists, CancellationToken ct);
 23    public Task<RecapAIResponse> GenerateWeeklyRecapAsync(IEnumerable<ConstructedGist> gists, CancellationToken ct);
 24}
 25
 26public class AIHandler : IAIHandler
 27{
 28    private readonly IEmbeddingClientHandler _embeddingClientHandler;
 29    private readonly HttpClient _httpClient;
 30    private readonly GptEncoding _encoding;
 31    private readonly IMemoryCache _embeddingCache;
 32
 33    private static readonly MemoryCacheEntryOptions CacheEntryOptions = new MemoryCacheEntryOptions()
 34        .SetSlidingExpiration(TimeSpan.FromHours(1))
 35        .SetAbsoluteExpiration(TimeSpan.FromHours(24));
 36
 37    public AIHandler(IEmbeddingClientHandler embeddingClientHandler, HttpClient httpClient,
 38        IOptions<AIHandlerOptions> options, IMemoryCache embeddingCache)
 39    {
 40        _embeddingClientHandler = embeddingClientHandler;
 41        _encoding = GptEncoding.GetEncodingForModel(embeddingClientHandler.Model);
 42        _httpClient = httpClient;
 43        _httpClient.BaseAddress = new Uri(options.Value.Host);
 44        _embeddingCache = embeddingCache;
 45    }
 46
 47    public async Task<float[]> GenerateEmbeddingAsync(string input, CancellationToken ct)
 48    {
 49        // Tokenize and truncate to stay under 8k context; keep a small buffer for prompts.
 50        const int maxTokens = 7500;
 51        var tokens = _encoding.Encode(input);
 52        var safeInput = tokens.Count > maxTokens ? _encoding.Decode(tokens.Take(maxTokens)) : input;
 53
 54        var cacheKey = $"embedding:{safeInput}";
 55        var result = await _embeddingCache.GetOrCreateAsync(cacheKey, async entry =>
 56        {
 57            entry.SetOptions(CacheEntryOptions);
 58            return await _embeddingClientHandler.GenerateEmbeddingAsync(safeInput, ct);
 59        });
 60
 61        return result!;
 62    }
 63
 64    public async Task<SummaryAIResponse> GenerateSummaryAIResponseAsync(Language feedLanguage, string title,
 65        string article, CancellationToken ct)
 66    {
 67        var request = new SummarizeRequest(title, article, feedLanguage.ToString());
 68        var response = await _httpClient.PostAsJsonAsync("/summarize", request, SerializerDefaults.JsonOptions, ct);
 69        if (!response.IsSuccessStatusCode)
 70        {
 71            throw new ExternalServiceException($"Failed to get summary from AI API: {response.StatusCode}");
 72        }
 73
 74        var aiResponse =
 75            await response.Content.ReadFromJsonAsync<SummaryAIResponse>(cancellationToken: ct,
 76                options: SerializerDefaults.JsonOptions);
 77        return aiResponse ?? throw new ExternalServiceException("Could not parse summary AI response");
 78    }
 79
 80    public Task<RecapAIResponse> GenerateDailyRecapAsync(IEnumerable<ConstructedGist> gists, CancellationToken ct) =>
 81        GenerateRecapAsync(gists, RecapType.Daily, ct);
 82
 83    public Task<RecapAIResponse> GenerateWeeklyRecapAsync(IEnumerable<ConstructedGist> gists, CancellationToken ct) =>
 84        GenerateRecapAsync(gists, RecapType.Weekly, ct);
 85
 86    private async Task<RecapAIResponse> GenerateRecapAsync(IEnumerable<ConstructedGist> gists, RecapType recapType,
 87        CancellationToken ct)
 88    {
 89        var summaries = gists.Select(gist => new SummaryForRecap(gist.Title, gist.Summary, gist.Id)).ToList();
 90        var request = new RecapRequest(summaries, recapType.ToString());
 91        var response = await _httpClient.PostAsJsonAsync("/recap", request, SerializerDefaults.JsonOptions, ct);
 92        if (!response.IsSuccessStatusCode)
 93        {
 94            throw new ExternalServiceException($"Failed to get recap from AI API: {response.StatusCode}");
 95        }
 96        var aiResponse =
 97            await response.Content.ReadFromJsonAsync<RecapAIResponse>(cancellationToken: ct,
 98                options: SerializerDefaults.JsonOptions);
 99        return aiResponse ?? throw new ExternalServiceException("Could not parse recap AI response");
 100    }
 101}

Methods/Properties

get_Title()