Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
6afa760
Listen for UserHexadPlayerTypeSetEvent, persist the received player-t…
MP-DG Nov 27, 2025
6f821b5
added skill level to prompt, stores skill level, no longer uses sql
MP-DG Dec 3, 2025
58d5596
added proactive feedback. Currently accessable through query in the f…
MP-DG Dec 14, 2025
d572852
bugfix filtering for sources
MP-DG Dec 14, 2025
6057ad7
tutor saves conversation context
MP-DG Dec 14, 2025
2606977
saves students code recieved in student-code-submitted event
MP-DG Dec 15, 2025
b0d0cc5
implemented ai tutor can answer questions about submitted code
MP-DG Dec 15, 2025
531b355
fixed proactive feedback for code assignment
MP-DG Dec 15, 2025
3d9009b
updated meitrex-common to full release
MP-DG Dec 15, 2025
8e57d1d
added tests for SubscriptionController
MP-DG Dec 15, 2025
8b9eef1
added tests for ConversationHistoryService
MP-DG Dec 15, 2025
42dd4d8
added tests for UserPlayerTypeService
MP-DG Dec 15, 2025
dc01749
added tests for UserSkillLevelService
MP-DG Dec 15, 2025
e27c568
added tests for UserPlyerTypeEntity
MP-DG Dec 16, 2025
57b2744
added tests for persistance/mapper/*
MP-DG Dec 16, 2025
a2f1ff4
added more testcases for ProactiveFeedbackService
MP-DG Dec 16, 2025
0e6e76d
updated tests for TutorService
MP-DG Dec 16, 2025
9a01a6d
added tests for TutorController
MP-DG Dec 16, 2025
8a981bb
updated test for UserSkillLevelMapper
MP-DG Dec 16, 2025
77e1757
added tests for HintService
MP-DG Dec 16, 2025
0ff0aa2
added missing @Transactional annotation
MP-DG Dec 16, 2025
400abed
fixed sonar qube issue with unboxing
MP-DG Dec 16, 2025
b21cad6
fixed sonar qube issue
MP-DG Dec 16, 2025
6a245ce
changed percentage.doubleValue to double (percentage)
MP-DG Dec 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,8 @@ out/
### VS Code ###
.vscode/

# Auto-generated API documentation
**/api.md
**/combined.graphql

node_modules/
6 changes: 4 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ repositories {
}

