1414namespace Sqs_AI_Lambda . Services
1515{
1616 /// <summary>
17- /// Service for generating AI-powered tender summaries using AWS Bedrock
18- /// Enhanced with retry logic, rate limiting, and robust error handling for production use
17+ /// Service for generating AI-powered tender summaries using AWS Bedrock.
18+ /// It now dynamically fetches prompts from a dedicated prompt service.
19+ /// It now uses Anthropic's Claude 3 Sonnet model.
1920 /// </summary>
2021 public class BedrockSummaryService : IBedrockSummaryService
2122 {
2223 private readonly ILogger < BedrockSummaryService > _logger ;
2324 private readonly AmazonBedrockRuntimeClient _bedrockClient ;
25+ private readonly IPromptService _promptService ;
2426
2527 // Rate limiting and retry configuration
2628 private static readonly SemaphoreSlim _rateLimitSemaphore = new ( 3 , 3 ) ; // Max 3 concurrent requests
2729 private const int MaxRetryAttempts = 5 ;
2830 private const int BaseDelayMs = 1000 ; // 1 second base delay
2931
30- private const string SystemPrompt = @"You are a professional tender analyst.
31- Analyse the tender data and create a structured summary with these sections:
32-
33- **PURPOSE:** What the tender is for and scope
34- **ELIGIBILITY:** Who can apply and requirements
35- **APPLICATION:** How to apply, deadlines, procedures
36- **LOCATION/CONTACT:** Location details and contact info
37- **DOCUMENTS:** Required/supporting documents
38- **DETAILS:** Other important information
39-
40- Guidelines:
41- - Use only provided information
42- - State Information not provided for missing info
43- - Be concise but comprehensive
44- - Use bullet points for clarity" ;
45-
46- public BedrockSummaryService ( ILogger < BedrockSummaryService > logger , AmazonBedrockRuntimeClient bedrockClient )
32+
33+ public BedrockSummaryService ( ILogger < BedrockSummaryService > logger , AmazonBedrockRuntimeClient bedrockClient , IPromptService promptService )
4734 {
4835 _logger = logger ;
4936 _bedrockClient = bedrockClient ;
37+ _promptService = promptService ;
5038 }
5139
5240 /// <summary>
@@ -59,33 +47,37 @@ public async Task<string> GenerateSummaryAsync(TenderMessageBase tenderMessage)
5947 var tenderNumber = tenderMessage . TenderNumber ?? "Unknown" ;
6048 var sourceType = tenderMessage . GetSourceType ( ) ;
6149
62- _logger . LogInformation ( "Starting Nova Pro summary with rate limiting - TenderNumber: {TenderNumber}, Source: {SourceType}" ,
50+ _logger . LogInformation ( "Starting summary generation - TenderNumber: {TenderNumber}, Source: {SourceType}" ,
6351 tenderNumber , sourceType ) ;
6452
6553 // Wait for rate limit semaphore to control concurrent requests
6654 await _rateLimitSemaphore . WaitAsync ( ) ;
6755
6856 try
6957 {
58+ // Fetch the dynamic, combined prompt for the specific source
59+ var combinedPrompt = await _promptService . GetPromptAsync ( sourceType ) ;
60+
7061 // Convert to compact JSON format to minimize tokens
7162 var tenderJson = ConvertTenderToCompactJson ( tenderMessage ) ;
7263
7364 _logger . LogDebug ( "Tender JSON created - TenderNumber: {TenderNumber}, JsonLength: {JsonLength}" ,
7465 tenderNumber , tenderJson . Length ) ;
7566
76- // Execute with retry logic for handling throttling
77- var summary = await ExecuteWithRetryAsync ( tenderJson , tenderNumber ) ;
67+ // Pass the combined prompt to the execution method
68+ var summary = await ExecuteWithRetryAsync ( combinedPrompt , tenderJson , tenderNumber ) ;
7869
7970 var duration = ( DateTime . UtcNow - startTime ) . TotalMilliseconds ;
80- _logger . LogInformation ( "Nova Pro summary completed successfully - TenderNumber: {TenderNumber}, Duration: {Duration}ms, Length: {Length}" ,
81- tenderNumber , duration , summary . Length ) ;
71+
72+ _logger . LogInformation ( "Summary completed successfully - TenderNumber: {TenderNumber}, Duration: {Duration}ms" ,
73+ tenderNumber , duration ) ;
8274
8375 return summary ;
8476 }
8577 catch ( Exception ex )
8678 {
8779 var duration = ( DateTime . UtcNow - startTime ) . TotalMilliseconds ;
88- _logger . LogError ( ex , "Nova Pro summary failed after all retries - TenderNumber: {TenderNumber}, Duration: {Duration}ms, ErrorType: {ErrorType}" ,
80+ _logger . LogError ( ex , "Summary generation failed after all retries - TenderNumber: {TenderNumber}, Duration: {Duration}ms, ErrorType: {ErrorType}" ,
8981 tenderNumber , duration , ex . GetType ( ) . Name ) ;
9082
9183 return GenerateFallbackSummary ( tenderMessage ) ;
@@ -98,9 +90,9 @@ public async Task<string> GenerateSummaryAsync(TenderMessageBase tenderMessage)
9890 }
9991
10092 /// <summary>
101- /// Executes Bedrock request with exponential backoff retry logic for handling throttling
93+ /// Executes Bedrock request with retry logic, now using a dynamic prompt.
10294 /// </summary>
103- private async Task < string > ExecuteWithRetryAsync ( string tenderJson , string tenderNumber )
95+ private async Task < string > ExecuteWithRetryAsync ( string combinedPrompt , string tenderJson , string tenderNumber )
10496 {
10597 var attempt = 0 ;
10698
@@ -113,9 +105,12 @@ private async Task<string> ExecuteWithRetryAsync(string tenderJson, string tende
113105 _logger . LogDebug ( "Bedrock request attempt {Attempt}/{MaxAttempts} - TenderNumber: {TenderNumber}" ,
114106 attempt , MaxRetryAttempts , tenderNumber ) ;
115107
116- // Optimised payload for token efficiency
108+ // Payload structured for Anthropic Claude 3 models
117109 var payload = new
118110 {
111+ anthropic_version = "bedrock-2023-05-31" , // Required for Claude 3
112+ max_tokens = 900 ,
113+ temperature = 0.3 ,
119114 messages = new [ ]
120115 {
121116 new
@@ -125,22 +120,17 @@ private async Task<string> ExecuteWithRetryAsync(string tenderJson, string tende
125120 {
126121 new
127122 {
128- text = $ "{ SystemPrompt } \n \n Tender: { tenderJson } "
123+ type = "text" ,
124+ text = $ "{ combinedPrompt } \n \n Tender Data:\n { tenderJson } "
129125 }
130126 }
131127 }
132- } ,
133- inferenceConfig = new
134- {
135- max_new_tokens = 800 , // Efficient token usage
136- temperature = 0.3 , // Low for consistent output
137- top_p = 0.9
138128 }
139129 } ;
140130
141131 var request = new InvokeModelRequest
142132 {
143- ModelId = "amazon.nova-pro- v1:0" ,
133+ ModelId = "anthropic.claude-3-sonnet-20240229- v1:0" , // Model ID changed to Claude 3 Sonnet
144134 ContentType = "application/json" ,
145135 Accept = "application/json" ,
146136 Body = new MemoryStream ( Encoding . UTF8 . GetBytes ( JsonSerializer . Serialize ( payload ) ) )
@@ -151,7 +141,8 @@ private async Task<string> ExecuteWithRetryAsync(string tenderJson, string tende
151141 using var responseStream = new StreamReader ( response . Body ) ;
152142 var responseText = await responseStream . ReadToEndAsync ( ) ;
153143
154- var summary = ParseNovaResponse ( responseText ) ;
144+ // Call the new response parser for Claude
145+ var summary = ParseClaudeResponse ( responseText ) ;
155146
156147 _logger . LogDebug ( "Bedrock request successful on attempt {Attempt} - TenderNumber: {TenderNumber}" ,
157148 attempt , tenderNumber ) ;
@@ -361,7 +352,7 @@ private void AddSanralSpecificFields(Dictionary<string, object> compactTender, S
361352 }
362353
363354 /// <summary>
364- /// Parses Amazon Nova Pro 's response to extract the summary content
355+ /// Parses Amazon Nova's response to extract the summary content
365356 /// </summary>
366357 private string ParseNovaResponse ( string responseJson )
367358 {
@@ -393,6 +384,38 @@ private string ParseNovaResponse(string responseJson)
393384 }
394385 }
395386
387+
388+ /// <summary>
389+ /// Parses the JSON response from an Anthropic Claude 3 model to extract the summary content.
390+ /// </summary>
391+ private string ParseClaudeResponse ( string responseJson )
392+ {
393+ try
394+ {
395+ using var document = JsonDocument . Parse ( responseJson ) ;
396+ var contentArray = document . RootElement . GetProperty ( "content" ) ;
397+
398+ if ( contentArray . ValueKind == JsonValueKind . Array )
399+ {
400+ foreach ( var contentItem in contentArray . EnumerateArray ( ) )
401+ {
402+ if ( contentItem . TryGetProperty ( "type" , out var typeProperty ) && typeProperty . GetString ( ) == "text" &&
403+ contentItem . TryGetProperty ( "text" , out var textProperty ) )
404+ {
405+ return textProperty . GetString ( ) ?? "Summary generated but content extraction failed." ;
406+ }
407+ }
408+ }
409+ _logger . LogWarning ( "Unexpected Claude 3 response format: 'text' field not found in content array." ) ;
410+ return "Summary generated but response parsing failed (unexpected format)." ;
411+ }
412+ catch ( Exception ex )
413+ {
414+ _logger . LogError ( ex , "Failed to parse Claude 3 response JSON: {ResponseJson}" , responseJson ) ;
415+ return "Summary generation completed but response parsing failed (exception)." ;
416+ }
417+ }
418+
396419 /// <summary>
397420 /// Generates a model-aware fallback summary when Bedrock fails
398421 /// Enhanced with throttling context information
0 commit comments