< Summary

Information
Class: GistBackend.Handlers.AIHandler.AIHandler
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
50%
Covered lines: 21
Uncovered lines: 21
Coverable lines: 42
Total lines: 101
Line coverage: 50%
Branch coverage
20%
Covered branches: 2
Total branches: 10
Branch coverage: 20%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
GenerateEmbeddingAsync()100%22100%
<GenerateEmbeddingAsync()100%11100%
GenerateSummaryAIResponseAsync()0%2040%
GenerateDailyRecapAsync(...)100%210%
GenerateWeeklyRecapAsync(...)100%210%
GenerateRecapAsync()0%2040%

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
 13public 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
 133    private static readonly MemoryCacheEntryOptions CacheEntryOptions = new MemoryCacheEntryOptions()
 134        .SetSlidingExpiration(TimeSpan.FromHours(1))
 135        .SetAbsoluteExpiration(TimeSpan.FromHours(24));
 36
 537    public AIHandler(IEmbeddingClientHandler embeddingClientHandler, HttpClient httpClient,
 538        IOptions<AIHandlerOptions> options, IMemoryCache embeddingCache)
 39    {
 540        _embeddingClientHandler = embeddingClientHandler;
 541        _encoding = GptEncoding.GetEncodingForModel(embeddingClientHandler.Model);
 542        _httpClient = httpClient;
 543        _httpClient.BaseAddress = new Uri(options.Value.Host);
 544        _embeddingCache = embeddingCache;
 545    }
 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;
 551        var tokens = _encoding.Encode(input);
 552        var safeInput = tokens.Count > maxTokens ? _encoding.Decode(tokens.Take(maxTokens)) : input;
 53
 554        var cacheKey = $"embedding:{safeInput}";
 555        var result = await _embeddingCache.GetOrCreateAsync(cacheKey, async entry =>
 556        {
 557            entry.SetOptions(CacheEntryOptions);
 558            return await _embeddingClientHandler.GenerateEmbeddingAsync(safeInput, ct);
 1059        });
 60
 561        return result!;
 562    }
 63
 64    public async Task<SummaryAIResponse> GenerateSummaryAIResponseAsync(Language feedLanguage, string title,
 65        string article, CancellationToken ct)
 66    {
 067        var request = new SummarizeRequest(title, article, feedLanguage.ToString());
 068        var response = await _httpClient.PostAsJsonAsync("/summarize", request, SerializerDefaults.JsonOptions, ct);
 069        if (!response.IsSuccessStatusCode)
 70        {
 071            throw new ExternalServiceException($"Failed to get summary from AI API: {response.StatusCode}");
 72        }
 73
 074        var aiResponse =
 075            await response.Content.ReadFromJsonAsync<SummaryAIResponse>(cancellationToken: ct,
 076                options: SerializerDefaults.JsonOptions);
 077        return aiResponse ?? throw new ExternalServiceException("Could not parse summary AI response");
 078    }
 79
 80    public Task<RecapAIResponse> GenerateDailyRecapAsync(IEnumerable<ConstructedGist> gists, CancellationToken ct) =>
 081        GenerateRecapAsync(gists, RecapType.Daily, ct);
 82
 83    public Task<RecapAIResponse> GenerateWeeklyRecapAsync(IEnumerable<ConstructedGist> gists, CancellationToken ct) =>
 084        GenerateRecapAsync(gists, RecapType.Weekly, ct);
 85
 86    private async Task<RecapAIResponse> GenerateRecapAsync(IEnumerable<ConstructedGist> gists, RecapType recapType,
 87        CancellationToken ct)
 88    {
 089        var summaries = gists.Select(gist => new SummaryForRecap(gist.Title, gist.Summary, gist.Id)).ToList();
 090        var request = new RecapRequest(summaries, recapType.ToString());
 091        var response = await _httpClient.PostAsJsonAsync("/recap", request, SerializerDefaults.JsonOptions, ct);
 092        if (!response.IsSuccessStatusCode)
 93        {
 094            throw new ExternalServiceException($"Failed to get recap from AI API: {response.StatusCode}");
 95        }
 096        var aiResponse =
 097            await response.Content.ReadFromJsonAsync<RecapAIResponse>(cancellationToken: ct,
 098                options: SerializerDefaults.JsonOptions);
 099        return aiResponse ?? throw new ExternalServiceException("Could not parse recap AI response");
 0100    }
 101}