Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6e2a9a9
Add UML exercise entities/repositories
ZiyaSelim Jan 5, 2026
71887dc
Add mutations/queries for uml exercise
ZiyaSelim Jan 5, 2026
61d709b
Add initial manual feedback generation
ZiyaSelim Jan 25, 2026
6d59025
Add new mutations/queries
ZiyaSelim Jan 25, 2026
d2d7330
Implement new mutations/queries
ZiyaSelim Jan 25, 2026
40e446e
Fix SQL query mistake
ZiyaSelim Jan 25, 2026
213a623
Add save and creation functions for a students solution
ZiyaSelim Jan 28, 2026
3d2db84
Remove unnecessary assignmentId
ZiyaSelim Jan 28, 2026
ece34c0
Change minimum required role in course to student for uml mutation
ZiyaSelim Jan 31, 2026
cfa1846
Change argument name to submit to match schema
ZiyaSelim Jan 31, 2026
2a2b6f1
Change submit to type Boolean
ZiyaSelim Jan 31, 2026
96f15dc
Change submit_at to be nullable
ZiyaSelim Jan 31, 2026
36be1d3
Fix evaluation mutation
ZiyaSelim Feb 2, 2026
695b2fe
Add check if creation of new solution is possible
ZiyaSelim Feb 2, 2026
aca5f56
Make amount of points randomized for testing purpose
ZiyaSelim Feb 2, 2026
20c7496
Add UML Diagram class
ZiyaSelim Feb 16, 2026
1a846a8
Create evaluation with LLM via the OllamaClient in meitrex-common
ZiyaSelim Feb 16, 2026
636631b
Update Uml Exercise
c-neli-r Mar 1, 2026
4c427b2
Merge branch 'fopro-uml-assessment' of https://github.com/MEITREX/ass…
c-neli-r Mar 1, 2026
2e61f14
Fix build error
c-neli-r Mar 1, 2026
b39e4ff
Add new api key
ZiyaSelim Apr 1, 2026
2e40afc
Add grading rules and fix some errors
ZiyaSelim Apr 1, 2026
3b6dff1
Add .env.example
ZiyaSelim Apr 1, 2026
cfa74b8
Update model and meitrex common version
ZiyaSelim Apr 1, 2026
e59850a
Add error handling if single request fails
ZiyaSelim Apr 1, 2026
48c5174
Revert accidental dockerfile commit
ZiyaSelim Apr 27, 2026
5d55cc8
Update versions
ZiyaSelim May 2, 2026
ffebbd8
Update env variables for new version of ollamaService
ZiyaSelim May 2, 2026
a8b1cd7
Add .env file in docker compose script
ZiyaSelim May 2, 2026
2acb0c6
Update prompts
ZiyaSelim May 2, 2026
9b7d110
Update Evaluation to use queues
ZiyaSelim May 2, 2026
466fbb3
Remove TestController that was used for evaluation
ZiyaSelim May 2, 2026
ce5b90e
Remove TestController that was used for evaluation and add analysis p…
ZiyaSelim May 7, 2026
e4fc723
Fix failing test
ZiyaSelim May 7, 2026
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Copy this file to .env and fill in real values.
# Never commit .env to git.

# OLLAMA API key used by Spring property: ollama.apiKey
OLLAMA_API_KEY=
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,8 @@ out/
.vscode/

node_modules/

# Local environment secrets
.env
.env.local
.env.*.local
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ It handles assignment creation, grading, and publishing grading results. Externa
| spring.datasource.url | PostgreSQL database URL | jdbc:postgresql://localhost:1132/assignment_service | jdbc:postgresql://assignment-service-db-postgresql:5432/assignment-service |
| spring.datasource.username | Database usernam | root | gits |
| spring.datasource.password | Database password | root | *secret* |
| OLLAMA_API_KEY | API key for LLM requests (ollama.apiKey) | set via local .env file | set via deployment secret/env var |
| DAPR_HTTP_PORT | Dapr HTTP Port* | 1100 | 3500 |
| server.port | Port on which the application runs | 1101 | 1101 |
| course_service.url | URL for course service GraphQL | http://localhost:2001/graphql | http://localhost:3500/v1.0/invoke/course-service/method/graphql |
Expand All @@ -36,6 +37,21 @@ It handles assignment creation, grading, and publishing grading results. Externa
| logging.level.root | Logging level for root logger | DEBUG | - |
| DAPR_GRPC_PORT | Dapr gRPC Port | - | 50001 |

## Local secrets with .env

For local development, keep secrets in a root-level `.env` file. The application imports this file via Spring configuration.

1. Create `.env` in the repository root.
2. Add your key:

```properties
OLLAMA_API_KEY=your_real_key_here
```

3. Do not commit `.env` (already ignored via `.gitignore`).

Use `.env.example` as the template for required keys.

## API description

The GraphQL API is described in the [api.md file](api.md).
Expand Down
6 changes: 3 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ repositories {
}

