Index: lams_central/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -ra08ef5f19d839ce969e8de4eac60b88789b3010d -r784731558fee35bec1bcb6ce76ecf352b5856db5 --- lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision a08ef5f19d839ce969e8de4eac60b88789b3010d) +++ lams_central/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 784731558fee35bec1bcb6ce76ecf352b5856db5) @@ -331,6 +331,7 @@ label.questions.choice.title = Choose questions label.questions.choice.select.all = Select all label.questions.choice.missing = Please check at least one question. +label.questions.choice.none.found = No questions available index.single.activity.lesson.title = Add single activity lesson index.single.activity.lesson.desc = or one-click activity: label.disable.lesson.sorting = Disable lesson sorting Index: lams_central/src/java/org/lamsfoundation/lams/authoring/template/web/OpenAiTemplateController.java =================================================================== diff -u --- lams_central/src/java/org/lamsfoundation/lams/authoring/template/web/OpenAiTemplateController.java (revision 0) +++ lams_central/src/java/org/lamsfoundation/lams/authoring/template/web/OpenAiTemplateController.java (revision 784731558fee35bec1bcb6ce76ecf352b5856db5) @@ -0,0 +1,166 @@ +package org.lamsfoundation.lams.authoring.template.web; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.LinkedList; +import java.util.List; + +import javax.net.ssl.HttpsURLConnection; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.lamsfoundation.lams.questions.Answer; +import org.lamsfoundation.lams.questions.Question; +import org.lamsfoundation.lams.util.JsonUtil; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; + +@Controller +@RequestMapping("/authoring/template/tbl/ai") +public class OpenAiTemplateController { + + private static final String API_KEY = "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"; + private static final String API_URL = "https://api.openai.com/v1/completions"; + private static final String API_MODEL = "text-davinci-003"; + private static final int API_MAX_TOKENS = 4000; + + private static final String ANSWER_LETTERS = "abcdefghi"; + + private static final Logger log = Logger.getLogger(OpenAiTemplateController.class); + + @GetMapping("") + @ResponseBody + public String generateTblContent(@RequestParam String subject) throws IOException { + String apiResponse = OpenAiTemplateController.callApi( + "Create Team Based Learning RAT 5 multiple choice questions with at least 4 answers each about " + + subject + ". For example:\\n\\n1. Urbanism is described as\\n" + + "A. The space between buildings\\n" + + "B. The connected system of public and private spaces\\n" + "C. Big cities\\n" + + "D. The public realm\\n" + "Answer: B. The connected system of public and private spaces", + 1.5); + List questions = OpenAiTemplateController.extractMcqQuestions(apiResponse); + StringBuilder responseBuilder = new StringBuilder(); + for (int questionIndex = 0; questionIndex < questions.size(); questionIndex++) { + Question question = questions.get(questionIndex); + responseBuilder.append(questionIndex + 1).append(". ").append(question.getTitle()).append("
"); + for (int answerIndex = 0; answerIndex < question.getAnswers().size(); answerIndex++) { + Answer answer = question.getAnswers().get(answerIndex); + if (answer.getScore() > 0) { + responseBuilder.append(""); + } + responseBuilder.append("  ").append(ANSWER_LETTERS.charAt(answerIndex)).append(") ") + .append(answer.getText()); + if (answer.getScore() > 0) { + responseBuilder.append(""); + } + responseBuilder.append("
"); + } + responseBuilder.append("
"); + } + + return responseBuilder.toString(); + } + + private static String callApi(String prompt, Double temperature) throws IOException { + URL url = new URL(API_URL); + HttpsURLConnection connection = (HttpsURLConnection) url.openConnection(); + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setRequestProperty("Accept-Charset", StandardCharsets.UTF_8.toString()); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON_VALUE); + connection.setRequestProperty("Authorization", "Bearer " + API_KEY); + + ObjectNode requestJSON = JsonNodeFactory.instance.objectNode(); + requestJSON.put("model", API_MODEL); + requestJSON.put("max_tokens", API_MAX_TOKENS); + requestJSON.put("prompt", prompt); + requestJSON.put("temperature", temperature == null ? 0 : temperature); + + try (OutputStream output = connection.getOutputStream()) { + output.write(requestJSON.toString().getBytes()); + + } + if (log.isDebugEnabled()) { + log.debug("Sending OpenAI request with prompt \"" + prompt + "\" and temperature " + temperature); + } + String response = IOUtils.toString(connection.getInputStream(), StandardCharsets.UTF_8.toString()); + if (log.isDebugEnabled()) { + log.debug( + "Received response from OpenAI:" + (StringUtils.isBlank(response) ? " " : "\n" + response)); + } + + ObjectNode responseJSON = JsonUtil.readObject(response); + ArrayNode choicesArray = JsonUtil.optArray(responseJSON, "choices"); + String producedChoice = choicesArray.get(0).get("text").asText(); + return producedChoice; + } + + private static List extractMcqQuestions(String text) { + List result = new LinkedList<>(); + if (StringUtils.isBlank(text)) { + return result; + } + text = text.strip(); + + String[] questionEntities = text.split("\\n\\n"); + Question question = null; + for (String questionEntity : questionEntities) { + if (StringUtils.isBlank(questionEntity)) { + continue; + } + questionEntity = questionEntity.strip().replace("\\n", "\n"); + String[] questionParts = questionEntity.split("\\n"); + for (String questionPart : questionParts) { + questionPart = questionPart.strip(); + if (questionPart.matches("^\\d+\\..+")) { + question = new Question(); + questionPart = questionPart.replaceFirst("\\d+\\.\\s*", ""); + question.setTitle(questionPart); + question.setAnswers(new LinkedList<>()); + result.add(question); + } else if (questionPart.matches("^\\p{Alpha}\\..+")) { + if (question == null) { + log.warn("Encountered answer without question: " + questionPart); + continue; + } + Answer answer = new Answer(); + question.getAnswers().add(answer); + answer.setDisplayOrder(question.getAnswers().size()); + answer.setText(questionPart); + answer.setScore(0f); + } else if (questionPart.startsWith("Answer: ")) { + if (question == null) { + log.warn("Encountered correct answer without question: " + questionPart); + continue; + } + questionPart = questionPart.strip().replaceFirst("Answer:\\s*", ""); + boolean correctAnswerFound = false; + for (Answer answer : question.getAnswers()) { + if (questionPart.equalsIgnoreCase(answer.getText())) { + answer.setScore(1f); + correctAnswerFound = true; + } + answer.setText(answer.getText().replaceFirst("^\\p{Alpha}\\.\\s*", "")); + } + if (!correctAnswerFound) { + log.warn("Could not find correct answer among existing answers: " + questionPart); + } + question = null; + } + } + } + + return result; + } +} \ No newline at end of file Index: lams_central/src/java/org/lamsfoundation/lams/web/QuestionsController.java =================================================================== diff -u -r52f835246c157f47eed9df1d47f9e7bb0c16315d -r784731558fee35bec1bcb6ce76ecf352b5856db5 --- lams_central/src/java/org/lamsfoundation/lams/web/QuestionsController.java (.../QuestionsController.java) (revision 52f835246c157f47eed9df1d47f9e7bb0c16315d) +++ lams_central/src/java/org/lamsfoundation/lams/web/QuestionsController.java (.../QuestionsController.java) (revision 784731558fee35bec1bcb6ce76ecf352b5856db5) @@ -3,14 +3,17 @@ import java.io.File; import java.io.FileInputStream; import java.io.InputStream; +import java.lang.reflect.Method; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.apache.commons.lang.StringUtils; +import org.lamsfoundation.lams.qb.model.QbCollection; import org.lamsfoundation.lams.qb.service.IQbService; import org.lamsfoundation.lams.questions.Question; import org.lamsfoundation.lams.questions.QuestionParser; @@ -45,25 +48,15 @@ private IQbService qbService; @RequestMapping("/questions") - public String execute(@RequestParam String tmpFileUploadId, @RequestParam String returnURL, - @RequestParam("limitType") String limitTypeParam, @RequestParam(required = false) String importType, - @RequestParam String callerID, @RequestParam(required = false) Boolean collectionChoice, - HttpServletRequest request) throws Exception { + public String execute(@RequestParam(required = false) String tmpFileUploadId, + @RequestParam(required = false) String returnURL, @RequestParam("limitType") String limitTypeParam, + @RequestParam(required = false) String importType, @RequestParam(required = false) String callerID, + @RequestParam(required = false) Boolean collectionChoice, HttpServletRequest request) throws Exception { MultiValueMap errorMap = new LinkedMultiValueMap<>(); + boolean isTextBasedInput = "openAi".equals(importType); String packageName = null; - File file = null; - File uploadDir = FileUtil.getTmpFileUploadDir(tmpFileUploadId); - if (uploadDir.canRead()) { - File[] files = uploadDir.listFiles(); - if (files.length > 1) { - errorMap.add("GLOBAL", "Uploaded more than 1 file"); - } else if (files.length == 1) { - file = files[0]; - packageName = file.getName().toLowerCase(); - } - } // this parameter is not really used at the moment request.setAttribute("returnURL", returnURL); @@ -74,17 +67,30 @@ // show only chosen types of questions request.setAttribute("limitType", limitTypeParam); + if (!isTextBasedInput) { + File uploadDir = FileUtil.getTmpFileUploadDir(tmpFileUploadId); + if (uploadDir.canRead()) { + File[] files = uploadDir.listFiles(); + if (files.length > 1) { + errorMap.add("GLOBAL", "Uploaded more than 1 file"); + } else if (files.length == 1) { + file = files[0]; + packageName = file.getName().toLowerCase(); + } + } + } + boolean isWordInput = "word".equals(importType); request.setAttribute("importType", importType); if (collectionChoice != null && collectionChoice) { // in the view a drop down with collections will be displayed - request.setAttribute("collections", qbService.getUserCollections(QuestionsController.getUserId())); + List collections = qbService.getUserCollections(QuestionsController.getUserId()); + request.setAttribute("collections", collections); } - // user did not choose a file - if (file == null || (isWordInput ? !packageName.endsWith(".docx") - : !(packageName.endsWith(".zip") || packageName.endsWith(".xml")))) { + if (!isTextBasedInput && (file == null || (isWordInput ? !packageName.endsWith(".docx") + : !(packageName.endsWith(".zip") || packageName.endsWith(".xml"))))) { errorMap.add("GLOBAL", messageService.getMessage("label.questions.file.missing")); } @@ -94,35 +100,48 @@ return "questions/questionFile"; } - String tempDirName = Configuration.get(ConfigurationKeys.LAMS_TEMP_DIR); - File tempDir = new File(tempDirName); - if (!tempDir.exists()) { - tempDir.mkdirs(); - } + Question[] questions = null; + if (isTextBasedInput) { + try { + Class clazz = Class.forName(Configuration.AI_MODULE_CLASS, false, Configuration.class.getClassLoader()); + if (clazz != null) { + Method method = clazz.getMethod("parseResponse", String.class); + questions = (Question[]) method.invoke(null, request.getParameter("textInput")); + } + } catch (Exception e) { + errorMap.add("GLOBAL", "Error while parsing text input: " + e.getMessage()); + } + } else { + Set limitType = null; + if (!StringUtils.isBlank(limitTypeParam)) { + limitType = new TreeSet<>(); + // comma delimited acceptable question types, for example "mc,fb" + Collections.addAll(limitType, limitTypeParam.split(",")); + } - Set limitType = null; - if (!StringUtils.isBlank(limitTypeParam)) { - limitType = new TreeSet<>(); - // comma delimited acceptable question types, for example "mc,fb" - Collections.addAll(limitType, limitTypeParam.split(",")); - } + String tempDirName = Configuration.get(ConfigurationKeys.LAMS_TEMP_DIR); + File tempDir = new File(tempDirName); + if (!tempDir.exists()) { + tempDir.mkdirs(); + } - InputStream uploadedFileStream = new FileInputStream(file); + InputStream uploadedFileStream = new FileInputStream(file); - Question[] questions; - if (packageName.endsWith(".xml")) { - questions = QuestionParser.parseQTIFile(uploadedFileStream, null, limitType); + if (packageName.endsWith(".xml")) { + questions = QuestionParser.parseQTIFile(uploadedFileStream, null, limitType); - } else if (packageName.endsWith(".docx")) { - questions = QuestionWordParser.parseWordFile(uploadedFileStream, packageName, limitType); + } else if (packageName.endsWith(".docx")) { + questions = QuestionWordParser.parseWordFile(uploadedFileStream, packageName, limitType); - } else { - questions = QuestionParser.parseQTIPackage(uploadedFileStream, limitType); + } else { + questions = QuestionParser.parseQTIPackage(uploadedFileStream, limitType); + } + + FileUtil.deleteTmpFileUploadDir(tmpFileUploadId); } + request.setAttribute("questions", questions); - FileUtil.deleteTmpFileUploadDir(tmpFileUploadId); - return "questions/questionChoice"; } Index: lams_central/web/questions/questionChoice.jsp =================================================================== diff -u -r0f295260a6f4f68d391ed74f3a7967fbf841af90 -r784731558fee35bec1bcb6ce76ecf352b5856db5 --- lams_central/web/questions/questionChoice.jsp (.../questionChoice.jsp) (revision 0f295260a6f4f68d391ed74f3a7967fbf841af90) +++ lams_central/web/questions/questionChoice.jsp (.../questionChoice.jsp) (revision 784731558fee35bec1bcb6ce76ecf352b5856db5) @@ -156,148 +156,163 @@ - - - - - -

- -
- - - - - - - - - <%-- Question itself --%> - ${questionStatus.index + 1}. - - - () - - - () - - - () - - - () - - - () - - - () - - - () - - - () - - - + + + + + - - - -
${question.text}

- - - - - - <%-- Question feedback --%> - - <%-- If question contains images, where to take them from --%> - - <%-- Answers, if required and exist --%> - -
- - - <%-- Answer itself --%> - - - ${answer.text}
-
- - <%-- Do not display answers if management is too difficult or pointless --%> - - -
- <%-- Answers score and feedback --%> - - -
- - - - +
+ +
+ + + + + + +

+ + + + + + + + + + + <%-- Question itself --%> + ${questionStatus.index + 1}. + + + () + + + () + + + () + + + () + + + () + + + () + + + () + + + () + + + + + + + +
${question.text}

+ + + + + + <%-- Question feedback --%> + + <%-- If question contains images, where to take them from --%> + + <%-- Answers, if required and exist --%> + +
+ + + <%-- Answer itself --%> + + + + + ${answer.text}
+
+
+ + <%-- Do not display answers if management is too difficult or pointless --%> + + +
+ <%-- Answers score and feedback --%> + + +
+ + + + + + + + + +
+
+ + <%-- Learning Outcomes, if required and exist --%> + + + + - - - -
-
- - <%-- Learning Outcomes, if required and exist --%> - - - - - -
- -
- - -
-
+ +
+ + +
+ + +
Index: lams_common/src/java/org/lamsfoundation/lams/qb/service/QbService.java =================================================================== diff -u -rea6095ed8109b6425e527f5a7df4e4810bb3e518 -r784731558fee35bec1bcb6ce76ecf352b5856db5 --- lams_common/src/java/org/lamsfoundation/lams/qb/service/QbService.java (.../QbService.java) (revision ea6095ed8109b6425e527f5a7df4e4810bb3e518) +++ lams_common/src/java/org/lamsfoundation/lams/qb/service/QbService.java (.../QbService.java) (revision 784731558fee35bec1bcb6ce76ecf352b5856db5) @@ -627,7 +627,12 @@ @Override public List getUserCollections(int userId) { Set collections = new LinkedHashSet<>(); - + + // even though it is covered by #getUserOwnCollections(), + // it creates user private collection when necessary + QbCollection privateCollection = getUserPrivateCollection(userId); + collections.add(privateCollection); + collections.addAll(getUserOwnCollections(userId)); QbCollection publicCollection = getPublicCollection(); Index: lams_common/src/java/org/lamsfoundation/lams/util/Configuration.java =================================================================== diff -u -rf959b9766e27fe24ae8e16747b92fec53c9a2dfc -r784731558fee35bec1bcb6ce76ecf352b5856db5 --- lams_common/src/java/org/lamsfoundation/lams/util/Configuration.java (.../Configuration.java) (revision f959b9766e27fe24ae8e16747b92fec53c9a2dfc) +++ lams_common/src/java/org/lamsfoundation/lams/util/Configuration.java (.../Configuration.java) (revision 784731558fee35bec1bcb6ce76ecf352b5856db5) @@ -70,7 +70,7 @@ public static final int ITEMS_ONLY_LDAP = 3; public static final String LTI_ADVANTAGE_MODULE_CLASS = "org.lamsfoundation.lams.lti.advantage.util.LtiAdvantageUtil"; - public static final String AI_MODULE_CLASS = "org.lamsfoundation.lams.ai.AiConstants"; + public static final String AI_MODULE_CLASS = "org.lamsfoundation.lams.ai.util.QuestionOpenAiParser"; private static Map items = null; Index: lams_tool_assessment/conf/language/lams/ApplicationResources.properties =================================================================== diff -u -r9ff1812d86a48db3faa6579f8977464d944776f7 -r784731558fee35bec1bcb6ce76ecf352b5856db5 --- lams_tool_assessment/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 9ff1812d86a48db3faa6579f8977464d944776f7) +++ lams_tool_assessment/conf/language/lams/ApplicationResources.properties (.../ApplicationResources.properties) (revision 784731558fee35bec1bcb6ce76ecf352b5856db5) @@ -247,6 +247,7 @@ label.authoring.basic.import.questions = Import label.authoring.basic.export.questions = Export label.authoring.advance.display.summary = Display all questions and answers once the learner finishes. +label.authoring.basic.import.openai = OpenAI label.authoring.basic.import.qti = IMS QTI advanced.reflectOnActivity = Add a notebook at end of Assessment with the following instructions: monitor.summary.td.addNotebook = Add a notebook at end of Assessment Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/AssessmentConstants.java =================================================================== diff -u -r88aed13531804313250bc33b09743ed763b2824e -r784731558fee35bec1bcb6ce76ecf352b5856db5 --- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/AssessmentConstants.java (.../AssessmentConstants.java) (revision 88aed13531804313250bc33b09743ed763b2824e) +++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/AssessmentConstants.java (.../AssessmentConstants.java) (revision 784731558fee35bec1bcb6ce76ecf352b5856db5) @@ -59,9 +59,9 @@ public static final String PARAM_NOT_A_NUMBER = "nan"; public static final String PARAM_GRADE = "grade"; - + public static final String PARAM_MARKER_COMMENT = "markerComment"; - + public static final String PARAM_COLUMN = "column"; public static final String PARAM_MAX_MARK = "maxMark"; @@ -215,11 +215,13 @@ // configuration keys public static final String CONFIG_KEY_HIDE_TITLES = "hideTitles"; - + public static final String CONFIG_KEY_AUTO_EXPAND_JUSTIFICATION = "autoexpandJustification"; public static final String ATTR_IS_QUESTION_ETHERPAD_ENABLED = "isQuestionEtherpadEnabled"; - + + public static final String ATTR_IS_AI_ENABLED = "isAiEnabled"; + public static final String ATTR_CODE_STYLES = "codeStyles"; public static final String ATTR_ALL_GROUP_USERS = "allGroupUsers"; Index: lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/AuthoringController.java =================================================================== diff -u -r09cb8620b7ebc847ae6a700d11ba7c24435a279a -r784731558fee35bec1bcb6ce76ecf352b5856db5 --- lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/AuthoringController.java (.../AuthoringController.java) (revision 09cb8620b7ebc847ae6a700d11ba7c24435a279a) +++ lams_tool_assessment/src/java/org/lamsfoundation/lams/tool/assessment/web/controller/AuthoringController.java (.../AuthoringController.java) (revision 784731558fee35bec1bcb6ce76ecf352b5856db5) @@ -205,6 +205,9 @@ boolean questionEtherpadEnabled = StringUtils.isNotBlank(Configuration.get(ConfigurationKeys.ETHERPAD_API_KEY)); sessionMap.put(AssessmentConstants.ATTR_IS_QUESTION_ETHERPAD_ENABLED, questionEtherpadEnabled); + boolean aiEnabled = Configuration.isLamsModuleAvailable(Configuration.AI_MODULE_CLASS); + sessionMap.put(AssessmentConstants.ATTR_IS_AI_ENABLED, aiEnabled); + Hibernate.initialize(assessment.getSections()); return "pages/authoring/start"; Index: lams_tool_assessment/web/pages/authoring/basic.jsp =================================================================== diff -u -rd578cfde655760533b6ede422e7cf675a8c4ca6d -r784731558fee35bec1bcb6ce76ecf352b5856db5 --- lams_tool_assessment/web/pages/authoring/basic.jsp (.../basic.jsp) (revision d578cfde655760533b6ede422e7cf675a8c4ca6d) +++ lams_tool_assessment/web/pages/authoring/basic.jsp (.../basic.jsp) (revision 784731558fee35bec1bcb6ce76ecf352b5856db5) @@ -2,6 +2,7 @@ +