From a3b0b7284b86c84074174685db8fbd588a4f8c45 Mon Sep 17 00:00:00 2001 From: Can Date: Thu, 7 May 2026 18:28:45 +0200 Subject: [PATCH] Update to new ollamaClient version --- .env.example | 5 + .gitignore | 5 + build.gradle | 6 +- docker-compose.yml | 2 + package-lock.json | 116 ++++++++++++ package.json | 5 + .../TutorServiceApplication.java | 1 + .../config/OllamaClientConfiguration.java | 40 ++++ .../tutor_service/config/OllamaConfig.java | 20 -- .../tutor_service/service/HintService.java | 57 +++--- .../tutor_service/service/OllamaService.java | 175 ------------------ .../service/ProactiveFeedbackService.java | 36 ++-- .../tutor_service/service/TutorService.java | 72 ++++--- .../service/models/OllamaRequest.java | 26 --- .../service/models/OllamaResponse.java | 147 --------------- .../service/models/TemplateArgs.java | 13 -- src/main/resources/application-dev.properties | 8 +- .../resources/application-prod.properties | 8 +- src/main/resources/application.properties | 8 +- .../service/HintServiceTest.java | 115 ++++++------ .../service/ProactiveFeedbackServiceTest.java | 25 ++- .../service/TutorServiceTest.java | 44 ++--- src/test/resources/application.properties | 8 +- 23 files changed, 356 insertions(+), 586 deletions(-) create mode 100644 .env.example create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/main/java/de/unistuttgart/iste/meitrex/tutor_service/config/OllamaClientConfiguration.java delete mode 100644 src/main/java/de/unistuttgart/iste/meitrex/tutor_service/config/OllamaConfig.java delete mode 100644 src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/OllamaService.java delete mode 100644 src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/models/OllamaRequest.java delete mode 100644 src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/models/OllamaResponse.java delete mode 100644 src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/models/TemplateArgs.java diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3775d3c --- /dev/null +++ b/.env.example @@ -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= diff --git a/.gitignore b/.gitignore index 0a638ed..a64bb8c 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,8 @@ out/ **/combined.graphql node_modules/ + +# Local environment secrets +.env +.env.local +.env.*.local diff --git a/build.gradle b/build.gradle index eb29c1f..e91e845 100644 --- a/build.gradle +++ b/build.gradle @@ -108,7 +108,7 @@ repositories { } dependencies { - implementation 'de.unistuttgart.iste.meitrex:meitrex-common:1.4.12' + implementation 'de.unistuttgart.iste.meitrex:meitrex-common:1.6.0' 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' @@ -119,7 +119,7 @@ dependencies { implementation 'org.modelmapper:modelmapper:3.+' implementation 'com.graphql-java:graphql-java-extended-scalars:22.0' implementation 'com.graphql-java:graphql-java-extended-validation:22.0' - implementation 'de.unistuttgart.iste.meitrex:content_service:1.0.9' + implementation 'de.unistuttgart.iste.meitrex:content_service:1.6.1' implementation 'com.fasterxml.jackson.module:jackson-module-jsonSchema:2.17.1' implementation 'io.dapr:dapr-sdk-springboot:1.12.0' compileOnly 'org.projectlombok:lombok' @@ -127,7 +127,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' diff --git a/docker-compose.yml b/docker-compose.yml index 62c2850..0240bce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,8 @@ services: - "1301:1301" depends_on: - database + env_file: + - ./../tutor_service/.env environment: SPRING_DATASOURCE_URL: jdbc:postgresql://database:5432/tutor_service SPRING_DATASOURCE_USERNAME: root diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3134afe --- /dev/null +++ b/package-lock.json @@ -0,0 +1,116 @@ +{ + "name": "tutor_service", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "graphql-markdown": "^7.3.0" + } + }, + "node_modules/deep-diff": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz", + "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==", + "license": "MIT" + }, + "node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-markdown": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/graphql-markdown/-/graphql-markdown-7.3.0.tgz", + "integrity": "sha512-xTgP+W156DD3k55KpwvNUtV7RTJmGKQhvCzseUXSnJf7KrZozhyyxhq4bv1IiOJgGeRjt5zwHBbEcAEEWCacrg==", + "license": "MIT", + "dependencies": { + "deep-diff": "^1.0.2", + "lodash.isplainobject": "^4.0.6", + "minimist": "^1.2.6", + "node-fetch": "^2.0.0", + "resolve-from": "^5.0.0" + }, + "bin": { + "graphql-markdown": "src/index.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "graphql": "^14.0.2 || ^15.0.0 || ^16.0.0" + } + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..421e172 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "graphql-markdown": "^7.3.0" + } +} diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/TutorServiceApplication.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/TutorServiceApplication.java index dd009ea..196c591 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/TutorServiceApplication.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/TutorServiceApplication.java @@ -16,3 +16,4 @@ public static void main(String[] args) { } } + diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/config/OllamaClientConfiguration.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/config/OllamaClientConfiguration.java new file mode 100644 index 0000000..ec904b8 --- /dev/null +++ b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/config/OllamaClientConfiguration.java @@ -0,0 +1,40 @@ +package de.unistuttgart.iste.meitrex.tutor_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); + } +} diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/config/OllamaConfig.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/config/OllamaConfig.java deleted file mode 100644 index 9bcbb63..0000000 --- a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/config/OllamaConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package de.unistuttgart.iste.meitrex.tutor_service.config; - -import lombok.Getter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; - -@Configuration() -@Getter -public class OllamaConfig { - - @Value("${ollama.url}") - private String url; - - @Value("${ollama.model}") - private String model; - - @Value("${ollama.endpoint}") - private String endpoint; - -} diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/HintService.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/HintService.java index 1534154..b966b21 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/HintService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/HintService.java @@ -2,6 +2,7 @@ import de.unistuttgart.iste.meitrex.common.event.HexadPlayerType; import de.unistuttgart.iste.meitrex.common.user_handling.LoggedInUser; +import de.unistuttgart.iste.meitrex.common.ollama.OllamaClient; import de.unistuttgart.iste.meitrex.generated.dto.*; import de.unistuttgart.iste.meitrex.tutor_service.service.models.*; import lombok.RequiredArgsConstructor; @@ -9,6 +10,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -18,7 +20,7 @@ @Service @RequiredArgsConstructor public class HintService { - private final OllamaService ollamaService; + private final OllamaClient ollamaClient; private final SemanticSearchService semanticSearchService; private final UserPlayerTypeService userPlayerTypeService; @@ -41,11 +43,11 @@ public HintResponse generateHintWithQuestion( // First fill the prompt corresponding to the question type HintGenerationData generationData = getGenerationData(input); String promptName = PROMPT_TEMPLATES.get("QUESTION").replace("{QUESTION_TYPE}", input.getType().toString()); - String questionPrompt = ollamaService.getTemplate(promptName); - List questionPromptArgs = List.of( - TemplateArgs.builder().argumentName("questionText").argumentValue(generationData.getQuestionText()).build(), - TemplateArgs.builder().argumentName("options").argumentValue(generationData.getOptionsText()).build()); - questionPrompt = ollamaService.fillTemplate(questionPrompt, questionPromptArgs); + String questionPrompt = ollamaClient.getTemplate(promptName); + Map questionPromptArgs = new HashMap<>(); + questionPromptArgs.put("questionText", generationData.getQuestionText()); + questionPromptArgs.put("options", generationData.getOptionsText()); + questionPrompt = ollamaClient.fillTemplate(questionPrompt, questionPromptArgs); /* * Perform a semantic search using the query defined in HintGenerationData @@ -72,17 +74,20 @@ public HintResponse generateHintWithQuestion( * Now generate the hint given the question, answer options (which is injected via the questionPrompt) and * the text inside segments which have been found relevant via the semantic search */ - String prompt = ollamaService.getTemplate(PROMPT_TEMPLATES.get("GENERATION")); String contentString = semanticSearchService.formatIntoNumberedListForPrompt( documentSegments.stream().map(DocumentRecordSegment::getText).toList()); String gamificationPrompt = generateGamificationPrompt(courseId, currentUser); - List promptArgs = List.of( - TemplateArgs.builder().argumentName("questionPrompt").argumentValue(questionPrompt).build(), - TemplateArgs.builder().argumentName("content").argumentValue(contentString).build(), - TemplateArgs.builder().argumentName("gamificationPrompt").argumentValue(gamificationPrompt).build() + Map promptArgs = new HashMap<>(); + promptArgs.put("questionPrompt", questionPrompt); + promptArgs.put("content", contentString); + promptArgs.put("gamificationPrompt", gamificationPrompt); + + return ollamaClient.startQuery( + HintResponse.class, + PROMPT_TEMPLATES.get("GENERATION"), + promptArgs, + new HintResponse("An error occurred") ); - - return ollamaService.startQuery(HintResponse.class, prompt, promptArgs, new HintResponse("An error occurred")); } /** @@ -160,12 +165,14 @@ private HintGenerationData buildAssociationData(HintAssociationInput input) { .toList()); // Generate a semantic search query based on the question text and the pairs - List promptArgs = List.of( - TemplateArgs.builder().argumentName("pairs").argumentValue(optionsString).build() + Map promptArgs = new HashMap<>(); + promptArgs.put("pairs", optionsString); + SemanticSearchQuery semanticSearchQuery = ollamaClient.startQuery( + SemanticSearchQuery.class, + PROMPT_TEMPLATES.get("SEMANTIC_SEARCH_QUERY_ASSOCIATION"), + promptArgs , + new SemanticSearchQuery(questionText) ); - String promptName = ollamaService.getTemplate(PROMPT_TEMPLATES.get("SEMANTIC_SEARCH_QUERY_ASSOCIATION")); - SemanticSearchQuery semanticSearchQuery = ollamaService.startQuery( - SemanticSearchQuery.class, promptName, promptArgs , new SemanticSearchQuery(questionText)); log.info("Generated search query {}", semanticSearchQuery.getQuery()); @@ -185,13 +192,15 @@ private HintGenerationData buildClozeData(HintClozeInput input) { String optionsString = semanticSearchService.formatIntoNumberedListForPrompt(blanks); // Generate a semantic search query based on the cloze and its corresponding answer options - List promptArgs = List.of( - TemplateArgs.builder().argumentName("clozeText").argumentValue(questionText).build(), - TemplateArgs.builder().argumentName("answers").argumentValue(optionsString).build() + Map promptArgs = new HashMap<>(); + promptArgs.put("clozeText", questionText); + promptArgs.put("answers", optionsString); + SemanticSearchQuery semanticSearchQuery = ollamaClient.startQuery( + SemanticSearchQuery.class, + PROMPT_TEMPLATES.get("SEMANTIC_SEARCH_QUERY_CLOZE"), + promptArgs, + new SemanticSearchQuery(questionText) ); - String promptName = ollamaService.getTemplate(PROMPT_TEMPLATES.get("SEMANTIC_SEARCH_QUERY_CLOZE")); - SemanticSearchQuery semanticSearchQuery = ollamaService.startQuery( - SemanticSearchQuery.class, promptName, promptArgs , new SemanticSearchQuery(questionText)); log.info("Generated search query {}", semanticSearchQuery.getQuery()); diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/OllamaService.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/OllamaService.java deleted file mode 100644 index 826bef8..0000000 --- a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/OllamaService.java +++ /dev/null @@ -1,175 +0,0 @@ -package de.unistuttgart.iste.meitrex.tutor_service.service; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import de.unistuttgart.iste.meitrex.tutor_service.config.OllamaConfig; -import de.unistuttgart.iste.meitrex.tutor_service.service.models.OllamaRequest; -import de.unistuttgart.iste.meitrex.tutor_service.service.models.OllamaResponse; -import de.unistuttgart.iste.meitrex.tutor_service.service.models.TemplateArgs; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.io.*; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -@Slf4j -@Service -@RequiredArgsConstructor -public class OllamaService { - - private final OllamaConfig config; - - private final ObjectMapper jsonMapper = new ObjectMapper(); - private final HttpClient client = HttpClient.newHttpClient(); - @Autowired - private JsonSchemaService jsonSchemaService; - - /** - * Loads a prompt template from the prompt_templates resource folder and returns its content as a string. - * - * @param templateFileName the name of the template file - * @return the content of the template file as a UTF-8 encoded string - * @throws RuntimeException if the file is not found or cannot be read - */ - public String getTemplate(String templateFileName) { - try{ - InputStream inputStream = this.getClass().getResourceAsStream("/prompt_templates/" + templateFileName); - if (inputStream == null) { - throw new FileNotFoundException("Template file not found: " + templateFileName); - } - log.info("Reading template: {}", templateFileName); - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); - StringBuilder template = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - template.append(line).append("\n"); - } - reader.close(); - return template.toString(); - } catch (IOException e) { - log.error("Failed to read template file: {}", templateFileName, e); - throw new RuntimeException("Failed to read template file: " + templateFileName, e); - } - } - - /** - * Replaces placeholders in a given prompt template with the corresponding argument values. - *