dependencies {
implementation 'de.unistuttgart.iste.meitrex:meitrex-common:1.4.1'
implementation 'de.unistuttgart.iste.meitrex:meitrex-common:1.4.12'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-graphql'
implementation 'org.springframework.boot:spring-boot-starter-validation'
Expand All @@ -121,12 +121,14 @@ dependencies {
implementation 'com.graphql-java:graphql-java-extended-validation:22.0'
implementation 'de.unistuttgart.iste.meitrex:content_service:1.0.9'
implementation 'com.fasterxml.jackson.module:jackson-module-jsonSchema:2.17.1'
implementation 'io.dapr:dapr-sdk-springboot:1.12.0'
// implementation 'de.unistuttgart.iste.meitrex:gamification_service:1.0.0'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Soll das auskommentiert sein?

compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'de.unistuttgart.iste.meitrex:meitrex-common-test:1.2.6'
testImplementation 'de.unistuttgart.iste.meitrex:meitrex-common-test:1.4.12'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework:spring-webflux'
testImplementation 'org.springframework.graphql:spring-graphql-test'
Expand Down
3 changes: 3 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ sourceControl {
gitRepository(uri('https://github.com/MEITREX/content_service')) {
producesModule('de.unistuttgart.iste.meitrex:content_service')
}
gitRepository(uri('https://github.com/MP-DG/gamification_service')) {
producesModule('de.unistuttgart.iste.meitrex:gamification_service')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package de.unistuttgart.iste.meitrex.tutor_service.controller;

import de.unistuttgart.iste.meitrex.common.event.ContentProgressedEvent;
import de.unistuttgart.iste.meitrex.common.event.HexadPlayerType;
import de.unistuttgart.iste.meitrex.common.event.StudentCodeSubmittedEvent;
import de.unistuttgart.iste.meitrex.common.event.UserHexadPlayerTypeSetEvent;
import de.unistuttgart.iste.meitrex.common.event.skilllevels.UserSkillLevelChangedEvent;
import de.unistuttgart.iste.meitrex.tutor_service.service.ProactiveFeedbackService;
import de.unistuttgart.iste.meitrex.tutor_service.service.StudentCodeSubmissionService;
import de.unistuttgart.iste.meitrex.tutor_service.service.UserPlayerTypeService;
import de.unistuttgart.iste.meitrex.tutor_service.service.UserSkillLevelService;
import io.dapr.Topic;
import io.dapr.client.domain.CloudEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;

/**
* Controller for subscribing to Dapr pub/sub events.
*/
@Slf4j
@RestController
@RequiredArgsConstructor
public class SubscriptionController {

private final UserPlayerTypeService userPlayerTypeService;
private final UserSkillLevelService userSkillLevelService;
private final ProactiveFeedbackService proactiveFeedbackService;
private final StudentCodeSubmissionService studentCodeSubmissionService;

/**
* Handles the user-hexad-player-type-set event.
* Saves the user's player type information when received.
*
* @param cloudEvent the cloud event containing the user hexad player type data
* @param headers request headers from Dapr
* @return Mono<Void> for reactive processing
*/
@Topic(name = "user-hexad-player-type-set", pubsubName = "meitrex")
@PostMapping(path = "/user-hexad-player-type-set-pubsub")
public Mono<Void> onUserHexadPlayerTypeSetEvent(@RequestBody CloudEvent<UserHexadPlayerTypeSetEvent> cloudEvent,
@RequestHeader Map<String, String> headers) {
return Mono.fromRunnable(() -> {
UserHexadPlayerTypeSetEvent event = cloudEvent.getData();

log.info("Received UserHexadPlayerTypeSetEvent for user: {}, primaryType: {}",
event.getUserId(),
event.getPrimaryPlayerType());

// Convert Float playerTypePercentages to Double
Map<HexadPlayerType, Double> playerTypePercentages = new HashMap<>();
event.getPlayerTypePercentages().forEach((type, percentage) -> {
if (percentage != null) {
playerTypePercentages.put(type, (double) percentage);
}
});

// Save the player type information
userPlayerTypeService.saveUserPlayerType(
event.getUserId(),
event.getPrimaryPlayerType(),
playerTypePercentages
);
});
}

/**
* Handles the user-skill-level-changed event.
* Saves the user's skill level information when received.
*
* @param cloudEvent the cloud event containing the user skill level data
* @param headers request headers from Dapr
* @return Mono<Void> for reactive processing
*/
@Topic(name = "user-skill-level-changed", pubsubName = "meitrex")
@PostMapping(path = "/user-skill-level-changed-pubsub")
public Mono<Void> onUserSkillLevelChangedEvent(@RequestBody CloudEvent<UserSkillLevelChangedEvent> cloudEvent,
@RequestHeader Map<String, String> headers) {
return Mono.fromRunnable(() -> {
UserSkillLevelChangedEvent event = cloudEvent.getData();

userSkillLevelService.saveUserSkillLevel(
event.getUserId(),
event.getSkillId(),
event.getNewValue()
);
});
}

/**
* Handles the content-progressed event.
* Generates proactive tutor feedback when a user completes an assignment.
* Currently supports proactive feedback for assignments and quizzes.
*
* @param cloudEvent the cloud event containing the content progressed data
* @param headers request headers from Dapr
* @return Mono<Void> for reactive processing
*/
@Topic(name = "content-progressed", pubsubName = "meitrex")
@PostMapping(path = "/content-progressed-pubsub")
public Mono<Void> onContentProgressedEvent(@RequestBody CloudEvent<ContentProgressedEvent> cloudEvent,
@RequestHeader Map<String, String> headers) {
return Mono.fromRunnable(() -> {
ContentProgressedEvent event = cloudEvent.getData();

if (event == null) {
log.warn("Received ContentProgressedEvent with null data");
return;
}

log.info("Received ContentProgressedEvent for user: {}, content: {}, type: {}",
event.getUserId(),
event.getContentId(),
event.getContentType());

if (event.getContentType() == ContentProgressedEvent.ContentType.ASSIGNMENT ||
event.getContentType() == ContentProgressedEvent.ContentType.QUIZ) {
try {
proactiveFeedbackService.generateFeedback(event);
} catch (Exception e) {
log.error("Failed to generate feedback for user {} on content {}: {}",
event.getUserId(), event.getContentId(), e.getMessage(), e);
}
}
});
}

/**
* Handles the student-code-submitted event.
* Saves the student's code submission when received.
* Only keeps the latest submission per student per assignment.
*
* @param cloudEvent the cloud event containing the student code submission data
* @param headers request headers from Dapr
* @return Mono<Void> for reactive processing
*/
@Topic(name = "student-code-submitted", pubsubName = "meitrex")
@PostMapping(path = "/student-code-submitted-pubsub")
public Mono<Void> onStudentCodeSubmittedEvent(@RequestBody CloudEvent<StudentCodeSubmittedEvent> cloudEvent,
@RequestHeader Map<String, String> headers) {
return Mono.fromRunnable(() -> {
StudentCodeSubmittedEvent event = cloudEvent.getData();

if (event == null) {
log.warn("Received StudentCodeSubmittedEvent with null data");
return;
}

log.info("Received StudentCodeSubmittedEvent for student: {}, assignment: {}, commit: {}",
event.getStudentId(),
event.getAssignmentId(),
event.getCommitSha());

try {
studentCodeSubmissionService.saveCodeSubmission(
event.getStudentId(),
event.getAssignmentId(),
event.getCourseId(),
event.getRepositoryUrl(),
event.getCommitSha(),
event.getCommitTimestamp(),
event.getFiles(),
event.getBranch()
);
} catch (Exception e) {
log.error("Error processing StudentCodeSubmittedEvent for student {} on assignment {}: {}",
event.getStudentId(), event.getAssignmentId(), e.getMessage(), e);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package de.unistuttgart.iste.meitrex.tutor_service.controller;

import de.unistuttgart.iste.meitrex.generated.dto.ProactiveFeedback;
import de.unistuttgart.iste.meitrex.generated.dto.HintGenerationInput;
import de.unistuttgart.iste.meitrex.generated.dto.HintResponse;
import de.unistuttgart.iste.meitrex.common.user_handling.LoggedInUser;
import de.unistuttgart.iste.meitrex.generated.dto.LectureQuestionResponse;
import de.unistuttgart.iste.meitrex.tutor_service.persistence.entity.ProactiveFeedbackEntity;
import de.unistuttgart.iste.meitrex.tutor_service.service.ProactiveFeedbackService;
import de.unistuttgart.iste.meitrex.tutor_service.service.HintService;
import de.unistuttgart.iste.meitrex.tutor_service.service.TutorService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.ContextValue;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
import org.springframework.stereotype.Controller;

import java.util.List;
Expand All @@ -24,6 +29,7 @@ public class TutorController {

private final TutorService tutorService;
private final HintService hintService;
private final ProactiveFeedbackService proactiveFeedbackService;

@MutationMapping
public LectureQuestionResponse sendMessage(
Expand Down Expand Up @@ -52,4 +58,48 @@ public HintResponse generateHint(
return hintService.generateHintWithQuestion(questionInput, courseId, currentUser);
}

@QueryMapping
public ProactiveFeedback proactiveFeedback(
@Argument final UUID assessmentId,
@ContextValue final LoggedInUser currentUser
) {
return proactiveFeedbackService.getFeedbackForAssignment(currentUser.getId(), assessmentId)
.map(this::entityToDto)
.orElse(null);
}

@QueryMapping
public List<ProactiveFeedback> allProactiveFeedback(
@ContextValue final LoggedInUser currentUser
) {
return proactiveFeedbackService.getAllFeedbackForUser(currentUser.getId())
.stream()
.map(this::entityToDto)
.toList();
}

/**
* GraphQL subscription for real-time proactive feedback.
* Frontend subscribes to this and receives feedback automatically when generated.
*
* @param userId the user to receive feedback for
* @return publisher emitting ProactiveFeedback events
*/
@SubscriptionMapping
public Publisher<ProactiveFeedback> proactiveFeedbackAdded(@Argument final UUID userId) {
log.info("User {} subscribed to proactive feedback", userId);
return proactiveFeedbackService.proactiveFeedbackStream(userId);
}

private ProactiveFeedback entityToDto(ProactiveFeedbackEntity entity) {
return ProactiveFeedback.builder()
.setId(entity.getId())
.setAssessmentId(entity.getAssessmentId())
.setFeedbackText(entity.getFeedbackText())
.setCorrectness(entity.getCorrectness())
.setSuccess(entity.getSuccess())
.setCreatedAt(entity.getCreatedAt())
.build();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package de.unistuttgart.iste.meitrex.tutor_service.persistence.entity;

import jakarta.persistence.*;
import lombok.*;

import java.time.OffsetDateTime;
import java.util.UUID;

/**
* Entity representing a conversation exchange between user and tutor.
* Stores question-answer pairs for context in subsequent interactions.
*/
@Entity
@Table(name = "conversation_history", indexes = {
@Index(name = "idx_conversation_history_user_id", columnList = "user_id"),
@Index(name = "idx_conversation_history_course_id", columnList = "course_id"),
@Index(name = "idx_conversation_history_timestamp", columnList = "timestamp"),
@Index(name = "idx_conversation_history_user_course", columnList = "user_id,course_id")
})
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ConversationHistoryEntity {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", nullable = false)
private UUID id;

@Column(name = "user_id", nullable = false)
private UUID userId;

@Column(name = "course_id", nullable = false)
private UUID courseId;

@Column(name = "user_message", nullable = false, columnDefinition = "TEXT")
private String userMessage;

@Column(name = "tutor_response", nullable = false, columnDefinition = "TEXT")
private String tutorResponse;

@Column(name = "timestamp", nullable = false)
private OffsetDateTime timestamp;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package de.unistuttgart.iste.meitrex.tutor_service.persistence.entity;

import jakarta.persistence.*;
import lombok.*;

import java.time.OffsetDateTime;
import java.util.UUID;

/**
* Entity representing feedback generated for a user after completing an assignment.
*/
@Entity
@Table(name = "proactive_feedback", indexes = {
@Index(name = "idx_proactive_feedback_user_id", columnList = "user_id"),
@Index(name = "idx_proactive_feedback_assessment_id", columnList = "assessment_id"),
@Index(name = "idx_proactive_feedback_user_assessment", columnList = "user_id,assessment_id")
})
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProactiveFeedbackEntity {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id", nullable = false)
private UUID id;

@Column(name = "user_id", nullable = false)
private UUID userId;

@Column(name = "assessment_id", nullable = false)
private UUID assessmentId;

@Column(name = "feedback_text", nullable = false, columnDefinition = "TEXT")
private String feedbackText;

@Column(name = "correctness", nullable = false)
private Double correctness;

@Column(name = "success", nullable = false)
private Boolean success;

@Column(name = "created_at", nullable = false)
private OffsetDateTime createdAt;
}
Loading
Loading