dependencies {
implementation 'de.unistuttgart.iste.meitrex:meitrex-common:1.4.12'
implementation 'de.unistuttgart.iste.meitrex:content_service:1.5.0rc7'
implementation 'de.unistuttgart.iste.meitrex:meitrex-common:1.6.0'
implementation 'de.unistuttgart.iste.meitrex:content_service:1.6.1'
implementation 'de.unistuttgart.iste.meitrex:course_service:1.1.0rc2'
implementation 'de.unistuttgart.iste.meitrex:user_service:1.0.0rc1'
implementation 'com.google.code.gson:gson:2.13.1'
Expand All @@ -134,7 +134,7 @@ dependencies {
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.4.12'
testImplementation 'de.unistuttgart.iste.meitrex:meitrex-common-test:1.6.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework:spring-webflux'
testImplementation 'org.springframework.graphql:spring-graphql-test'
Expand Down
21 changes: 14 additions & 7 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,26 @@ services:
- "1101:1101"
depends_on:
- database
env_file:
- ./../assignment_service/.env
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://database:5432/assignment_service
SPRING_DATASOURCE_USERNAME: root
SPRING_DATASOURCE_PASSWORD: root
dapr-assignment:
image: "daprio/daprd"
command: [
"./daprd",
"--app-id", "assignment_service",
"--app-port", "1101",
"--dapr-http-port", "1100",
"--resources-path", "./components"
]
command:
[
"./daprd",
"--app-id",
"assignment_service",
"--app-port",
"1101",
"--dapr-http-port",
"1100",
"--resources-path",
"./components",
]
volumes:
- "./../assignment_service/components/:/components" # Mount our components folder for the runtime to use. The mounted location must match the --resources-path argument.
depends_on:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.EnableAsync;

import java.util.Arrays;

Expand All @@ -11,6 +13,8 @@
* <p>
*/
@SpringBootApplication
@EnableScheduling
@EnableAsync
@Slf4j
public class AssignmentServiceApplication {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package de.unistuttgart.iste.meitrex.assignment_service.config;

import de.unistuttgart.iste.meitrex.common.ollama.OllamaClient;
import de.unistuttgart.iste.meitrex.common.config.OllamaConfig;
import de.unistuttgart.iste.meitrex.common.service.JsonSchemaGeneratorService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.net.http.HttpClient;
import java.time.Duration;

@Configuration
public class OllamaClientConfiguration {

@Bean
public OllamaConfig ollamaConfig() {
return new OllamaConfig();
}

@Bean
public HttpClient ollamaHttpClient() {
return HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.build();
}

@Bean
public JsonSchemaGeneratorService jsonSchemaGeneratorService() {
return new JsonSchemaGeneratorService();
}

@Bean
public OllamaClient ollamaClient(OllamaConfig config,
JsonSchemaGeneratorService schemaService,
ObjectMapper objectMapper,
HttpClient ollamaHttpClient) {
return new OllamaClient(config, schemaService, objectMapper, ollamaHttpClient);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package de.unistuttgart.iste.meitrex.assignment_service.controller;

import de.unistuttgart.iste.meitrex.assignment_service.persistence.entity.umlExercise.UmlEvaluationJobStatus;
import de.unistuttgart.iste.meitrex.assignment_service.service.uml_assignment.UmlExerciseService;
import de.unistuttgart.iste.meitrex.assignment_service.service.uml_assignment.UmlEvaluationQueueService;
import de.unistuttgart.iste.meitrex.common.user_handling.LoggedInUser;
import de.unistuttgart.iste.meitrex.generated.dto.*;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.graphql.data.method.annotation.*;
import org.springframework.stereotype.Controller;

import java.util.List;
import java.util.UUID;

@Slf4j
@Controller
@RequiredArgsConstructor
public class UmlExerciseController {

private final UmlExerciseService umlExerciseService;
private final UmlEvaluationQueueService umlEvaluationQueueService;

@MutationMapping(name = "_internal_noauth_createUmlExercise")
public UmlExercise createUmlExercise(@Argument final UUID courseId,
@Argument final UUID assessmentId,
@Argument final CreateUmlExerciseInput input) {
return umlExerciseService.createExercise(courseId, assessmentId, input);
}

@MutationMapping
public UmlExerciseMutation mutateUmlExercise(@Argument final UUID assessmentId,
@ContextValue final LoggedInUser currentUser) {
return umlExerciseService.mutateUmlExercise(assessmentId, currentUser);
}

@SchemaMapping(typeName = "UmlExerciseMutation")
public UmlExercise updateTutorSolution(final UmlExerciseMutation mutation,
@Argument final UmlDiagramInput tutorSolution) {
return umlExerciseService.updateTutorSolution(mutation.getAssessmentId(), tutorSolution);
}

@SchemaMapping(typeName = "UmlExerciseMutation")
public UmlExercise updateUmlExercise(final UmlExerciseMutation mutation,
@Argument final UpdateUmlExerciseInput input) {
return umlExerciseService.updateUmlExercise(mutation.getAssessmentId(), input);
}

@SchemaMapping(typeName = "UmlExerciseMutation")
public UmlStudentSolution createUmlSolution(final UmlExerciseMutation mutation,
@Argument UUID studentId,
@Argument boolean createFromPrevious) {
log.info("Mutation: createUmlSolution for assessmentId={}, studentId={}", mutation.getAssessmentId(), studentId);
return umlExerciseService.createNewSolution(mutation.getAssessmentId(), studentId, createFromPrevious);
}

@SchemaMapping(typeName = "UmlExerciseMutation")
public UmlStudentSolution saveStudentSolution(final UmlExerciseMutation mutation,
@Argument final UUID studentId,
@Argument final UmlDiagramInput diagram,
@Argument final UUID solutionId,
@Argument final Boolean submit) {
return umlExerciseService.saveStudentSolution(
mutation.getAssessmentId(), studentId, diagram, solutionId, submit != null && submit);
}

@QueryMapping
public UmlExercise getUmlExerciseByAssessmentId(@Argument UUID assessmentId) {
return umlExerciseService.getExerciseByAssessmentId(assessmentId);
}

@SchemaMapping(typeName = "UmlExercise")
public List<UmlStudentSolution> solutionsByStudent(UmlExercise exercise, @Argument UUID studentId) {
return umlExerciseService.getSolutionsByStudent(exercise, studentId);
}

@SchemaMapping(typeName = "UmlExercise")
public UmlStudentSolution latestSolution(UmlExercise exercise, @Argument UUID studentId) {
return umlExerciseService.getSolutionsByStudent(exercise, studentId).stream()
.findFirst()
.orElse(null);
}

@SchemaMapping(typeName = "UmlExerciseMutation")
public UmlStudentSolution evaluateLatestSolution(
final UmlExerciseMutation mutation,
@Argument UUID studentId) {
return umlExerciseService.enqueueLatestSolutionForEvaluation(mutation.getAssessmentId(), studentId);
}

@SchemaMapping(typeName = "UmlStudentSolution")
public UmlEvaluationJobStatus evaluationStatus(UmlStudentSolution solution) {
if (solution == null || solution.getId() == null) {
return null;
}
return umlEvaluationQueueService.getJobStatus(solution.getId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.unistuttgart.iste.meitrex.assignment_service.exception;

public class AiEvaluationException extends RuntimeException {
public AiEvaluationException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package de.unistuttgart.iste.meitrex.assignment_service.persistence.entity.umlExercise;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import lombok.*;

@Embeddable
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UmlDiagram {

@Column(name = "diagram", columnDefinition = "TEXT")
private String diagramCode;

@Column(columnDefinition = "TEXT")
private String semanticModel;

/**
* Helper to check if the diagram is actually "empty"
*/
public boolean isEmpty() {
return (diagramCode == null || diagramCode.isBlank()) && (semanticModel == null || semanticModel.isBlank());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package de.unistuttgart.iste.meitrex.assignment_service.persistence.entity.umlExercise;

import jakarta.persistence.*;
import lombok.*;
import de.unistuttgart.iste.meitrex.common.persistence.IWithId;

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

@Entity(name = "UmlEvaluationJob")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UmlEvaluationJobEntity implements IWithId<UUID> {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "solution_id", nullable = false)
private UmlStudentSolutionEntity solution;

@Enumerated(EnumType.STRING)
@Column(nullable = false)
private UmlEvaluationJobStatus status;

@Column(columnDefinition = "TEXT")
private String errorMessage;

@Column
private OffsetDateTime createdAt;

@Column
private OffsetDateTime startedAt;

@Column
private OffsetDateTime completedAt;

@Version
private Long version;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package de.unistuttgart.iste.meitrex.assignment_service.persistence.entity.umlExercise;

public enum UmlEvaluationJobStatus {
ENQUEUED,
PROCESSING,
DONE,
FAILED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package de.unistuttgart.iste.meitrex.assignment_service.persistence.entity.umlExercise;

import jakarta.persistence.*;
import lombok.*;
import de.unistuttgart.iste.meitrex.common.persistence.IWithId;

import java.util.List;
import java.util.UUID;

@Entity(name = "UmlExercise")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UmlExerciseEntity implements IWithId<UUID> {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;

@Column(nullable = false, unique = true)
private UUID assessmentId;

@Column(nullable = false)
private UUID courseId;

@Column(nullable = false, columnDefinition = "TEXT")
private String description;

@Column(nullable = false)
private boolean showSolution;

@Embedded
private UmlDiagram tutorSolution;

@Column(columnDefinition = "TEXT")
private String gradingRules;

@Column(nullable = false)
private int totalPoints;

@Column(nullable = false)
private double requiredPercentage;

@OneToMany(mappedBy = "exercise", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UmlStudentSubmissionEntity> studentSubmissions;
}
Loading
Loading