history and snake case fixe
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,167 @@
|
||||
import type Config from '@typing/config';
|
||||
import { ROLE, CONTENT_TYPE, type MessageContent, type Message } from '@typing/message';
|
||||
import imageToBase64 from '@utils/image-to-base64';
|
||||
import isGPTModelGreaterOrEqualTo4 from '@utils/version-support-images';
|
||||
|
||||
// @TODO: implement cmid, attempt, effacer choix
|
||||
|
||||
type History = {
|
||||
url: string | null;
|
||||
params: Record<string, string>;
|
||||
history: { role: ROLE; content: MessageContent }[];
|
||||
};
|
||||
|
||||
const INSTRUCTION: string = `
|
||||
Act as a quiz solver for the best notation with the following rules:
|
||||
- If no answer(s) are given, answer the statement as usual without following the other rules, providing the most detailed, complete and precise explanation.
|
||||
But for the calculation provide this format 'result: <result of the equation>\nexplanation: <explanation>'
|
||||
- For 'put in order' questions, provide the position of the answer separated by a new line (e.g., '1\n3\n2') and ignore other rules.- Always reply in this format: '<answer 1>\n<answer 2>\n...'
|
||||
- Always reply in the format: '<answer 1>\n<answer 2>\n...'.
|
||||
- Retain only the correct answer(s).
|
||||
- Maintain the same order for the answers as in the text.
|
||||
- Retain all text from the answer with its description, content or definition.
|
||||
- Only provide answers that exactly match the given answer in the text.
|
||||
- The question always has the correct answer(s), so you should always provide an answer.
|
||||
- Always respond in the same language as the user's question.
|
||||
`.trim();
|
||||
|
||||
const SYSTEM_INSTRUCTION_MESSAGE = {
|
||||
role: ROLE.SYSTEM,
|
||||
content: INSTRUCTION
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Get the content to send to ChatGPT API (it allows to includes images if supported)
|
||||
* @param config
|
||||
*/
|
||||
async function getContent(
|
||||
config: Config,
|
||||
questionElement: HTMLElement,
|
||||
question: string
|
||||
): Promise<MessageContent> {
|
||||
const imagesElements = questionElement.querySelectorAll('img');
|
||||
|
||||
if (
|
||||
config.includeImages &&
|
||||
isGPTModelGreaterOrEqualTo4(config.model) &&
|
||||
imagesElements.length === 0
|
||||
) {
|
||||
return question;
|
||||
}
|
||||
|
||||
let content: MessageContent = [];
|
||||
|
||||
const base64Images = Array.from(imagesElements).map(imgEl => imageToBase64(imgEl));
|
||||
const results = await Promise.all(base64Images);
|
||||
const filteredResults = results.filter(value => value !== null) as string[];
|
||||
|
||||
for (const result of filteredResults) {
|
||||
content.push({
|
||||
type: CONTENT_TYPE.IMAGE,
|
||||
image_url: { url: result }
|
||||
});
|
||||
}
|
||||
|
||||
if (content.length > 0) {
|
||||
content.push({
|
||||
type: CONTENT_TYPE.TEXT,
|
||||
text: question
|
||||
});
|
||||
} else {
|
||||
content = question;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new history object from the current page
|
||||
* @returns
|
||||
*/
|
||||
function createNewHistory(): History {
|
||||
const url_params = new URLSearchParams(document.location.search).entries();
|
||||
const params: Record<string, string> = {};
|
||||
|
||||
for (const [key, value] of url_params) {
|
||||
if (key === 'page') continue;
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
return {
|
||||
url: document.location.host,
|
||||
params,
|
||||
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 areHistorySameOrigin(a: History, b: History): boolean {
|
||||
if (a.url !== b.url) return false;
|
||||
|
||||
if (Object.keys(a.params).length !== Object.keys(b.params).length) return false;
|
||||
|
||||
for (const [key, value] of Object.entries(a.params)) {
|
||||
if (!(key in b.params) || b.params[key] !== value) 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, ...Message[]];
|
||||
saveResponse?: (response: string) => void;
|
||||
}> {
|
||||
const content = await getContent(config, questionElement, question);
|
||||
const message = { role: ROLE.USER, content };
|
||||
|
||||
if (!config.history) return { messages: [SYSTEM_INSTRUCTION_MESSAGE, message] };
|
||||
|
||||
let history: History;
|
||||
|
||||
const pastHistory: History | null = loadPastHistory();
|
||||
const newHistory: History = createNewHistory();
|
||||
|
||||
if (pastHistory === null || !areHistorySameOrigin(pastHistory, newHistory)) {
|
||||
history = newHistory;
|
||||
} else {
|
||||
history = pastHistory;
|
||||
}
|
||||
|
||||
return {
|
||||
messages: [SYSTEM_INSTRUCTION_MESSAGE, ...history.history, message],
|
||||
saveResponse(response: string) {
|
||||
// Register the conversation
|
||||
if (config.history) {
|
||||
history.history.push(message);
|
||||
history.history.push({ role: ROLE.ASSISTANT, content: response });
|
||||
sessionStorage.moodleGPTHistory = JSON.stringify(history);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default getContentWithHistory;
|
||||
+6
-116
@@ -1,105 +1,7 @@
|
||||
import type Config from '@typing/config';
|
||||
import type GPTAnswer from '@typing/gptAnswer';
|
||||
import imageToBase64 from '@utils/image-to-base64';
|
||||
import type GPTAnswer from '@typing/gpt-answer';
|
||||
import normalizeText from '@utils/normalize-text';
|
||||
import isGPTModelGreaterOrEqualTo4 from '@utils/version-support-images';
|
||||
|
||||
type Content =
|
||||
| string
|
||||
| Array<
|
||||
| {
|
||||
type: CONTENT_TYPE.TEXT;
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: CONTENT_TYPE.IMAGE;
|
||||
image_url: { url: string };
|
||||
}
|
||||
>;
|
||||
|
||||
type History = {
|
||||
url: string | null;
|
||||
system: { role: ROLE; content: Content };
|
||||
history: { role: ROLE; content: Content }[];
|
||||
};
|
||||
|
||||
enum ROLE {
|
||||
SYSTEM = 'system',
|
||||
USER = 'user',
|
||||
ASSISTANT = 'assistant'
|
||||
}
|
||||
|
||||
enum CONTENT_TYPE {
|
||||
TEXT = 'text',
|
||||
IMAGE = 'image_url'
|
||||
}
|
||||
|
||||
const INSTRUCTION: string = `
|
||||
Act as a quiz solver for the best notation with the following rules:
|
||||
- If no answer(s) are given, answer the statement as usual without following the other rules, providing the most detailed, complete and precise explanation.
|
||||
But for the calculation provide this format 'result: <result of the equation>\nexplanation: <explanation>'
|
||||
- For 'put in order' questions, provide the position of the answer separated by a new line (e.g., '1\n3\n2') and ignore other rules.- Always reply in this format: '<answer 1>\n<answer 2>\n...'
|
||||
- Always reply in the format: '<answer 1>\n<answer 2>\n...'.
|
||||
- Retain only the correct answer(s).
|
||||
- Maintain the same order for the answers as in the text.
|
||||
- Retain all text from the answer with its description, content or definition.
|
||||
- Only provide answers that exactly match the given answer in the text.
|
||||
- The question always has the correct answer(s), so you should always provide an answer.
|
||||
- Always respond in the same language as the user's question.
|
||||
`.trim();
|
||||
|
||||
const history: History = {
|
||||
url: null,
|
||||
system: {
|
||||
role: ROLE.SYSTEM,
|
||||
content: INSTRUCTION
|
||||
},
|
||||
history: []
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the content to send to ChatGPT API (it allows to includes images if supported)
|
||||
* @param config
|
||||
*/
|
||||
async function getContent(
|
||||
config: Config,
|
||||
questionElement: HTMLElement,
|
||||
question: string
|
||||
): Promise<Content> {
|
||||
const imagesElements = questionElement.querySelectorAll('img');
|
||||
|
||||
if (
|
||||
config.includeImages &&
|
||||
isGPTModelGreaterOrEqualTo4(config.model) &&
|
||||
imagesElements.length === 0
|
||||
) {
|
||||
return question;
|
||||
}
|
||||
|
||||
let content: Content = [];
|
||||
|
||||
const base64Images = Array.from(imagesElements).map(imgEl => imageToBase64(imgEl));
|
||||
const results = await Promise.all(base64Images);
|
||||
const filteredResults = results.filter(value => value !== null) as string[];
|
||||
|
||||
for (const result of filteredResults) {
|
||||
content.push({
|
||||
type: CONTENT_TYPE.IMAGE,
|
||||
image_url: { url: result }
|
||||
});
|
||||
}
|
||||
|
||||
if (content.length > 0) {
|
||||
content.push({
|
||||
type: CONTENT_TYPE.TEXT,
|
||||
text: question
|
||||
});
|
||||
} else {
|
||||
content = question;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
import getContentWithHistory from './get-content-with-history';
|
||||
|
||||
/**
|
||||
* Get the response from chatGPT api
|
||||
@@ -112,19 +14,10 @@ async function getChatGPTResponse(
|
||||
questionElement: HTMLElement,
|
||||
question: string
|
||||
): Promise<GPTAnswer> {
|
||||
const URL = location.hostname + location.pathname;
|
||||
|
||||
// We reset the history when we enter a new moodle quiz or when it's desactivate
|
||||
if (!config.history || history.url !== URL) {
|
||||
history.url = URL;
|
||||
history.history = [];
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutControler = setTimeout(() => controller.abort(), 20 * 1000);
|
||||
|
||||
const content = await getContent(config, questionElement, question);
|
||||
const message = { role: ROLE.USER, content };
|
||||
const contentHandler = await getContentWithHistory(config, questionElement, question);
|
||||
|
||||
const req = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
@@ -135,7 +28,7 @@ async function getChatGPTResponse(
|
||||
signal: config.timeout ? controller.signal : null,
|
||||
body: JSON.stringify({
|
||||
model: config.model,
|
||||
messages: [history.system, ...history.history, message],
|
||||
messages: contentHandler.messages,
|
||||
|
||||
temperature: 0.1, // Controls the randomness of the generated responses, with lower values producing more deterministic and predictable outputs. With set to 0.1 instead of 0 for more creativity.
|
||||
top_p: 1, // Determines the diversity of the generated responses
|
||||
@@ -149,11 +42,8 @@ async function getChatGPTResponse(
|
||||
const rep = await req.json();
|
||||
const response = rep.choices[0].message.content;
|
||||
|
||||
// Register the conversation
|
||||
if (config.history) {
|
||||
history.history.push(message);
|
||||
history.history.push({ role: ROLE.ASSISTANT, content: response });
|
||||
}
|
||||
// Save the response into the history
|
||||
if (contentHandler.saveResponse) contentHandler.saveResponse(response);
|
||||
|
||||
return {
|
||||
question,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type GPTAnswer from '@typing/gptAnswer';
|
||||
import type GPTAnswer from '@typing/gpt-answer';
|
||||
import type Config from '@typing/config';
|
||||
import handleClipboard from '@core/questions/clipboard';
|
||||
import handleContentEditable from '@core/questions/contenteditable';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type Config from '@typing/config';
|
||||
import type GPTAnswer from '@typing/gptAnswer';
|
||||
import type GPTAnswer from '@typing/gpt-answer';
|
||||
import handleClipboard from '@core/questions/clipboard';
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type GPTAnswer from '@typing/gptAnswer';
|
||||
import type GPTAnswer from '@typing/gpt-answer';
|
||||
|
||||
type Props = {
|
||||
questionElement: HTMLElement;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type Config from '@typing/config';
|
||||
import type GPTAnswer from '@typing/gptAnswer';
|
||||
import type GPTAnswer from '@typing/gpt-answer';
|
||||
import Logs from '@utils/logs';
|
||||
import normalizeText from '@utils/normalize-text';
|
||||
import { pickBestReponse } from '@utils/pick-best-response';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type Config from '@typing/config';
|
||||
import type GPTAnswer from '@typing/gptAnswer';
|
||||
import type GPTAnswer from '@typing/gpt-answer';
|
||||
import titleIndications from '@utils/title-indications';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type Config from '@typing/config';
|
||||
import type GPTAnswer from '@typing/gptAnswer';
|
||||
import type GPTAnswer from '@typing/gpt-answer';
|
||||
|
||||
/**
|
||||
* Hanlde contenteditable elements
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type Config from '@typing/config';
|
||||
import type GPTAnswer from '@typing/gptAnswer';
|
||||
import type GPTAnswer from '@typing/gpt-answer';
|
||||
|
||||
/**
|
||||
* Handle number input
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type Config from '@typing/config';
|
||||
import type GPTAnswer from '@typing/gptAnswer';
|
||||
import type GPTAnswer from '@typing/gpt-answer';
|
||||
import Logs from '@utils/logs';
|
||||
import normalizeText from '@utils/normalize-text';
|
||||
import { pickBestReponse } from '@utils/pick-best-response';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type Config from '@typing/config';
|
||||
import type GPTAnswer from '@typing/gptAnswer';
|
||||
import type GPTAnswer from '@typing/gpt-answer';
|
||||
import Logs from '@utils/logs';
|
||||
import normalizeText from '@utils/normalize-text';
|
||||
import { pickBestReponse } from '@utils/pick-best-response';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type Config from '@typing/config';
|
||||
import type GPTAnswer from '@typing/gptAnswer';
|
||||
import type GPTAnswer from '@typing/gpt-answer';
|
||||
|
||||
/**
|
||||
* Handle textbox
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export enum ROLE {
|
||||
SYSTEM = 'system',
|
||||
USER = 'user',
|
||||
ASSISTANT = 'assistant'
|
||||
}
|
||||
|
||||
export enum CONTENT_TYPE {
|
||||
TEXT = 'text',
|
||||
IMAGE = 'image_url'
|
||||
}
|
||||
|
||||
export type MessageContent =
|
||||
| string
|
||||
| Array<
|
||||
| {
|
||||
type: CONTENT_TYPE.TEXT;
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: CONTENT_TYPE.IMAGE;
|
||||
image_url: { url: string };
|
||||
}
|
||||
>;
|
||||
|
||||
export type Message = {
|
||||
role: ROLE;
|
||||
content: MessageContent;
|
||||
};
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import GPTAnswer from '@typing/gptAnswer';
|
||||
import GPTAnswer from '@typing/gpt-answer';
|
||||
import { toPourcentage } from './pick-best-response';
|
||||
|
||||
class Logs {
|
||||
|
||||
Reference in New Issue
Block a user