@@ -34,6 +34,24 @@ public class GeminiExtractionService implements GeminiService {
3434 @ Value ("${gemini.api.url}" )
3535 private String apiUrl ;
3636
37+ @ Value ("${groq.api.key:}" )
38+ private String groqApiKey ;
39+
40+ @ Value ("${groq.api.url:}" )
41+ private String groqApiUrl ;
42+
43+ @ Value ("${groq.api.model:}" )
44+ private String groqModel ;
45+
46+ @ Value ("${openrouter.api.key:}" )
47+ private String openRouterApiKey ;
48+
49+ @ Value ("${openrouter.api.url:}" )
50+ private String openRouterApiUrl ;
51+
52+ @ Value ("${openrouter.api.model:}" )
53+ private String openRouterModel ;
54+
3755 public GeminiExtractionService () {
3856 this .restClient = RestClient .create ();
3957 this .objectMapper = new ObjectMapper ()
@@ -45,26 +63,10 @@ public JobDTO extractJobFromEmail(String from, String subject, String body) {
4563 String prompt = buildPrompt (from , subject , body );
4664
4765 try {
48- Map <String , Object > requestBody = Map .of (
49- "contents" , List .of (
50- Map .of (
51- "role" , "user" ,
52- "parts" , List .of (Map .of ("text" , prompt ))
53- )
54- )
55- );
56-
57- String response = restClient .post ()
58- .uri (apiUrl + "?key=" + apiKey )
59- .contentType (MediaType .APPLICATION_JSON )
60- .body (requestBody )
61- .retrieve ()
62- .body (String .class );
63-
64- return parseGeminiResponse (response );
65-
66+ String contentText = executeWithFallback (prompt );
67+ return parseExtractedText (contentText );
6668 } catch (Exception e ) {
67- log .error ("AI Extraction failed or timed out" , e );
69+ log .error ("AI Extraction failed or timed out across all providers " , e );
6870 return null ;
6971 }
7072 }
@@ -76,24 +78,91 @@ public List<JobDTO> extractJobsFromBatch(List<EmailBatchItem> items) {
7678 String prompt = buildBatchPrompt (items );
7779
7880 try {
79- Map <String , Object > requestBody = Map .of (
80- "contents" , List .of (
81- Map .of ("role" , "user" , "parts" , List .of (Map .of ("text" , prompt )))
82- )
83- );
81+ String contentText = executeWithFallback (prompt );
82+ return parseBulkExtractedText (contentText );
83+ } catch (Exception e ) {
84+ log .error ("Bulk AI Extraction failed across all providers" , e );
85+ return List .of ();
86+ }
87+ }
88+
89+ private String executeWithFallback (String prompt ) {
90+ try {
91+ return executeGeminiCall (prompt );
92+ } catch (Exception e ) {
93+ log .warn ("Gemini AI failed, falling back to Groq AI: {}" , e .getMessage ());
94+ try {
95+ return executeGroqCall (prompt );
96+ } catch (Exception ex ) {
97+ log .warn ("Groq AI failed, falling back to OpenRouter AI: {}" , ex .getMessage ());
98+ return executeOpenRouterCall (prompt );
99+ }
100+ }
101+ }
84102
85- String response = restClient . post ()
86- . uri ( apiUrl + "?key=" + apiKey )
87- . contentType ( MediaType . APPLICATION_JSON )
88- . body ( requestBody )
89- . retrieve ( )
90- . body ( String . class );
103+ private String executeGeminiCall ( String prompt ) {
104+ Map < String , Object > requestBody = Map . of (
105+ "contents" , List . of (
106+ Map . of ( "role" , "user" , "parts" , List . of ( Map . of ( "text" , prompt )) )
107+ )
108+ );
91109
92- return parseBulkGeminiResponse (response );
110+ String response = restClient .post ()
111+ .uri (apiUrl + "?key=" + apiKey )
112+ .contentType (MediaType .APPLICATION_JSON )
113+ .body (requestBody )
114+ .retrieve ()
115+ .body (String .class );
93116
117+ try {
118+ JsonNode root = objectMapper .readTree (response );
119+ JsonNode candidates = root .path ("candidates" );
120+ if (candidates .isMissingNode () || candidates .isEmpty ()) {
121+ throw new RuntimeException ("Empty Gemini response" );
122+ }
123+ return candidates .get (0 ).path ("content" ).path ("parts" ).get (0 ).path ("text" ).asText ();
94124 } catch (Exception e ) {
95- log .error ("Bulk AI Extraction failed" , e );
96- return List .of ();
125+ throw new RuntimeException ("Failed to parse Gemini response: " + e .getMessage (), e );
126+ }
127+ }
128+
129+ private String executeGroqCall (String prompt ) {
130+ return executeOpenAIFormatCall (prompt , groqApiUrl , groqApiKey , groqModel );
131+ }
132+
133+ private String executeOpenRouterCall (String prompt ) {
134+ return executeOpenAIFormatCall (prompt , openRouterApiUrl , openRouterApiKey , openRouterModel );
135+ }
136+
137+ private String executeOpenAIFormatCall (String prompt , String url , String key , String model ) {
138+ if (key == null || key .isBlank () || url == null || url .isBlank ()) {
139+ throw new RuntimeException ("API key or URL is not configured for provider" );
140+ }
141+
142+ Map <String , Object > requestBody = Map .of (
143+ "model" , model ,
144+ "messages" , List .of (
145+ Map .of ("role" , "user" , "content" , prompt )
146+ )
147+ );
148+
149+ String response = restClient .post ()
150+ .uri (url )
151+ .header ("Authorization" , "Bearer " + key )
152+ .contentType (MediaType .APPLICATION_JSON )
153+ .body (requestBody )
154+ .retrieve ()
155+ .body (String .class );
156+
157+ try {
158+ JsonNode root = objectMapper .readTree (response );
159+ JsonNode choices = root .path ("choices" );
160+ if (choices .isMissingNode () || choices .isEmpty ()) {
161+ throw new RuntimeException ("Empty response from AI provider" );
162+ }
163+ return choices .get (0 ).path ("message" ).path ("content" ).asText ();
164+ } catch (Exception e ) {
165+ throw new RuntimeException ("Failed to parse AI response: " + e .getMessage (), e );
97166 }
98167 }
99168
@@ -450,17 +519,10 @@ private String buildBatchPrompt(List<EmailBatchItem> items) {
450519 ]""" .formatted (emailListBuilder .toString ());
451520 }
452521
453- private List <JobDTO > parseBulkGeminiResponse (String rawResponse ) {
522+ private List <JobDTO > parseBulkExtractedText (String contentText ) {
454523 try {
455- JsonNode root = objectMapper .readTree (rawResponse );
456- JsonNode candidates = root .path ("candidates" );
524+ if (contentText == null ) return List .of ();
457525
458- if (candidates .isMissingNode () || candidates .isEmpty ()) return List .of ();
459-
460- String contentText = candidates .get (0 )
461- .path ("content" ).path ("parts" ).get (0 )
462- .path ("text" ).asText ();
463-
464526 contentText = contentText .replaceAll ("```json" , "" ).replaceAll ("```" , "" ).trim ();
465527
466528 if (contentText .equals ("[]" ) || contentText .equalsIgnoreCase ("null" )) {
@@ -487,7 +549,7 @@ private List<JobDTO> parseBulkGeminiResponse(String rawResponse) {
487549 return jobs ;
488550
489551 } catch (Exception e ) {
490- log .error ("Failed to parse Bulk AI response: {}" , rawResponse );
552+ log .error ("Failed to parse Bulk AI response: {}" , contentText );
491553 return List .of ();
492554 }
493555 }
@@ -579,19 +641,10 @@ Return ONLY raw JSON (no markdown blocks, no explanations):
579641 """ .formatted (from , subject , safeBody );
580642 }
581643
582- private JobDTO parseGeminiResponse (String rawResponse ) {
644+ private JobDTO parseExtractedText (String contentText ) {
583645 try {
584- JsonNode root = objectMapper .readTree (rawResponse );
585- JsonNode candidates = root .path ("candidates" );
646+ if (contentText == null ) return null ;
586647
587- if (candidates .isMissingNode () || candidates .isEmpty ()) {
588- return null ;
589- }
590-
591- String contentText = candidates .get (0 )
592- .path ("content" ).path ("parts" ).get (0 )
593- .path ("text" ).asText ();
594-
595648 contentText = contentText .replaceAll ("```json" , "" ).replaceAll ("```" , "" ).trim ();
596649
597650 if (contentText .equalsIgnoreCase ("null" )) {
@@ -618,7 +671,7 @@ private JobDTO parseGeminiResponse(String rawResponse) {
618671 return job ;
619672
620673 } catch (Exception e ) {
621- log .error ("Failed to parse AI response: {}" , rawResponse );
674+ log .error ("Failed to parse AI response: {}" , contentText );
622675 return null ;
623676 }
624677 }
0 commit comments