Skip to content

Commit b56dfb8

Browse files
authored
Merge pull request #797 from nminaya/gemini-api-implementation
Gemini API implementation
2 parents 769b9ea + 7b0dfab commit b56dfb8

File tree

10 files changed

+251
-1
lines changed

10 files changed

+251
-1
lines changed

GrammarNazi.App/Startup.cs

+4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using GrammarNazi.Core.Extensions;
66
using GrammarNazi.Core.Repositories;
77
using GrammarNazi.Core.Services;
8+
using GrammarNazi.Core.Services.GrammarServices;
89
using GrammarNazi.Core.Utilities;
910
using GrammarNazi.Domain.Clients;
1011
using GrammarNazi.Domain.Entities.Configs;
@@ -60,6 +61,7 @@ private void ConfigureDependencies(IServiceCollection services)
6061
services.Configure<TwitterBotSettings>(Configuration.GetSection("AppSettings:TwitterBotSettings"));
6162
services.Configure<GithubSettings>(Configuration.GetSection("AppSettings:GithubSettings"));
6263
services.Configure<DiscordSettings>(d => d.Token = Environment.GetEnvironmentVariable("DISCORD_API_KEY"));
64+
services.Configure<GeminiApiSettings>(d => d.ApiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY"));
6365
services.Configure<MeaningCloudSettings>(m =>
6466
{
6567
m.MeaningCloudLanguageHostUrl = Configuration.GetSection("AppSettings:MeaningCloudSettings:MeaningCloudLanguageHostUrl").Value;
@@ -81,6 +83,7 @@ private void ConfigureDependencies(IServiceCollection services)
8183
services.AddTransient<IYandexSpellerApiClient, YandexSpellerApiClient>();
8284
services.AddTransient<IDatamuseApiClient, DatamuseApiClient>();
8385
services.AddTransient<ISentimApiClient, SentimApiClient>();
86+
services.AddTransient<IGeminiApiClient, GeminiApiClient>();
8487
services.AddTransient<IChatConfigurationService, ChatConfigurationService>();
8588
services.AddTransient<IScheduledTweetService, ScheduledTweetService>();
8689
services.AddTransient<ITwitterMentionLogService, TwitterMentionLogService>();
@@ -89,6 +92,7 @@ private void ConfigureDependencies(IServiceCollection services)
8992
services.AddTransient<IGrammarService, InternalFileGrammarService>();
9093
services.AddTransient<IGrammarService, YandexSpellerApiService>();
9194
services.AddTransient<IGrammarService, DatamuseApiService>();
95+
services.AddTransient<IGrammarService, GeminiApiService>();
9296
services.AddTransient<ITwitterLogService, TwitterLogService>();
9397
services.AddTransient<IGithubService, GithubService>();
9498
services.AddTransient<ITelegramCommandHandlerService, TelegramCommandHandlerService>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using GrammarNazi.Domain.Clients;
2+
using GrammarNazi.Domain.Entities.GeminiAPI;
3+
using GrammarNazi.Domain.Entities.Settings;
4+
using Microsoft.Extensions.Options;
5+
using Newtonsoft.Json;
6+
using System;
7+
using System.Net;
8+
using System.Net.Http;
9+
using System.Net.Http.Json;
10+
using System.Threading.Tasks;
11+
12+
namespace GrammarNazi.Core.Clients;
13+
14+
public class GeminiApiClient(IHttpClientFactory httpClientFactory, IOptions<GeminiApiSettings> options) : IGeminiApiClient
15+
{
16+
private readonly string _apiKey = options.Value.ApiKey;
17+
18+
public async Task<GenerateContentResponse> GenerateContent(string promt)
19+
{
20+
var httpClient = httpClientFactory.CreateClient("geminiApi");
21+
22+
var request = new HttpRequestMessage(HttpMethod.Post, $"v1beta/models/gemini-1.5-flash:generateContent?key={_apiKey}")
23+
{
24+
Content = JsonContent.Create(GenerateContentRequest.CreateRequestObject(promt))
25+
};
26+
27+
var response = await httpClient.SendAsync(request);
28+
29+
if (response.StatusCode != HttpStatusCode.OK)
30+
{
31+
throw new InvalidOperationException($"Unsuccessful Gemini API response {response.StatusCode}", new(await response.Content.ReadAsStringAsync()));
32+
}
33+
34+
var content = await response.Content.ReadAsStringAsync();
35+
36+
return JsonConvert.DeserializeObject<GenerateContentResponse>(content);
37+
}
38+
}

GrammarNazi.Core/Extensions/ServiceCollectionExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public static IServiceCollection AddNamedHttpClients(this IServiceCollection ser
2323
serviceCollection.AddHttpClient("languageToolApi", c => c.BaseAddress = new Uri("https://languagetool.org/"));
2424
serviceCollection.AddHttpClient("yandexSpellerApi", c => c.BaseAddress = new Uri("https://speller.yandex.net/"));
2525
serviceCollection.AddHttpClient("sentimApi", c => c.BaseAddress = new Uri("https://sentim-api.herokuapp.com/"));
26+
serviceCollection.AddHttpClient("geminiApi", c => c.BaseAddress = new Uri("https://generativelanguage.googleapis.com/"));
2627

2728
var meaningCloudSettings = serviceCollection.BuildServiceProvider().GetService<IOptions<MeaningCloudSettings>>().Value;
2829

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using GrammarNazi.Core.Extensions;
2+
using GrammarNazi.Domain.Clients;
3+
using GrammarNazi.Domain.Entities;
4+
using GrammarNazi.Domain.Enums;
5+
using GrammarNazi.Domain.Services;
6+
using Newtonsoft.Json;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Threading.Tasks;
10+
11+
namespace GrammarNazi.Core.Services.GrammarServices;
12+
13+
public class GeminiApiService(IGeminiApiClient geminiApiClient) : BaseGrammarService, IGrammarService
14+
{
15+
public GrammarAlgorithms GrammarAlgorith => GrammarAlgorithms.Gemini;
16+
17+
public async Task<GrammarCheckResult> GetCorrections(string text)
18+
{
19+
var prompt = GetCorrectionPrompt(text);
20+
21+
var result = await geminiApiClient.GenerateContent(prompt);
22+
23+
if (result.Candidates.Count == 0)
24+
{
25+
// Empty result received
26+
return new(default);
27+
}
28+
29+
var resultJsonString = result.Candidates.First().Content.Parts.FirstOrDefault()?.Text;
30+
31+
if (string.IsNullOrEmpty(resultJsonString))
32+
{
33+
// Empty result received
34+
return new(default);
35+
}
36+
37+
var cleanedJson = resultJsonString.Replace("```json", "").Replace("```", "").Trim();
38+
39+
var grammarCorrections = JsonConvert.DeserializeObject<IEnumerable<GrammarCorrection>>(cleanedJson);
40+
41+
if (!grammarCorrections.Any())
42+
{
43+
// No corrections
44+
return new(default);
45+
}
46+
47+
return new(grammarCorrections.Where(x => x.PossibleReplacements.Any()));
48+
}
49+
50+
private string GetCorrectionPrompt(string text)
51+
{
52+
var languageSection = SelectedLanguage == SupportedLanguages.Auto
53+
? "Auto detect the language"
54+
: $"The language is {SelectedLanguage.GetDescription()}";
55+
56+
return @$"Analyze the following text for any grammar, spelling, or orthographic errors. For each mistake, provide the result in the JSON format below. {languageSection}.
57+
Build the results in that same language. If the word doesn't have possibleReplacements, don't add it to the results.
58+
59+
[{{
60+
61+
  ""wrongWord"": """",
62+
63+
  ""message"": """",
64+
65+
  ""possibleReplacements"": [""""]
66+
67+
}}]
68+
69+
Here is the text for analysis:
70+
71+
{text}";
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using GrammarNazi.Domain.Entities.GeminiAPI;
2+
using System.Threading.Tasks;
3+
4+
namespace GrammarNazi.Domain.Clients;
5+
6+
public interface IGeminiApiClient
7+
{
8+
Task<GenerateContentResponse> GenerateContent(string promt);
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using Newtonsoft.Json;
2+
using System.Collections.Generic;
3+
4+
namespace GrammarNazi.Domain.Entities.GeminiAPI;
5+
6+
public class GenerateContentRequest
7+
{
8+
[JsonProperty("contents")]
9+
public List<Content> Contents { get; set; }
10+
11+
public static GenerateContentRequest CreateRequestObject(string promt)
12+
{
13+
return new()
14+
{
15+
Contents =
16+
[
17+
new()
18+
{
19+
Parts =
20+
[
21+
new()
22+
{
23+
Text = promt
24+
}
25+
]
26+
}
27+
]
28+
};
29+
}
30+
}
31+
32+
public class Content
33+
{
34+
[JsonProperty("parts")]
35+
public List<Part> Parts { get; set; }
36+
}
37+
38+
public class Part
39+
{
40+
[JsonProperty("text")]
41+
public string Text { get; set; }
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
using Newtonsoft.Json;
2+
using System.Collections.Generic;
3+
4+
namespace GrammarNazi.Domain.Entities.GeminiAPI;
5+
6+
public class GenerateContentResponse
7+
{
8+
[JsonProperty("candidates")]
9+
public List<Candidate> Candidates { get; set; }
10+
11+
[JsonProperty("usageMetadata")]
12+
public UsageMetadata UsageMetadata { get; set; }
13+
14+
[JsonProperty("modelVersion")]
15+
public string ModelVersion { get; set; }
16+
}
17+
18+
public class Candidate
19+
{
20+
[JsonProperty("content")]
21+
public ContentResponse Content { get; set; }
22+
23+
[JsonProperty("finishReason")]
24+
public string FinishReason { get; set; }
25+
26+
[JsonProperty("avgLogprobs")]
27+
public decimal AvgLogprobs { get; set; }
28+
}
29+
30+
public class CandidatesTokensDetail
31+
{
32+
[JsonProperty("modality")]
33+
public string Modality { get; set; }
34+
35+
[JsonProperty("tokenCount")]
36+
public int TokenCount { get; set; }
37+
}
38+
39+
public class ContentResponse
40+
{
41+
[JsonProperty("parts")]
42+
public List<Part> Parts { get; set; }
43+
44+
[JsonProperty("role")]
45+
public string Role { get; set; }
46+
}
47+
48+
public class PromptTokensDetail
49+
{
50+
[JsonProperty("modality")]
51+
public string Modality { get; set; }
52+
53+
[JsonProperty("tokenCount")]
54+
public int TokenCount { get; set; }
55+
}
56+
57+
public class UsageMetadata
58+
{
59+
[JsonProperty("promptTokenCount")]
60+
public int PromptTokenCount { get; set; }
61+
62+
[JsonProperty("candidatesTokenCount")]
63+
public int CandidatesTokenCount { get; set; }
64+
65+
[JsonProperty("totalTokenCount")]
66+
public int TotalTokenCount { get; set; }
67+
68+
[JsonProperty("promptTokensDetails")]
69+
public List<PromptTokensDetail> PromptTokensDetails { get; set; }
70+
71+
[JsonProperty("candidatesTokensDetails")]
72+
public List<CandidatesTokensDetail> CandidatesTokensDetails { get; set; }
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace GrammarNazi.Domain.Entities.Settings;
2+
3+
public class GeminiApiSettings
4+
{
5+
public string ApiKey { get; set; }
6+
}

GrammarNazi.Domain/Enums/GrammarAlgorithms.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,8 @@ public enum GrammarAlgorithms
1414
YandexSpellerApi = 3,
1515

1616
[Description("Datamuse API")]
17-
DatamuseApi = 4
17+
DatamuseApi = 4,
18+
19+
[Description("Gemini")]
20+
Gemini = 5
1821
}

GrammarNazi.Tests/Services/TelegramCommandHandlerServiceTests.cs

+1
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public async Task HandleCallBackQuery_LanguageChange_Should_ChangeSelectedLangua
6868
[InlineData("GrammarAlgorithms.LanguageToolApi", GrammarAlgorithms.LanguageToolApi)]
6969
[InlineData("GrammarAlgorithms.YandexSpellerApi", GrammarAlgorithms.YandexSpellerApi)]
7070
[InlineData("GrammarAlgorithms.InternalAlgorithm", GrammarAlgorithms.InternalAlgorithm)]
71+
[InlineData("GrammarAlgorithms.Gemini", GrammarAlgorithms.Gemini)]
7172
public async Task HandleCallBackQuery_AlgorithmChange_Should_ChangeSelectedAlgorithm(string callBackQueryData, GrammarAlgorithms grammarAlgorithm)
7273
{
7374
// Arrange

0 commit comments

Comments
 (0)