- * Each placeholder must be wrapped in double curly braces, e.g., {{argumentName}}. - * For every {@link TemplateArgs} provided, the method searches for its placeholder in the template - * and replaces it with the associated argument value. - *

- * - * @param promptTemplate the template string containing placeholders in the form {{argumentName}} - * @param args a list of {@link TemplateArgs} objects providing argument names and their values - * @return the template string with all placeholders replaced by their corresponding values - * @throws IllegalArgumentException if the template does not contain a placeholder for any provided argument - */ - public String fillTemplate(String promptTemplate, List args) { - String filledTemplate = promptTemplate; - for (TemplateArgs arg : args) { - String placeholder = "{{" + arg.getArgumentName() + "}}"; - if(!promptTemplate.contains(placeholder)){ - throw new IllegalArgumentException("No such argument in this prompt: " + placeholder); - } - filledTemplate = filledTemplate.replace(placeholder, arg.getArgumentValue()); - } - return filledTemplate; - } - - /** - * Starts a query to the LLM by filling a prompt template, sending it to Ollama, - * and parsing the response into the given type. - * - * @param responseType the target class to parse the response into - * @param prompt the template prompt text - * @param templateArgs the arguments used to fill the template - * @param error the fallback value if parsing or the request fails - * @return the parsed response or the fallback error value - */ - public ResponseType startQuery( - Class responseType, String prompt, List templateArgs, ResponseType error) { - try { - String filledPrompt = fillTemplate(prompt, templateArgs); - - TypeReference> typeRef = new TypeReference<>() {}; - Map schemaObject = jsonMapper.readValue( - jsonSchemaService.getJsonSchema(responseType), typeRef); - - OllamaRequest request = new OllamaRequest( - this.config.getModel(), filledPrompt, false, schemaObject); - OllamaResponse response = queryLLM(request); - Optional parsedResponse = - parseResponse(response, responseType); - return parsedResponse.orElse(error); - }catch (IOException | RuntimeException exception){ - log.error("Error while starting query: {}", exception.getMessage(), exception); - return error; - } catch (InterruptedException e) { - log.error("Query interrupted: {}", e.getMessage(), e); - Thread.currentThread().interrupt(); - return error; - } - } - - /** - * Sends the given request to the Ollama LLM endpoint and returns the raw response. - * - * @param request the request payload - * @return the response from Ollama - * @throws IOException if the request or response handling fails - * @throws InterruptedException if the HTTP call is interrupted - */ - private OllamaResponse queryLLM(OllamaRequest request) throws IOException, InterruptedException { - final String json = jsonMapper.writeValueAsString(request); - - HttpRequest req = HttpRequest.newBuilder() - .uri(URI.create(this.config.getUrl() + "/" + this.config.getEndpoint())) - .header("Content-Type", "application/json") - .POST(HttpRequest.BodyPublishers.ofString(json)) - .build(); - - HttpResponse response = client.send(req, HttpResponse.BodyHandlers.ofString()); - - OllamaResponse result = jsonMapper.readValue(response.body(), OllamaResponse.class); - - if (result.getError() != null) { - throw new RuntimeException("Ollama returned error: " + result.getError()); - } - - return result; - } - - /** - * parse an ollama response to a specify type. It expects the response to be a valid json - * If it fails to parse the response, it returns an empty optional - * - * @param ollamaResponse the response from the ollama server - * @param responseType the type to parse the response to - * @return an optional of the parsed response - * @param the type to cast to - */ - public Optional parseResponse(OllamaResponse ollamaResponse, Class responseType) { - final String response = ollamaResponse.getResponse(); - if(responseType == null || response == null) { - log.info("Response is null or empty: {}", response); - return Optional.empty(); - } - try { - return Optional.of(jsonMapper.readValue(response, responseType)); - } catch (IOException e) { - log.error("Failed to parse response: {}", response, e); - return Optional.empty(); - } - } -} diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/ProactiveFeedbackService.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/ProactiveFeedbackService.java index 0b2094d..0345e98 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/ProactiveFeedbackService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/ProactiveFeedbackService.java @@ -2,11 +2,10 @@ import de.unistuttgart.iste.meitrex.common.event.ContentProgressedEvent; import de.unistuttgart.iste.meitrex.common.event.HexadPlayerType; +import de.unistuttgart.iste.meitrex.common.ollama.OllamaClient; import de.unistuttgart.iste.meitrex.generated.dto.ProactiveFeedback; import de.unistuttgart.iste.meitrex.tutor_service.persistence.entity.ProactiveFeedbackEntity; -import de.unistuttgart.iste.meitrex.tutor_service.persistence.entity.UserSkillLevelEntity; import de.unistuttgart.iste.meitrex.tutor_service.persistence.repository.ProactiveFeedbackRepository; -import de.unistuttgart.iste.meitrex.tutor_service.service.models.TemplateArgs; import de.unistuttgart.iste.meitrex.tutor_service.service.models.TutorAnswer; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -29,7 +28,7 @@ @RequiredArgsConstructor public class ProactiveFeedbackService { - private final OllamaService ollamaService; + private final OllamaClient ollamaClient; private final UserPlayerTypeService userPlayerTypeService; private final UserSkillLevelService userSkillLevelService; private final ProactiveFeedbackRepository proactiveFeedbackRepository; @@ -105,33 +104,20 @@ public String generateFeedback(ContentProgressedEvent event) { codeContext = getCodeContextForAssignment(event.getUserId(), event.getContentId()); } - String prompt = ollamaService.getTemplate(FEEDBACK_PROMPT_TEMPLATE); - List promptArgs = List.of( - TemplateArgs.builder() - .argumentName("correctness") - .argumentValue(String.format("%.2f", event.getCorrectness())) - .build(), - TemplateArgs.builder() - .argumentName("performance") - .argumentValue(performanceContext) - .build(), - TemplateArgs.builder() - .argumentName("individualizedPrompt") - .argumentValue(individualizedPrompt) - .build(), - TemplateArgs.builder() - .argumentName("codeContext") - .argumentValue(codeContext) - .build() - ); + Map promptArgs = Map.of( + "correctness", String.format("%.2f", event.getCorrectness()), + "performance", performanceContext, + "individualizedPrompt", individualizedPrompt, + "codeContext", codeContext + ); String error = "Oops, something went wrong generating proactive feedback! Your correctness was " + String.format("%.2f", event.getCorrectness()) + ". Please try again later."; - TutorAnswer feedback = ollamaService.startQuery( - TutorAnswer.class, - prompt, + TutorAnswer feedback = ollamaClient.startQuery( + TutorAnswer.class, + FEEDBACK_PROMPT_TEMPLATE, promptArgs, new TutorAnswer(error) ); diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/TutorService.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/TutorService.java index 81f264b..a05811e 100644 --- a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/TutorService.java +++ b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/TutorService.java @@ -6,6 +6,7 @@ import de.unistuttgart.iste.meitrex.common.event.TutorCategory; import de.unistuttgart.iste.meitrex.common.user_handling.LoggedInUser; import de.unistuttgart.iste.meitrex.common.dapr.TopicPublisher; +import de.unistuttgart.iste.meitrex.common.ollama.OllamaClient; import de.unistuttgart.iste.meitrex.generated.dto.DocumentSource; import de.unistuttgart.iste.meitrex.generated.dto.LectureQuestionResponse; import de.unistuttgart.iste.meitrex.generated.dto.Source; @@ -18,7 +19,9 @@ import org.springframework.stereotype.Service; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -28,7 +31,7 @@ @RequiredArgsConstructor public class TutorService { - private final OllamaService ollamaService; + private final OllamaClient ollamaClient; private final SemanticSearchService semanticSearchService; private final TopicPublisher topicPublisher; private final UserPlayerTypeService userPlayerTypeService; @@ -79,7 +82,7 @@ public class TutorService { public LectureQuestionResponse handleUserQuestion(String userQuestion, UUID courseId, LoggedInUser currentUser){ log.info("[TUTOR] User {} asked question: {}", currentUser.getId(), userQuestion); - CategorizedQuestion categorizedQuestion = preprocessQuestion(userQuestion); + CategorizedQuestion categorizedQuestion = preprocessQuestion(userQuestion); TutorCategory category = categorizedQuestion.getCategory(); @@ -158,28 +161,23 @@ private LectureQuestionResponse answerLectureQuestion(String question, UUID cour double averageSkillLevel = getAverageSkillLevel(currentUser.getId()); log.info("User {} average skill level: {}", currentUser.getId(), averageSkillLevel); - String skillLevelPromotContent = getSkillBasedFeedbackStyle(averageSkillLevel); + String skillLevelPromptContent = getSkillBasedFeedbackStyle(averageSkillLevel); LectureQuestionResponse errorResponse = new LectureQuestionResponse(ERROR_MESSAGE, List.of()); - String prompt = ollamaService.getTemplate(PROMPT_TEMPLATES.get(1)); String contentString = semanticSearchService.formatIntoNumberedListForPrompt( documentSegments.stream().map(DocumentRecordSegment::getText).toList()); String conversationHistory = conversationHistoryService.formatHistoryForPrompt( currentUser.getId(), courseId); - - log.info("Conversation history for user {} in course {}: {}", - currentUser.getId(), courseId, conversationHistory.isEmpty() ? "No history" : "Has history"); - List promptArgs = List.of( - TemplateArgs.builder().argumentName("question").argumentValue(question).build(), - TemplateArgs.builder().argumentName("content").argumentValue(contentString).build(), - TemplateArgs.builder().argumentName("skill").argumentValue(skillLevelPromotContent).build(), - TemplateArgs.builder().argumentName("conversationHistory").argumentValue(conversationHistory).build() - ); + Map promptArgs = new HashMap<>(); + promptArgs.put("question", question); + promptArgs.put("content", contentString); + promptArgs.put("skill", skillLevelPromptContent); + promptArgs.put("conversationHistory", conversationHistory); - TutorAnswer response = ollamaService.startQuery( - TutorAnswer.class, prompt, promptArgs, new TutorAnswer(ERROR_MESSAGE)); + TutorAnswer response = ollamaClient.startQuery( + TutorAnswer.class, PROMPT_TEMPLATES.get(1), promptArgs, new TutorAnswer(ERROR_MESSAGE)); conversationHistoryService.addConversationExchange( currentUser.getId(), courseId, question, response.getAnswer()); @@ -269,23 +267,21 @@ private LectureQuestionResponse handleFollowUpQuestion(String question, UUID cou String skillLevelPromotContent = getSkillBasedFeedbackStyle(averageSkillLevel); - String prompt = ollamaService.getTemplate(PROMPT_TEMPLATES.get(3)); String contentString = semanticSearchService.formatIntoNumberedListForPrompt( documentSegments.stream().map(DocumentRecordSegment::getText).toList()); log.info("Processing follow-up question for user {} in course {}", currentUser.getId(), courseId); - List promptArgs = List.of( - TemplateArgs.builder().argumentName("question").argumentValue(question).build(), - TemplateArgs.builder().argumentName("content").argumentValue(contentString).build(), - TemplateArgs.builder().argumentName("skill").argumentValue(skillLevelPromotContent).build(), - TemplateArgs.builder().argumentName("conversationHistory").argumentValue(conversationHistory).build(), - TemplateArgs.builder().argumentName("codeContext").argumentValue(codeContext).build() - ); + Map promptArgs = new HashMap<>(); + promptArgs.put("question", question); + promptArgs.put("content", contentString); + promptArgs.put("skill", skillLevelPromotContent); + promptArgs.put("conversationHistory", conversationHistory); + promptArgs.put("codeContext", codeContext); - TutorAnswer response = ollamaService.startQuery( - TutorAnswer.class, prompt, promptArgs, new TutorAnswer(ERROR_MESSAGE)); + TutorAnswer response = ollamaClient.startQuery( + TutorAnswer.class, PROMPT_TEMPLATES.get(3), promptArgs, new TutorAnswer(ERROR_MESSAGE)); conversationHistoryService.addConversationExchange( currentUser.getId(), courseId, question, response.getAnswer()); @@ -310,12 +306,10 @@ private LectureQuestionResponse handleFollowUpQuestion(String question, UUID cou private CategorizedQuestion preprocessQuestion(final String userQuestion){ CategorizedQuestion error = new CategorizedQuestion("", TutorCategory.ERROR); String templateName = PROMPT_TEMPLATES.get(0); - List preprocessArgs = List.of(TemplateArgs.builder() - .argumentName("question") - .argumentValue(userQuestion) - .build()); - String prompt = ollamaService.getTemplate(templateName); - return ollamaService.startQuery(CategorizedQuestion.class, prompt, preprocessArgs, error); + Map preprocessArgs = new HashMap<>(); + preprocessArgs.put("question", userQuestion); + + return ollamaClient.startQuery(CategorizedQuestion.class, templateName, preprocessArgs, error); } private Source generateSource(SemanticSearchResult result){ @@ -397,16 +391,14 @@ private LectureQuestionResponse answerCodeFeedbackQuestion(String question, UUID log.info("[TUTOR-CODE-FEEDBACK] Preparing to query LLM for code feedback - user: {}, assignment: {}, code context length: {}", currentUser.getId(), mostRecentSubmission.getPrimaryKey().getAssignmentId(), codeContext.get().length()); - String prompt = ollamaService.getTemplate(PROMPT_TEMPLATES.get(2)); - List promptArgs = List.of( - TemplateArgs.builder().argumentName("question").argumentValue(question).build(), - TemplateArgs.builder().argumentName("codeContext").argumentValue(codeContext.get()).build(), - TemplateArgs.builder().argumentName("feedbackStyle").argumentValue(feedbackStyle).build(), - TemplateArgs.builder().argumentName("conversationHistory").argumentValue(conversationHistory).build() - ); + Map promptArgs = new HashMap<>(); + promptArgs.put("question", question); + promptArgs.put("codeContext", codeContext.get()); + promptArgs.put("feedbackStyle", feedbackStyle); + promptArgs.put("conversationHistory", conversationHistory); - TutorAnswer response = ollamaService.startQuery( - TutorAnswer.class, prompt, promptArgs, new TutorAnswer(ERROR_MESSAGE)); + TutorAnswer response = ollamaClient.startQuery( + TutorAnswer.class, PROMPT_TEMPLATES.get(2), promptArgs, new TutorAnswer(ERROR_MESSAGE)); conversationHistoryService.addConversationExchange( currentUser.getId(), courseId, question, response.getAnswer()); diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/models/OllamaRequest.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/models/OllamaRequest.java deleted file mode 100644 index 1018d9c..0000000 --- a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/models/OllamaRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.unistuttgart.iste.meitrex.tutor_service.service.models; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Getter; - -import java.util.Map; - -@Getter -public class OllamaRequest { - - @JsonProperty("model") - final String model; - @JsonProperty("prompt") - final String prompt; - @JsonProperty("stream") - final boolean stream; - @JsonProperty("format") - final Map format; - - public OllamaRequest(String model, String prompt, boolean stream, Map format) { - this.model = model; - this.prompt = prompt; - this.stream = stream; - this.format = format; - } -} diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/models/OllamaResponse.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/models/OllamaResponse.java deleted file mode 100644 index 2570b9f..0000000 --- a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/models/OllamaResponse.java +++ /dev/null @@ -1,147 +0,0 @@ -package de.unistuttgart.iste.meitrex.tutor_service.service.models; - -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonProperty; - -public class OllamaResponse { - - @JsonProperty("total_duration") - long totalDuration; - @JsonProperty("load_duration") - long loadDuration; - @JsonProperty("prompt_eval_count") - long promptEvalCount; - @JsonProperty("prompt_eval_duration") - long promptEvalDuration; - @JsonProperty("eval_count") - long evalCount; - @JsonProperty("eval_duration") - long evalDuration; - @JsonProperty("model") - String model; - @JsonProperty("created_at") - String createdAt; - @JsonProperty("response") - String response; - @JsonProperty("done") - boolean done; - @JsonProperty("done_reason") - String doneReason; - @JsonProperty("context") - long[] context; - @JsonProperty("error") - String error; - - - /** - * - * @return The total duration of the request in milliseconds. - */ - @JsonIgnore - public long getTotalDuration() { - return totalDuration; - } - - /** - * - * @return The duration of the model loading in milliseconds. - */ - @JsonIgnore - public long getLoadDuration() { - return loadDuration; - } - - /** - * - * @return The number of prompt evaluations. - */ - @JsonIgnore - public long getPromptEvalCount() { - return promptEvalCount; - } - - /** - * - * @return The duration of the prompt evaluation in milliseconds. - */ - @JsonIgnore - public long getPromptEvalDuration() { - return promptEvalDuration; - } - - /** - * - * @return The number of evaluations. - */ - @JsonIgnore - public long getEvalCount() { - return evalCount; - } - - /** - * - * @return The duration of the evaluation in milliseconds. - */ - @JsonIgnore - public long getEvalDuration() { - return evalDuration; - } - - /** - * - * @return The model used for the request. - */ - @JsonIgnore - public String getModel() { - return model; - } - - /** - * - * @return The creation time of the request in ISO 8601 format. - */ - @JsonIgnore - public String getCreatedAt() { - return createdAt; - } - - /** - * - * @return The response from the model. - */ - @JsonIgnore - public String getResponse() { - return response; - } - - /** - * - * @return Whether the request is done or not. - */ - @JsonIgnore - public boolean isDone() { - return done; - } - - - /** - * - * @return The reason why the request is done. - */ - @JsonIgnore - public String getDoneReason() { - return doneReason; - } - - /** - * - * @return The context of the request. - */ - @JsonIgnore - public long[] getContext() { - return context; - } - - @JsonIgnore - public String getError() {return error;} -} diff --git a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/models/TemplateArgs.java b/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/models/TemplateArgs.java deleted file mode 100644 index efeb974..0000000 --- a/src/main/java/de/unistuttgart/iste/meitrex/tutor_service/service/models/TemplateArgs.java +++ /dev/null @@ -1,13 +0,0 @@ -package de.unistuttgart.iste.meitrex.tutor_service.service.models; - -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; - -@Getter -@Setter -@Builder -public class TemplateArgs { - private String argumentName; - private String argumentValue; -} diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index b66a953..4bfc1ef 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -8,9 +8,11 @@ spring.jpa.show-sql=true spring.sql.init.continue-on-error=true spring.jpa.hibernate.ddl-auto=update -ollama.url=http://localhost:11434 -ollama.endpoint=api/generate -ollama.model=llama3:8b-instruct-q4_0 +ollama.url=http://129.69.217.245:4000 +ollama.model=qwen3-coder-80B-A10B +ollama.endpoint=v1/chat/completions +ollama.promptFolder=prompt_templates +ollama.apiKey=${OLLAMA_API_KEY:} semantic.search.threshold.hint=0.4 semantic.search.threshold.tutor=0.4 diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 5ca5e25..9f2f91d 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -9,9 +9,11 @@ spring.jpa.show-sql=true spring.sql.init.continue-on-error=true spring.jpa.hibernate.ddl-auto=update -ollama.url=${OLLAMA_URL:http://129.69.217.248:11434} -ollama.endpoint=${OLLAMA_ENDPOINT:api/generate} -ollama.model=${OLLAMA_MODEL:llama3:8b-instruct-q4_0} +ollama.url=${OLLAMA_URL:http://129.69.217.245:4000} +ollama.endpoint=${OLLAMA_ENDPOINT:v1/chat/completions} +ollama.model=${OLLAMA_MODEL:qwen3-coder-80B-A10B} +ollama.promptFolder=${OLLAMA_PROMPT_FOLDER:prompt_templates} +ollama.apiKey=${OLLAMA_API_KEY:} semantic.search.threshold.hint=0.4 semantic.search.threshold.tutor=0.4 diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 351a299..e46b9ca 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -17,9 +17,11 @@ management.endpoint.health.probes.enabled=true management.health.livenessstate.enabled=true management.health.readinessState.enabled=true -ollama.url=http://localhost:11434 -ollama.endpoint=api/generate -ollama.model=llama3:8b-instruct-q4_0 +ollama.url=http://129.69.217.245:4000 +ollama.model=qwen3-coder-80B-A10B +ollama.endpoint=v1/chat/completions +ollama.promptFolder=prompt_templates +ollama.apiKey=${OLLAMA_API_KEY:} semantic.search.threshold.hint=0.4 semantic.search.threshold.tutor=0.4 diff --git a/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/service/HintServiceTest.java b/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/service/HintServiceTest.java index 286c50d..5f151d7 100644 --- a/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/service/HintServiceTest.java +++ b/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/service/HintServiceTest.java @@ -3,6 +3,7 @@ import de.unistuttgart.iste.meitrex.common.event.HexadPlayerType; import de.unistuttgart.iste.meitrex.common.testutil.InjectCurrentUserHeader; import de.unistuttgart.iste.meitrex.common.user_handling.LoggedInUser; +import de.unistuttgart.iste.meitrex.common.ollama.OllamaClient; import de.unistuttgart.iste.meitrex.generated.dto.*; import de.unistuttgart.iste.meitrex.tutor_service.service.models.*; import org.junit.jupiter.api.BeforeEach; @@ -14,6 +15,7 @@ import org.springframework.test.util.ReflectionTestUtils; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -30,7 +32,7 @@ class HintServiceTest { @Mock - private OllamaService ollamaService; + private OllamaClient ollamaClient; @Mock private SemanticSearchService semanticSearchService; @@ -72,7 +74,6 @@ void testGenerateHintWithQuestion_multipleChoice_success() { .mediaRecordSegment(DocumentRecordSegment.builder() .text("Java is a high-level programming language.") .page(1) - .build()) .build() ); @@ -80,19 +81,17 @@ void testGenerateHintWithQuestion_multipleChoice_success() { String expectedHint = "Think about what Java is commonly used for in software development."; HintResponse expectedResponse = new HintResponse(expectedHint); - when(ollamaService.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) - .thenReturn("Question: {questionText}\nOptions: {options}"); - when(ollamaService.fillTemplate(anyString(), anyList())) + when(ollamaClient.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) + .thenReturn("Question: {{questionText}}\nOptions: {{options}}"); + when(ollamaClient.fillTemplate(anyString(), anyMap())) .thenReturn("Filled question prompt"); when(semanticSearchService.semanticSearch(anyString(), eq(courseId), eq(currentUser))) .thenReturn(searchResults); when(semanticSearchService.formatIntoNumberedListForPrompt(anyList())) .thenReturn("1. Java is a high-level programming language."); - when(ollamaService.getTemplate(contains("generate_hint"))) - .thenReturn("Generate hint template"); when(userPlayerTypeService.getPrimaryPlayerType(currentUser.getId())) .thenReturn(Optional.empty()); - when(ollamaService.startQuery(eq(HintResponse.class), anyString(), anyList(), any())) + when(ollamaClient.startQuery(eq(HintResponse.class), anyString(), anyMap(), any())) .thenReturn(expectedResponse); HintResponse result = hintService.generateHintWithQuestion(input, courseId, currentUser); @@ -100,7 +99,7 @@ void testGenerateHintWithQuestion_multipleChoice_success() { assertNotNull(result); assertEquals(expectedHint, result.getHint()); verify(semanticSearchService).semanticSearch(eq("What is Java?"), eq(courseId), eq(currentUser)); - verify(ollamaService).startQuery(eq(HintResponse.class), anyString(), anyList(), any()); + verify(ollamaClient).startQuery(eq(HintResponse.class), anyString(), anyMap(), any()); } @Test @@ -122,29 +121,28 @@ void testGenerateHintWithQuestion_cloze_success() { .mediaRecordSegment(DocumentRecordSegment.builder() .text("Java supports object-oriented programming principles.") .page(2) - .build()) .build() ); String semanticSearchQuery = "Java programming language paradigm"; - when(ollamaService.getTemplate(contains("question_prompt_CLOZE"))) - .thenReturn("Question: {questionText}\nOptions: {options}"); - when(ollamaService.fillTemplate(anyString(), anyList())) + when(ollamaClient.getTemplate(contains("question_prompt_CLOZE"))) + .thenReturn("Question: {{questionText}}\nOptions: {{options}}"); + when(ollamaClient.fillTemplate(anyString(), anyMap())) .thenReturn("Filled cloze prompt"); - when(ollamaService.getTemplate(contains("generate_semantic_search_query_cloze"))) - .thenReturn("Generate search query for cloze"); - when(ollamaService.startQuery(eq(SemanticSearchQuery.class), anyString(), anyList(), any())) + + when(ollamaClient.startQuery(eq(SemanticSearchQuery.class), anyString(), anyMap(), any())) .thenReturn(new SemanticSearchQuery(semanticSearchQuery)); + when(semanticSearchService.semanticSearch(eq(semanticSearchQuery), eq(courseId), eq(currentUser))) .thenReturn(searchResults); when(semanticSearchService.formatIntoNumberedListForPrompt(anyList())) .thenReturn("1. Java supports object-oriented programming principles."); - when(ollamaService.getTemplate(contains("generate_hint"))) - .thenReturn("Generate hint template"); + when(userPlayerTypeService.getPrimaryPlayerType(currentUser.getId())) .thenReturn(Optional.empty()); - when(ollamaService.startQuery(eq(HintResponse.class), anyString(), anyList(), any())) + + when(ollamaClient.startQuery(eq(HintResponse.class), anyString(), anyMap(), any())) .thenReturn(new HintResponse("Consider the programming paradigms Java supports.")); HintResponse result = hintService.generateHintWithQuestion(input, courseId, currentUser); @@ -176,29 +174,25 @@ void testGenerateHintWithQuestion_association_success() { .mediaRecordSegment(DocumentRecordSegment.builder() .text("JVM is the Java Virtual Machine that executes Java bytecode.") .page(3) - .build()) .build() ); String semanticSearchQuery = "JVM JDK Java tools components"; - when(ollamaService.getTemplate(contains("question_prompt_ASSOCIATION"))) - .thenReturn("Question: {questionText}\nPairs: {options}"); - when(ollamaService.fillTemplate(anyString(), anyList())) + when(ollamaClient.getTemplate(contains("question_prompt_ASSOCIATION"))) + .thenReturn("Question: {{questionText}}\nPairs: {{options}}"); + when(ollamaClient.fillTemplate(anyString(), anyMap())) .thenReturn("Filled association prompt"); - when(ollamaService.getTemplate(contains("generate_semantic_search_query_association"))) - .thenReturn("Generate search query for association"); - when(semanticSearchService.formatIntoNumberedListForPrompt(anyList())) - .thenReturn("1. Left: JVM <-> Right: Java Virtual Machine\n2. Left: JDK <-> Right: Java Development Kit"); - when(ollamaService.startQuery(eq(SemanticSearchQuery.class), anyString(), anyList(), any())) + + when(ollamaClient.startQuery(eq(SemanticSearchQuery.class), anyString(), anyMap(), any())) .thenReturn(new SemanticSearchQuery(semanticSearchQuery)); + when(semanticSearchService.semanticSearch(eq(semanticSearchQuery), eq(courseId), eq(currentUser))) .thenReturn(searchResults); - when(ollamaService.getTemplate(contains("generate_hint"))) - .thenReturn("Generate hint template"); when(userPlayerTypeService.getPrimaryPlayerType(currentUser.getId())) .thenReturn(Optional.empty()); - when(ollamaService.startQuery(eq(HintResponse.class), anyString(), anyList(), any())) + + when(ollamaClient.startQuery(eq(HintResponse.class), anyString(), anyMap(), any())) .thenReturn(new HintResponse("Think about what each acronym stands for.")); HintResponse result = hintService.generateHintWithQuestion(input, courseId, currentUser); @@ -220,9 +214,9 @@ void testGenerateHintWithQuestion_noSearchResults() { .setMultipleChoice(multipleChoiceInput) .build(); - when(ollamaService.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) - .thenReturn("Question: {questionText}"); - when(ollamaService.fillTemplate(anyString(), anyList())) + when(ollamaClient.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) + .thenReturn("Question: {{questionText}}"); + when(ollamaClient.fillTemplate(anyString(), anyMap())) .thenReturn("Filled question prompt"); when(semanticSearchService.semanticSearch(anyString(), eq(courseId), eq(currentUser))) .thenReturn(List.of()); @@ -231,7 +225,7 @@ void testGenerateHintWithQuestion_noSearchResults() { assertNotNull(result); assertEquals("No relevant content found in the lecture for this question", result.getHint()); - verify(ollamaService, never()).startQuery(eq(HintResponse.class), anyString(), anyList(), any()); + verify(ollamaClient, never()).startQuery(eq(HintResponse.class), anyString(), anyMap(), any()); } @Test @@ -258,9 +252,9 @@ void testGenerateHintWithQuestion_noDocumentSegments() { .build() ); - when(ollamaService.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) - .thenReturn("Question: {questionText}"); - when(ollamaService.fillTemplate(anyString(), anyList())) + when(ollamaClient.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) + .thenReturn("Question: {{questionText}}"); + when(ollamaClient.fillTemplate(anyString(), anyMap())) .thenReturn("Filled question prompt"); when(semanticSearchService.semanticSearch(anyString(), eq(courseId), eq(currentUser))) .thenReturn(searchResults); @@ -295,19 +289,18 @@ void testGenerateHintWithQuestion_withAchieverPlayerType() { .build() ); - when(ollamaService.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) - .thenReturn("Question: {questionText}"); - when(ollamaService.fillTemplate(anyString(), anyList())) + when(ollamaClient.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) + .thenReturn("Question: {{questionText}}"); + when(ollamaClient.fillTemplate(anyString(), anyMap())) .thenReturn("Filled question prompt"); when(semanticSearchService.semanticSearch(anyString(), eq(courseId), eq(currentUser))) .thenReturn(searchResults); when(semanticSearchService.formatIntoNumberedListForPrompt(anyList())) .thenReturn("1. Polymorphism allows objects to take multiple forms."); - when(ollamaService.getTemplate(contains("generate_hint"))) - .thenReturn("Generate hint template"); when(userPlayerTypeService.getPrimaryPlayerType(currentUser.getId())) .thenReturn(Optional.of(HexadPlayerType.ACHIEVER)); - when(ollamaService.startQuery(eq(HintResponse.class), anyString(), anyList(), any())) + + when(ollamaClient.startQuery(eq(HintResponse.class), anyString(), anyMap(), any())) .thenReturn(new HintResponse("Focus on mastering the concept of polymorphism.")); HintResponse result = hintService.generateHintWithQuestion(input, courseId, currentUser); @@ -341,19 +334,18 @@ void testGenerateHintWithQuestion_withPhilanthropistPlayerType() { .build() ); - when(ollamaService.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) - .thenReturn("Question: {questionText}"); - when(ollamaService.fillTemplate(anyString(), anyList())) + when(ollamaClient.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) + .thenReturn("Question: {{questionText}}"); + when(ollamaClient.fillTemplate(anyString(), anyMap())) .thenReturn("Filled question prompt"); when(semanticSearchService.semanticSearch(anyString(), eq(courseId), eq(currentUser))) .thenReturn(searchResults); when(semanticSearchService.formatIntoNumberedListForPrompt(anyList())) .thenReturn("1. Inheritance allows classes to inherit properties from parent classes."); - when(ollamaService.getTemplate(contains("generate_hint"))) - .thenReturn("Generate hint template"); when(userPlayerTypeService.getPrimaryPlayerType(currentUser.getId())) .thenReturn(Optional.of(HexadPlayerType.PHILANTHROPIST)); - when(ollamaService.startQuery(eq(HintResponse.class), anyString(), anyList(), any())) + + when(ollamaClient.startQuery(eq(HintResponse.class), anyString(), anyMap(), any())) .thenReturn(new HintResponse("Consider how inheritance helps share knowledge between classes.")); HintResponse result = hintService.generateHintWithQuestion(input, courseId, currentUser); @@ -386,19 +378,18 @@ void testGenerateHintWithQuestion_withPlayerPlayerType() { .build() ); - when(ollamaService.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) - .thenReturn("Question: {questionText}"); - when(ollamaService.fillTemplate(anyString(), anyList())) + when(ollamaClient.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) + .thenReturn("Question: {{questionText}}"); + when(ollamaClient.fillTemplate(anyString(), anyMap())) .thenReturn("Filled question prompt"); when(semanticSearchService.semanticSearch(anyString(), eq(courseId), eq(currentUser))) .thenReturn(searchResults); when(semanticSearchService.formatIntoNumberedListForPrompt(anyList())) .thenReturn("1. Encapsulation is bundling data and methods together."); - when(ollamaService.getTemplate(contains("generate_hint"))) - .thenReturn("Generate hint template"); when(userPlayerTypeService.getPrimaryPlayerType(currentUser.getId())) .thenReturn(Optional.of(HexadPlayerType.PLAYER)); - when(ollamaService.startQuery(eq(HintResponse.class), anyString(), anyList(), any())) + + when(ollamaClient.startQuery(eq(HintResponse.class), anyString(), anyMap(), any())) .thenReturn(new HintResponse("Be careful not to reveal the answer directly!")); HintResponse result = hintService.generateHintWithQuestion(input, courseId, currentUser); @@ -431,19 +422,19 @@ void testGenerateHintWithQuestion_playerTypeFetchException() { .build() ); - when(ollamaService.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) - .thenReturn("Question: {questionText}"); - when(ollamaService.fillTemplate(anyString(), anyList())) + when(ollamaClient.getTemplate(contains("question_prompt_MULTIPLE_CHOICE"))) + .thenReturn("Question: {{questionText}}"); + when(ollamaClient.fillTemplate(anyString(), anyMap())) .thenReturn("Filled question prompt"); when(semanticSearchService.semanticSearch(anyString(), eq(courseId), eq(currentUser))) .thenReturn(searchResults); when(semanticSearchService.formatIntoNumberedListForPrompt(anyList())) .thenReturn("1. Abstraction hides implementation details."); - when(ollamaService.getTemplate(contains("generate_hint"))) - .thenReturn("Generate hint template"); + when(userPlayerTypeService.getPrimaryPlayerType(currentUser.getId())) .thenThrow(new RuntimeException("Player type fetch error")); - when(ollamaService.startQuery(eq(HintResponse.class), anyString(), anyList(), any())) + + when(ollamaClient.startQuery(eq(HintResponse.class), anyString(), anyMap(), any())) .thenReturn(new HintResponse("Think about how abstraction simplifies complex systems.")); HintResponse result = hintService.generateHintWithQuestion(input, courseId, currentUser); diff --git a/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/service/ProactiveFeedbackServiceTest.java b/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/service/ProactiveFeedbackServiceTest.java index 3a7d2bf..561406e 100644 --- a/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/service/ProactiveFeedbackServiceTest.java +++ b/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/service/ProactiveFeedbackServiceTest.java @@ -2,8 +2,9 @@ import de.unistuttgart.iste.meitrex.common.event.ContentProgressedEvent; import de.unistuttgart.iste.meitrex.common.event.HexadPlayerType; + +import de.unistuttgart.iste.meitrex.common.ollama.OllamaClient; import de.unistuttgart.iste.meitrex.tutor_service.persistence.entity.ProactiveFeedbackEntity; -import de.unistuttgart.iste.meitrex.tutor_service.persistence.entity.StudentCodeSubmissionEntity; import de.unistuttgart.iste.meitrex.tutor_service.persistence.entity.UserPlayerTypeEntity; import de.unistuttgart.iste.meitrex.tutor_service.persistence.repository.ProactiveFeedbackRepository; import de.unistuttgart.iste.meitrex.tutor_service.service.models.TutorAnswer; @@ -17,12 +18,12 @@ import java.time.OffsetDateTime; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; @@ -33,7 +34,7 @@ class ProactiveFeedbackServiceTest { @Mock - private OllamaService ollamaService; + private OllamaClient ollamaClient; @Mock private UserPlayerTypeService userPlayerTypeService; @@ -79,8 +80,7 @@ void testGenerateFeedback_forAssignmentWithCode() { String codeContext = "Repository: https://github.com/test/repo\nFile: Main.java\npublic class Main {}"; String expectedFeedback = "Great job on your assignment! Your code shows good structure."; - when(ollamaService.getTemplate(anyString())).thenReturn("Mock template"); - when(ollamaService.startQuery(eq(TutorAnswer.class), anyString(), anyList(), any())) + when(ollamaClient.startQuery(eq(TutorAnswer.class), anyString(), anyMap(), any())) .thenReturn(new TutorAnswer(expectedFeedback)); when(userPlayerTypeService.getPrimaryPlayerType(userId)).thenReturn(Optional.empty()); when(studentCodeSubmissionService.getCodeSubmissionContextForTutor(userId, assignmentId)) @@ -112,8 +112,7 @@ void testGenerateFeedback_forQuizWithoutCode() { String expectedFeedback = "Good work on the quiz! You demonstrated solid understanding."; - when(ollamaService.getTemplate(anyString())).thenReturn("Mock template"); - when(ollamaService.startQuery(eq(TutorAnswer.class), anyString(), anyList(), any())) + when(ollamaClient.startQuery(eq(TutorAnswer.class), anyString(), anyMap(), any())) .thenReturn(new TutorAnswer(expectedFeedback)); when(userPlayerTypeService.getPrimaryPlayerType(userId)).thenReturn(Optional.empty()); when(proactiveFeedbackRepository.save(any())).thenAnswer(invocation -> { @@ -144,8 +143,7 @@ void testGenerateFeedback_forAssignmentWithoutCodeSubmission() { String expectedFeedback = "Excellent work on your assignment!"; - when(ollamaService.getTemplate(anyString())).thenReturn("Mock template"); - when(ollamaService.startQuery(eq(TutorAnswer.class), anyString(), anyList(), any())) + when(ollamaClient.startQuery(eq(TutorAnswer.class), anyString(), anyMap(), any())) .thenReturn(new TutorAnswer(expectedFeedback)); when(userPlayerTypeService.getPrimaryPlayerType(userId)).thenReturn(Optional.empty()); when(studentCodeSubmissionService.getCodeSubmissionContextForTutor(userId, assignmentId)) @@ -188,8 +186,7 @@ void testGenerateFeedback_withPlayerType() { .disruptorScore(0.2) .build(); - when(ollamaService.getTemplate(anyString())).thenReturn("Mock template"); - when(ollamaService.startQuery(eq(TutorAnswer.class), anyString(), anyList(), any())) + when(ollamaClient.startQuery(eq(TutorAnswer.class), anyString(), anyMap(), any())) .thenReturn(new TutorAnswer(expectedFeedback)); when(userPlayerTypeService.getPrimaryPlayerType(userId)) .thenReturn(Optional.of(HexadPlayerType.ACHIEVER)); @@ -231,8 +228,7 @@ void testGenerateFeedback_withDisruptorFallbackToHighestType() { .disruptorScore(0.85) .build(); - when(ollamaService.getTemplate(anyString())).thenReturn("Mock template"); - when(ollamaService.startQuery(eq(TutorAnswer.class), anyString(), anyList(), any())) + when(ollamaClient.startQuery(eq(TutorAnswer.class), anyString(), anyMap(), any())) .thenReturn(new TutorAnswer(expectedFeedback)); when(userPlayerTypeService.getPrimaryPlayerType(userId)) .thenReturn(Optional.of(HexadPlayerType.DISRUPTOR)); @@ -263,7 +259,8 @@ void testGenerateFeedback_withException() { .success(true) .build(); - when(ollamaService.getTemplate(anyString())).thenThrow(new RuntimeException("Test exception")); + when(ollamaClient.startQuery(eq(TutorAnswer.class), anyString(), anyMap(), any())) + .thenThrow(new RuntimeException("Test exception")); String result = proactiveFeedbackService.generateFeedback(event); diff --git a/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/service/TutorServiceTest.java b/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/service/TutorServiceTest.java index f89e201..28914c6 100644 --- a/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/service/TutorServiceTest.java +++ b/src/test/java/de/unistuttgart/iste/meitrex/tutor_service/service/TutorServiceTest.java @@ -3,6 +3,7 @@ import de.unistuttgart.iste.meitrex.common.dapr.TopicPublisher; import de.unistuttgart.iste.meitrex.common.event.TutorCategory; +import de.unistuttgart.iste.meitrex.common.ollama.OllamaClient; import de.unistuttgart.iste.meitrex.common.testutil.InjectCurrentUserHeader; import de.unistuttgart.iste.meitrex.common.user_handling.LoggedInUser; import de.unistuttgart.iste.meitrex.generated.dto.LectureQuestionResponse; @@ -24,7 +25,7 @@ public class TutorServiceTest { - private final OllamaService ollamaService = Mockito.mock(OllamaService.class); + private final OllamaClient ollamaClient = Mockito.mock(OllamaClient.class); private final SemanticSearchService semanticSearchService = Mockito.mock(SemanticSearchService.class); private final TopicPublisher topicPublisher = Mockito.mock(TopicPublisher.class); private final UserPlayerTypeService userPlayerTypeService = Mockito.mock(UserPlayerTypeService.class); @@ -36,7 +37,7 @@ public class TutorServiceTest { @BeforeEach void setUp() { - tutorService = new TutorService(ollamaService, semanticSearchService, topicPublisher, + tutorService = new TutorService(ollamaClient, semanticSearchService, topicPublisher, userPlayerTypeService, userSkillLevelService, proactiveFeedbackService, conversationHistoryService, studentCodeSubmissionService); ReflectionTestUtils.setField(tutorService, "scoreThreshold", 0.4); @@ -50,8 +51,7 @@ void setUp() { void testHandleUserQuestion_withUnrecognizableCategory() { String question = "jchbjshbcjhsdbc"; CategorizedQuestion categorizedQuestion = new CategorizedQuestion(question, TutorCategory.UNRECOGNIZABLE); - when(ollamaService.startQuery(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(categorizedQuestion); - when(ollamaService.getTemplate(Mockito.any())).thenReturn("Mocked Prompt"); + when(ollamaClient.startQuery(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(categorizedQuestion); LectureQuestionResponse response = tutorService.handleUserQuestion(question, null, loggedInUser); assertEquals("Unfortunately, I couldn't understand your question. " + @@ -63,8 +63,7 @@ void testHandleUserQuestion_withUnrecognizableCategory() { void testHandleUserQuestion_withOtherCategory() { String question = "Gib mir ein Rezept für Schokokuchen"; CategorizedQuestion categorizedQuestion = new CategorizedQuestion(question,TutorCategory.OTHER); - when(ollamaService.startQuery(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(categorizedQuestion); - when(ollamaService.getTemplate(Mockito.any())).thenReturn("Mocked Prompt"); + when(ollamaClient.startQuery(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(categorizedQuestion); when(conversationHistoryService.formatHistoryForPrompt(Mockito.any(), Mockito.any())).thenReturn(""); when(studentCodeSubmissionService.getCodeSubmissionsForStudent(Mockito.any())).thenReturn(List.of()); @@ -78,8 +77,7 @@ void testHandleUserQuestion_withOtherCategory() { void testHandleUserQuestion_withLectureCategoryNoCourseId() { String question = "What is the difference between supervised and unsupervised training?"; CategorizedQuestion categorizedQuestion = new CategorizedQuestion(question,TutorCategory.LECTURE); - when(ollamaService.startQuery(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(categorizedQuestion); - when(ollamaService.getTemplate(Mockito.any())).thenReturn("Mocked Prompt"); + when(ollamaClient.startQuery(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(categorizedQuestion); String expectedAnswer = "Something went wrong! If your question is about lecture materials, " + "please navigate to the course it relates to. Thank you! :)"; @@ -107,11 +105,10 @@ void testHandleUserQuestion_withLectureCategoryValidCourseId() { String expectedAnswer = dummyResults.size() + " relevant segments were found. " + "At the moment, I’m not yet able to answer questions about the course material :("; - when(ollamaService.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(categorizedQuestion); - when(ollamaService.getTemplate(Mockito.any())).thenReturn("Mocked Prompt"); + when(ollamaClient.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(categorizedQuestion); when(semanticSearchService.semanticSearch(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(dummyResults); when(semanticSearchService.formatIntoNumberedListForPrompt(Mockito.any())).thenReturn("Mocked content"); - when(ollamaService.startQuery(Mockito.eq(TutorAnswer.class), Mockito.any(), Mockito.any(), + when(ollamaClient.startQuery(Mockito.eq(TutorAnswer.class), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(new TutorAnswer(expectedAnswer)); when(userSkillLevelService.getAllSkillLevelsForUser(Mockito.any())).thenReturn(List.of()); when(conversationHistoryService.formatHistoryForPrompt(Mockito.any(), Mockito.any())).thenReturn(""); @@ -125,9 +122,8 @@ void testHandleUserQuestion_withLectureCategoryValidCourseId() { void testHandleUserQuestion_withSystemCategory() { String question = "Where do i upload my assignment?"; CategorizedQuestion categorizedQuestion = new CategorizedQuestion(question,TutorCategory.SYSTEM); - when(ollamaService.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), + when(ollamaClient.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(categorizedQuestion); - when(ollamaService.getTemplate(Mockito.any())).thenReturn("Mocked Prompt"); LectureQuestionResponse response = tutorService.handleUserQuestion(question, null, loggedInUser); assertEquals("At the moment, I can't answer any questions about the MEITREX system :(", @@ -140,9 +136,8 @@ void testAnswerLectureQuestion_withoutRelevantSegments() { String expectedAnswer = "No answer was found in the lecture."; // Mock PreProcess to be a Lecture Question - when(ollamaService.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), + when(ollamaClient.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(new CategorizedQuestion(question,TutorCategory.LECTURE)); - when(ollamaService.getTemplate(Mockito.any())).thenReturn("Mocked Prompt"); when(semanticSearchService.semanticSearch(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(List.of()); LectureQuestionResponse response = tutorService.handleUserQuestion(question, courseId, loggedInUser); @@ -167,9 +162,8 @@ void testAnswerLectureQuestion_withoutDocumentSegments() { ); // Mock PreProcess to be a Lecture Question - when(ollamaService.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), + when(ollamaClient.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(new CategorizedQuestion(question,TutorCategory.LECTURE)); - when(ollamaService.getTemplate(Mockito.any())).thenReturn("Mocked Prompt"); when(semanticSearchService.semanticSearch(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn(dummyResults); @@ -181,9 +175,8 @@ void testAnswerLectureQuestion_withoutDocumentSegments() { void testHandleUserQuestion_withCodeFeedbackCategoryNoCourseId() { String question = "Can you review my code?"; CategorizedQuestion categorizedQuestion = new CategorizedQuestion(question, TutorCategory.CODE_FEEDBACK); - when(ollamaService.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())) + when(ollamaClient.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(categorizedQuestion); - when(ollamaService.getTemplate(Mockito.any())).thenReturn("Mocked Prompt"); String expectedAnswer = "Something went wrong! If your question is about code for an assignment, " + "please navigate to the course it relates to. Thank you! :)"; @@ -197,9 +190,8 @@ void testHandleUserQuestion_withCodeFeedbackNoSubmissions() { String question = "Can you review my code?"; CategorizedQuestion categorizedQuestion = new CategorizedQuestion(question, TutorCategory.CODE_FEEDBACK); - when(ollamaService.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())) + when(ollamaClient.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(categorizedQuestion); - when(ollamaService.getTemplate(Mockito.any())).thenReturn("Mocked Prompt"); when(studentCodeSubmissionService.getCodeSubmissionsForStudent(Mockito.any())).thenReturn(List.of()); String expectedAnswer = "I couldn't find any code submission from you. " + @@ -254,11 +246,10 @@ void testHandleUserQuestion_withFollowUpQuestionWithCodeContext() { .build() ); - when(ollamaService.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())) + when(ollamaClient.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(categorizedQuestion); - when(ollamaService.startQuery(Mockito.eq(TutorAnswer.class), Mockito.any(), Mockito.any(), Mockito.any())) + when(ollamaClient.startQuery(Mockito.eq(TutorAnswer.class), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(tutorAnswer); - when(ollamaService.getTemplate(Mockito.any())).thenReturn("Mocked Prompt"); when(conversationHistoryService.formatHistoryForPrompt(Mockito.any(), Mockito.any())) .thenReturn("Previous conversation history"); when(studentCodeSubmissionService.getCodeSubmissionsForStudent(Mockito.any())) @@ -287,11 +278,10 @@ void testHandleUserQuestion_withFollowUpQuestionNoCodeContext() { .build() ); - when(ollamaService.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())) + when(ollamaClient.startQuery(Mockito.eq(CategorizedQuestion.class), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(categorizedQuestion); - when(ollamaService.startQuery(Mockito.eq(TutorAnswer.class), Mockito.any(), Mockito.any(), Mockito.any())) + when(ollamaClient.startQuery(Mockito.eq(TutorAnswer.class), Mockito.any(), Mockito.any(), Mockito.any())) .thenReturn(tutorAnswer); - when(ollamaService.getTemplate(Mockito.any())).thenReturn("Mocked Prompt"); when(conversationHistoryService.formatHistoryForPrompt(Mockito.any(), Mockito.any())) .thenReturn("Previous conversation history"); when(studentCodeSubmissionService.getCodeSubmissionsForStudent(Mockito.any())) diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index ef0f9a3..fcbc010 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,4 +1,10 @@ spring.datasource.url=jdbc:tc:postgresql:///tutor_service spring.datasource.username=root spring.datasource.password=root -spring.jpa.hibernate.ddl-auto=create \ No newline at end of file +spring.jpa.hibernate.ddl-auto=create + +ollama.url=http://localhost:4000 +ollama.model=qwen3-coder-80B-A10B +ollama.endpoint=v1/chat/completions +ollama.apiKey=test-key +ollama.promptFolder=prompt_templates \ No newline at end of file