Skip to content

Commit c7a1b47

Browse files
authored
Merge pull request #69 from AET-DevOps25/68-genai-enhance-course-generation-api-confirmation-chat-and-crawling
Features * Background Scheduler * Crawls and embeds FreeCodeCamp blog articles every 12 hours * Uses cache to skip already embedded URLs * Provides REST endpoints to trigger and control the scheduler * GenAI Course Generation Enhancements * Improved system prompt for more structured courses * Auto-detects content types (VIDEO, TEXT, HTML) in generated lessons * API & Model Updates * Adds Pydantic models for scheduler control and status * Updates OpenAPI tags and documentation accordingly Commit Summary * feat(scheduler): implement blog embedder with async scheduling and cache * feat(course-gen): improve GenAI prompts and content inference * feat(api): add endpoints for scheduler control and manual execution * chore: expose scheduler utils and update OpenAPI tags * docs: document scheduler usage and configuration
2 parents 9c67a51 + 7e45900 commit c7a1b47

File tree

5 files changed

+581
-303
lines changed

5 files changed

+581
-303
lines changed

server/skillforge-course/src/main/java/com/gitittogether/skillForge/server/course/controller/courses/CourseController.java

Lines changed: 45 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,6 @@
1212
import lombok.extern.slf4j.Slf4j;
1313
import org.springframework.beans.factory.annotation.Value;
1414
import org.springframework.http.ResponseEntity;
15-
import org.springframework.web.client.RestTemplate;
16-
import org.springframework.http.HttpHeaders;
17-
import com.fasterxml.jackson.core.type.TypeReference;
18-
import com.fasterxml.jackson.databind.JsonNode;
19-
import com.fasterxml.jackson.databind.ObjectMapper;
20-
import org.springframework.http.HttpEntity;
21-
import org.springframework.http.HttpMethod;
2215
import jakarta.servlet.http.HttpServletRequest;
2316
import org.springframework.web.bind.annotation.*;
2417

