Skip to content

Commit f211639

Browse files
konardclaude
andcommitted
Implement GitHub bot grammar checker functionality
- Add GrammarCheckTrigger that uses LanguageTool API for grammar and spelling checks - Integrate trigger into main bot program alongside existing triggers - Create comprehensive example documentation for usage - Bot responds to issues with titles containing "grammar", "spell check", "proofread", etc. - Posts detailed comments with error descriptions, context, and correction suggestions - Supports real-time grammar checking using free LanguageTool public API Fixes #81 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent d6b666e commit f211639

File tree

7 files changed

+561
-1
lines changed

7 files changed

+561
-1
lines changed

csharp/Platform.Bot/Program.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ private static async Task<int> Main(string[] args)
9595
var dbContext = new FileStorage(databaseFilePath?.FullName ?? new TemporaryFile().Filename);
9696
Console.WriteLine($"Bot has been started. {Environment.NewLine}Press CTRL+C to close");
9797
var githubStorage = new GitHubStorage(githubUserName, githubApiToken, githubApplicationName);
98-
var issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new OrganizationLastMonthActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage));
98+
var issueTracker = new IssueTracker(githubStorage, new HelloWorldTrigger(githubStorage, dbContext, fileSetName), new GrammarCheckTrigger(githubStorage), new OrganizationLastMonthActivityTrigger(githubStorage), new LastCommitActivityTrigger(githubStorage), new AdminAuthorIssueTriggerDecorator(new ProtectDefaultBranchTrigger(githubStorage), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationRepositoriesDefaultBranchTrigger(githubStorage, dbContext), githubStorage), new AdminAuthorIssueTriggerDecorator(new ChangeOrganizationPullRequestsBaseBranchTrigger(githubStorage, dbContext), githubStorage));
9999
var pullRequenstTracker = new PullRequestTracker(githubStorage, new MergeDependabotBumpsTrigger(githubStorage));
100100
var timestampTracker = new DateTimeTracker(githubStorage, new CreateAndSaveOrganizationRepositoriesMigrationTrigger(githubStorage, dbContext, Path.Combine(Directory.GetCurrentDirectory(), "/github-migrations")));
101101
var cancellation = new CancellationTokenSource();
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Net.Http;
4+
using System.Text;
5+
using System.Text.Json;
6+
using System.Threading.Tasks;
7+
using Interfaces;
8+
using Octokit;
9+
using Storage.Remote.GitHub;
10+
11+
namespace Platform.Bot.Triggers
12+
{
13+
using TContext = Issue;
14+
15+
/// <summary>
16+
/// <para>
17+
/// Represents a grammar and spelling check trigger using LanguageTool API.
18+
/// </para>
19+
/// <para></para>
20+
/// </summary>
21+
/// <seealso cref="ITrigger{TContext}"/>
22+
internal class GrammarCheckTrigger : ITrigger<TContext>
23+
{
24+
private readonly GitHubStorage _storage;
25+
private readonly HttpClient _httpClient;
26+
private const string LanguageToolApiUrl = "https://api.languagetool.org/v2/check";
27+
28+
/// <summary>
29+
/// <para>
30+
/// Initializes a new <see cref="GrammarCheckTrigger"/> instance.
31+
/// </para>
32+
/// <para></para>
33+
/// </summary>
34+
/// <param name="storage">
35+
/// <para>A GitHub storage instance.</para>
36+
/// <para></para>
37+
/// </param>
38+
public GrammarCheckTrigger(GitHubStorage storage)
39+
{
40+
_storage = storage;
41+
_httpClient = new HttpClient();
42+
}
43+
44+
/// <summary>
45+
/// <para>
46+
/// Determines whether this instance should be triggered based on the issue context.
47+
/// </para>
48+
/// <para></para>
49+
/// </summary>
50+
/// <param name="context">
51+
/// <para>The issue context.</para>
52+
/// <para></para>
53+
/// </param>
54+
/// <returns>
55+
/// <para>True if the issue title contains "grammar check" or "spell check", otherwise false.</para>
56+
/// <para></para>
57+
/// </returns>
58+
public async Task<bool> Condition(TContext context)
59+
{
60+
var title = context.Title.ToLowerInvariant();
61+
return title.Contains("grammar check") ||
62+
title.Contains("spell check") ||
63+
title.Contains("proofread") ||
64+
title.Contains("grammar") ||
65+
title.Contains("spelling");
66+
}
67+
68+
/// <summary>
69+
/// <para>
70+
/// Performs grammar and spelling check on the issue description and posts corrections as a comment.
71+
/// </para>
72+
/// <para></para>
73+
/// </summary>
74+
/// <param name="context">
75+
/// <para>The issue context.</para>
76+
/// <para></para>
77+
/// </param>
78+
public async Task Action(TContext context)
79+
{
80+
try
81+
{
82+
// Get the text to check (issue description)
83+
var textToCheck = context.Body ?? "";
84+
85+
if (string.IsNullOrWhiteSpace(textToCheck))
86+
{
87+
await _storage.CreateIssueComment(context.Repository.Id, context.Number,
88+
"⚠️ No text found to check for grammar or spelling errors.");
89+
return;
90+
}
91+
92+
// Call LanguageTool API to check grammar and spelling
93+
var suggestions = await CheckGrammarAndSpelling(textToCheck);
94+
95+
if (suggestions.Count == 0)
96+
{
97+
await _storage.CreateIssueComment(context.Repository.Id, context.Number,
98+
"✅ No grammar or spelling issues found in the issue description!");
99+
return;
100+
}
101+
102+
// Format the response with suggestions
103+
var responseBuilder = new StringBuilder();
104+
responseBuilder.AppendLine("## Grammar and Spelling Check Results");
105+
responseBuilder.AppendLine();
106+
responseBuilder.AppendLine($"Found **{suggestions.Count}** potential issue(s):");
107+
responseBuilder.AppendLine();
108+
109+
for (int i = 0; i < suggestions.Count; i++)
110+
{
111+
var suggestion = suggestions[i];
112+
responseBuilder.AppendLine($"**{i + 1}.** {suggestion.Message}");
113+
114+
if (!string.IsNullOrEmpty(suggestion.Context))
115+
{
116+
responseBuilder.AppendLine($" - Context: \"{suggestion.Context}\"");
117+
}
118+
119+
if (suggestion.Replacements?.Count > 0)
120+
{
121+
responseBuilder.AppendLine($" - Suggested correction(s): {string.Join(", ", suggestion.Replacements)}");
122+
}
123+
124+
responseBuilder.AppendLine();
125+
}
126+
127+
responseBuilder.AppendLine("---");
128+
responseBuilder.AppendLine("*Grammar checking powered by LanguageTool API*");
129+
130+
// Post the comment
131+
await _storage.CreateIssueComment(context.Repository.Id, context.Number, responseBuilder.ToString());
132+
}
133+
catch (Exception ex)
134+
{
135+
await _storage.CreateIssueComment(context.Repository.Id, context.Number,
136+
$"❌ Error occurred during grammar check: {ex.Message}");
137+
}
138+
}
139+
140+
/// <summary>
141+
/// <para>
142+
/// Calls the LanguageTool API to check grammar and spelling.
143+
/// </para>
144+
/// <para></para>
145+
/// </summary>
146+
/// <param name="text">
147+
/// <para>The text to check.</para>
148+
/// <para></para>
149+
/// </param>
150+
/// <returns>
151+
/// <para>A list of grammar and spelling suggestions.</para>
152+
/// <para></para>
153+
/// </returns>
154+
private async Task<List<GrammarSuggestion>> CheckGrammarAndSpelling(string text)
155+
{
156+
try
157+
{
158+
var formData = new FormUrlEncodedContent(new[]
159+
{
160+
new KeyValuePair<string, string>("text", text),
161+
new KeyValuePair<string, string>("language", "en-US")
162+
});
163+
164+
var response = await _httpClient.PostAsync(LanguageToolApiUrl, formData);
165+
response.EnsureSuccessStatusCode();
166+
167+
var jsonResponse = await response.Content.ReadAsStringAsync();
168+
var apiResponse = JsonSerializer.Deserialize<LanguageToolResponse>(jsonResponse);
169+
170+
var suggestions = new List<GrammarSuggestion>();
171+
172+
if (apiResponse?.matches != null)
173+
{
174+
foreach (var match in apiResponse.matches)
175+
{
176+
var suggestion = new GrammarSuggestion
177+
{
178+
Message = match.message ?? "Grammar/spelling issue detected",
179+
Context = GetContextFromMatch(text, match),
180+
Replacements = match.replacements?.ConvertAll(r => r.value) ?? new List<string>()
181+
};
182+
183+
suggestions.Add(suggestion);
184+
}
185+
}
186+
187+
return suggestions;
188+
}
189+
catch (Exception ex)
190+
{
191+
throw new Exception($"Failed to check grammar with LanguageTool API: {ex.Message}", ex);
192+
}
193+
}
194+
195+
/// <summary>
196+
/// <para>
197+
/// Extracts context around a grammar/spelling error.
198+
/// </para>
199+
/// <para></para>
200+
/// </summary>
201+
/// <param name="text">
202+
/// <para>The full text.</para>
203+
/// <para></para>
204+
/// </param>
205+
/// <param name="match">
206+
/// <para>The match information from LanguageTool.</para>
207+
/// <para></para>
208+
/// </param>
209+
/// <returns>
210+
/// <para>A context string showing the error in context.</para>
211+
/// <para></para>
212+
/// </returns>
213+
private static string GetContextFromMatch(string text, LanguageToolMatch match)
214+
{
215+
try
216+
{
217+
var start = Math.Max(0, match.offset - 20);
218+
var end = Math.Min(text.Length, match.offset + match.length + 20);
219+
var context = text.Substring(start, end - start);
220+
221+
// Highlight the error in the context
222+
var errorText = text.Substring(match.offset, match.length);
223+
context = context.Replace(errorText, $"**{errorText}**");
224+
225+
return context;
226+
}
227+
catch
228+
{
229+
return "";
230+
}
231+
}
232+
}
233+
234+
/// <summary>
235+
/// <para>
236+
/// Represents a grammar or spelling suggestion.
237+
/// </para>
238+
/// <para></para>
239+
/// </summary>
240+
internal class GrammarSuggestion
241+
{
242+
public string Message { get; set; } = "";
243+
public string Context { get; set; } = "";
244+
public List<string> Replacements { get; set; } = new();
245+
}
246+
247+
/// <summary>
248+
/// <para>
249+
/// Response model for LanguageTool API.
250+
/// </para>
251+
/// <para></para>
252+
/// </summary>
253+
internal class LanguageToolResponse
254+
{
255+
public List<LanguageToolMatch>? matches { get; set; }
256+
}
257+
258+
/// <summary>
259+
/// <para>
260+
/// Match model for LanguageTool API response.
261+
/// </para>
262+
/// <para></para>
263+
/// </summary>
264+
internal class LanguageToolMatch
265+
{
266+
public string? message { get; set; }
267+
public int offset { get; set; }
268+
public int length { get; set; }
269+
public List<LanguageToolReplacement>? replacements { get; set; }
270+
}
271+
272+
/// <summary>
273+
/// <para>
274+
/// Replacement model for LanguageTool API response.
275+
/// </para>
276+
/// <para></para>
277+
/// </summary>
278+
internal class LanguageToolReplacement
279+
{
280+
public string value { get; set; } = "";
281+
}
282+
}

