Files
MoodleGPT/src/background/core/get-content-with-history.ts
T

178 lines
5.5 KiB
TypeScript

import type Config from '../types/config';
import imageToBase64 from 'background/utils/image-to-base64';
import isGPTModelGreaterOrEqualTo4 from 'background/utils/version-support-images';
import { ChatCompletionMessageParam, ChatCompletionUserMessageParam } from 'openai/resources';
import { parseMoodleQuestion } from './parse-question';
import { MoodleQuestionQuery /* MoodleQuestionType */ } from '../types/question-types';
// The attempt and the cmid allow us to identify a quiz
type History = {
host: string;
cmid: string; // The id of the quiz
attempt: string; // The attempt of the current quiz
history: ChatCompletionMessageParam[];
};
const INSTRUCTION = `
You are an expert quiz solver.
Please solve the provided question based on its type and provide the correct result.
- For choice questions, output the exact index(es) of the correct answer(s).
- For text/numerical questions, provide the exact wording or number.
- For essay questions, provide a highly detailed and complete response.
Always output strict JSON according to the requested schema block.
`.trim();
const SYSTEM_INSTRUCTION_MESSAGE = {
role: 'system',
content: INSTRUCTION
} as const satisfies ChatCompletionMessageParam;
/**
* Get the content to send to ChatGPT API (it allows to includes images if supported)
* @param config
*/
async function getContent(
config: Config,
questionElement: HTMLElement,
// We provide the structured JSON if parsed, otherwise fallback to normalized text string
textContent: string
): Promise<ChatCompletionUserMessageParam['content']> {
const imagesElements = questionElement.querySelectorAll('img');
if (
!config.includeImages ||
!isGPTModelGreaterOrEqualTo4(config.model) ||
imagesElements.length === 0
) {
return textContent;
}
const contentWithImages: ChatCompletionUserMessageParam['content'] = [];
const base64Images = Array.from(imagesElements).map(imgEl => imageToBase64(imgEl));
const base64ImagesResolved = await Promise.allSettled(base64Images);
for (const result of base64ImagesResolved) {
if (result.status === 'fulfilled') {
contentWithImages.push({
type: 'image_url',
image_url: { url: result.value }
});
} else if (config.logs) {
console.error(result.reason);
}
}
contentWithImages.push({
type: 'text',
text: textContent
});
return contentWithImages;
}
/**
* Create a new history object from the current page
* @returns
*/
function createNewHistory(): History {
const urlParams = new URLSearchParams(document.location.search);
return {
host: document.location.host,
cmid: urlParams.get('cmid') ?? '',
attempt: urlParams.get('attempt') ?? '',
history: []
};
}
/**
* Load the past history from the session storage otherwise return the default history object
* @returns
*/
function loadPastHistory(): History | null {
return JSON.parse(sessionStorage.moodleGPTHistory ?? 'null');
}
/**
* Check if two history are from the same origin
* @param a
* @param b
* @returns
*/
function areHistoryFromSameQuiz(a: History, b: History): boolean {
const KEYS_TO_COMPARE: (keyof History)[] = ['host', 'cmid', 'attempt'];
for (const key of KEYS_TO_COMPARE) {
if (a[key] !== b[key]) return false;
}
return true;
}
/**
* Return the content to send to chatgpt api with history if needed
* @param config
* @param questionElement
* @param question
* @returns
*/
async function getContentWithHistory(
config: Config,
questionElement: HTMLElement,
question: string
): Promise<{
messages: [typeof SYSTEM_INSTRUCTION_MESSAGE, ...ChatCompletionMessageParam[]];
saveResponse?: (response: string) => void;
query: MoodleQuestionQuery | null;
}> {
const parsedQuery = parseMoodleQuestion(questionElement, question);
const textContent = parsedQuery ? JSON.stringify(parsedQuery, null, 2) : question;
const content = await getContent(config, questionElement, textContent);
const message: ChatCompletionMessageParam = { role: 'user', content };
const buildResult = (historyMsg: ChatCompletionMessageParam[]) => {
const historyObj = { history: historyMsg };
return {
messages: [SYSTEM_INSTRUCTION_MESSAGE, ...historyMsg, message] as [
typeof SYSTEM_INSTRUCTION_MESSAGE,
...ChatCompletionMessageParam[]
],
query: parsedQuery,
saveResponse(response: string) {
if (config.history) {
historyObj.history.push(message);
historyObj.history.push({ role: 'assistant', content: response });
// Note we probably need the full 'history' object here to stringify it:
// We will recreate it or reuse the loaded one
let historyToSave: History;
const pastHistory: History | null = loadPastHistory();
const newHistory: History = createNewHistory();
if (pastHistory === null || !areHistoryFromSameQuiz(pastHistory, newHistory)) {
historyToSave = newHistory;
} else {
historyToSave = pastHistory;
}
historyToSave.history = historyObj.history;
sessionStorage.moodleGPTHistory = JSON.stringify(historyToSave);
}
}
};
};
if (!config.history) {
return buildResult([]);
}
const pastHistory: History | null = loadPastHistory();
const newHistory: History = createNewHistory();
if (pastHistory === null || !areHistoryFromSameQuiz(pastHistory, newHistory)) {
return buildResult(newHistory.history);
} else {
return buildResult(pastHistory.history);
}
}
export default getContentWithHistory;