Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: update entry services refactoring #190

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 73 additions & 75 deletions backend/src/controllers/entry/entry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as CdGptServices from '../../models/services/CdGpt.js';
import * as EntryServices from '../../models/services/entry/entry.js';
import {
Entry,
Expand All @@ -9,6 +10,21 @@ import { NextFunction, Request, Response } from 'express';
import ExpressError from '../../utils/ExpressError.js';
import mongoose from 'mongoose';

/**
* Request body for create and update Entry operations,
* i.e. all requests that go through validateEntry
*/
export interface UpdateEntryRequestBody {
title?: string;
content?: string;
mood?: string;
tags?: string[];
privacy_settings?: {
public: boolean;
shared_with: string[];
};
}

/**
* Get all entries in a specific journal.
*/
Expand Down Expand Up @@ -37,27 +53,24 @@ export const createEntry = async (
next: NextFunction
) => {
const { journalId } = req.params;
const entryData = req.body;

// Verify journal and journal.config exist
const journal = await Journal.findById(journalId);
if (!journal) {
return next(new ExpressError('Journal not found.', 404));
}
if (!journal.config) {
return next(new ExpressError('Journal config not found.', 404));
}
const entryData: UpdateEntryRequestBody = req.body;

const { errMessage, entry: newEntry } = await EntryServices.createEntry(
journalId,
journal.config.toString(),
entryData
);
try {
const configId = await verifyJournalExists(journalId);

if (errMessage) {
req.flash('info', errMessage);
const { errMessage, entry: newEntry } = await EntryServices.createEntry(
journalId,
configId,
entryData
);

if (errMessage) {
req.flash('info', errMessage);
}
res.status(201).json({ ...newEntry.toObject(), flash: req.flash() });
} catch (err) {
return next(err);
}
res.status(201).json({ ...newEntry.toObject(), flash: req.flash() });
};

/**
Expand Down Expand Up @@ -88,57 +101,32 @@ export const updateEntry = async (
next: NextFunction
) => {
const { entryId, journalId } = req.params;
const entryData = req.body;
const entryData: UpdateEntryRequestBody = req.body;

// Verify journal and journal.config exist
const journal = await Journal.findById(journalId);
if (!journal) {
return next(new ExpressError('Journal not found.', 404));
}
if (!journal.config) {
return next(new ExpressError('Journal config not found.', 404));
}

const updatedEntry = await Entry.findById(entryId);
if (!updatedEntry) {
return next(new ExpressError('Entry not found.', 404));
}

if (entryData.content) {
// Update the entry with the new data
updatedEntry.content = entryData.content;
try {
const configId = await verifyJournalExists(journalId);

// Update the analysis content for the entry with a new analysis
const oldAnalysis = await EntryAnalysis.findOne({ entry: entryId });
if (!oldAnalysis) {
return next(new ExpressError('Entry analysis not found.', 404));
}
try {
const analysis = await oldAnalysis.getAnalysisContent(
journal.config.toString(),
updatedEntry.content
);

if (analysis) {
updatedEntry.title = analysis.title;
updatedEntry.mood = analysis.mood;
updatedEntry.tags = analysis.tags;

oldAnalysis.analysis_content = analysis.analysis_content;
const { errMessage, entry: updatedEntry } = await EntryServices
.updateEntry(
entryId,
configId,
entryData,
);

if (errMessage) {
req.flash('info', errMessage);
}
} catch (analysisError) {
req.flash('info', (analysisError as Error).message);
} finally {
await updatedEntry.save();
await oldAnalysis.save();
req.flash('success', 'Successfully updated entry.');
res.status(200).json({ ...updatedEntry.toObject(), flash: req.flash() });
} catch (updateError) {
// Possible error from failing to find matching documents for entryId
// Setting appropriate error code here
throw new ExpressError((updateError as Error).message, 404);
}
} else if (entryData.title) {
updatedEntry.title = entryData.title;
await updatedEntry.save();
} catch (err) {
return next(err);
}

req.flash('success', 'Successfully updated entry.');
res.status(200).json({ ...updatedEntry.toObject(), flash: req.flash() });
};

/**
Expand Down Expand Up @@ -236,7 +224,7 @@ export const updateEntryAnalysis = async (
return next(new ExpressError('Journal config not found.', 404));
}
try {
const analysis = await entryAnalysis.getAnalysisContent(
const analysis = await CdGptServices.getAnalysisContent(
journal.config.toString(),
entry.content
);
Expand Down Expand Up @@ -291,20 +279,13 @@ export const createEntryConversation = async (
const { entryId, journalId } = req.params;
const messageData = req.body;

// Verify journal and journal.config exist
const journal = await Journal.findById(journalId);
if (!journal) {
return next(new ExpressError('Journal not found.', 404));
}
if (!journal.config) {
return next(new ExpressError('Journal config not found.', 404));
}

// Create new EntryConversation
try {
const configId = await verifyJournalExists(journalId);

const response = await EntryServices.createEntryConversation(
entryId,
journal.config.toString(),
configId,
messageData
);

Expand Down Expand Up @@ -355,7 +336,7 @@ export const updateEntryConversation = async (
return next(new ExpressError('Journal config not found.', 404));
}
try {
const llmResponse = await conversation.getChatContent(
const llmResponse = await CdGptServices.getChatContent(
journal.config.toString(),
analysis.id,
messageData.messages[0].message_content,
Expand Down Expand Up @@ -385,3 +366,20 @@ export const updateEntryConversation = async (

res.status(200).json({ ...response.toObject(), flash: req.flash() });
};

/**
* Checks if journal with jounalId exists and returns configId in journal.
*
* @param journalId id of journal to check
* @returns configId
*/
async function verifyJournalExists(journalId: string): Promise<string> {
const journal = await Journal.findById(journalId);
if (!journal) {
throw new ExpressError('Journal not found.', 404);
}
if (!journal.config) {
throw new ExpressError('Journal config not found.', 404);
}
return journal.config.toString();
}
71 changes: 2 additions & 69 deletions backend/src/models/entry/entryAnalysis.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { Model, Schema, Types, model } from 'mongoose';
import CdGpt from '../../assistants/gpts/CdGpt.js';
import { Config } from '../index.js';

import Joi from 'joi';

Expand All @@ -11,19 +9,13 @@ export interface EntryAnalysisType {
updated_at?: Date,
}

/* eslint-disable @typescript-eslint/no-explicit-any */
// TODO: any should be replaced with return type from cdgpt response
interface EntryAnalysisMethods {
getAnalysisContent(configId: string, content: string): Promise<any>,
}

/* eslint-disable @typescript-eslint/no-empty-object-type */
// Done to match: https://mongoosejs.com/docs/typescript/statics-and-methods.html
interface EntryAnalysisStatics extends Model<EntryAnalysisType, {}, EntryAnalysisMethods> {
interface EntryAnalysisStatics extends Model<EntryAnalysisType, {}> {
joi(obj: unknown, options?: object): Joi.ValidationResult,
}

const entryAnalysisSchema = new Schema<EntryAnalysisType, EntryAnalysisStatics, EntryAnalysisMethods>({
const entryAnalysisSchema = new Schema<EntryAnalysisType, EntryAnalysisStatics>({
entry: { type: Schema.Types.ObjectId, ref: 'Entry', required: true },
analysis_content: { type: String, default: 'Analysis not available' },
created_at: { type: Date, default: Date.now },
Expand All @@ -50,63 +42,4 @@ entryAnalysisSchema.pre('save', function (next) {
next();
});

//TODO: pull out this method to somewhere else. dependency on CdGpt not great
// Get the analysis content for an entry
entryAnalysisSchema.methods.getAnalysisContent = async function (configId: string, content: string): Promise<any> {
const config = await Config.findById(configId);

if (!config) {
throw new Error('Configure your account settings to get an analysis.');
} else if (config.apiKey) {
try {
// Remove an API key from a legacy config
await Config.findByIdAndUpdate(config._id, { $unset: { apiKey: 1 } });
} catch (err) {
if (typeof err === 'string') {
throw new Error(err);
} else if (err instanceof Error) {
throw err;
}
}
}
if (!config.model.analysis) {
config.model.analysis = '';
}


if (!process.env.OPENAI_API_KEY) {
throw new Error('API key is not available)');
}

const cdGpt = new CdGpt(process.env.OPENAI_API_KEY, config.model.analysis);

cdGpt.seedAnalysisMessages();
cdGpt.addUserMessage({ analysis: content });

const analysisCompletion = await cdGpt.getAnalysisCompletion();

if (analysisCompletion.error) {
throw new Error(analysisCompletion.error.message);
}

/**
* TODO: define a type for content field and refactor
* The next two lines suggests that the message content
* is missing a type because it gets unpacked into several expected fields
* It would also make sense to extract the next few lines into their own function.
*/
const response = JSON.parse(analysisCompletion.choices[0].message.content);
const { reframed_thought: reframing, distortion_analysis: analysis, impact_assessment: impact, affirmation, is_healthy: isHealthy } = response;

if (!isHealthy) {
if (!analysis || !impact || !reframing) {
throw new Error('Analysis content is not available.');
}

response.analysis_content = analysis + ' ' + impact + ' Think, "' + reframing + '"' || affirmation;
} else response.analysis_content = affirmation;

return response;
};

export default model<EntryAnalysisType, EntryAnalysisStatics>('EntryAnalysis', entryAnalysisSchema);
Loading