examples/GrammarCheckExample.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Grammar Check Bot Example
2+
3+
This document demonstrates how to use the GitHub bot's grammar checking functionality.
4+
5+
## Overview
6+
7+
The bot includes a `GrammarCheckTrigger` that automatically checks issues for grammar and spelling errors using the LanguageTool API. When triggered, the bot will:
8+
9+
1. Analyze the issue description for grammar and spelling mistakes
10+
2. Post a comment with suggested corrections
11+
3. Provide context and replacement suggestions for each error found
12+
13+
## How to Trigger
14+
15+
The grammar checker will activate when you create an issue with a title containing any of these keywords:
16+
- "grammar check"
17+
- "spell check"
18+
- "proofread"
19+
- "grammar"
20+
- "spelling"
21+
22+
## Example Usage
23+
24+
### Issue Title Examples
25+
- "Grammar check please"
26+
- "Can you proofread this text?"
27+
- "Check spelling errors in this document"
28+
- "Grammar and spelling review needed"
29+
30+
### Expected Bot Response
31+
32+
If the issue description contains text like:
33+
```
34+
This is a test with grammer mistakes and speling erors.
35+
```
36+
37+
The bot will respond with:
38+
```
39+
## Grammar and Spelling Check Results
40+
41+
Found **3** potential issue(s):
42+
43+
**1.** Possible spelling mistake found.
44+
- Context: "This is a test with **grammer** mistakes and speling erors."
45+
- Suggested correction(s): grammar, grimmer, rammer, crammer, g rammer
46+
47+
**2.** Possible spelling mistake found.
48+
- Context: "...his is a test with grammer mistakes and **speling** erors."
49+
- Suggested correction(s): spelling, spewing, spieling
50+
51+
**3.** Possible spelling mistake found.
52+
- Context: "... test with grammer mistakes and speling **erors**."
53+
- Suggested correction(s): errors, Eros, errs
54+
55+
---
56+
*Grammar checking powered by LanguageTool API*
57+
```
58+
59+
## Technical Details
60+
61+
- **API**: Uses LanguageTool public API (https://api.languagetool.org/v2/check)
62+
- **Language**: Defaults to English (US)
63+
- **Trigger Logic**: Located in `csharp/Platform.Bot/Triggers/GrammarCheckTrigger.cs`
64+
- **Integration**: Added to the main issue tracker in `Program.cs`
65+
66+
## Configuration
67+
68+
The trigger is automatically enabled when the bot starts. No additional configuration is required.
69+
70+
## Limitations
71+
72+
- Uses the free LanguageTool API (limited requests per day)
73+
- Only checks the issue description, not comments
74+
- Currently supports English language only
75+
- Requires internet connectivity to reach the LanguageTool API

0 commit comments

Comments
 (0)