import { Chat } from "@models/Chat.model";
import { Investigation } from "@models/investigation/investigation.model";
import { AuthGrpcService } from "@services/auth.service";
import { ContradictionDetectorService } from "@services/contradictionDetector.service";
import { ContradictionProcessingService } from "@services/contradictionProcessing.service";
import { InvestigationService } from "@services/investigation.service";
import { INVESTIGATION_SCHEMA_FORMAT } from "@typez/investigation";
import { toInvestigationResponseDto, } from "@typez/investigation/dto";
import { getLogger } from "@utils/asyncLocalStorage";
import { enrichInvestigationResponse } from "@utils/investigation-response.utils";
import { fetchUserProfile } from "@utils/user-profile.utils";
import yamlModule from "js-yaml";
import { Types } from "mongoose";
import { formatHistory } from "../helpers/history";
import { convertInvestigationFromModel } from "../helpers/investigation";
// TODO: Add updateInvestigation method for AI assistant updates
// This method should accept investigationId and key-value pairs for updates
// Example: PUT /api/investigation/:id/ai-update
// Body: { field: "title", value: "New Title", aiGenerated: true }
// TODO: Add getInvestigation method for retrieving single investigation
// GET /api/investigation/:id
// TODO: Add listInvestigations method for listing with filters
// GET /api/investigations?status=draft&grade=5&curriculum=math
// TODO: Add deleteInvestigation method for soft/hard deletion
// DELETE /api/investigation/:id
// TODO: Add lockInvestigation method for editing locks
// POST /api/investigation/:id/lock
// TODO: Add unlockInvestigation method
// DELETE /api/investigation/:id/lock
// TODO: Add updateInvestigationStatus method for status transitions
// PUT /api/investigation/:id/status
// TODO: Update DTO to send data to client (statuses need to be renamed for example)
// - Consider renaming status values to be more client-friendly:
// - "draft_incomplete" -> "draft"
// - "draft_non_contradictory_complete" -> "review_ready"
// - "draft_contradictory_complete" -> "needs_review"
// - "in_development" -> "in_progress"
// - "published" -> "published" (keep as is)
// TODO: Remember to change the author to required in the future!
// - Make author field required in the future - currently optional
// - This should be enforced in validation schemas and service layer
// TODO: Implement a service function that will take a key and a value from AI assistant and update it in the db
// - Create updateInvestigationByAI method in InvestigationService
// - Method should accept investigationId, field name, value, and optional userId
// - Should validate field names and update the specified field
// - Should handle AI-generated data properly (aiGeneratedValue, humanEdited: false, etc.)
/**
* Investigation Controller
* Handles investigation-related HTTP requests
* @category Controllers
*/
export class InvestigationController {
/**
* Create a new investigation.
* @param {ICreateInvestigationRequest} req Express request with investigation payload and user context.
* @param {Response} res Express response used to return the created investigation.
* @returns {Promise<void>} Resolves when the response has been sent.
*/
static async createInvestigation(req, res) {
const logger = getLogger();
logger.info({ body: req.body }, "Creating new investigation");
logger.info({ user: req.user }, "User information");
try {
const userId = req.user?.userId;
const authenticatedAuthorId = userId && Types.ObjectId.isValid(userId) ? new Types.ObjectId(userId) : null;
if (userId && Types.ObjectId.isValid(userId) && !req.body.author) {
req.body.author = new Types.ObjectId(userId);
}
const investigationService = new InvestigationService();
const contradictionProcessor = new ContradictionProcessingService();
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
const profileCache = new Map();
// Create the investigation
const createdInvestigation = await investigationService.createInvestigation({
...req.body,
lastChangeWithAI: false,
});
// Process contradictions after creation
try {
// Get the full investigation to process contradictions
const fullInvestigation = await investigationService.getInvestigationById(createdInvestigation._id.toString());
// Process contradictions using the service
const finalInvestigation = await contradictionProcessor.processAndApplyContradictions(fullInvestigation, async (updateFields) => {
const result = await investigationService.updateInvestigation(createdInvestigation._id.toString(), updateFields, { updatedBy: authenticatedAuthorId, skipVersioning: true });
if (!result.updatedInvestigation) {
throw new Error("Failed to update investigation during contradiction processing");
}
return { updatedInvestigation: result.updatedInvestigation };
});
const dto = toInvestigationResponseDto(finalInvestigation);
await enrichInvestigationResponse(finalInvestigation, dto, grpcClient, profileCache);
res.status(201).json({
success: true,
message: "Investigation created successfully",
data: dto,
});
}
catch (contradictionError) {
logger.warn({ error: contradictionError, investigationId: createdInvestigation._id.toString() }, "Contradiction processing failed, returning original investigation");
const dto = toInvestigationResponseDto(createdInvestigation);
await enrichInvestigationResponse(createdInvestigation, dto, grpcClient, profileCache);
res.status(201).json({
success: true,
message: "Investigation created successfully",
data: dto,
});
}
}
catch (error) {
logger.error({ error, body: req.body }, "Failed to create investigation");
res.status(500).json({
success: false,
message: "Failed to create investigation",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* Get investigations with pagination, grouped by curriculum.
* @param {IAuthenticatedRequest} req Express request containing validated query parameters.
* @param {Response} res Express response used to return the investigations list and pagination metadata.
* @returns {Promise<void>} Resolves when the response has been sent.
*/
static async getInvestigations(req, res) {
const logger = getLogger();
logger.info({ query: req.query, user: req.user }, "Getting investigations grouped by curriculum");
// Extract query parameters (already validated by middleware)
const searchParams = {
limit: req.query.limit,
offset: req.query.offset,
sortBy: req.query.sortBy,
sortOrder: req.query.sortOrder,
search: req.query.search,
};
// Pass search parameters to service
const investigationService = new InvestigationService();
const result = await investigationService.getInvestigations(searchParams);
let itemsWithAuthorProfiles = result.items;
if (result.items.length) {
try {
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
const profileCache = new Map();
itemsWithAuthorProfiles = await Promise.all(result.items.map(async (item) => {
const authorId = item.author?.id ?? undefined;
if (!authorId) {
return {
...item,
author: {
id: null,
name: null,
email: null,
avatar: null,
},
};
}
try {
const profile = await fetchUserProfile(authorId, profileCache, grpcClient);
return {
...item,
author: {
id: authorId,
name: profile.name,
email: profile.email,
avatar: profile.avatar,
},
};
}
catch (profileError) {
logger.warn({ profileError, authorId }, "Failed to fetch author profile for investigation list item");
return {
...item,
author: {
id: authorId,
name: null,
email: null,
avatar: null,
},
};
}
}));
}
catch (error) {
logger.warn({ error }, "Failed to enrich investigation list with author profiles");
}
}
res.status(200).json({
data: {
items: itemsWithAuthorProfiles,
pagination: result.pagination,
},
});
}
/**
* Get a single investigation by ID.
* @param {IAuthenticatedRequest} req Express request containing the investigation identifier.
* @param {Response} res Express response used to return the investigation.
* @param {NextFunction} _next Express next function (unused).
* @returns {Promise<void>} Resolves when the response has been sent.
*/
static async getInvestigation(req, res, _next) {
const logger = getLogger();
const { id } = req.params;
if (!id) {
res.status(400).json({
success: false,
message: "Investigation ID is required",
});
return;
}
logger.info({ investigationId: id, user: req.user }, "Fetching investigation");
try {
const investigationService = new InvestigationService();
const investigation = await investigationService.getInvestigationById(id);
const responseDto = toInvestigationResponseDto(investigation);
try {
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
const profileCache = new Map();
await enrichInvestigationResponse(investigation, responseDto, grpcClient, profileCache);
}
catch (profileError) {
logger.warn({ profileError, investigationId: id }, "Failed to enrich investigation metadata with user profiles");
}
res.status(200).json({
success: true,
message: "Investigation retrieved successfully",
data: responseDto,
});
}
catch (error) {
logger.error({ error, investigationId: id }, "Failed to fetch investigation");
res.status(500).json({
success: false,
message: "Failed to fetch investigation",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* Update an investigation.
* @param {IUpdateInvestigationRequest} req Express request with update payload and params.
* @param {Response} res Express response used to return the updated investigation.
* @param {NextFunction} _next Express next function (unused).
* @returns {Promise<void>} Resolves when the response has been sent.
*/
static async updateInvestigation(req, res, _next) {
const logger = getLogger();
const { id } = req.params;
logger.info({ user: req.user }, "User information");
if (!id) {
res.status(400).json({
success: false,
message: "Investigation ID is required",
});
return;
}
logger.info({ investigationId: id, updateData: req.body }, "Updating investigation");
try {
const investigationService = new InvestigationService();
const contradictionProcessor = new ContradictionProcessingService();
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
const profileCache = new Map();
const userId = req.user?.userId;
const authenticatedAuthorId = userId && Types.ObjectId.isValid(userId) ? new Types.ObjectId(userId) : null;
const bodyWithMetadata = {
...req.body,
};
let authorOverride = null;
const { metadata: incomingMetadata, ...restPayload } = bodyWithMetadata;
if (incomingMetadata && typeof incomingMetadata === "object") {
const providedAuthor = incomingMetadata.author;
if (providedAuthor instanceof Types.ObjectId) {
authorOverride = providedAuthor;
}
else if (typeof providedAuthor === "string") {
const trimmed = providedAuthor.trim();
if (trimmed && Types.ObjectId.isValid(trimmed)) {
authorOverride = new Types.ObjectId(trimmed);
}
}
}
const updatePayload = restPayload;
// Update the investigation
let result = await investigationService.updateInvestigation(id, updatePayload, {
needsBuildUpdate: true,
updatedBy: authenticatedAuthorId,
isAiGenerated: false,
});
if ((authorOverride || authenticatedAuthorId) &&
(!result.updatedInvestigation?.metadata || !result.updatedInvestigation.metadata.author)) {
const ensured = await investigationService.ensureInvestigationAuthor(id, authorOverride ?? authenticatedAuthorId);
if (ensured) {
result = {
...result,
updatedInvestigation: ensured,
};
}
}
if (result.updatedInvestigation) {
// Process contradictions after update
try {
// Process contradictions using the service
const finalInvestigation = await contradictionProcessor.processAndApplyContradictions(result.updatedInvestigation, async (updateFields) => {
const updateResult = await investigationService.updateInvestigation(id, updateFields, {
updatedBy: authenticatedAuthorId,
isAiGenerated: false,
skipVersioning: true,
});
if (!updateResult.updatedInvestigation) {
throw new Error("Failed to update investigation during contradiction processing");
}
return { updatedInvestigation: updateResult.updatedInvestigation };
});
const dto = toInvestigationResponseDto(finalInvestigation);
await enrichInvestigationResponse(finalInvestigation, dto, grpcClient, profileCache);
res.status(200).json({
success: result.success,
updatedInvestigation: dto,
});
}
catch (contradictionError) {
logger.warn({ error: contradictionError, investigationId: id }, "Contradiction processing failed, returning original update");
// Get the original investigation response
const originalInvestigationResponse = await investigationService.getInvestigationById(id);
const dto = toInvestigationResponseDto(originalInvestigationResponse);
await enrichInvestigationResponse(originalInvestigationResponse, dto, grpcClient, profileCache);
res.status(200).json({
success: result.success,
updatedInvestigation: dto,
});
}
}
else {
res.status(500).json({
success: false,
message: "Failed to update investigation",
});
}
}
catch (error) {
logger.error({ error, investigationId: id, updateData: req.body }, "Failed to update investigation");
res.status(500).json({
success: false,
message: "Failed to update investigation",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* Delete an investigation and its related chat history.
* @param {IAuthenticatedRequest} req Express request with the investigation identifier to remove.
* @param {Response} res Express response used to confirm the deletion result.
* @param {NextFunction} _next Express next function (unused).
* @returns {Promise<void>} Resolves when the response has been sent.
*/
static async deleteInvestigation(req, res, _next) {
const logger = getLogger();
const { id } = req.params;
if (!id) {
res.status(400).json({
success: false,
message: "Investigation ID is required",
});
return;
}
logger.info({ investigationId: id, user: req.user }, "Deleting investigation");
try {
const investigationService = new InvestigationService();
const { investigationDeleted, deletedChats } = await investigationService.deleteInvestigationById(id);
if (!investigationDeleted) {
res.status(404).json({
success: false,
message: "Investigation not found",
});
return;
}
res.status(200).json({
success: true,
message: "Investigation deleted successfully",
data: {
investigationId: id,
deletedChats,
},
});
}
catch (error) {
logger.error({ error, investigationId: id }, "Failed to delete investigation");
res.status(500).json({
success: false,
message: "Failed to delete investigation",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* Get investigation schema format.
* @param {IAuthenticatedRequest} req Express request.
* @param {Response} res Express response used to return the schema format.
* @param {NextFunction} _next Express next function (unused).
* @returns {void}
*/
static getInvestigationFormat(req, res, _next) {
const logger = getLogger();
logger.info("Fetching investigation schema format");
try {
res.status(200).json({ format: INVESTIGATION_SCHEMA_FORMAT });
}
catch (error) {
logger.error({ error }, "Failed to fetch investigation schema format");
res.status(500).json({
success: false,
message: "Failed to fetch investigation schema format",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* Clone an investigation.
* @param {ICloneInvestigationRequest} req Express request with the investigation identifier to clone.
* @param {Response} res Express response used to return the cloned investigation.
* @param {NextFunction} _next Express next function (unused).
* @returns {Promise<void>} Resolves when the response has been sent.
*/
static async cloneInvestigation(req, res, _next) {
const logger = getLogger();
const { id } = req.params;
logger.info({ investigationId: id, user: req.user }, "Cloning investigation");
try {
const userId = req.user?.userId;
const authenticatedAuthorId = userId && Types.ObjectId.isValid(userId) ? new Types.ObjectId(userId) : null;
const investigationService = new InvestigationService();
const contradictionProcessor = new ContradictionProcessingService();
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
const profileCache = new Map();
// Clone the investigation (without contradiction processing)
const clonedInvestigation = await investigationService.cloneInvestigation(id);
// Process contradictions after cloning
try {
logger.info({
investigationId: clonedInvestigation._id,
investigationStatus: clonedInvestigation.status,
}, "Starting contradiction detection and update processes after investigation cloning");
// Process contradictions using the service
const finalInvestigation = await contradictionProcessor.processAndApplyContradictions(clonedInvestigation, async (updateFields) => {
const result = await investigationService.updateInvestigation(clonedInvestigation._id.toString(), updateFields, { updatedBy: authenticatedAuthorId });
if (!result.updatedInvestigation) {
throw new Error("Failed to update investigation during contradiction processing");
}
return { updatedInvestigation: result.updatedInvestigation };
});
logger.info({
investigationId: clonedInvestigation._id,
finalInvestigationId: finalInvestigation._id,
finalInvestigationStatus: finalInvestigation.status,
}, "Contradiction processing completed successfully after investigation cloning");
const dto = toInvestigationResponseDto(finalInvestigation);
await enrichInvestigationResponse(finalInvestigation, dto, grpcClient, profileCache);
res.status(201).json({
success: true,
message: "Investigation cloned successfully",
data: dto,
});
}
catch (contradictionError) {
logger.warn({
error: contradictionError,
investigationId: clonedInvestigation._id.toString(),
investigationStatus: clonedInvestigation.status,
}, "Contradiction processing failed after investigation cloning, returning original cloned investigation");
const dto = toInvestigationResponseDto(clonedInvestigation);
await enrichInvestigationResponse(clonedInvestigation, dto, grpcClient, profileCache);
res.status(201).json({
success: true,
message: "Investigation cloned successfully",
data: dto,
});
}
}
catch (error) {
logger.error({ error, investigationId: id }, "Failed to clone investigation");
res.status(500).json({
success: false,
message: "Failed to clone investigation",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* Get history for a specific field.
* @param {IAuthenticatedRequest} req Express request containing investigation ID and field path.
* @param {Response} res Express response used to return the field history.
* @param {NextFunction} _next Express next function (unused).
* @returns {Promise<void>} Resolves when the response has been sent.
*/
static async getFieldHistory(req, res, _next) {
const logger = getLogger();
const { id, fieldPath } = req.params;
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 50;
if (!id) {
res.status(400).json({
success: false,
message: "Investigation ID is required",
});
return;
}
if (!fieldPath) {
res.status(400).json({
success: false,
message: "Field path is required",
});
return;
}
logger.info({ investigationId: id, fieldPath, limit }, "Fetching field history");
try {
const investigationService = new InvestigationService();
const result = await investigationService.getFieldHistory(id, fieldPath, limit);
res.status(200).json({
success: true,
message: "Field history retrieved successfully",
data: result,
});
}
catch (error) {
logger.error({ error, investigationId: id, fieldPath }, "Failed to fetch field history");
res.status(500).json({
success: false,
message: "Failed to fetch field history",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* Get investigation versions.
* @param {IAuthenticatedRequest} req Express request with the investigation identifier to get versions.
* @param {Response} res Express response used to return the investigation versions.
* @param {NextFunction} _next Express next function (unused).
* @returns {Promise<void>} Resolves when the response has been sent.
*/
static async getInvestigationVersions(req, res, _next) {
const logger = getLogger();
const { id } = req.params;
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 20;
if (!id) {
res.status(400).json({
success: false,
message: "Investigation ID is required",
});
return;
}
try {
const investigationService = new InvestigationService();
const versions = await investigationService.getInvestigationVersions(id, limit);
res.status(200).json({
success: true,
message: "Investigation versions retrieved successfully",
data: versions,
});
}
catch (error) {
logger.error({ error, investigationId: id }, "Failed to fetch investigation versions");
res.status(500).json({
success: false,
message: "Failed to fetch investigation versions",
error: error instanceof Error ? error.message : "Unknown error",
});
}
}
/**
* Undo an investigation.
* @param {IAuthenticatedRequest} req Express request with the investigation identifier to undo.
* @param {Response} res Express response used to return the undone investigation.
* @param {NextFunction} _next Express next function (unused).
* @returns {Promise<void>} Resolves when the response has been sent.
*/
static async undoInvestigation(req, res, _next) {
const logger = getLogger();
const { id } = req.params;
if (!id) {
res.status(400).json({
success: false,
message: "Investigation ID is required",
});
return;
}
try {
const investigationService = new InvestigationService();
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
const profileCache = new Map();
const investigation = await investigationService.undoInvestigation(id);
const responseDto = toInvestigationResponseDto(investigation);
await enrichInvestigationResponse(investigation, responseDto, grpcClient, profileCache);
res.status(200).json({
success: true,
message: "Investigation undone successfully",
data: responseDto,
});
}
catch (error) {
logger.error({ error, investigationId: id }, "Failed to undo investigation");
res.status(500).json({
success: false,
message: error instanceof Error ? error.message : "Failed to undo investigation",
});
}
}
/**
* Redo an investigation.
* @param {IAuthenticatedRequest} req Express request with the investigation identifier to redo.
* @param {Response} res Express response used to return the redone investigation.
* @param {NextFunction} _next Express next function (unused).
* @returns {Promise<void>} Resolves when the response has been sent.
*/
static async redoInvestigation(req, res, _next) {
const logger = getLogger();
const { id } = req.params;
if (!id) {
res.status(400).json({
success: false,
message: "Investigation ID is required",
});
return;
}
try {
const investigationService = new InvestigationService();
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
const profileCache = new Map();
const investigation = await investigationService.redoInvestigation(id);
const responseDto = toInvestigationResponseDto(investigation);
await enrichInvestigationResponse(investigation, responseDto, grpcClient, profileCache);
res.status(200).json({
success: true,
message: "Investigation redone successfully",
data: responseDto,
});
}
catch (error) {
logger.error({ error, investigationId: id }, "Failed to redo investigation");
res.status(500).json({
success: false,
message: error instanceof Error ? error.message : "Failed to redo investigation",
});
}
}
/**
* Resolve contradiction.
* @param {IAuthenticatedRequest} req Express request with the investigation identifier to redo.
* @param {Response} res Express response used to return the investigation with resolved contradiction.
* @param {NextFunction} _next Express next function (unused).
* @returns {Promise<void>} Resolves when the response has been sent.
*/
static async resolveContradiction(req, res, _next) {
const logger = getLogger();
const { id } = req.params;
const { fieldName } = req.body;
if (!id) {
res.status(400).json({
success: false,
message: "Investigation ID is required",
});
return;
}
if (!fieldName) {
res.status(400).json({
success: false,
message: "Field name is required",
});
return;
}
try {
const contradictionService = new ContradictionDetectorService();
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
const profileCache = new Map();
const investigation = await Investigation.findById(id);
if (!investigation) {
throw new Error("Investigation not found");
}
const currentChat = await Chat.findOne({ investigationId: investigation.id });
if (!currentChat) {
throw new Error(`Chat not found for the investigation with id: ${id}`);
}
let oldInvestigation = JSON.parse(JSON.stringify(investigation));
const resolvedContradictionMessage = await contradictionService.resolveContradiction(investigation, fieldName);
const responseDto = toInvestigationResponseDto(investigation);
await enrichInvestigationResponse(investigation, responseDto, grpcClient, profileCache);
const investigationService = new InvestigationService();
await investigationService.updateInvestigationByVersion(oldInvestigation, investigation, new Types.ObjectId(req.user?.userId));
currentChat.history.push({
user: { content: "" },
assistant: {
content: resolvedContradictionMessage,
metadata: JSON.stringify(convertInvestigationFromModel(investigation)),
content_for_llm: "[investigation_provided]",
},
});
await currentChat.save();
await investigation.save();
res.status(200).json({
success: true,
message: "Successfully resolved contradictions",
data: { investigation: responseDto, message: resolvedContradictionMessage },
});
}
catch (error) {
logger.error({ error, investigationId: id }, "Failed to resolve contradiction");
res.status(500).json({
success: false,
message: error instanceof Error ? error.message : "Failed to resolve contradiction",
});
}
}
/**
* Regenerate other fields based on a specific field.
* @param {IAuthenticatedRequest} req Express request containing investigation ID and field name.
* @param {Response} res Express response used to return the updated investigation.
* @param {NextFunction} _next Express next function (unused).
* @returns {Promise<void>} Resolves when the response has been sent.
*/
static async regenerateOtherFields(req, res, _next) {
const logger = getLogger();
const { id } = req.params;
const { fieldName } = req.body;
if (!id) {
res.status(400).json({
success: false,
message: "Investigation ID is required",
});
return;
}
if (!fieldName) {
res.status(400).json({
success: false,
message: "Field name is required",
});
return;
}
logger.info({ investigationId: id, fieldName }, "Regenerating other fields");
try {
const investigationService = new InvestigationService();
const contradictionProcessor = new ContradictionProcessingService();
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : undefined;
const grpcClient = req.grpcClient ?? new AuthGrpcService(token);
const profileCache = new Map();
const userId = req.user?.userId;
const updatedBy = userId && Types.ObjectId.isValid(userId) ? new Types.ObjectId(userId) : null;
const investigation = await Investigation.findById(id);
if (!investigation) {
throw new Error("Investigation not found");
}
const currentChat = await Chat.findOne({ investigationId: investigation.id });
if (!currentChat) {
throw new Error(`Chat not found for the investigation with id: ${id}`);
}
const formattedHistory = formatHistory(currentChat.history || []);
const yaml = yamlModule;
const chatHistoryYaml = yaml.dump(formattedHistory);
const updatedInvestigation = await investigationService.regenerateOtherFields(id, fieldName, updatedBy, chatHistoryYaml);
if (!updatedInvestigation) {
throw new Error("Failed to regenerate other fields");
}
// Process contradictions after regeneration
if (updatedInvestigation) {
try {
await contradictionProcessor.processAndApplyContradictions(updatedInvestigation, async (updateFields) => {
const updateResult = await investigationService.updateInvestigation(id, updateFields, {
updatedBy,
isAiGenerated: false,
skipVersioning: true,
});
if (!updateResult.updatedInvestigation) {
throw new Error("Failed to update investigation during contradiction processing");
}
return { updatedInvestigation: updateResult.updatedInvestigation };
});
}
catch (contradictionError) {
logger.warn({ error: contradictionError, investigationId: id, fieldName }, "Failed to process contradictions after regeneration, but regeneration succeeded");
// Continue with the investigation even if contradiction detection fails
}
}
const finalInvestigationDocument = await Investigation.findById(id);
if (!finalInvestigationDocument) {
throw new Error("Failed to reload investigation after regeneration");
}
const responseDto = toInvestigationResponseDto(finalInvestigationDocument);
await enrichInvestigationResponse(finalInvestigationDocument, responseDto, grpcClient, profileCache);
const hasChanges = updatedInvestigation
._regenerationHasChanges ?? true;
const changedFields = updatedInvestigation
._regenerationChangedFields ?? [];
let regenerationMessage;
if (!hasChanges || changedFields.length === 0) {
regenerationMessage = `AI assistant is unable to regenerate the remaining fields as no modifications are required or the input is invalid (contains useless or invalid data)`;
}
else {
regenerationMessage = `Successfully regenerated other fields`;
}
currentChat.history.push({
user: { content: "" },
assistant: {
content: regenerationMessage,
metadata: JSON.stringify(convertInvestigationFromModel(finalInvestigationDocument)),
content_for_llm: "[investigation_provided]",
},
});
await currentChat.save();
await finalInvestigationDocument.save();
res.status(200).json({
success: true,
message: hasChanges
? "Other fields regenerated successfully"
: "Regeneration completed with no changes",
data: {
investigation: responseDto,
message: regenerationMessage,
hasChanges,
changedFields: hasChanges ? changedFields : [],
},
});
}
catch (error) {
logger.error({ error, investigationId: id, fieldName }, "Failed to regenerate other fields");
res.status(500).json({
success: false,
message: error instanceof Error ? error.message : "Failed to regenerate other fields",
});
}
}
}
Source