Source

services/contradictionDetector.service.js

import { AIProcessor } from "@helpers/ai_processor";
import { Metadata } from "@models/Metadata.model";
import { Prompt } from "@models/Prompt.model";
import { toInvestigationResponseDto } from "@typez/investigation";
import { getLogger } from "@utils/asyncLocalStorage";
import yamlModule from "js-yaml";
import set from "lodash/set";
import { AssistantContradictionDetectionFormat, AssistantContradictionResolutionFormat, } from "schemas/assistant.validation";
/**
 * Contradiction Detector Service
 * @category Services
 */
export class ContradictionDetectorService {
    aiProcessor;
    /**
     * Constructor
     * @category Services
     */
    constructor() {
        this.aiProcessor = new AIProcessor();
    }
    /**
     * Detects contradictions between investigation fields.
     * @category Services
     * @param {ICreateInvestigationDto} investigation - The investigation in which to detect contradictions.
     * @returns {Promise<IAssistantContradictionDetectionFormat>} - A detected contradiction for the given investigation.
     */
    async detectContradiction(investigation) {
        const logger = getLogger();
        try {
            logger.info(`Started to detecting contradiction`);
            const prompt = await Prompt.findOne({ name: "detect_contradiction" });
            if (!prompt) {
                throw new Error("Contradiction detection prompt not found.");
            }
            const unit = investigation.unitNumberAndTitle?.split(":")[0]?.split(" ")[1];
            const lesson = investigation.lessonNumberAndTitle?.split(":")[0]?.split(" ")[1] || "0";
            const lessonNumber = parseInt(lesson, 10);
            const metadata = await Metadata.findOne({
                unit: unit,
                lessonNumber: Number.isNaN(lessonNumber) ? undefined : lessonNumber,
            });
            let metadataYaml;
            if (metadata) {
                const investigationMetadataObject = metadata.toObject({
                    versionKey: false,
                    transform: (doc, ret) => {
                        delete ret._id;
                        delete ret.hasInvestigation;
                        return ret;
                    },
                });
                const yaml = yamlModule;
                metadataYaml = yaml.dump(investigationMetadataObject);
            }
            else {
                metadataYaml = " ";
            }
            const promptTemplate = prompt.template
                .replace("{investigation}", JSON.stringify(investigation))
                .replace("{guide}", metadataYaml);
            let retry = 0;
            let response = null;
            while (retry < 3) {
                response = (await this.aiProcessor.fetchLLMResponse(promptTemplate, AssistantContradictionDetectionFormat));
                if (response.detectedContradiction === undefined ||
                    response.detectedContradiction === null ||
                    response.isContradictionDetected === undefined ||
                    response.isContradictionDetected === null) {
                    retry += 1;
                }
                else {
                    break;
                }
            }
            if (retry === 3 || !response) {
                throw new Error(`Failed to detect contradiction. Response is: ${JSON.stringify(response)}.`);
            }
            logger.info(`Has contradiction been detected: ${JSON.stringify(response)}`);
            return response;
        }
        catch (error) {
            logger.error(`An error occurred during contradiction detection: ${String(error)}`);
            throw error;
        }
    }
    /**
     * Gets the resolution of contradictions in an investigation by requesting the LLM.
     * @category Services
     * @param {IInvestigationResponseDto} investigation - The investigation in which to resolve contradictions.
     * @param {string} [fieldName] - The field name for which to resolve contradictions. If not provided, resolves all contradictions.
     * @returns {Promise<IAssistantContradictionResolutionFormat>} - The resolution of the contradiction.
     */
    async getContradictionResolution(investigation, fieldName) {
        const logger = getLogger();
        try {
            logger.info(`Started to get contradiction resolution${fieldName ? ` for field: ${fieldName}` : " for all fields"}`);
            const prompt = await Prompt.findOne({ name: "resolve_contradiction" });
            if (!prompt) {
                throw new Error("Resolve contradiction prompt not found.");
            }
            const unit = investigation.unitNumberAndTitle.value?.split(":")[0]?.split(" ")[1];
            const lesson = investigation.lessonNumberAndTitle.value?.split(":")[0]?.split(" ")[1] || "0";
            const lessonNumber = parseInt(lesson, 10);
            const metadata = await Metadata.findOne({
                unit: unit,
                lessonNumber: Number.isNaN(lessonNumber) ? undefined : lessonNumber,
            });
            let metadataYaml;
            if (metadata) {
                const investigationMetadataObject = metadata.toObject({
                    versionKey: false,
                    transform: (doc, ret) => {
                        delete ret._id;
                        delete ret.hasInvestigation;
                        return ret;
                    },
                });
                const yaml = yamlModule;
                metadataYaml = yaml.dump(investigationMetadataObject);
            }
            else {
                metadataYaml = " ";
            }
            let promptTemplate = prompt.template
                .replace("{investigation}", JSON.stringify(investigation))
                .replace("{guide}", metadataYaml);
            // Replace {fieldName} placeholder if it exists in the template
            if (fieldName) {
                promptTemplate = promptTemplate.replace("{fieldName}", fieldName);
            }
            else {
                // If no fieldName provided, replace with empty string or remove the placeholder
                promptTemplate = promptTemplate.replace("{fieldName}", "");
            }
            let retry = 0;
            let response = null;
            while (retry < 3) {
                response = (await this.aiProcessor.fetchLLMResponse(promptTemplate, AssistantContradictionResolutionFormat));
                if (response.resolvedContradiction === undefined ||
                    response.resolvedContradiction === null ||
                    response.message === undefined ||
                    response.message === null) {
                    retry += 1;
                }
                else {
                    break;
                }
            }
            if (retry === 3 || !response) {
                throw new Error(`Failed to get contradiction resolution. Response is: ${JSON.stringify(response)}.`);
            }
            logger.info(`Response for getting contradiction resolution is: ${JSON.stringify(response)}`);
            return response;
        }
        catch (error) {
            logger.error(`An error occurred during getting contradiction resolution: ${String(error)}`);
            throw error;
        }
    }
    /**
     * Resolves contradictions in an investigation.
     * @category Services
     * @param {IInvestigation} investigation - The investigation in which to resolve contradictions.
     * @param {string} [fieldName] - The field name for which to resolve contradictions. If not provided, resolves all contradictions.
     * @returns {Promise<string>} - A message describing the resolved contradictions.
     */
    async resolveContradiction(investigation, fieldName) {
        const logger = getLogger();
        try {
            logger.info(`Started to resolving contradiction${fieldName ? ` for field: ${fieldName}` : " for all fields"}`);
            const investigationWithContradictions = toInvestigationResponseDto(investigation);
            const contradictionResolution = await this.getContradictionResolution(investigationWithContradictions, fieldName);
            logger.info({ contradictionResolution });
            try {
                for (const resolvedContradiction of contradictionResolution.resolvedContradiction ?? []) {
                    if (resolvedContradiction.fieldName != "" && resolvedContradiction.value != "") {
                        const fieldName = resolvedContradiction.fieldName.includes(".value")
                            ? resolvedContradiction.fieldName.split(".").slice(0, -1).join(".")
                            : resolvedContradiction.fieldName;
                        logger.info({ fieldName, resolvedContradiction });
                        set(investigation, `${fieldName}.value`, resolvedContradiction.value);
                        set(investigation, `${fieldName}.aiGeneratedValue`, resolvedContradiction.value);
                        set(investigation, `${fieldName}.isContradicting`, false);
                        set(investigation, `${fieldName}.contradictionReason`, null);
                        set(investigation, `${fieldName}.targetFieldName`, null);
                    }
                }
                set(investigation, "metadata.dateModified", new Date());
            }
            catch (error) {
                logger.error(`An error occurred during applying contradiction resolution: ${String(error)}`);
                throw error;
            }
            return contradictionResolution.message;
        }
        catch (error) {
            logger.error(`An error occurred during resolving contradiction: ${String(error)}`);
            throw error;
        }
    }
}