Source

services/classificator.service.js

import { Metadata } from "@models/Metadata.model";
import { ContradictionDetectorService } from "@services/contradictionDetector.service";
import { IntentProcessorService } from "@services/intentProcessor.service";
import { InvestigationBuilder } from "@services/investigation.builder";
import { InvestigationService } from "@services/investigation.service";
import { Intents, ModifyInvestigationIntents } from "@typez/intent/enums";
import { InvestigationStatus } from "@typez/investigation";
import { getLogger } from "@utils/asyncLocalStorage";
import { getLessonData } from "@utils/lessonsFactory";
import _ from "lodash";
import set from "lodash/set";
/**
 * Classificator Service
 * @category Services
 */
export class ClassificatorService {
    investigationService;
    intentProcessor;
    contradictionDetector;
    /**
     * Constructor
     * @category Services
     */
    constructor() {
        this.investigationService = new InvestigationService();
        this.intentProcessor = new IntentProcessorService();
        this.contradictionDetector = new ContradictionDetectorService();
    }
    /**
     * Processes the classificator according to the detected intent.
     * @category Services
     * @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
     * @returns {Promise<IClassificatorResponseFormat>} - The processed investigation or question message.
     * @throws {Error} If something goes wrong when requesting the LLM.
     */
    async process(messageData) {
        try {
            // Mapping of intents to processing methods
            const processMethodFactory = {
                [Intents.GENERATE_INVESTIGATION]: this.generateInvestigation.bind(this),
                [Intents.GENERATE_INVESTIGATION_BY_ASSISTANT]: this.generateInvestigationByAssistant.bind(this),
                [Intents.MODIFY_INVESTIGATION]: this.modifyInvestigation.bind(this),
                [Intents.RESOLVE_CONTRADICTION]: this.resolveContradiction.bind(this),
                [Intents.DELETE_INVESTIGATION]: this.deleteInvestigation.bind(this),
            };
            const processMethod = processMethodFactory[messageData.intent];
            if (!processMethod) {
                throw new Error(`No process method found for intent: ${messageData.intent}`);
            }
            const processResult = await processMethod(messageData);
            if (!processResult) {
                throw new Error(`Unable to process classificator for ${messageData.intent} intent`);
            }
            return processResult;
        }
        catch (error) {
            throw error;
        }
    }
    /**
     * Generates an investigation based on the knowledge base.
     * @category Services
     * @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
     * @returns {Promise<IClassificatorResponseFormat>} - The generated investigation or question message.
     * @throws {Error} If something goes wrong when requesting the LLM.
     */
    async generateInvestigation(messageData) {
        const logger = getLogger();
        try {
            logger.info("Started generating investigation based on the knowledge base.");
            let retry = 0;
            let detectedUnitResponse = null;
            while (retry < 3) {
                detectedUnitResponse = await this.intentProcessor.detectUnitFromMessage(messageData.message, messageData.history);
                if (!detectedUnitResponse?.question || !detectedUnitResponse?.unit) {
                    retry += 1;
                }
                else if (detectedUnitResponse.question != "-") {
                    return {
                        message: detectedUnitResponse.question,
                    };
                }
                else if (detectedUnitResponse.unit != "-") {
                    break;
                }
                else {
                    retry += 1;
                }
            }
            if (detectedUnitResponse === null) {
                logger.error({
                    error: "Failed to generate investigation",
                    retry: retry,
                    userMessage: messageData.message,
                });
                throw new Error("Unable to generate investigation: Please try again later!!!");
            }
            const unit = detectedUnitResponse?.unit;
            const lessonDataWithInvestigation = await getLessonData(unit, true);
            let investigationMetadata;
            retry = 0;
            while (retry < 3) {
                const detectedLessonWithInvestigationResponse = await this.intentProcessor.detectLessonWithInvestigationFromMessage(messageData.message, lessonDataWithInvestigation, messageData.history);
                if (detectedLessonWithInvestigationResponse.question === "-") {
                    if (detectedLessonWithInvestigationResponse.lesson === "-") {
                        const lessonDataWithoutInvestigation = await getLessonData(unit, false);
                        let retryForDetectLessonWithoutInvestigation = 0;
                        while (retryForDetectLessonWithoutInvestigation < 3) {
                            const detectedLessonWithoutInvestigationResponse = await this.intentProcessor.detectLessonWithoutInvestigationFromMessage(messageData.message, lessonDataWithoutInvestigation, messageData.history);
                            if (detectedLessonWithoutInvestigationResponse.question != "-") {
                                return {
                                    message: detectedLessonWithoutInvestigationResponse.question,
                                };
                            }
                            if (detectedLessonWithoutInvestigationResponse.lesson === "-") {
                                retryForDetectLessonWithoutInvestigation += 1;
                            }
                            else {
                                investigationMetadata = await Metadata.findOne({
                                    unit: unit,
                                    lessonNumber: parseInt(detectedLessonWithoutInvestigationResponse.lesson, 10),
                                });
                                if (investigationMetadata?.hasInvestigation === true) {
                                    retryForDetectLessonWithoutInvestigation += 1;
                                }
                                else {
                                    break;
                                }
                            }
                        }
                        if (investigationMetadata) {
                            break;
                        }
                        else {
                            retry += 1;
                        }
                    }
                    else {
                        investigationMetadata = await Metadata.findOne({
                            unit: unit,
                            lessonNumber: parseInt(detectedLessonWithInvestigationResponse.lesson, 10),
                        });
                        if (investigationMetadata?.hasInvestigation === false) {
                            retry += 1;
                        }
                        else {
                            break;
                        }
                    }
                }
                else {
                    return {
                        message: detectedLessonWithInvestigationResponse.question,
                    };
                }
            }
            if (retry === 3 || !investigationMetadata) {
                logger.error({
                    error: `Failed to generate investigation: Unable to detect a lesson for unit ${unit}. Switching to assistant knowledge.`,
                    retry: retry,
                    userMessage: messageData.message,
                });
                messageData.attempts = messageData.attempts ? messageData.attempts + 1 : 1;
                if (messageData.attempts >= 3) {
                    throw new Error("Unable to generate investigation: Please try again later!!!");
                }
                return this.generateInvestigationByAssistant(messageData);
                // throw new Error("Unable to generate investigation: Please try again later!!!");
            }
            const investigation = (await this.investigationService.generateInvestigation(messageData.message, messageData.history, investigationMetadata));
            logger.info({ AIGeneratedInvestigation: investigation });
            if (!investigation) {
                throw new Error("Unable to generate investigation based on the knowledge base");
            }
            let investigationData = InvestigationBuilder.build(investigation, true, InvestigationStatus.DRAFT_NON_CONTRADICTORY_COMPLETE);
            if (messageData.investigationModel) {
                investigationData = this.mergeInvestigationPreservingUserEdits(messageData.investigationModel, investigationData);
            }
            logger.info({ generated: investigationData }, "Investigation has been successfully generated based on the knowledge base.");
            return {
                message: "Your investigation is ready! Please check it out.",
                data: investigationData,
            };
        }
        catch (error) {
            throw error;
        }
    }
    /**
     * Generates an investigation based on the Assistant's knowledge.
     * @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
     * @returns {Promise<IClassificatorResponseFormat>} - The generated investigation or question message.
     * @throws {Error} If something goes wrong when requesting the LLM.
     */
    async generateInvestigationByAssistant(messageData) {
        const logger = getLogger();
        try {
            logger.info("Started generating investigation based on the assistant knowledge.");
            let investigationMetadata;
            if (messageData.lessonWithoutInvestigation !== null) {
                investigationMetadata = await Metadata.findOne({
                    unit: messageData.lessonWithoutInvestigation?.split("_")[0],
                    lessonNumber: parseInt(messageData.lessonWithoutInvestigation?.split("_")[1], 10),
                });
            }
            if (!investigationMetadata || messageData.lessonWithoutInvestigation === null) {
                let retry = 0;
                let detectedUnitResponse = null;
                while (retry < 3) {
                    detectedUnitResponse = await this.intentProcessor.detectUnitFromMessage(messageData.message, messageData.history);
                    if (detectedUnitResponse.question != "-") {
                        return {
                            message: detectedUnitResponse.question,
                        };
                    }
                    if (detectedUnitResponse.unit === "-") {
                        retry += 1;
                    }
                    else {
                        break;
                    }
                }
                if (detectedUnitResponse === null) {
                    logger.error({
                        error: "Failed to generate investigation",
                        retry: retry,
                        userMessage: messageData.message,
                    });
                    throw new Error("Unable to generate investigation: Please try again later!!!");
                }
                const unit = detectedUnitResponse?.unit;
                const lessonDataWithoutInvestigation = await getLessonData(unit, false);
                retry = 0;
                while (retry < 3) {
                    const detectedLessonWithoutInvestigationResponse = await this.intentProcessor.detectLessonWithInvestigationFromMessage(messageData.message, lessonDataWithoutInvestigation, messageData.history);
                    if (detectedLessonWithoutInvestigationResponse.question != "-") {
                        return {
                            message: detectedLessonWithoutInvestigationResponse.question,
                        };
                    }
                    if (detectedLessonWithoutInvestigationResponse.lesson === "-") {
                        retry += 1;
                    }
                    else {
                        investigationMetadata = await Metadata.findOne({
                            unit: unit,
                            lessonNumber: parseInt(detectedLessonWithoutInvestigationResponse.lesson, 10),
                        });
                        if (investigationMetadata?.hasInvestigation === true) {
                            retry += 1;
                        }
                        else {
                            break;
                        }
                    }
                }
                if (retry === 3) {
                    logger.error({
                        error: `Failed to generate investigation: Unable to detect a lesson for unit ${unit}. switching to not assistant knowledge.`,
                        retry: retry,
                        userMessage: messageData.message,
                    });
                    messageData.attempts = messageData.attempts ? messageData.attempts + 1 : 1;
                    if (messageData.attempts >= 3) {
                        throw new Error("Unable to generate investigation: Please try again later!!!");
                    }
                    return this.generateInvestigation(messageData);
                }
            }
            const investigation = (await this.investigationService.generateInvestigation(messageData.message, messageData.history, investigationMetadata));
            logger.info({ AIGeneratedInvestigation: investigation });
            if (!investigation) {
                throw new Error("Unable to generate investigation based on the assistant knowledge");
            }
            let investigationData = InvestigationBuilder.build(investigation, true, InvestigationStatus.DRAFT_NON_CONTRADICTORY_COMPLETE);
            if (messageData.investigationModel) {
                investigationData = this.mergeInvestigationPreservingUserEdits(messageData.investigationModel, investigationData);
            }
            logger.info({ generated: investigationData }, "Investigation has been successfully generated based on the assistant knowledge.");
            return {
                message: "Your investigation is ready! Please check it out.",
                data: investigationData,
            };
        }
        catch (error) {
            throw error;
        }
    }
    /**
     * Deletes an investigation.
     * @category Services
     * @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
     * @returns {Promise<IClassificatorResponseFormat>} - The deleted investigation or question message.
     * @throws {Error} If something goes wrong when requesting the LLM.
     */
    async deleteInvestigation(messageData) {
        const logger = getLogger();
        try {
            logger.info("Started deleting investigation data.");
            let investigationData = this.investigationService.deleteInvestigationData(messageData.investigationModel);
            if (messageData.investigationModel) {
                messageData.investigationModel = investigationData;
            }
            logger.info({ generated: investigationData }, "Investigation data has been successfully deleted.");
            return Promise.resolve({
                message: "Your investigation data is deleted! Please check it out.",
                data: investigationData,
            });
        }
        catch (error) {
            throw error;
        }
    }
    /**
     * Modifies an investigation.
     * @category Services
     * @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
     * @returns {Promise<IClassificatorResponseFormat>} - The modified investigation or question message.
     * @throws {Error} If something goes wrong when requesting the LLM.
     */
    async modifyInvestigation(messageData) {
        const logger = getLogger();
        try {
            //TODO: query investigation
            //const investigation = await Investigation.findOne({id: messageData.investigationId})
            logger.info("Started modifying investigation.");
            if (!messageData.investigationModel) {
                throw new Error("Investigation must be exists in messageData when modifying it.");
            }
            let modificationIntentResponse = await this.intentProcessor.detectModifyInvestigationIntent(messageData.message, messageData.history, messageData.investigationDto);
            // when assistant gives a question for clarifying process
            if ("question" in modificationIntentResponse.response) {
                return { message: modificationIntentResponse.response.question };
            }
            // Mapping of intents to processing methods
            const modifyInvestigationProcessMethodFactory = {
                [ModifyInvestigationIntents.EDIT_FIELD]: this.intentProcessor.editInvestigationField.bind(this.intentProcessor),
                [ModifyInvestigationIntents.ADD_FIELD]: this.intentProcessor.addInvestigationField.bind(this.intentProcessor),
                [ModifyInvestigationIntents.REMOVE_FIELD]: this.intentProcessor.removeInvestigationField.bind(this.intentProcessor),
            };
            // map intents to promises
            if ("intents" in modificationIntentResponse.response) {
                // create an array of promises returning objects
                const promises = modificationIntentResponse.response.intents.map(async (intent) => ({
                    intent,
                    result: await modifyInvestigationProcessMethodFactory[intent](messageData.message, messageData.history, messageData.investigationDto),
                }));
                const questions = [];
                // await them all
                const modificationResults = await Promise.all(promises);
                for (const modificationResult of modificationResults) {
                    const value = modificationResult.result;
                    if ("question" in value.response && typeof value.response.question === "string") {
                        questions.push(value.response.question);
                    }
                }
                if (questions.length > 0) {
                    let summarizedQuestion = questions[0];
                    if (questions.length > 1) {
                        summarizedQuestion = await this.intentProcessor.summarizeMessages(questions);
                    }
                    return { message: summarizedQuestion };
                }
                const response = await this.processModificationSteps(messageData, modificationResults);
                logger.info("Investigation has been successfully modified.");
                return { message: response, data: messageData.investigationModel };
            }
            throw new Error("Something went wrong when modifying investigation.");
        }
        catch (error) {
            throw error;
        }
    }
    /**
     * Resolves a contradiction.
     * @category Services
     * @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
     * @returns {Promise<IClassificatorResponseFormat>} - The investigation with the resolved contradiction.
     * @throws {Error} If something goes wrong when requesting the LLM.
     */
    async resolveContradiction(messageData) {
        const logger = getLogger();
        try {
            logger.info("Started resolving contradiction.");
            const contradictionResolutionMessage = await this.contradictionDetector.resolveContradiction(messageData.investigationModel);
            if (!contradictionResolutionMessage) {
                throw new Error("Unable to resolve contradiction");
            }
            logger.info("Contradiction has been successfully resolved.");
            return {
                message: contradictionResolutionMessage,
                data: messageData.investigationModel,
            };
        }
        catch (error) {
            throw error;
        }
    }
    /**
     * Processes modification steps.
     * @category Services
     * @param {IClassificatorInputFormat} messageData - Data containing the user's message, intent, current investigation, and chat history.
     * @param {IModifyInvestigationProcessFormat[]} modificationSteps - The detected steps that need to be modified in the current investigation.
     * @returns {Promise<string>} - A message describing the modifications.
     * @throws {Error} If something goes wrong when requesting the LLM.
     */
    async processModificationSteps(messageData, modificationSteps) {
        try {
            // Mapping of intents to processing methods
            const modificationStepsFactory = {
                [ModifyInvestigationIntents.EDIT_FIELD]: this.editInvestigationFields.bind(this),
                [ModifyInvestigationIntents.ADD_FIELD]: this.addInvestigationFields.bind(this),
                [ModifyInvestigationIntents.REMOVE_FIELD]: this.removeInvestigationFields.bind(this),
                // [ModifyInvestigationIntents.REGENERATE_FIELD]: this.regenerateInvestigationFields.bind(this),
                // [ModifyInvestigationIntents.RESOLVE_CONTRADICTION]: this.resolveContradiction.bind(this),
            };
            const isUserChangeExist = await this.intentProcessor.detectUserChangeExist(messageData.message, messageData.history);
            // Store isUserChangeExist on the investigation object for later use in versioning
            if (messageData.investigationModel) {
                messageData.investigationModel._isUserChangeExist = isUserChangeExist;
            }
            // create an array of promises returning objects
            const promises = modificationSteps.map(async (step) => {
                return await modificationStepsFactory[step.intent](messageData.investigationModel, step, isUserChangeExist);
            });
            // wait for all promises
            const results = await Promise.all(promises);
            if (results.length > 1) {
                return await this.intentProcessor.summarizeMessages(results);
            }
            return results[0];
        }
        catch (error) {
            throw error;
        }
    }
    /**
     * Edits investigation fields.
     * @category Services
     * @param {IInvestigation} investigation - The current investigation from the database.
     * @param {IModifyInvestigationProcessFormat} modificationStep - The detected step for editing fields in the current investigation.
     * @param {boolean} isUserChangeExist - If there is user change in the user message
     * @returns {Promise<string>} - A message describing the edited fields.
     * @throws {Error} If something goes wrong when requesting the LLM.
     */
    async editInvestigationFields(investigation, modificationStep, isUserChangeExist) {
        try {
            const editInvestigationStepResult = modificationStep.result;
            const stepResponse = editInvestigationStepResult.response;
            for (const key in stepResponse.editableFields) {
                if (key == "objects" && stepResponse.editableFields.objects != null) {
                    for (const [index, object] of stepResponse.editableFields?.objects.entries()) {
                        for (const objectKey in object) {
                            set(investigation, `objects[${index}].${objectKey}.value`, object[objectKey]);
                            if (!isUserChangeExist) {
                                set(investigation, `objects[${index}].${objectKey}.aiGeneratedValue`, object[objectKey]);
                            }
                        }
                    }
                    if (stepResponse.editableFields.objects.length < investigation.objects.length) {
                        const removableObjects = investigation.objects.length - stepResponse.editableFields.objects.length;
                        investigation.objects.splice(-removableObjects, removableObjects);
                    }
                }
                else if (key == "steps" && stepResponse.editableFields.steps != null) {
                    for (const [index, step] of stepResponse.editableFields?.steps.entries()) {
                        for (const stepKey in step) {
                            set(investigation, `steps[${index}].${stepKey}.value`, step[stepKey]);
                            if (!isUserChangeExist) {
                                set(investigation, `steps[${index}].${stepKey}.aiGeneratedValue`, step[stepKey]);
                            }
                        }
                    }
                    if (stepResponse.editableFields.steps.length < investigation.steps.length) {
                        const removableSteps = investigation.steps.length - stepResponse.editableFields.steps.length;
                        investigation.steps.splice(-removableSteps, removableSteps);
                    }
                }
                else {
                    set(investigation, `${key}.value`, stepResponse.editableFields[key]);
                    if (!isUserChangeExist) {
                        set(investigation, `${key}.aiGeneratedValue`, stepResponse.editableFields[key]);
                    }
                }
            }
            set(investigation, "metadata.dateModified", new Date());
            return await Promise.resolve(stepResponse.changes);
        }
        catch (error) {
            throw error;
        }
    }
    /**
     * Adds fields to an investigation.
     * @category Services
     * @param {IInvestigation} investigation - The current investigation from the database.
     * @param {IModifyInvestigationProcessFormat} modificationStep - The detected step for adding data to existing fields in the current investigation.
     * @param {boolean} isUserChangeExist - If there is user change in the user message
     * @returns {Promise<string>} - A message describing the added data.
     * @throws {Error} If something goes wrong when requesting the LLM.
     */
    async addInvestigationFields(investigation, modificationStep, isUserChangeExist) {
        try {
            const addInvestigationStepResult = modificationStep.result;
            const stepResponse = addInvestigationStepResult.response;
            for (const step of stepResponse.addedFields) {
                if (!step.addedField.includes("[")) {
                    set(investigation, `${step.addedField}.value`, step.content);
                    if (!isUserChangeExist) {
                        set(investigation, `${step.addedField}.aiGeneratedValue`, step.content);
                    }
                }
                else {
                    try {
                        set(investigation, `${step.addedField}`, this.transformObjects(JSON.parse(step.content), isUserChangeExist));
                    }
                    catch {
                        set(investigation, `${step.addedField}`, this.transformObjects(step.content, isUserChangeExist));
                    }
                }
            }
            set(investigation, "metadata.dateModified", new Date());
            return await Promise.resolve(stepResponse.changes);
        }
        catch (error) {
            throw error;
        }
    }
    /**
     * Removes fields from an investigation.
     * @category Services
     * @param {IInvestigation} investigation - The current investigation from the database.
     * @param {IModifyInvestigationProcessFormat} modificationStep - The detected step for removing data from the current investigation.
     * @returns {Promise<string>} - A message describing the removed data.
     * @throws {Error} If something goes wrong when requesting the LLM.
     */
    async removeInvestigationFields(investigation, modificationStep) {
        try {
            const removeInvestigationStepResult = modificationStep.result;
            const stepResponse = removeInvestigationStepResult.response;
            for (const removedField of stepResponse.removedFields.slice().reverse()) {
                const match = removedField.match(/(.*)\[(\d+)\]$/);
                if (match) {
                    const arrayPath = match[1];
                    const arrayIndex = match[2];
                    if (!arrayIndex)
                        continue;
                    const index = parseInt(arrayIndex, 10);
                    const arr = _.get(investigation, arrayPath);
                    if (!Array.isArray(arr))
                        continue;
                    // Remove the element by index
                    const newArr = arr.filter((_, i) => i !== index);
                    // Replace the array in the document
                    _.set(investigation, arrayPath, newArr);
                }
                else {
                    if (Array.isArray(_.get(investigation, removedField))) {
                        set(investigation, removedField, []);
                    }
                    else if (_.isString(_.get(investigation, `${removedField}.value`))) {
                        set(investigation, `${removedField}.value`, null);
                        set(investigation, `${removedField}.aiGeneratedValue`, null);
                    }
                    else {
                        set(investigation, removedField, null);
                    }
                }
            }
            set(investigation, "metadata.dateModified", new Date());
            return await Promise.resolve(stepResponse.changes);
        }
        catch (error) {
            throw error;
        }
    }
    /**
     * Transforms a step for adding investigation fields.
     * @category Services
     * @param {string} obj - The content of the field addition modification step.
     * @param {boolean} isUserChangeExist - If there is user change in the user message
     * @returns {object} - An object containing `value` and `aiGeneratedValue` fields.
     */
    transformObjects(obj, isUserChangeExist) {
        return _.mapValues(obj, (value) => {
            if (_.isPlainObject(value)) {
                return this.transformObjects(value, isUserChangeExist);
            }
            else {
                if (isUserChangeExist) {
                    return {
                        value: String(value),
                    };
                }
                return {
                    value: String(value),
                    aiGeneratedValue: String(value),
                };
            }
        });
    }
    /**
     * Merges AI-generated investigation with existing investigation,
     * preserving fields where value !== aiGeneratedValue (user-edited fields).
     * @category Services
     * @param {IInvestigation} existing - The existing investigation with potentially user-edited fields.
     * @param {IInvestigation} generated - The newly AI-generated investigation.
     * @returns {IInvestigation} - The merged investigation preserving user-edited fields.
     */
    mergeInvestigationPreservingUserEdits(existing, generated) {
        const merged = { ...generated };
        // List of editable field names
        const editableFields = [
            "title",
            "curriculum",
            "unitNumberAndTitle",
            "grade",
            "lessonNumberAndTitle",
            "objectives",
            "ngss",
            "analyticalFacts",
            "goals",
            "day",
        ];
        // Check each editable field
        for (const fieldName of editableFields) {
            const existingField = existing[fieldName];
            const generatedField = generated[fieldName];
            if (existingField && generatedField) {
                // If value !== aiGeneratedValue, it means user edited it - preserve it
                const isUserEdited = existingField.value !== null &&
                    existingField.aiGeneratedValue !== null &&
                    existingField.value !== existingField.aiGeneratedValue;
                if (isUserEdited) {
                    // Preserve the user-edited field
                    merged[fieldName] = existingField;
                }
                // Otherwise, use the generated field (value === aiGeneratedValue means it's still AI-generated)
            }
            else if (existingField) {
                // If field exists in existing but not in generated, preserve it
                merged[fieldName] = existingField;
            }
        }
        // Handle steps - preserve user-edited step fields
        if (existing.steps && generated.steps) {
            merged.steps = generated.steps.map((generatedStep, index) => {
                const existingStep = existing.steps?.[index];
                if (!existingStep) {
                    return generatedStep;
                }
                const mergedStep = { ...generatedStep };
                const stepFields = [
                    "title",
                    "descriptionEn",
                    "desiredOutcome",
                    "alternativeOutcome",
                ];
                for (const stepFieldName of stepFields) {
                    const existingStepField = existingStep[stepFieldName];
                    const generatedStepField = generatedStep[stepFieldName];
                    if (existingStepField && generatedStepField) {
                        const isUserEdited = existingStepField.value !== null &&
                            existingStepField.aiGeneratedValue !== null &&
                            existingStepField.value !== existingStepField.aiGeneratedValue;
                        if (isUserEdited) {
                            mergedStep[stepFieldName] = existingStepField;
                        }
                    }
                    else if (existingStepField) {
                        mergedStep[stepFieldName] = existingStepField;
                    }
                }
                return mergedStep;
            });
        }
        else if (existing.steps) {
            // If existing has steps but generated doesn't, preserve existing steps
            merged.steps = existing.steps;
        }
        // Handle objects - preserve user-edited object fields
        if (existing.objects && generated.objects) {
            merged.objects = generated.objects.map((generatedObj, index) => {
                const existingObj = existing.objects?.[index];
                if (!existingObj) {
                    return generatedObj;
                }
                const mergedObj = { ...generatedObj };
                // Check name field
                if (existingObj.name && generatedObj.name) {
                    const isUserEdited = existingObj.name.value !== null &&
                        existingObj.name.aiGeneratedValue !== null &&
                        existingObj.name.value !== existingObj.name.aiGeneratedValue;
                    if (isUserEdited) {
                        mergedObj.name = existingObj.name;
                    }
                }
                else if (existingObj.name) {
                    mergedObj.name = existingObj.name;
                }
                // Check objectId field
                if (existingObj.objectId && generatedObj.objectId) {
                    const isUserEdited = existingObj.objectId.value !== null &&
                        existingObj.objectId.aiGeneratedValue !== null &&
                        existingObj.objectId.value !== existingObj.objectId.aiGeneratedValue;
                    if (isUserEdited) {
                        mergedObj.objectId = existingObj.objectId;
                    }
                }
                else if (existingObj.objectId) {
                    mergedObj.objectId = existingObj.objectId;
                }
                // Check size field
                if (existingObj.size && generatedObj.size) {
                    const isUserEdited = existingObj.size.value !== null &&
                        existingObj.size.aiGeneratedValue !== null &&
                        existingObj.size.value !== existingObj.size.aiGeneratedValue;
                    if (isUserEdited) {
                        mergedObj.size = existingObj.size;
                    }
                }
                else if (existingObj.size) {
                    mergedObj.size = existingObj.size;
                }
                return mergedObj;
            });
        }
        else if (existing.objects) {
            // If existing has objects but generated doesn't, preserve existing objects
            merged.objects = existing.objects;
        }
        return merged;
    }
}