From 1ded593121b6b44770b973abfacc52a16728f431 Mon Sep 17 00:00:00 2001 From: pabpergas Date: Sun, 22 Feb 2026 08:33:48 +0100 Subject: [PATCH] feat: extend problem wizard --- controllers/problemGeneratorController.js | 7 ++- services/evaluationService.js | 6 +- services/problemGeneratorService.js | 67 ++++++++++++++++++++--- 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/controllers/problemGeneratorController.js b/controllers/problemGeneratorController.js index ac98bc2..10027d0 100644 --- a/controllers/problemGeneratorController.js +++ b/controllers/problemGeneratorController.js @@ -7,21 +7,22 @@ const problemGeneratorService = require("../services/problemGeneratorService"); const generateProblem = async (req, res) => { try { const { subject, additionalDetails, exampleProblem } = req.body; + const normalizedSubject = typeof subject === "string" ? subject.trim() : ""; - if (!subject) { + if (!normalizedSubject) { return res.status(400).json({ error: "Subject is required", }); } - if (!exampleProblem) { + if (!exampleProblem || typeof exampleProblem !== "object") { return res.status(400).json({ error: "Example problem is required", }); } const generatedProblem = await problemGeneratorService.generateProblem({ - subject, + subject: normalizedSubject, additionalDetails, exampleProblem, }); diff --git a/services/evaluationService.js b/services/evaluationService.js index dacbc78..18465d3 100644 --- a/services/evaluationService.js +++ b/services/evaluationService.js @@ -31,8 +31,8 @@ class EvaluationService { } const expectedSolution = leia.spec.problem.spec.solution; - const solutionFormat = leia.spec.problem.solutionFormat; - const evaluationPrompt = leia.spec.problem.evaluationPrompt; + const solutionFormat = leia.spec.problem.spec.solutionFormat; + const evaluationPrompt = leia.spec.problem.spec.evaluationPrompt; const prompt = this.generatePrompt(result, expectedSolution, solutionFormat, evaluationPrompt) @@ -63,7 +63,7 @@ class EvaluationService { } } - return `Evaluate the following solution (${solutionFormat} format): + return `Evaluate the following solution (${format} format): Student's solution: ${studentSolution} diff --git a/services/problemGeneratorService.js b/services/problemGeneratorService.js index 790a717..639b1eb 100644 --- a/services/problemGeneratorService.js +++ b/services/problemGeneratorService.js @@ -1,6 +1,6 @@ const { OpenAI } = require("openai"); const z = require("zod"); -const { zodResponseFormat, zodTextFormat } = require("openai/helpers/zod"); +const { zodTextFormat } = require("openai/helpers/zod"); // Schema for generated problem @@ -9,8 +9,34 @@ const ProblemSpecSchema = z.object({ personaBackground: z.string().describe("Background context for the persona in this problem scenario"), details: z.string().describe("Extended details about the problem, including specific requirements"), solution: z.string().describe("The expected solution in the specified format"), + evaluationPrompt: z.string().optional().describe("Prompt used to evaluate the student solution"), + extendsJson: z.string().optional().describe("JSON object string for extends (e.g., {\"persona\": {\"spec\": {\"personality\": [\"very busy\"]}}})"), + overridesJson: z.string().optional().describe("JSON object string for overrides (e.g., {\"behaviour\": {\"spec\": {\"role\": \"tax inspector\"}}})"), }); +const isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value); +const hasKeys = (value) => isObject(value) && Object.keys(value).length > 0; +const safeStringifyObject = (value) => { + if (!isObject(value)) return "Not provided"; + try { + return JSON.stringify(value, null, 2); + } catch { + return "Not provided"; + } +}; +const safeParseJsonObject = (value) => { + if (typeof value !== "string" || !value.trim()) { + return null; + } + + try { + const parsed = JSON.parse(value); + return isObject(parsed) ? parsed : null; + } catch { + return null; + } +}; + class ProblemGeneratorService { constructor() { const openai = new OpenAI({ @@ -22,7 +48,7 @@ class ProblemGeneratorService { /** * Generates a new problem based on an example problem with a different subject * @param {Object} params - Generation parameters - * @param {string} params.subject - The new subject for the problem (e.g., "Prison Management System") + * @param {string} params.subject - The new subject for the problem (e.g., "Library Management System") * @param {string} params.additionalDetails - Optional additional instructions from the user * @param {Object} params.exampleProblem - The example problem to use as a template * @returns {Promise} - Generated problem matching the Problem schema @@ -33,6 +59,12 @@ class ProblemGeneratorService { } const exampleSpec = exampleProblem.spec || exampleProblem; + const templateHasEvaluationPrompt = Boolean( + typeof exampleSpec.evaluationPrompt === "string" && + exampleSpec.evaluationPrompt.trim() + ); + const templateHasExtends = hasKeys(exampleSpec.extends); + const templateHasOverrides = hasKeys(exampleSpec.overrides); const prompt = `Generate a new problem for an educational exercise. @@ -58,6 +90,9 @@ You can use these placeholders in the problem fields (description, personaBackgr - Details: ${exampleSpec.details || "Not provided"} - Solution: ${exampleSpec.solution || "Not provided"} - Solution Format: ${exampleSpec.solutionFormat || "text"} +- Evaluation Prompt: ${templateHasEvaluationPrompt ? exampleSpec.evaluationPrompt : "Not provided"} +- Extends (JSON): ${safeStringifyObject(exampleSpec.extends)} +- Overrides (JSON): ${safeStringifyObject(exampleSpec.overrides)} ## NEW PROBLEM REQUIREMENTS: - Topic/Domain: "${subject}" @@ -71,7 +106,17 @@ ${additionalDetails ? `- Additional instructions: ${additionalDetails}` : ""} 5. The SOLUTION must be complete and in ${exampleSpec.solutionFormat || "text"} format (if "mermaid", generate a valid UML class diagram) 6. Maintain the same level of complexity and detail as the example 7. Content should be realistic and educational for students -8. Use template tags ({{persona.*}}, {{behaviour.*}}) to make the content dynamic where it makes sense`; +8. Use template tags ({{persona.*}}, {{behaviour.*}}) to make the content dynamic where it makes sense +9. ${templateHasEvaluationPrompt + ? "Generate an adapted EVALUATION PROMPT for the new domain keeping the same intent and strictness." + : "If there is no evaluation prompt in the template, return an empty string or omit it."} +10. ${templateHasExtends + ? "Adapt EXTENDS to the new domain. Keep the same structure and keys, but update domain-specific values." + : "If extends is not provided in the template, return an empty object or omit it."} +11. ${templateHasOverrides + ? "Adapt OVERRIDES to the new domain. Keep the same structure and keys, but update domain-specific values." + : "If overrides is not provided in the template, return an empty object or omit it."} +12. IMPORTANT: Return extends and overrides as valid JSON STRINGS in fields extendsJson and overridesJson. Use "{}" when empty.`; const response = await this.client.responses.parse({ model: process.env.OPENAI_MODEL || "gpt-4o-mini", @@ -95,21 +140,29 @@ Always respond in the same language as the example problem provided.`, } }); - const generatedSpec = response.output_parsed; + const generatedSpec = response.output_parsed || {}; + const generatedExtends = safeParseJsonObject(generatedSpec.extendsJson); + const generatedOverrides = safeParseJsonObject(generatedSpec.overridesJson); // Build the complete problem object return { apiVersion: "v1", metadata: { - name: subject.toLowerCase().replace(/\s+/g, "-"), + name: subject.trim().toLowerCase().replace(/\s+/g, "-"), version: "1.0.0", }, spec: { ...generatedSpec, solutionFormat: exampleSpec.solutionFormat || "text", process: exampleSpec.process || [], - extends: exampleSpec.extends || {}, - overrides: exampleSpec.overrides || {}, + evaluationPrompt: generatedSpec.evaluationPrompt + || (templateHasEvaluationPrompt ? exampleSpec.evaluationPrompt : ""), + extends: hasKeys(generatedExtends) + ? generatedExtends + : (exampleSpec.extends || {}), + overrides: hasKeys(generatedOverrides) + ? generatedOverrides + : (exampleSpec.overrides || {}), constrainedTo: exampleSpec.constrainedTo || {}, }, };