|
|
@@ -5,6 +5,24 @@ protocol GrammarCheckServicing {
|
|
|
}
|
|
|
|
|
|
struct GrammarCheckService: GrammarCheckServicing {
|
|
|
+ private static let chunkMaxWords = 350
|
|
|
+ private static let maxConcurrentChunks = 4
|
|
|
+ private static let issuesOnlySystemPrompt = """
|
|
|
+ You are a professional grammar checker. Analyze the user's text for grammar, \
|
|
|
+ punctuation, and clarity issues only. Respond with JSON only using this schema:
|
|
|
+ {
|
|
|
+ "issues": [
|
|
|
+ {
|
|
|
+ "original": "exact problematic phrase from the input",
|
|
|
+ "suggestion": "corrected phrase",
|
|
|
+ "explanation": "brief reason for the change"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ Return only real issues in the issues array. Do not rewrite or return the full text. \
|
|
|
+ Preserve markdown, tables, code, file paths, and technical terms unchanged.
|
|
|
+ """
|
|
|
+
|
|
|
private let openAI: OpenAIChatServicing
|
|
|
|
|
|
init(openAI: OpenAIChatServicing = OpenAIChatService.shared) {
|
|
|
@@ -18,46 +36,67 @@ struct GrammarCheckService: GrammarCheckServicing {
|
|
|
}
|
|
|
|
|
|
do {
|
|
|
- let response: GrammarCheckResponse = try await openAI.completeJSON(
|
|
|
- systemPrompt: """
|
|
|
- You are a professional grammar checker. Analyze the user's text for grammar, \
|
|
|
- punctuation, and clarity issues. Respond with JSON only using this schema:
|
|
|
- {
|
|
|
- "correctedText": "full corrected version of the input",
|
|
|
- "issues": [
|
|
|
- {
|
|
|
- "original": "problematic phrase or word",
|
|
|
- "suggestion": "corrected phrase or word",
|
|
|
- "explanation": "brief reason for the change"
|
|
|
- }
|
|
|
- ]
|
|
|
- }
|
|
|
- If there are no issues, return the original text in correctedText and an empty issues array.
|
|
|
- """,
|
|
|
- userPrompt: trimmed,
|
|
|
- as: GrammarCheckResponse.self
|
|
|
- )
|
|
|
+ let chunks = trimmed.chunked(maxWords: Self.chunkMaxWords)
|
|
|
+ let issues = try await checkChunks(chunks)
|
|
|
+ let correctedText = trimmed.applyingGrammarCorrections(issues)
|
|
|
|
|
|
return GrammarCheckResult(
|
|
|
- correctedText: response.correctedText,
|
|
|
- issues: response.issues.map {
|
|
|
- GrammarIssue(original: $0.original, suggestion: $0.suggestion, explanation: $0.explanation)
|
|
|
- }
|
|
|
+ correctedText: correctedText,
|
|
|
+ issues: issues
|
|
|
)
|
|
|
} catch let error as OpenAIServiceError {
|
|
|
throw GrammarCheckServiceError.fromOpenAI(error)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private func checkChunks(_ chunks: [String]) async throws -> [GrammarIssue] {
|
|
|
+ var allIssues: [GrammarIssue] = []
|
|
|
+
|
|
|
+ for batchStart in stride(from: 0, to: chunks.count, by: Self.maxConcurrentChunks) {
|
|
|
+ let batchEnd = min(batchStart + Self.maxConcurrentChunks, chunks.count)
|
|
|
+ let batch = Array(chunks[batchStart..<batchEnd])
|
|
|
+
|
|
|
+ let batchIssues = try await withThrowingTaskGroup(of: [GrammarIssue].self) { group in
|
|
|
+ for chunk in batch {
|
|
|
+ group.addTask {
|
|
|
+ try await self.checkSingleChunk(chunk)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var issues: [GrammarIssue] = []
|
|
|
+ for try await chunkIssues in group {
|
|
|
+ issues.append(contentsOf: chunkIssues)
|
|
|
+ }
|
|
|
+ return issues
|
|
|
+ }
|
|
|
+
|
|
|
+ allIssues.append(contentsOf: batchIssues)
|
|
|
+ }
|
|
|
+
|
|
|
+ return allIssues
|
|
|
+ }
|
|
|
+
|
|
|
+ private func checkSingleChunk(_ text: String) async throws -> [GrammarIssue] {
|
|
|
+ let response: GrammarIssuesResponse = try await openAI.completeJSON(
|
|
|
+ systemPrompt: Self.issuesOnlySystemPrompt,
|
|
|
+ userPrompt: text,
|
|
|
+ maxTokens: 1_500,
|
|
|
+ as: GrammarIssuesResponse.self
|
|
|
+ )
|
|
|
+
|
|
|
+ return response.issues.map {
|
|
|
+ GrammarIssue(original: $0.original, suggestion: $0.suggestion, explanation: $0.explanation)
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-private struct GrammarCheckResponse: Decodable {
|
|
|
+private struct GrammarIssuesResponse: Decodable {
|
|
|
struct Issue: Decodable {
|
|
|
let original: String
|
|
|
let suggestion: String
|
|
|
let explanation: String
|
|
|
}
|
|
|
|
|
|
- let correctedText: String
|
|
|
let issues: [Issue]
|
|
|
}
|
|
|
|