@learnpack/learnpack 5.0.294 → 5.0.296

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -625,6 +625,79 @@ class ServeCommand extends SessionCommand_1.default {
625
625
  res.status(500).json({ error: error.message });
626
626
  }
627
627
  });
628
+ app.post("/webhooks/:courseSlug/:exercisePosition/process-coding-challege/", async (req, res) => {
629
+ const { courseSlug, exercisePosition } = req.params;
630
+ const body = req.body;
631
+ console.log("RECEIVING CODING CHALLENGE WEBHOOK for course:", courseSlug, "exercise position:", exercisePosition);
632
+ console.log("Webhook body:", JSON.stringify(body, null, 2));
633
+ try {
634
+ if (body.status === "ERROR") {
635
+ console.error("❌ Error in coding challenge generation:", body.error);
636
+ return res.json({ status: "ERROR" });
637
+ }
638
+ // Parse the RigoBot response
639
+ if (body.parsed) {
640
+ const { files, reasoning } = body.parsed;
641
+ console.log("📋 Reasoning:", reasoning);
642
+ console.log("📁 Files received:", files);
643
+ if (files && Array.isArray(files)) {
644
+ console.log("✅ Processing files for coding challenge...");
645
+ // Get the current exercise info to determine the exercise directory
646
+ const syllabus = await getSyllabus(courseSlug, bucket);
647
+ const exercise = syllabus.lessons[parseInt(exercisePosition)];
648
+ if (!exercise) {
649
+ console.error(`❌ Exercise not found at position ${exercisePosition}`);
650
+ return res.status(404).json({ error: "Exercise not found" });
651
+ }
652
+ const exSlug = (0, creatorUtilities_2.slugify)(exercise.id + "-" + exercise.title);
653
+ const exerciseDir = `courses/${courseSlug}/exercises/${exSlug}`;
654
+ for (const fileStr of files) {
655
+ try {
656
+ const fileObj = JSON.parse(fileStr);
657
+ console.log(`📄 Processing file: ${fileObj.name}`);
658
+ // Save the main file with content
659
+ if (fileObj.name && fileObj.content) {
660
+ const filePath = `${exerciseDir}/${fileObj.name}`;
661
+ // eslint-disable-next-line no-await-in-loop
662
+ await uploadFileToBucket(bucket, fileObj.content, filePath);
663
+ console.log(`✅ Saved file: ${filePath}`);
664
+ }
665
+ // Save the solution file if it exists
666
+ if (fileObj.name && fileObj.solution) {
667
+ const nameParts = fileObj.name.split(".");
668
+ if (nameParts.length > 1) {
669
+ const extension = nameParts.pop();
670
+ const baseName = nameParts.join(".");
671
+ const solutionFileName = `${baseName}.solution.hide.${extension}`;
672
+ const solutionFilePath = `${exerciseDir}/${solutionFileName}`;
673
+ // eslint-disable-next-line no-await-in-loop
674
+ await uploadFileToBucket(bucket, fileObj.solution, solutionFilePath);
675
+ console.log(`✅ Saved solution file: ${solutionFilePath}`);
676
+ }
677
+ else {
678
+ // If no extension, just add .solution.hide
679
+ const solutionFileName = `${fileObj.name}.solution.hide`;
680
+ const solutionFilePath = `${exerciseDir}/${solutionFileName}`;
681
+ // eslint-disable-next-line no-await-in-loop
682
+ await uploadFileToBucket(bucket, fileObj.solution, solutionFilePath);
683
+ console.log(`✅ Saved solution file: ${solutionFilePath}`);
684
+ }
685
+ }
686
+ }
687
+ catch (parseError) {
688
+ console.error(`❌ Error parsing file:`, parseError);
689
+ }
690
+ }
691
+ console.log("✅ All coding challenge files saved successfully");
692
+ }
693
+ }
694
+ res.json({ status: "SUCCESS" });
695
+ }
696
+ catch (error) {
697
+ console.error("❌ Error processing coding challenge webhook:", error);
698
+ res.status(500).json({ error: error.message });
699
+ }
700
+ });
628
701
  app.post("/actions/continue-generating/:courseSlug/:position", async (req, res) => {
629
702
  const { courseSlug, position } = req.params;
630
703
  const { feedback, mode } = req.body;
@@ -695,6 +768,65 @@ class ServeCommand extends SessionCommand_1.default {
695
768
  await (0, exports.processImage)(image.url, image.alt, rigoToken, courseSlug);
696
769
  res.json({ status: "QUEUED" });
697
770
  });
771
+ app.post("/actions/generate-code-challenge", async (req, res) => {
772
+ const rigoToken = req.header("x-rigo-token");
773
+ const { code_challenge, lesson_content, exercise_position, course_slug } = req.body;
774
+ if (!rigoToken) {
775
+ return res.status(400).json({
776
+ error: "Rigo token is required. x-rigo-token header is missing",
777
+ });
778
+ }
779
+ if (!code_challenge) {
780
+ return res.status(400).json({
781
+ error: "code_challenge is required",
782
+ });
783
+ }
784
+ if (!lesson_content) {
785
+ return res.status(400).json({
786
+ error: "lesson_content is required",
787
+ });
788
+ }
789
+ if (!course_slug) {
790
+ return res.status(400).json({
791
+ error: "course_slug is required",
792
+ });
793
+ }
794
+ if (exercise_position === undefined || exercise_position === null) {
795
+ return res.status(400).json({
796
+ error: "exercise_position is required",
797
+ });
798
+ }
799
+ if (typeof exercise_position !== "number" || exercise_position < 0) {
800
+ return res.status(400).json({
801
+ error: "exercise_position must be a valid number >= 0",
802
+ });
803
+ }
804
+ try {
805
+ const webhookUrl = `${process.env.HOST}/webhooks/${course_slug}/${exercise_position}/process-coding-challege/`;
806
+ const result = await (0, rigoActions_1.generateCodeChallenge)(rigoToken, {
807
+ lesson_content,
808
+ challenge_proposal: code_challenge,
809
+ }, webhookUrl);
810
+ if (!result) {
811
+ return res.status(500).json({
812
+ error: "Failed to generate code challenge",
813
+ });
814
+ }
815
+ console.log("Code challenge generation started:", result);
816
+ res.json({
817
+ status: "QUEUED",
818
+ id: result.id,
819
+ message: "Code challenge generation started",
820
+ });
821
+ }
822
+ catch (error) {
823
+ console.error("Error generating code challenge:", error);
824
+ res.status(500).json({
825
+ error: "Failed to start code challenge generation",
826
+ details: error.message,
827
+ });
828
+ }
829
+ });
698
830
  app.post("/webhooks/:courseSlug/exercise-processor/:lessonID/:rigoToken", async (req, res) => {
699
831
  // console.log("Receiving a webhook to exercise processor")
700
832
  const { courseSlug, lessonID, rigoToken } = req.params;
@@ -105,4 +105,9 @@ type TAddInteractivityInputs = {
105
105
  current_syllabus: string;
106
106
  };
107
107
  export declare const addInteractivity: (token: string, inputs: TAddInteractivityInputs, webhookUrl?: string) => Promise<any>;
108
+ type TGenerateCodeChallengeInputs = {
109
+ lesson_content: string;
110
+ challenge_proposal: string;
111
+ };
112
+ export declare const generateCodeChallenge: (token: string, inputs: TGenerateCodeChallengeInputs, webhookUrl?: string) => Promise<any>;
108
113
  export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.addInteractivity = exports.initialContentGenerator = exports.getLanguageCodes = exports.isPackageAuthor = exports.fillSidebarJSON = exports.generateCourseShortName = exports.isValidRigoToken = exports.translateCourseMetadata = exports.createStructuredPreviewReadme = exports.readmeCreator = exports.createCodingReadme = exports.createCodeFile = exports.interactiveCreation = exports.generateCourseIntroduction = exports.translateExercise = exports.generateImage = exports.hasCreatorPermission = exports.createReadme = void 0;
3
+ exports.generateCodeChallenge = exports.addInteractivity = exports.initialContentGenerator = exports.getLanguageCodes = exports.isPackageAuthor = exports.fillSidebarJSON = exports.generateCourseShortName = exports.isValidRigoToken = exports.translateCourseMetadata = exports.createStructuredPreviewReadme = exports.readmeCreator = exports.createCodingReadme = exports.createCodeFile = exports.interactiveCreation = exports.generateCourseIntroduction = exports.translateExercise = exports.generateImage = exports.hasCreatorPermission = exports.createReadme = void 0;
4
4
  exports.downloadImage = downloadImage;
5
5
  exports.createPreviewReadme = createPreviewReadme;
6
6
  exports.makeReadmeReadable = makeReadmeReadable;
@@ -325,3 +325,24 @@ const addInteractivity = async (token, inputs, webhookUrl) => {
325
325
  }
326
326
  };
327
327
  exports.addInteractivity = addInteractivity;
328
+ const generateCodeChallenge = async (token, inputs, webhookUrl) => {
329
+ try {
330
+ const response = await axios_1.default.post(`${api_1.RIGOBOT_HOST}/v1/prompting/completion/generate-code-challenge-files/`, {
331
+ inputs,
332
+ include_purpose_objective: false,
333
+ execute_async: !!webhookUrl,
334
+ webhook_url: webhookUrl,
335
+ }, {
336
+ headers: {
337
+ "Content-Type": "application/json",
338
+ Authorization: "Token " + token.trim(),
339
+ },
340
+ });
341
+ return response.data;
342
+ }
343
+ catch (error) {
344
+ console.error("Error in generateCodeChallenge:", error);
345
+ return null;
346
+ }
347
+ };
348
+ exports.generateCodeChallenge = generateCodeChallenge;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@learnpack/learnpack",
3
3
  "description": "Seamlessly build, sell and/or take interactive & auto-graded tutorials, start learning now or build a new tutorial to your audience.",
4
- "version": "5.0.294",
4
+ "version": "5.0.296",
5
5
  "author": "Alejandro Sanchez @alesanchezr",
6
6
  "contributors": [
7
7
  {
@@ -54,6 +54,7 @@
54
54
  "mkdirp": "^3.0.1",
55
55
  "moment": "^2.27.0",
56
56
  "multer": "^2.0.0",
57
+ "newrelic": "^13.4.0",
57
58
  "node-emoji": "^1.10.0",
58
59
  "node-fetch": "^2.7.0",
59
60
  "node-persist": "^3.1.0",
@@ -1,3 +1,4 @@
1
+ import * as newrelic from "newrelic"
1
2
  import { flags } from "@oclif/command"
2
3
 
3
4
  import { Buffer } from "buffer"
@@ -36,6 +37,7 @@ import {
36
37
  getLanguageCodes,
37
38
  initialContentGenerator,
38
39
  addInteractivity,
40
+ generateCodeChallenge,
39
41
  } from "../utils/rigoActions"
40
42
  import * as dotenv from "dotenv"
41
43
 
@@ -994,6 +996,115 @@ export default class ServeCommand extends SessionCommand {
994
996
  }
995
997
  })
996
998
 
999
+ app.post(
1000
+ "/webhooks/:courseSlug/:exercisePosition/process-coding-challege/",
1001
+ async (req, res) => {
1002
+ const { courseSlug, exercisePosition } = req.params
1003
+ const body = req.body
1004
+
1005
+ console.log(
1006
+ "RECEIVING CODING CHALLENGE WEBHOOK for course:",
1007
+ courseSlug,
1008
+ "exercise position:",
1009
+ exercisePosition
1010
+ )
1011
+ console.log("Webhook body:", JSON.stringify(body, null, 2))
1012
+
1013
+ try {
1014
+ if (body.status === "ERROR") {
1015
+ console.error(
1016
+ "❌ Error in coding challenge generation:",
1017
+ body.error
1018
+ )
1019
+ return res.json({ status: "ERROR" })
1020
+ }
1021
+
1022
+ // Parse the RigoBot response
1023
+ if (body.parsed) {
1024
+ const { files, reasoning } = body.parsed
1025
+
1026
+ console.log("📋 Reasoning:", reasoning)
1027
+ console.log("📁 Files received:", files)
1028
+
1029
+ if (files && Array.isArray(files)) {
1030
+ console.log("✅ Processing files for coding challenge...")
1031
+
1032
+ // Get the current exercise info to determine the exercise directory
1033
+ const syllabus = await getSyllabus(courseSlug, bucket)
1034
+ const exercise = syllabus.lessons[parseInt(exercisePosition)]
1035
+
1036
+ if (!exercise) {
1037
+ console.error(
1038
+ `❌ Exercise not found at position ${exercisePosition}`
1039
+ )
1040
+ return res.status(404).json({ error: "Exercise not found" })
1041
+ }
1042
+
1043
+ const exSlug = slugify(exercise.id + "-" + exercise.title)
1044
+ const exerciseDir = `courses/${courseSlug}/exercises/${exSlug}`
1045
+
1046
+ for (const fileStr of files) {
1047
+ try {
1048
+ const fileObj = JSON.parse(fileStr)
1049
+ console.log(`📄 Processing file: ${fileObj.name}`)
1050
+
1051
+ // Save the main file with content
1052
+ if (fileObj.name && fileObj.content) {
1053
+ const filePath = `${exerciseDir}/${fileObj.name}`
1054
+ // eslint-disable-next-line no-await-in-loop
1055
+ await uploadFileToBucket(bucket, fileObj.content, filePath)
1056
+ console.log(`✅ Saved file: ${filePath}`)
1057
+ }
1058
+
1059
+ // Save the solution file if it exists
1060
+ if (fileObj.name && fileObj.solution) {
1061
+ const nameParts = fileObj.name.split(".")
1062
+ if (nameParts.length > 1) {
1063
+ const extension = nameParts.pop()
1064
+ const baseName = nameParts.join(".")
1065
+ const solutionFileName = `${baseName}.solution.hide.${extension}`
1066
+ const solutionFilePath = `${exerciseDir}/${solutionFileName}`
1067
+ // eslint-disable-next-line no-await-in-loop
1068
+ await uploadFileToBucket(
1069
+ bucket,
1070
+ fileObj.solution,
1071
+ solutionFilePath
1072
+ )
1073
+ console.log(
1074
+ `✅ Saved solution file: ${solutionFilePath}`
1075
+ )
1076
+ } else {
1077
+ // If no extension, just add .solution.hide
1078
+ const solutionFileName = `${fileObj.name}.solution.hide`
1079
+ const solutionFilePath = `${exerciseDir}/${solutionFileName}`
1080
+ // eslint-disable-next-line no-await-in-loop
1081
+ await uploadFileToBucket(
1082
+ bucket,
1083
+ fileObj.solution,
1084
+ solutionFilePath
1085
+ )
1086
+ console.log(
1087
+ `✅ Saved solution file: ${solutionFilePath}`
1088
+ )
1089
+ }
1090
+ }
1091
+ } catch (parseError) {
1092
+ console.error(`❌ Error parsing file:`, parseError)
1093
+ }
1094
+ }
1095
+
1096
+ console.log("✅ All coding challenge files saved successfully")
1097
+ }
1098
+ }
1099
+
1100
+ res.json({ status: "SUCCESS" })
1101
+ } catch (error) {
1102
+ console.error("❌ Error processing coding challenge webhook:", error)
1103
+ res.status(500).json({ error: (error as Error).message })
1104
+ }
1105
+ }
1106
+ )
1107
+
997
1108
  app.post(
998
1109
  "/actions/continue-generating/:courseSlug/:position",
999
1110
  async (req, res) => {
@@ -1090,6 +1201,80 @@ export default class ServeCommand extends SessionCommand {
1090
1201
  res.json({ status: "QUEUED" })
1091
1202
  })
1092
1203
 
1204
+ app.post("/actions/generate-code-challenge", async (req, res) => {
1205
+ const rigoToken = req.header("x-rigo-token")
1206
+ const { code_challenge, lesson_content, exercise_position, course_slug } =
1207
+ req.body
1208
+
1209
+ if (!rigoToken) {
1210
+ return res.status(400).json({
1211
+ error: "Rigo token is required. x-rigo-token header is missing",
1212
+ })
1213
+ }
1214
+
1215
+ if (!code_challenge) {
1216
+ return res.status(400).json({
1217
+ error: "code_challenge is required",
1218
+ })
1219
+ }
1220
+
1221
+ if (!lesson_content) {
1222
+ return res.status(400).json({
1223
+ error: "lesson_content is required",
1224
+ })
1225
+ }
1226
+
1227
+ if (!course_slug) {
1228
+ return res.status(400).json({
1229
+ error: "course_slug is required",
1230
+ })
1231
+ }
1232
+
1233
+ if (exercise_position === undefined || exercise_position === null) {
1234
+ return res.status(400).json({
1235
+ error: "exercise_position is required",
1236
+ })
1237
+ }
1238
+
1239
+ if (typeof exercise_position !== "number" || exercise_position < 0) {
1240
+ return res.status(400).json({
1241
+ error: "exercise_position must be a valid number >= 0",
1242
+ })
1243
+ }
1244
+
1245
+ try {
1246
+ const webhookUrl = `${process.env.HOST}/webhooks/${course_slug}/${exercise_position}/process-coding-challege/`
1247
+
1248
+ const result = await generateCodeChallenge(
1249
+ rigoToken,
1250
+ {
1251
+ lesson_content,
1252
+ challenge_proposal: code_challenge,
1253
+ },
1254
+ webhookUrl
1255
+ )
1256
+
1257
+ if (!result) {
1258
+ return res.status(500).json({
1259
+ error: "Failed to generate code challenge",
1260
+ })
1261
+ }
1262
+
1263
+ console.log("Code challenge generation started:", result)
1264
+ res.json({
1265
+ status: "QUEUED",
1266
+ id: result.id,
1267
+ message: "Code challenge generation started",
1268
+ })
1269
+ } catch (error) {
1270
+ console.error("Error generating code challenge:", error)
1271
+ res.status(500).json({
1272
+ error: "Failed to start code challenge generation",
1273
+ details: (error as Error).message,
1274
+ })
1275
+ }
1276
+ })
1277
+
1093
1278
  app.post(
1094
1279
  "/webhooks/:courseSlug/exercise-processor/:lessonID/:rigoToken",
1095
1280
  async (req, res) => {