11package com .alessandra_alessandro .ketchapp .utils ;
22
3+ import com .alessandra_alessandro .ketchapp .models .Schema ;
34import com .alessandra_alessandro .ketchapp .models .dto .PlanBuilderRequestDto ;
45import com .alessandra_alessandro .ketchapp .models .dto .PlanBuilderResponseDto ;
56import com .fasterxml .jackson .core .JsonProcessingException ;
67import com .fasterxml .jackson .databind .JsonNode ;
78import com .fasterxml .jackson .databind .ObjectMapper ;
89import com .fasterxml .jackson .databind .node .ArrayNode ;
910import com .fasterxml .jackson .databind .node .ObjectNode ;
10- import org .springframework .beans .factory .annotation .Value ;
11- import org .springframework .stereotype .Component ;
12-
13- import org .slf4j .Logger ;
14- import org .slf4j .LoggerFactory ;
15-
1611import java .net .URI ;
1712import java .net .http .HttpClient ;
1813import java .net .http .HttpRequest ;
1914import java .net .http .HttpResponse ;
2015import java .time .LocalDate ;
21-
22- import com .alessandra_alessandro .ketchapp .models .Schema ;
23-
24- import java .util .HashMap ;
2516import java .util .Arrays ;
17+ import java .util .HashMap ;
18+ import org .slf4j .Logger ;
19+ import org .slf4j .LoggerFactory ;
20+ import org .springframework .beans .factory .annotation .Value ;
21+ import org .springframework .stereotype .Component ;
2622
2723@ Component
2824public class GeminiApi {
29- public static final String BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models/" ;
25+
26+ public static final String BASE_URL =
27+ "https://generativelanguage.googleapis.com/v1beta/models/" ;
3028 private final String apiKey ;
3129 public static final String MODEL = "gemini-2.5-flash" ;
3230 private final String endpoint ;
@@ -50,7 +48,9 @@ public GeminiApi(@Value("${GEMINI_API_KEY}") String apiKey) {
5048 public PlanBuilderRequestDto ask (PlanBuilderResponseDto dto ) {
5149 assert dto != null : "Request DTO cannot be null" ;
5250 if (apiKey == null || apiKey .isEmpty ()) {
53- log .error ("Error: GEMINI_API_KEY property not set in application.properties." );
51+ log .error (
52+ "Error: GEMINI_API_KEY property not set in application.properties."
53+ );
5454 return null ;
5555 }
5656 log .debug ("Received PlanBuilderResponseDto: {}" , dto );
@@ -67,11 +67,24 @@ public PlanBuilderRequestDto ask(PlanBuilderResponseDto dto) {
6767 String jsonPayload = buildGeminiPayload (dtoJson , question );
6868 log .debug ("Built Gemini API payload: {}" , jsonPayload );
6969 HttpRequest request = buildHttpRequest (jsonPayload );
70- log .debug ("Sending HTTP request to Gemini API endpoint: {}" , endpoint );
71- HttpResponse <String > response = HTTP_CLIENT .send (request , HttpResponse .BodyHandlers .ofString ());
72- log .debug ("Received response from Gemini API: status={}, body={}" , response .statusCode (), response .body ());
70+ log .debug (
71+ "Sending HTTP request to Gemini API endpoint: {}" ,
72+ endpoint
73+ );
74+ HttpResponse <String > response = HTTP_CLIENT .send (
75+ request ,
76+ HttpResponse .BodyHandlers .ofString ()
77+ );
78+ log .debug (
79+ "Received response from Gemini API: status={}, body={}" ,
80+ response .statusCode (),
81+ response .body ()
82+ );
7383 if (response .statusCode () != 200 ) {
74- log .error ("Error: Gemini API returned status code {}" , response .statusCode ());
84+ log .error (
85+ "Error: Gemini API returned status code {}" ,
86+ response .statusCode ()
87+ );
7588 return null ;
7689 }
7790 return extractDtoFromGeminiResponse (response .body ());
@@ -112,28 +125,37 @@ private static String getPauseFromDto(PlanBuilderResponseDto dto) {
112125 * @param dto the PlanBuilderResponseDto containing subjects and other info
113126 * @return a formatted question string for the Gemini API
114127 */
115- private static String buildQuestion (String session , String pause , PlanBuilderResponseDto dto ) {
128+ private static String buildQuestion (
129+ String session ,
130+ String pause ,
131+ PlanBuilderResponseDto dto
132+ ) {
116133 StringBuilder subjectsInfo = new StringBuilder ();
117134 if (dto .getSubjects () != null && !dto .getSubjects ().isEmpty ()) {
118135 subjectsInfo .append ("Subjects to study:\n " );
119136 for (var subject : dto .getSubjects ()) {
120- subjectsInfo .append ("- " )
121- .append (subject .getName ())
122- .append ("\n " );
137+ subjectsInfo
138+ .append ("- " )
139+ .append (subject .getName ())
140+ .append ("\n " );
123141 }
124142 }
125- return String .format ("""
126- Today's date is %s.
127- Look at the events you have in your calendar and, based on those, create a study plan for me that allows me to study without overlapping with my scheduled commitments.
128-
129- Subjects to study: %s
130- Each study session lasts %s and the break is %s.
131- Leave a margin of 30 minutes before and after each calendar event.
132- Remember that Start_at, End_at, and Pause_end_at are in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ).""" ,
133- LocalDate .now (),
134- subjectsInfo ,
135- session ,
136- pause );
143+ return String .format (
144+ """
145+ Today's date is %s.
146+ Look at the events you have in your calendar and, based on those, create a study plan for me that allows me to study without overlapping with my scheduled commitments.
147+
148+ Subjects to study: %s
149+ Each study session lasts %s and the break is %s.
150+ Leave a margin of 30 minutes before and after each calendar event.
151+ Remember that Start_at, End_at, and Pause_end_at are in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ).
152+ IMPORTANT Ensure that all study sessions (tomatoes) are scheduled exclusively for future times.
153+ """ ,
154+ LocalDate .now (),
155+ subjectsInfo ,
156+ session ,
157+ pause
158+ );
137159 }
138160
139161 /**
@@ -146,7 +168,8 @@ private static String buildQuestion(String session, String pause, PlanBuilderRes
146168 * @return the complete JSON payload as a string
147169 * @throws JsonProcessingException if serialization fails
148170 */
149- private static String buildGeminiPayload (String dtoJson , String question ) throws JsonProcessingException {
171+ private static String buildGeminiPayload (String dtoJson , String question )
172+ throws JsonProcessingException {
150173 ObjectNode payload = OBJECT_MAPPER .createObjectNode ();
151174 ArrayNode contentsArray = OBJECT_MAPPER .createArrayNode ();
152175 ObjectNode contentNode = OBJECT_MAPPER .createObjectNode ();
@@ -162,7 +185,10 @@ private static String buildGeminiPayload(String dtoJson, String question) throws
162185 ObjectNode generationConfig = OBJECT_MAPPER .createObjectNode ();
163186 generationConfig .put ("responseMimeType" , "application/json" );
164187 generationConfig .put ("temperature" , 1 );
165- generationConfig .set ("responseSchema" , OBJECT_MAPPER .valueToTree (buildResponseSchema ()));
188+ generationConfig .set (
189+ "responseSchema" ,
190+ OBJECT_MAPPER .valueToTree (buildResponseSchema ())
191+ );
166192
167193 payload .set ("contents" , contentsArray );
168194 payload .set ("generationConfig" , generationConfig );
@@ -181,23 +207,41 @@ private static Schema.ObjectSchema buildResponseSchema() {
181207 calendarItemProps .put ("title" , new Schema .PropertySchema ("string" ));
182208 calendarItemProps .put ("start_at" , new Schema .PropertySchema ("string" ));
183209 calendarItemProps .put ("end_at" , new Schema .PropertySchema ("string" ));
184- Schema .ObjectSchema calendarItemSchema = new Schema .ObjectSchema (calendarItemProps , Arrays .asList ("title" , "start_at" , "end_at" ));
210+ Schema .ObjectSchema calendarItemSchema = new Schema .ObjectSchema (
211+ calendarItemProps ,
212+ Arrays .asList ("title" , "start_at" , "end_at" )
213+ );
185214
186215 HashMap <String , Object > tomatoItemProps = new HashMap <>();
187216 tomatoItemProps .put ("start_at" , new Schema .PropertySchema ("string" ));
188217 tomatoItemProps .put ("end_at" , new Schema .PropertySchema ("string" ));
189- tomatoItemProps .put ("pause_end_at" , new Schema .PropertySchema ("string" ));
190- Schema .ObjectSchema tomatoItemSchema = new Schema .ObjectSchema (tomatoItemProps , Arrays .asList ("start_at" , "end_at" , "pause_end_at" ));
218+ tomatoItemProps .put (
219+ "pause_end_at" ,
220+ new Schema .PropertySchema ("string" )
221+ );
222+ Schema .ObjectSchema tomatoItemSchema = new Schema .ObjectSchema (
223+ tomatoItemProps ,
224+ Arrays .asList ("start_at" , "end_at" , "pause_end_at" )
225+ );
191226
192227 HashMap <String , Object > subjectItemProps = new HashMap <>();
193228 subjectItemProps .put ("name" , new Schema .PropertySchema ("string" ));
194- subjectItemProps .put ("tomatoes" , new Schema .ArraySchema (tomatoItemSchema ));
195- Schema .ObjectSchema subjectItemSchema = new Schema .ObjectSchema (subjectItemProps , Arrays .asList ("name" , "tomatoes" ));
229+ subjectItemProps .put (
230+ "tomatoes" ,
231+ new Schema .ArraySchema (tomatoItemSchema )
232+ );
233+ Schema .ObjectSchema subjectItemSchema = new Schema .ObjectSchema (
234+ subjectItemProps ,
235+ Arrays .asList ("name" , "tomatoes" )
236+ );
196237
197238 HashMap <String , Object > mainProps = new HashMap <>();
198239 mainProps .put ("calendar" , new Schema .ArraySchema (calendarItemSchema ));
199240 mainProps .put ("subjects" , new Schema .ArraySchema (subjectItemSchema ));
200- return new Schema .ObjectSchema (mainProps , Arrays .asList ("calendar" , "subjects" ));
241+ return new Schema .ObjectSchema (
242+ mainProps ,
243+ Arrays .asList ("calendar" , "subjects" )
244+ );
201245 }
202246
203247 /**
@@ -208,10 +252,10 @@ private static Schema.ObjectSchema buildResponseSchema() {
208252 */
209253 private HttpRequest buildHttpRequest (String jsonPayload ) {
210254 return HttpRequest .newBuilder ()
211- .uri (URI .create (endpoint ))
212- .header ("Content-Type" , "application/json" )
213- .POST (HttpRequest .BodyPublishers .ofString (jsonPayload ))
214- .build ();
255+ .uri (URI .create (endpoint ))
256+ .header ("Content-Type" , "application/json" )
257+ .POST (HttpRequest .BodyPublishers .ofString (jsonPayload ))
258+ .build ();
215259 }
216260
217261 /**
@@ -221,18 +265,29 @@ private HttpRequest buildHttpRequest(String jsonPayload) {
221265 * @param responseBody the raw JSON response from the Gemini API
222266 * @return the extracted PlanBuilderRequestDto, or null if extraction fails
223267 */
224- private static PlanBuilderRequestDto extractDtoFromGeminiResponse (String responseBody ) {
268+ private static PlanBuilderRequestDto extractDtoFromGeminiResponse (
269+ String responseBody
270+ ) {
225271 try {
226272 JsonNode root = OBJECT_MAPPER .readTree (responseBody );
227273 JsonNode candidates = root .path ("candidates" );
228274 if (candidates .isArray () && !candidates .isEmpty ()) {
229- JsonNode parts = candidates .get (0 ).path ("content" ).path ("parts" );
275+ JsonNode parts = candidates
276+ .get (0 )
277+ .path ("content" )
278+ .path ("parts" );
230279 if (parts .isArray () && !parts .isEmpty ()) {
231280 String jsonText = parts .get (0 ).path ("text" ).asText ();
232- return OBJECT_MAPPER .readValue (jsonText , PlanBuilderRequestDto .class );
281+ return OBJECT_MAPPER .readValue (
282+ jsonText ,
283+ PlanBuilderRequestDto .class
284+ );
233285 }
234286 }
235- log .error ("Could not extract DTO from Gemini API response: {}" , responseBody );
287+ log .error (
288+ "Could not extract DTO from Gemini API response: {}" ,
289+ responseBody
290+ );
236291 } catch (Exception e ) {
237292 log .error ("Error parsing Gemini API response: {}" , e .getMessage ());
238293 }
0 commit comments