Skip to content

Commit b38bff8

Browse files
authored
Merge pull request #8 from ketchapp-for-study/main
merge
2 parents a657b99 + 0d50fdd commit b38bff8

7 files changed

Lines changed: 296 additions & 51 deletions

File tree

.github/workflows/main.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Qodana
2+
on:
3+
workflow_dispatch:
4+
pull_request:
5+
push:
6+
branches: # Specify your branches here
7+
- main # The 'main' branch
8+
- 'releases/*' # The release branches
9+
10+
jobs:
11+
qodana:
12+
runs-on: ubuntu-latest
13+
permissions:
14+
contents: write
15+
pull-requests: write
16+
checks: write
17+
steps:
18+
- uses: actions/checkout@v3
19+
with:
20+
ref: ${{ github.event.pull_request.head.sha }} # to check out the actual pull request commit, not the merge commit
21+
fetch-depth: 0 # a full history is required for pull request analysis
22+
- name: 'Qodana Scan'
23+
uses: JetBrains/qodana-action@v2025.1
24+
with:
25+
pr-mode: false
26+
env:
27+
QODANA_TOKEN: ${{ secrets.QODANA_TOKEN }}
28+
QODANA_ENDPOINT: 'https://qodana.cloud'

.zed/tasks.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,5 +46,18 @@
4646
"shell": "system",
4747
// Represents the tags for inline runnable indicators, or spawning multiple tasks at once.
4848
"tags": []
49+
},
50+
{
51+
"label": "Test Tomatoes Integration",
52+
"command": "bash gradlew test --tests '*TomatoesRestControllerIntegrationTest*'",
53+
"cwd": "KetchApp-App-Api",
54+
"env": {},
55+
"use_new_terminal": false,
56+
"allow_concurrent_runs": false,
57+
"reveal": "always",
58+
"reveal_target": "dock",
59+
"hide": "never",
60+
"shell": "system",
61+
"tags": []
4962
}
5063
]

src/main/java/com/alessandra_alessandro/ketchapp/config/SecurityConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.alessandra_alessandro.ketchapp.jwt.JwtAuthenticationFilter;
55
import org.springframework.context.annotation.Bean;
66
import org.springframework.context.annotation.Configuration;
7+
import org.springframework.context.annotation.Profile;
78
import org.springframework.security.authentication.AuthenticationManager;
89
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
910
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;

src/main/java/com/alessandra_alessandro/ketchapp/kafka/KafkaProducer.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import com.alessandra_alessandro.ketchapp.models.dto.PlanBuilderRequestDto;
44
import com.fasterxml.jackson.core.JsonProcessingException;
55
import com.fasterxml.jackson.databind.ObjectMapper;
6-
import java.util.HashMap;
7-
import java.util.Map;
86
import lombok.extern.slf4j.Slf4j;
97
import org.springframework.beans.factory.annotation.Autowired;
108
import org.springframework.beans.factory.annotation.Value;

src/main/java/com/alessandra_alessandro/ketchapp/utils/GeminiApi.java

Lines changed: 103 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,30 @@
11
package com.alessandra_alessandro.ketchapp.utils;
22

3+
import com.alessandra_alessandro.ketchapp.models.Schema;
34
import com.alessandra_alessandro.ketchapp.models.dto.PlanBuilderRequestDto;
45
import com.alessandra_alessandro.ketchapp.models.dto.PlanBuilderResponseDto;
56
import com.fasterxml.jackson.core.JsonProcessingException;
67
import com.fasterxml.jackson.databind.JsonNode;
78
import com.fasterxml.jackson.databind.ObjectMapper;
89
import com.fasterxml.jackson.databind.node.ArrayNode;
910
import 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-
1611
import java.net.URI;
1712
import java.net.http.HttpClient;
1813
import java.net.http.HttpRequest;
1914
import java.net.http.HttpResponse;
2015
import java.time.LocalDate;
21-
22-
import com.alessandra_alessandro.ketchapp.models.Schema;
23-
24-
import java.util.HashMap;
2516
import 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
2824
public 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
}

src/main/resources/application.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ GEMINI_API_KEY=AIzaSyClXWWxzTCaB6mMZl1Uqcr_3Pe2xZgFHyE
3131

3232
# Kafka Configuration
3333
app.kafka.topic.mail-service=MailService
34-
spring.kafka.bootstrap-servers=localhost:29092
34+
spring.kafka.bootstrap-servers=151.42.165.160:29092
3535
spring.kafka.consumer.group-id=KafkaID
3636
spring.kafka.consumer.auto-offset-reset=earliest
3737
# important for initial consumer

0 commit comments

Comments
 (0)