1+ using System . Net ;
12using System . Text ;
23using System . Text . Json ;
34using Microsoft . Extensions . Logging ;
@@ -7,28 +8,18 @@ namespace SWEN3.Paperless.RabbitMq.GenAI;
78
89/// <summary>
910/// Google Gemini AI implementation of <see cref="ITextSummarizer" /> for document summarization.
10- /// <para>
11- /// Provides structured text summarization with automatic retry logic, timeout handling, and robust error
12- /// recovery.
13- /// </para>
14- /// <para>Integrates with Google's Generative Language API using the Gemini 2.0 Flash model by default.</para>
11+ /// <para>Resilience (retries, circuit breaker, timeouts) is handled by Microsoft.Extensions.Http.Resilience.</para>
1512/// </summary>
16- /// <remarks>
17- /// Resilience (retries, circuit breaker, timeouts) is handled automatically by
18- /// Microsoft.Extensions.Http.Resilience via AddStandardResilienceHandler.
19- /// </remarks>
20- public sealed class GeminiService : ITextSummarizer
13+ public sealed partial class GeminiService : ITextSummarizer
2114{
2215 private readonly HttpClient _httpClient ;
2316 private readonly ILogger < GeminiService > _logger ;
2417 private readonly GeminiOptions _options ;
2518
26- /// <summary>
27- /// Initializes a new instance of the <see cref="GeminiService" /> class.
28- /// </summary>
29- /// <param name="httpClient">The HTTP client for making API requests (configured with resilience handlers).</param>
30- /// <param name="options">Configuration options containing API key, model selection, and timeout settings.</param>
31- /// <param name="logger">Logger instance for diagnostic output and error tracking.</param>
19+ /// <summary>Initializes a new instance of <see cref="GeminiService" />.</summary>
20+ /// <param name="httpClient">HTTP client configured with resilience handlers.</param>
21+ /// <param name="options">Gemini API options (API key, model, timeout).</param>
22+ /// <param name="logger">Logger for diagnostics.</param>
3223 public GeminiService ( HttpClient httpClient , IOptions < GeminiOptions > options , ILogger < GeminiService > logger )
3324 {
3425 _httpClient = httpClient ;
@@ -37,66 +28,15 @@ public GeminiService(HttpClient httpClient, IOptions<GeminiOptions> options, ILo
3728 _httpClient . Timeout = TimeSpan . FromSeconds ( _options . TimeoutSeconds ) ;
3829 }
3930
40- /// <summary>
41- /// Asynchronously generates a structured summary of the provided OCR-extracted text using the Google Gemini AI model.
42- /// </summary>
43- /// <param name="text">
44- /// The raw text content extracted from a document (e.g., via OCR).
45- /// <para>
46- /// If the text is null, empty, or whitespace, the method returns <see langword="null" /> immediately without
47- /// making an API call.
48- /// </para>
49- /// </param>
50- /// <param name="cancellationToken">
51- /// A <see cref="CancellationToken" /> to observe while waiting for the API response.
52- /// </param>
53- /// <returns>
54- /// A <see cref="Task{TResult}" /> representing the asynchronous operation. The result is a <see cref="string" />
55- /// containing the generated summary,
56- /// which typically includes an executive summary, key points, document type, and extracted entities.
57- /// <para>Returns <see langword="null" /> if:</para>
58- /// <list type="bullet">
59- /// <item>
60- /// <description>The input <paramref name="text" /> is invalid (null/whitespace).</description>
61- /// </item>
62- /// <item>
63- /// <description>The API call fails (non-success status code).</description>
64- /// </item>
65- /// <item>
66- /// <description>The API response is malformed or missing expected content.</description>
67- /// </item>
68- /// <item>
69- /// <description>The operation is canceled or times out.</description>
70- /// </item>
71- /// </list>
72- /// </returns>
73- /// <remarks>
74- /// The summary is generated based on a specific prompt structure that requests:
75- /// <list type="number">
76- /// <item>
77- /// <description>A 2-3 sentence executive summary.</description>
78- /// </item>
79- /// <item>
80- /// <description>3-5 key points.</description>
81- /// </item>
82- /// <item>
83- /// <description>Document type identification.</description>
84- /// </item>
85- /// <item>
86- /// <description>Extraction of important dates, numbers, or entities.</description>
87- /// </item>
88- /// </list>
89- /// <para>
90- /// Exceptions such as <see cref="HttpRequestException" />, <see cref="TaskCanceledException" />, and
91- /// <see cref="JsonException" />
92- /// are caught internally and logged, resulting in a <see langword="null" /> return value to ensure resilience.
93- /// </para>
94- /// </remarks>
31+ /// <summary>Generates a structured summary for the provided text using Gemini.</summary>
32+ /// <param name="text">OCR-extracted text to summarize.</param>
33+ /// <param name="cancellationToken">Cancellation token.</param>
34+ /// <returns>The generated summary, or null on validation/API failure.</returns>
9535 public async Task < string ? > SummarizeAsync ( string text , CancellationToken cancellationToken = default )
9636 {
9737 if ( string . IsNullOrWhiteSpace ( text ) )
9838 {
99- _logger . LogWarning ( "Empty text supplied to summarizer" ) ;
39+ GeminiServiceLog . EmptyText ( _logger ) ;
10040 return null ;
10141 }
10242
@@ -112,8 +52,7 @@ public GeminiService(HttpClient httpClient, IOptions<GeminiOptions> options, ILo
11252
11353 if ( ! response . IsSuccessStatusCode )
11454 {
115- _logger . LogError ( "Gemini API responded {StatusCode}: {Reason}" , response . StatusCode ,
116- response . ReasonPhrase ) ;
55+ GeminiServiceLog . GeminiApiError ( _logger , response . StatusCode , response . ReasonPhrase ) ;
11756 return null ;
11857 }
11958
@@ -122,41 +61,37 @@ public GeminiService(HttpClient httpClient, IOptions<GeminiOptions> options, ILo
12261 }
12362 catch ( Exception ex ) when ( ex is HttpRequestException or TaskCanceledException or OperationCanceledException )
12463 {
125- _logger . LogError ( ex , "Gemini API call failed" ) ;
64+ GeminiServiceLog . GeminiApiCallFailed ( _logger , ex ) ;
12665 return null ;
12766 }
12867 }
12968
130- private static string BuildPrompt ( string text )
131- {
132- return $ """
133- You are a document summarization assistant for a Document Management System (DMS).
134- Your task is to analyse the following OCR-extracted text and provide a structured summary.
135-
136- Instructions:
137- 1. Create a concise executive summary (2-3 sentences)
138- 2. List 3-5 key points from the document
139- 3. Identify the document type if possible
140- 4. Extract any important dates, numbers or entities mentioned
141- 5. Keep the summary factual and objective - do not add interpretations
142-
143- Document text:
144- ---
145- { text }
146- ---
147-
148- Provide the summary now.
149- """ ;
150- }
69+ private static string BuildPrompt ( string text ) =>
70+ $ """
71+ You are a document summarization assistant for a Document Management System (DMS).
72+ Your task is to analyse the following OCR-extracted text and provide a structured summary.
15173
152- private static object BuildRequestBody ( string prompt )
153- {
154- return new
74+ Instructions:
75+ 1. Create a concise executive summary (2-3 sentences)
76+ 2. List 3-5 key points from the document
77+ 3. Identify the document type if possible
78+ 4. Extract any important dates, numbers or entities mentioned
79+ 5. Keep the summary factual and objective - do not add interpretations
80+
81+ Document text:
82+ ---
83+ { text }
84+ ---
85+
86+ Provide the summary now.
87+ """ ;
88+
89+ private static object BuildRequestBody ( string prompt ) =>
90+ new
15591 {
15692 contents = new [ ] { new { parts = new [ ] { new { text = prompt } } } } ,
15793 generationConfig = new { temperature = 0.3 , topK = 40 , topP = 0.95 , maxOutputTokens = 1024 }
15894 } ;
159- }
16095
16196 private string ? ExtractSummary ( string json )
16297 {
@@ -167,38 +102,38 @@ private static object BuildRequestBody(string prompt)
167102
168103 if ( ! root . TryGetProperty ( "candidates" , out var candidates ) )
169104 {
170- _logger . LogWarning ( "No candidates in Gemini response" ) ;
105+ GeminiServiceLog . NoCandidates ( _logger ) ;
171106 return null ;
172107 }
173108
174109 if ( candidates . GetArrayLength ( ) is 0 )
175110 {
176- _logger . LogWarning ( "Empty candidates array in Gemini response" ) ;
111+ GeminiServiceLog . EmptyCandidates ( _logger ) ;
177112 return null ;
178113 }
179114
180115 var firstCandidate = candidates [ 0 ] ;
181116 if ( ! firstCandidate . TryGetProperty ( "content" , out var content ) )
182117 {
183- _logger . LogWarning ( "No content in first candidate" ) ;
118+ GeminiServiceLog . NoContent ( _logger ) ;
184119 return null ;
185120 }
186121
187122 if ( ! content . TryGetProperty ( "parts" , out var parts ) )
188123 {
189- _logger . LogWarning ( "No parts in content" ) ;
124+ GeminiServiceLog . NoParts ( _logger ) ;
190125 return null ;
191126 }
192127
193128 if ( parts . GetArrayLength ( ) is 0 )
194129 {
195- _logger . LogWarning ( "Empty parts array in content" ) ;
130+ GeminiServiceLog . EmptyParts ( _logger ) ;
196131 return null ;
197132 }
198133
199134 if ( ! parts [ 0 ] . TryGetProperty ( "text" , out var textElement ) )
200135 {
201- _logger . LogWarning ( "No text in first part" ) ;
136+ GeminiServiceLog . NoText ( _logger ) ;
202137 return null ;
203138 }
204139
@@ -207,8 +142,41 @@ private static object BuildRequestBody(string prompt)
207142 }
208143 catch ( Exception ex )
209144 {
210- _logger . LogError ( ex , "Failed to parse Gemini response" ) ;
145+ GeminiServiceLog . ParseError ( _logger , ex ) ;
211146 return null ;
212147 }
213148 }
149+
150+ internal static partial class GeminiServiceLog
151+ {
152+ [ LoggerMessage ( EventId = 2001 , Level = LogLevel . Warning , Message = "Empty text supplied to summarizer" ) ]
153+ public static partial void EmptyText ( ILogger logger ) ;
154+
155+ [ LoggerMessage ( EventId = 2002 , Level = LogLevel . Error , Message = "Gemini API responded {StatusCode}: {Reason}" ) ]
156+ public static partial void GeminiApiError ( ILogger logger , HttpStatusCode statusCode , string ? reason ) ;
157+
158+ [ LoggerMessage ( EventId = 2003 , Level = LogLevel . Error , Message = "Gemini API call failed" ) ]
159+ public static partial void GeminiApiCallFailed ( ILogger logger , Exception exception ) ;
160+
161+ [ LoggerMessage ( EventId = 2004 , Level = LogLevel . Warning , Message = "No candidates in Gemini response" ) ]
162+ public static partial void NoCandidates ( ILogger logger ) ;
163+
164+ [ LoggerMessage ( EventId = 2005 , Level = LogLevel . Warning , Message = "Empty candidates array in Gemini response" ) ]
165+ public static partial void EmptyCandidates ( ILogger logger ) ;
166+
167+ [ LoggerMessage ( EventId = 2006 , Level = LogLevel . Warning , Message = "No content in first candidate" ) ]
168+ public static partial void NoContent ( ILogger logger ) ;
169+
170+ [ LoggerMessage ( EventId = 2007 , Level = LogLevel . Warning , Message = "No parts in content" ) ]
171+ public static partial void NoParts ( ILogger logger ) ;
172+
173+ [ LoggerMessage ( EventId = 2008 , Level = LogLevel . Warning , Message = "Empty parts array in content" ) ]
174+ public static partial void EmptyParts ( ILogger logger ) ;
175+
176+ [ LoggerMessage ( EventId = 2009 , Level = LogLevel . Warning , Message = "No text in first part" ) ]
177+ public static partial void NoText ( ILogger logger ) ;
178+
179+ [ LoggerMessage ( EventId = 2010 , Level = LogLevel . Error , Message = "Failed to parse Gemini response" ) ]
180+ public static partial void ParseError ( ILogger logger , Exception exception ) ;
181+ }
214182}
0 commit comments