From f51b13204684fe2cac4d0d42ad11d59364fea15c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arnau=20G=C3=B3mez?= Date: Sat, 28 Sep 2024 15:49:03 +0200 Subject: [PATCH] feat: update openai model --- .../ai-notes-generator-service-fake-impl.ts | 87 ++++++++++--------- .../ai-notes-generator-service-openai-impl.ts | 60 ++++++------- .../interfaces/ai-notes-generator-service.ts | 3 +- .../use-cases/generate-ai-notes-use-case.ts | 2 +- 4 files changed, 76 insertions(+), 76 deletions(-) diff --git a/src/ai-generator/data/services/ai-notes-generator-service-fake-impl.ts b/src/ai-generator/data/services/ai-notes-generator-service-fake-impl.ts index ef90503a..bf6a5459 100644 --- a/src/ai-generator/data/services/ai-notes-generator-service-fake-impl.ts +++ b/src/ai-generator/data/services/ai-notes-generator-service-fake-impl.ts @@ -1,3 +1,4 @@ +import type { NoteRowModel } from "@/src/notes/domain/models/note-row-model"; import type { AiNotesGeneratorService, GenerateAiNotesInputModel, @@ -12,48 +13,52 @@ import type { export class AiNotesGeneratorServiceFakeImpl implements AiNotesGeneratorService { - async generate({}: GenerateAiNotesInputModel): Promise { + async generate({}: GenerateAiNotesInputModel): Promise { return [ - [ - "¿Cómo se define una ecuación de segundo grado?", - "Es una ecuación algebraica cuyo mayor exponente de la incógnita es 2.", - ], - [ - "¿Cuál es la forma general de una ecuación de segundo grado?", - "ax^2 + bx + c = 0", - ], - [ - "¿Qué son las raíces de una ecuación de segundo grado?", - "Son los valores de x que satisfacen la ecuación y hacen que sea igual a cero.", - ], - [ - "¿Qué es el discriminante en una ecuación de segundo grado?", - "Es la expresión b^2 - 4ac que se encuentra dentro de la fórmula cuadrática.", - ], - [ - "¿Cuántas soluciones puede tener una ecuación de segundo grado?", - "Puede tener 0, 1 o 2 soluciones.", - ], - [ - "¿Cuál es la fórmula general para hallar las soluciones de una ecuación de segundo grado?", - "x = (-b ± √(b^2 - 4ac)) / 2a", - ], - [ - "¿Cómo se llama el proceso de encontrar las raíces de una ecuación de segundo grado?", - "Se llama resolver la ecuación.", - ], - [ - "¿Qué tipo de gráfica representa una ecuación de segundo grado?", - "Representa una parábola.", - ], - [ - "¿Qué relación existe entre los coeficientes a, b y c de una ecuación de segundo grado y las soluciones?", - "Los coeficientes determinan la naturaleza y cantidad de soluciones, según el discriminante.", - ], - [ - "¿Qué ocurre si el discriminante es positivo en una ecuación de segundo grado?", - "La ecuación tiene dos soluciones reales y distintas.", - ], + { + front: "¿Cómo se define una ecuación de segundo grado?", + back: "Es una ecuación algebraica cuyo mayor exponente de la incógnita es 2.", + }, + { + front: "¿Cuál es la forma general de una ecuación de segundo grado?", + back: "ax^2 + bx + c = 0", + }, + { + front: "¿Qué son las raíces de una ecuación de segundo grado?", + back: "Son los valores de x que satisfacen la ecuación y hacen que sea igual a cero.", + }, + { + front: "¿Qué es el discriminante en una ecuación de segundo grado?", + back: "Es la expresión b^2 - 4ac que se encuentra dentro de la fórmula cuadrática.", + }, + { + front: "¿Cuántas soluciones puede tener una ecuación de segundo grado?", + back: "Puede tener 0, 1 o 2 soluciones.", + }, + { + front: + "¿Cuál es la fórmula general para hallar las soluciones de una ecuación de segundo grado?", + back: "x = (-b ± √(b^2 - 4ac)) / 2a", + }, + { + front: + "¿Cómo se llama el proceso de encontrar las raíces de una ecuación de segundo grado?", + back: "Se llama resolver la ecuación.", + }, + { + front: "¿Qué tipo de gráfica representa una ecuación de segundo grado?", + back: "Representa una parábola.", + }, + { + front: + "¿Qué relación existe entre los coeficientes a, b y c de una ecuación de segundo grado y las soluciones?", + back: "Los coeficientes determinan la naturaleza y cantidad de soluciones, según el discriminante.", + }, + { + front: + "¿Qué ocurre si el discriminante es positivo en una ecuación de segundo grado?", + back: "La ecuación tiene dos soluciones reales y distintas.", + }, ]; } } diff --git a/src/ai-generator/data/services/ai-notes-generator-service-openai-impl.ts b/src/ai-generator/data/services/ai-notes-generator-service-openai-impl.ts index 3cfcc044..ddde5145 100644 --- a/src/ai-generator/data/services/ai-notes-generator-service-openai-impl.ts +++ b/src/ai-generator/data/services/ai-notes-generator-service-openai-impl.ts @@ -1,6 +1,9 @@ import type { EnvService } from "@/src/common/domain/interfaces/env-service"; import type { ErrorTrackingService } from "@/src/common/domain/interfaces/error-tracking-service"; +import type { NoteRowModel } from "@/src/notes/domain/models/note-row-model"; import { OpenAI, OpenAIError, RateLimitError } from "openai"; +import { zodResponseFormat } from "openai/helpers/zod"; +import { z, ZodError } from "zod"; import { AiGeneratorEmptyMessageError, AiGeneratorError, @@ -21,6 +24,15 @@ declare module global { let openaiClient: OpenAI; } +const ValidationSchema = z.object({ + flashcards: z.array( + z.object({ + front: z.string(), + back: z.string(), + }), + ), +}); + /** * Implementation of AiNotesGeneratorService using the gpt-3.5-turbo-0125 * model. It communicates with the model using the OpenAI SDK, which makes @@ -49,7 +61,7 @@ export class AiNotesGeneratorServiceOpenaiImpl noteTypes, notesCount, sourceType, - }: GenerateAiNotesInputModel): Promise { + }: GenerateAiNotesInputModel): Promise { const typesMap = { [AiGeneratorNoteType.qa]: "a question and the answer", [AiGeneratorNoteType.definition]: @@ -66,7 +78,7 @@ export class AiNotesGeneratorServiceOpenaiImpl { role: "system", content: `You are a flashard generator. -Output a list of flashcards in JSON format. The JSON should be an array of arrays, where each sub-array contains two strings: the question and the answer. +Output a list of flashcards. Each flashcard has a front side (the question) and a back side (the answer). The flashcards can contain: ${noteTypes.map((type) => typesMap[type]).join(", ")}. You must generate ${notesCount} flashcards based on the ${textOrTopic} provided by the user. The language of the flashcards should be the language of the ${textOrTopic} provided by the user. @@ -77,16 +89,21 @@ The language of the flashcards should be the language of the ${textOrTopic} prov content: `Generate ${notesCount} flashcards to help me study this ${textOrTopic}: ${text}`, }, ], - model: "gpt-3.5-turbo-0125", - response_format: { type: "json_object" }, + model: "gpt-4o-mini", + response_format: zodResponseFormat(ValidationSchema, "flashcards"), n: 1, }); - const responseText = completion.choices[0].message.content; - if (!responseText) { + + const message = completion.choices[0].message; + const responseText = message.content; + if (message.refusal || !responseText) { + this.errorTrackingService.captureError(message); throw new AiGeneratorEmptyMessageError(); } + const response = JSON.parse(responseText); - return this.parseResponse(response); + const parsed = ValidationSchema.parse(response); + return parsed.flashcards; } catch (e) { if (e instanceof RateLimitError) { this.errorTrackingService.captureError(e); @@ -94,34 +111,11 @@ The language of the flashcards should be the language of the ${textOrTopic} prov } else if (e instanceof OpenAIError) { this.errorTrackingService.captureError(e); throw new AiGeneratorError(); + } else if (e instanceof ZodError) { + this.errorTrackingService.captureError(e); + throw new AiGeneratorError(); } throw e; } } - - /** - * Analyzes the response from the AI, in search of a list of flashcards. - * Because the response from the AI cannot be predicted, this method - * traverses the response object to find the flashcards. - * - * @param response a JSON object with the response from the AI - * @returns an array of flashcards, where each flashcard is an array with two strings: the question and the answer - * @throws `AiGeneratorEmptyMessageError` if the response does not contain the expected data - */ - private parseResponse(response: unknown): string[][] { - if (!response) { - throw new AiGeneratorEmptyMessageError(); - } - if (Array.isArray(response)) { - return response; - } - if (typeof response === "object") { - for (const value of Object.values(response)) { - if (Array.isArray(value) && value.length) { - return value; - } - } - } - throw new AiGeneratorEmptyMessageError(); - } } diff --git a/src/ai-generator/domain/interfaces/ai-notes-generator-service.ts b/src/ai-generator/domain/interfaces/ai-notes-generator-service.ts index e408032e..fd537eaf 100644 --- a/src/ai-generator/domain/interfaces/ai-notes-generator-service.ts +++ b/src/ai-generator/domain/interfaces/ai-notes-generator-service.ts @@ -1,3 +1,4 @@ +import type { NoteRowModel } from "@/src/notes/domain/models/note-row-model"; import type { AiGeneratorNoteType } from "../models/ai-generator-note-type"; import type { AiNotesGeneratorSourceType } from "../models/ai-notes-generator-source-type"; @@ -14,7 +15,7 @@ export interface AiNotesGeneratorService { * consisting of two strings: the question (front side of the note) and the * answer (back side of the note) */ - generate(input: GenerateAiNotesInputModel): Promise; + generate(input: GenerateAiNotesInputModel): Promise; } /** diff --git a/src/ai-generator/domain/use-cases/generate-ai-notes-use-case.ts b/src/ai-generator/domain/use-cases/generate-ai-notes-use-case.ts index 562df6e4..35e5c42a 100644 --- a/src/ai-generator/domain/use-cases/generate-ai-notes-use-case.ts +++ b/src/ai-generator/domain/use-cases/generate-ai-notes-use-case.ts @@ -36,6 +36,6 @@ export class GenerateAiNotesUseCase { const generated = await this.aiNotesGeneratorService.generate(input); await this.rateLimitsRepository.increment(rateLimitKey); - return generated.map(([front, back]) => ({ front, back })); + return generated; } }