@@ -31,10 +24,9 @@
3124
public class CourseController {
3225

3326
private final CourseService courseService;
34-
private final RestTemplate restTemplate = new RestTemplate();
3527

36-
@Value("${user.service.uri:http://user-service:8082}")
37-
private String userServiceUri;
28+
@Value("${user.service.uri:http://user-service:8082}")
29+
private String userServiceUri;
3830

3931
@PostMapping
4032
public ResponseEntity<CourseResponse> createCourse(@RequestBody CourseRequest request) {
@@ -65,9 +57,9 @@ public ResponseEntity<List<CourseSummaryResponse>> getPublicCourses() {
6557
}
6658

6759
@GetMapping("/public/published")
68-
public ResponseEntity<List<CourseResponse>> getPublicPublishedCourses() {
60+
public ResponseEntity<List<CourseSummaryResponse>> getPublishedCourses() {
6961
log.info("Fetching public and published courses for landing page");
70-
List<CourseResponse> responses = courseService.getPublicPublishedCourses();
62+
List<CourseSummaryResponse> responses = courseService.getPublishedCourses();
7163
return ResponseEntity.ok(responses);
7264
}
7365

@@ -129,18 +121,23 @@ public ResponseEntity<Void> unbookmarkCourse(@PathVariable String courseId, @Pat
129121

130122
@GetMapping("/search")
131123
public ResponseEntity<List<CourseResponse>> searchCourses(
132-
@RequestParam(required = false) String instructor,
133-
@RequestParam(required = false) Level level,
134-
@RequestParam(required = false) Language language,
135-
@RequestParam(required = false) String skill,
136-
@RequestParam(required = false) String category,
137-
@RequestParam(required = false) String title
124+
@RequestParam(required = false) String instructor,
125+
@RequestParam(required = false) Level level,
126+
@RequestParam(required = false) Language language,
127+
@RequestParam(required = false) String skill,
128+
@RequestParam(required = false) String category,
129+
@RequestParam(required = false) String title,
130+
@RequestParam(required = false) boolean isPublished,
131+
@RequestParam(required = false) boolean isPublic
138132
) {
139-
log.info("Advanced search: instructor={}, level={}, language={}, skill={}, category={}, title={}", instructor, level, language, skill, category, title);
140-
List<CourseResponse> responses = courseService.advancedSearch(instructor, level, language, skill, category, title);
141-
return ResponseEntity.ok(responses);
142-
}
143-
133+
log.info("Advanced search: instructor={}, level={}, language={}, skill={}, category={}, title={}, isPublished={}, isPublic={}",
134+
instructor, level, language, skill, category, title, isPublished, isPublic);
135+
List<CourseResponse> responses = courseService.advancedSearch(instructor, level, language, skill, category, title, isPublished, isPublic);
136+
return ResponseEntity.ok(responses);
137+
}
138+
139+
140+
144141
@GetMapping("/search/instructor/{instructor}")
145142
public ResponseEntity<List<CourseResponse>> getCoursesByInstructor(@PathVariable String instructor) {
146143
log.info("Fetching courses by instructor: {}", instructor);
@@ -184,54 +181,32 @@ public ResponseEntity<List<CourseResponse>> searchCoursesByTitle(@PathVariable S
184181
}
185182

186183
/**
187-
* Generates a brand-new course via GenAI + RAG, then persists & returns it.
188-
* Chosen as POST because we are **creating** a new server-side resource
189-
* (the course) – even though the body only contains “input” data.
190-
*/
191-
/*
192-
* New variant that accepts userId in path.
193-
* Example: POST /api/v1/courses/generate/learning_path/{userId}
194-
*/
184+
* Generates a brand-new course via GenAI + RAG, then persists & returns it.
185+
* Chosen as POST because we are **creating** a new server-side resource
186+
*/
195187
@PostMapping("/generate/learning_path/{userId}")
196-
public ResponseEntity<CourseResponse> generateCourseForUser(@PathVariable String userId,
197-
@RequestBody LearningPathRequest req,
198-
HttpServletRequest servletRequest) {
199-
200-
201-
// Fetch user profile from user-service just for logging/demo
202-
try {
203-
String profileUrl = userServiceUri + "/api/v1/users/" + userId + "/profile";
204-
String authHeader = servletRequest.getHeader("Authorization");
205-
HttpHeaders headers = new HttpHeaders();
206-
if (authHeader != null && !authHeader.isBlank()) {
207-
headers.set("Authorization", authHeader);
208-
}
209-
HttpEntity<Void> entity = new HttpEntity<>(headers);
210-
String profileJson = restTemplate.exchange(profileUrl, HttpMethod.GET, entity, String.class).getBody();
211-
log.info("User profile fetched via user-service: {}", profileJson);
212-
// extract skills field from JSON
213-
List<String> extractedSkills = null;
214-
try {
215-
ObjectMapper mapper = new ObjectMapper();
216-
JsonNode root = mapper.readTree(profileJson);
217-
JsonNode skillsNode = root.get("skills");
218-
if (skillsNode != null && skillsNode.isArray()) {
219-
extractedSkills = mapper.convertValue(skillsNode, new TypeReference<List<String>>() {});
220-
}
221-
} catch (Exception parseEx) {
222-
log.warn("Could not parse skills from user profile: {}", parseEx.getMessage());
223-
}
224-
log.info("Generating course for user={} prompt='{}' skills={}", userId, req.prompt(), extractedSkills);
225-
req = new LearningPathRequest(req.prompt(), extractedSkills);
226-
227-
} catch (Exception ex) {
228-
log.warn("Failed to fetch user profile for {}: {}", userId, ex.getMessage());
229-
}
230-
231-
// create the course after we logged the profile
232-
CourseResponse generated = courseService.generateFromGenAi(req);
233-
CourseResponse enrolled = courseService.enrollUserInCourse(generated.getId(), userId);
234-
return ResponseEntity.ok(enrolled);
188+
public ResponseEntity<CourseRequest> generateCourseForUser(@PathVariable String userId, @RequestBody LearningPathRequest req, HttpServletRequest servletRequest) {
189+
log.info("Generating course for user: {}", userId);
190+
CourseRequest generated = courseService.generateCourseFromGenAi(req, userId, servletRequest.getHeader("Authorization"));
191+
return ResponseEntity.ok(generated);
235192
}
236193

194+
195+
/**
196+
* Confirms the generation of a course from a Learning Path request.
197+
* This method is called after the course has been generated and the user has reviewed it.
198+
* It retrieves the last generated course details, creates the course in the database, enrolls the user, and returns the course response.
199+
*
200+
* @param userId The ID of the user confirming the course generation.
201+
* @return CourseResponse containing the confirmed course details.
202+
*/
203+
@PostMapping("/generate/learning_path/{userId}/confirm")
204+
public ResponseEntity<CourseResponse> confirmGeneratedCourse(@PathVariable String userId) {
205+
log.info("Confirming course generation for user: {}", userId);
206+
CourseResponse confirmed = courseService.confirmCourseGeneration(userId);
207+
return ResponseEntity.ok(confirmed);
208+
}
209+
210+
211+
237212
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.gitittogether.skillForge.server.course.dto.response.utils;
2+
3+
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
9+
10+
@Data
11+
@AllArgsConstructor
12+
@NoArgsConstructor
13+
@Builder
14+
public class EmbedResult {
15+
private boolean success;
16+
private String url;
17+
private Integer chunksEmbedded;
18+
private String message;
19+
private String error;
20+
}
21+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
3+
4+
5+
package com.gitittogether.skillForge.server.course.dto.response.utils;
6+
7+
8+
import lombok.AllArgsConstructor;
9+
import lombok.Builder;
10+
import lombok.Data;
11+
import lombok.NoArgsConstructor;
12+
13+
14+
@Data
15+
@NoArgsConstructor
16+
@AllArgsConstructor
17+
@Builder
18+
public class PromptResponse {
19+
private String prompt;
20+
private String generated_text;
21+
private String provider;
22+
23+
24+
}
25+
26+

0 commit comments

Comments
 (0)