< 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
32%
Covered lines: 10
Uncovered lines: 21
Coverable lines: 31
Total lines: 87
Line coverage: 32.2%
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
.ctor(...)100%11100%
GenerateEmbeddingAsync(...)100%22100%
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.Options;
 6using SharpToken;
 7
 8namespace GistBackend.Handlers.AIHandler;
 9
 10public record SummarizeRequest(string Title, string Article, string Language);
 11
 12public record SummaryForRecap(string Title, string Summary, int Id);
 13
 14public record RecapRequest(List<SummaryForRecap> Summaries, string RecapType);
 15
 16public interface IAIHandler
 17{
 18    public Task<float[]> GenerateEmbeddingAsync(string input, CancellationToken ct);
 19    public Task<SummaryAIResponse> GenerateSummaryAIResponseAsync(Language feedLanguage, string title, string article,
 20        CancellationToken ct);
 21    public Task<RecapAIResponse> GenerateDailyRecapAsync(IEnumerable<ConstructedGist> gists, CancellationToken ct);
 22    public Task<RecapAIResponse> GenerateWeeklyRecapAsync(IEnumerable<ConstructedGist> gists, CancellationToken ct);
 23}
 24
 25public class AIHandler : IAIHandler
 26{
 27    private readonly IEmbeddingClientHandler _embeddingClientHandler;
 28    private readonly HttpClient _httpClient;
 29    private readonly GptEncoding _encoding;
 30
 531    public AIHandler(IEmbeddingClientHandler embeddingClientHandler, HttpClient httpClient,
 532        IOptions<AIHandlerOptions> options)
 33    {
 534        _embeddingClientHandler = embeddingClientHandler;
 535        _encoding = GptEncoding.GetEncodingForModel(embeddingClientHandler.Model);
 536        _httpClient = httpClient;
 537        _httpClient.BaseAddress = new Uri(options.Value.Host);
 538    }
 39
 40    public Task<float[]> GenerateEmbeddingAsync(string input, CancellationToken ct)
 41    {
 42        // Tokenize and truncate to stay under 8k context; keep a small buffer for prompts.
 43        const int maxTokens = 7500;
 544        var tokens = _encoding.Encode(input);
 545        var safeInput = tokens.Count > maxTokens ? _encoding.Decode(tokens.Take(maxTokens)) : input;
 46
 547        return _embeddingClientHandler.GenerateEmbeddingAsync(safeInput, ct);
 48    }
 49
 50    public async Task<SummaryAIResponse> GenerateSummaryAIResponseAsync(Language feedLanguage, string title,
 51        string article, CancellationToken ct)
 52    {
 053        var request = new SummarizeRequest(title, article, feedLanguage.ToString());
 054        var response = await _httpClient.PostAsJsonAsync("/summarize", request, SerializerDefaults.JsonOptions, ct);
 055        if (!response.IsSuccessStatusCode)
 56        {
 057            throw new ExternalServiceException($"Failed to get summary from AI API: {response.StatusCode}");
 58        }
 59
 060        var aiResponse =
 061            await response.Content.ReadFromJsonAsync<SummaryAIResponse>(cancellationToken: ct,
 062                options: SerializerDefaults.JsonOptions);
 063        return aiResponse ?? throw new ExternalServiceException("Could not parse summary AI response");
 064    }
 65
 66    public Task<RecapAIResponse> GenerateDailyRecapAsync(IEnumerable<ConstructedGist> gists, CancellationToken ct) =>
 067        GenerateRecapAsync(gists, RecapType.Daily, ct);
 68
 69    public Task<RecapAIResponse> GenerateWeeklyRecapAsync(IEnumerable<ConstructedGist> gists, CancellationToken ct) =>
 070        GenerateRecapAsync(gists, RecapType.Weekly, ct);
 71
 72    private async Task<RecapAIResponse> GenerateRecapAsync(IEnumerable<ConstructedGist> gists, RecapType recapType,
 73        CancellationToken ct)
 74    {
 075        var summaries = gists.Select(gist => new SummaryForRecap(gist.Title, gist.Summary, gist.Id)).ToList();
 076        var request = new RecapRequest(summaries, recapType.ToString());
 077        var response = await _httpClient.PostAsJsonAsync("/recap", request, SerializerDefaults.JsonOptions, ct);
 078        if (!response.IsSuccessStatusCode)
 79        {
 080            throw new ExternalServiceException($"Failed to get recap from AI API: {response.StatusCode}");
 81        }
 082        var aiResponse =
 083            await response.Content.ReadFromJsonAsync<RecapAIResponse>(cancellationToken: ct,
 084                options: SerializerDefaults.JsonOptions);
 085        return aiResponse ?? throw new ExternalServiceException("Could not parse recap AI response");
 086    }
 87}