diff --git a/codegen.json b/codegen.json index 0ba70d7a8f5..da711d6a9dd 100644 --- a/codegen.json +++ b/codegen.json @@ -85,6 +85,7 @@ "File": "./types/File#TFile", "GcalIntegration": "./types/GcalIntegration#GcalIntegrationSource", "GenerateGroupsSuccess": "./types/GenerateGroupsSuccess#GenerateGroupsSuccessSource", + "GenerateInsightSuccess": "./types/GenerateInsightSuccess#GenerateInsightSuccessSource", "GetTemplateSuggestionSuccess": "./types/GetTemplateSuggestionSuccess#GetTemplateSuggestionSuccessSource", "IntegrationProviderWebhook": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", "IntegrationProviderOAuth1": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", diff --git a/packages/client/mutations/GenerateInsightMutation.ts b/packages/client/mutations/GenerateInsightMutation.ts new file mode 100644 index 00000000000..d7bbe485b4f --- /dev/null +++ b/packages/client/mutations/GenerateInsightMutation.ts @@ -0,0 +1,51 @@ +import graphql from 'babel-plugin-relay/macro' +import {commitMutation} from 'react-relay' +import {GenerateInsightMutation as TGenerateInsightMutation} from '../__generated__/GenerateInsightMutation.graphql' +import {StandardMutation} from '../types/relayMutations' + +graphql` + fragment GenerateInsightMutation_team on GenerateInsightSuccess { + wins + challenges + } +` + +const mutation = graphql` + mutation GenerateInsightMutation( + $teamId: ID! + $startDate: DateTime! + $endDate: DateTime! + $useSummaries: Boolean + $prompt: String + ) { + generateInsight( + teamId: $teamId + startDate: $startDate + endDate: $endDate + useSummaries: $useSummaries + prompt: $prompt + ) { + ... on ErrorPayload { + error { + message + } + } + ...GenerateInsightMutation_team @relay(mask: false) + } + } +` + +const GenerateInsightMutation: StandardMutation = ( + atmosphere, + variables, + {onError, onCompleted} +) => { + return commitMutation(atmosphere, { + mutation, + variables, + onCompleted, + onError + }) +} + +export default GenerateInsightMutation diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts new file mode 100644 index 00000000000..903383135f0 --- /dev/null +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -0,0 +1,46 @@ +import getKysely from '../../../postgres/getKysely' +import standardError from '../../../utils/standardError' +import {MutationResolvers} from '../resolverTypes' +import {getSummaries} from './helpers/getSummaries' +import {getTopics} from './helpers/getTopics' + +const generateInsight: MutationResolvers['generateInsight'] = async ( + _source, + {teamId, startDate, endDate, useSummaries = true, prompt}, + {dataLoader} +) => { + if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) { + return standardError( + new Error('Invalid date format. Please use ISO 8601 format (e.g., 2024-01-01T00:00:00Z).') + ) + } + const oneWeekInMs = 7 * 24 * 60 * 60 * 1000 + if (endDate.getTime() - startDate.getTime() < oneWeekInMs) { + return standardError(new Error('The end date must be at least one week after the start date.')) + } + + const response = useSummaries + ? await getSummaries(teamId, startDate, endDate, prompt) + : await getTopics(teamId, startDate, endDate, dataLoader, prompt) + + if ('error' in response) { + return response + } + const {wins, challenges} = response + const pg = getKysely() + + await pg + .insertInto('Insight') + .values({ + teamId, + wins, + challenges, + startDateTime: startDate, + endDateTime: endDate + }) + .execute() + + return response +} + +export default generateInsight diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts new file mode 100644 index 00000000000..c80339a30f9 --- /dev/null +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -0,0 +1,57 @@ +import yaml from 'js-yaml' +import getRethink from '../../../../database/rethinkDriver' +import MeetingRetrospective from '../../../../database/types/MeetingRetrospective' +import OpenAIServerManager from '../../../../utils/OpenAIServerManager' +import standardError from '../../../../utils/standardError' + +export const getSummaries = async ( + teamId: string, + startDate: Date, + endDate: Date, + prompt?: string | null +) => { + const r = await getRethink() + const MIN_MILLISECONDS = 60 * 1000 // 1 minute + const MIN_REFLECTION_COUNT = 3 + + const rawMeetings = (await r + .table('NewMeeting') + .getAll(teamId, {index: 'teamId'}) + .filter((row: any) => + row('meetingType') + .eq('retrospective') + .and(row('createdAt').ge(startDate)) + .and(row('createdAt').le(endDate)) + .and(row('reflectionCount').gt(MIN_REFLECTION_COUNT)) + .and(r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'}).count().gt(1)) + .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) + .and(row.hasFields('summary')) + ) + .run()) as MeetingRetrospective[] + + if (!rawMeetings.length) { + return standardError(new Error('No meetings found')) + } + + const summaries = rawMeetings.map((meeting) => ({ + meetingName: meeting.name, + date: meeting.createdAt, + summary: meeting.summary + })) + + const yamlData = yaml.dump(summaries, { + noCompatMode: true + }) + + const openAI = new OpenAIServerManager() + const rawInsight = await openAI.generateInsight(yamlData, true, prompt) + if (!rawInsight) { + return standardError(new Error('No insights generated')) + } + + return { + wins: rawInsight.wins, + challenges: rawInsight.challenges, + meetingIds: rawMeetings.map((meeting) => meeting.id) + } +} diff --git a/packages/server/graphql/public/mutations/helpers/getTopics.ts b/packages/server/graphql/public/mutations/helpers/getTopics.ts new file mode 100644 index 00000000000..bca0f2a4cec --- /dev/null +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -0,0 +1,229 @@ +import yaml from 'js-yaml' +import getRethink from '../../../../database/rethinkDriver' +import MeetingRetrospective from '../../../../database/types/MeetingRetrospective' +import getKysely from '../../../../postgres/getKysely' +import OpenAIServerManager from '../../../../utils/OpenAIServerManager' +import sendToSentry from '../../../../utils/sendToSentry' +import standardError from '../../../../utils/standardError' +import {DataLoaderWorker} from '../../../graphql' + +const getComments = async (reflectionGroupId: string, dataLoader: DataLoaderWorker) => { + const pg = getKysely() + const IGNORE_COMMENT_USER_IDS = ['parabolAIUser'] + const discussion = await pg + .selectFrom('Discussion') + .selectAll() + .where('discussionTopicId', '=', reflectionGroupId) + .limit(1) + .executeTakeFirst() + if (!discussion) return null + const {id: discussionId} = discussion + const rawComments = await dataLoader.get('commentsByDiscussionId').load(discussionId) + const humanComments = rawComments.filter((c) => !IGNORE_COMMENT_USER_IDS.includes(c.createdBy)) + const rootComments = humanComments.filter((c) => !c.threadParentId) + rootComments.sort((a, b) => { + return a.createdAt.getTime() < b.createdAt.getTime() ? -1 : 1 + }) + const comments = await Promise.all( + rootComments.map(async (comment) => { + const {createdBy, isAnonymous, plaintextContent} = comment + const creator = await dataLoader.get('users').loadNonNull(createdBy) + const commentAuthor = isAnonymous ? 'Anonymous' : creator.preferredName + const commentReplies = await Promise.all( + humanComments + .filter((c) => c.threadParentId === comment.id) + .sort((a, b) => { + return a.createdAt.getTime() < b.createdAt.getTime() ? -1 : 1 + }) + .map(async (reply) => { + const {createdBy, isAnonymous, plaintextContent} = reply + const creator = await dataLoader.get('users').loadNonNull(createdBy) + const replyAuthor = isAnonymous ? 'Anonymous' : creator.preferredName + return { + text: plaintextContent, + author: replyAuthor + } + }) + ) + return commentReplies.length === 0 + ? { + text: plaintextContent, + author: commentAuthor + } + : { + text: plaintextContent, + author: commentAuthor, + replies: commentReplies + } + }) + ) + return comments +} + +type MeetingLookup = Record +const meetingLookup: MeetingLookup = {} + +const processLines = (lines: string[]): string[] => { + const meetingURL = 'https://action.parabol.co/meet/' + return lines + .map((line) => { + if (line.includes(meetingURL)) { + let processedLine = line + const regex = new RegExp(`${meetingURL}\\S+`, 'g') + const matches = processedLine.match(regex) || [] + + let isValid = true + matches.forEach((match) => { + const shortMeetingId = match.split(meetingURL)[1]?.split(/[),\s]/)[0] // Split by closing parenthesis, comma, or space + const actualMeetingId = shortMeetingId && (meetingLookup[shortMeetingId] as string) + + if (shortMeetingId && actualMeetingId) { + processedLine = processedLine.replace(shortMeetingId, actualMeetingId) + } else { + const error = new Error( + `AI hallucinated. Unable to find meetingId for ${shortMeetingId}. Line: ${line}` + ) + sendToSentry(error) + isValid = false + } + }) + return isValid ? processedLine : '' + } + return line + }) + .filter((line) => line.trim() !== '') +} + +const processSection = (section: string[]): string[] => { + return section + .flatMap((item) => { + const lines = item.split('\n') + return processLines(lines) + }) + .filter((processedItem) => processedItem.trim() !== '') +} + +export const getTopics = async ( + teamId: string, + startDate: Date, + endDate: Date, + dataLoader: DataLoaderWorker, + prompt?: string | null +) => { + const r = await getRethink() + const MIN_REFLECTION_COUNT = 3 + const MIN_MILLISECONDS = 60 * 1000 // 1 minute + const rawMeetings = await r + .table('NewMeeting') + .getAll(teamId, {index: 'teamId'}) + .filter((row: any) => + row('meetingType') + .eq('retrospective') + .and(row('createdAt').ge(startDate)) + .and(row('createdAt').le(endDate)) + .and(row('reflectionCount').gt(MIN_REFLECTION_COUNT)) + .and(r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'}).count().gt(1)) + .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) + ) + .run() + + const meetings = await Promise.all( + rawMeetings.map(async (meeting) => { + const { + id: meetingId, + disableAnonymity, + name: meetingName, + createdAt: meetingDate + } = meeting as MeetingRetrospective + const rawReflectionGroups = await dataLoader + .get('retroReflectionGroupsByMeetingId') + .load(meetingId) + const reflectionGroups = Promise.all( + rawReflectionGroups + .filter((g) => g.voterIds.length > 0) + .map(async (group) => { + const {id: reflectionGroupId, voterIds, title} = group + const [comments, rawReflections] = await Promise.all([ + getComments(reflectionGroupId, dataLoader), + dataLoader.get('retroReflectionsByGroupId').load(group.id) + ]) + const reflections = await Promise.all( + rawReflections.map(async (reflection) => { + const {promptId, creatorId, plaintextContent} = reflection + const [prompt, creator] = await Promise.all([ + dataLoader.get('reflectPrompts').load(promptId), + creatorId ? dataLoader.get('users').loadNonNull(creatorId) : null + ]) + const {question} = prompt + const creatorName = + disableAnonymity && creator ? creator.preferredName : 'Anonymous' + return { + prompt: question, + author: creatorName, + text: plaintextContent + } + }) + ) + const res = { + voteCount: voterIds.length, + title: title, + comments, + reflections, + meetingName, + date: meetingDate, + meetingId + } + + if (!res.comments || !res.comments.length) { + delete (res as any).comments + } + return res + }) + ) + return reflectionGroups + }) + ) + + const hotTopics = meetings + .flat() + .filter((t) => t.voteCount > 2) + .sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1)) + + const idGenerator = { + meeting: 1 + } + + const shortTokenedTopics = hotTopics + .map((t) => { + const {date, meetingId} = t + const shortMeetingId = `m${idGenerator.meeting++}` + const shortMeetingDate = new Date(date).toISOString().split('T')[0] + meetingLookup[shortMeetingId] = meetingId + return { + ...t, + date: shortMeetingDate, + meetingId: shortMeetingId + } + }) + .filter((t) => t) + + if (shortTokenedTopics.length === 0) { + return standardError(new Error('No meeting content found for the specified date range.')) + } + + const yamlData = yaml.dump(shortTokenedTopics, { + noCompatMode: true + }) + + const openAI = new OpenAIServerManager() + const rawInsight = await openAI.generateInsight(yamlData, false, prompt) + if (!rawInsight) { + return standardError(new Error('Unable to generate insight.')) + } + + const wins = processSection(rawInsight.wins) + const challenges = processSection(rawInsight.challenges) + const meetingIds = rawMeetings.map((meeting) => meeting.id) + + return {wins, challenges, meetingIds} +} diff --git a/packages/server/graphql/public/permissions.ts b/packages/server/graphql/public/permissions.ts index 41bcc679ea7..a74143e3064 100644 --- a/packages/server/graphql/public/permissions.ts +++ b/packages/server/graphql/public/permissions.ts @@ -32,6 +32,7 @@ const permissionMap: PermissionMap = { // don't check isAuthenticated for acceptTeamInvitation here because there are special cases handled in the resolver acceptTeamInvitation: rateLimit({perMinute: 50, perHour: 100}), createImposterToken: isSuperUser, + generateInsight: isSuperUser, loginWithGoogle: and( not(isEnvVarTrue('AUTH_GOOGLE_DISABLED')), rateLimit({perMinute: 50, perHour: 500}) diff --git a/packages/server/graphql/public/typeDefs/generateInsight.graphql b/packages/server/graphql/public/typeDefs/generateInsight.graphql new file mode 100644 index 00000000000..d07b656a8f0 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/generateInsight.graphql @@ -0,0 +1,34 @@ +extend type Mutation { + """ + Generate an insight for a team + """ + generateInsight( + teamId: ID! + startDate: DateTime! + endDate: DateTime! + useSummaries: Boolean + prompt: String + ): GenerateInsightPayload! +} + +""" +Return value for generateInsight, which could be an error +""" +union GenerateInsightPayload = ErrorPayload | GenerateInsightSuccess + +type GenerateInsightSuccess { + """ + The insights generated focusing on the wins of the team + """ + wins: [String!]! + + """ + The insights generated focusing on the challenges team are facing + """ + challenges: [String!]! + + """ + The meetings that were used to generate the insights + """ + meetings: [RetrospectiveMeeting!]! +} diff --git a/packages/server/graphql/public/types/GenerateInsightSuccess.ts b/packages/server/graphql/public/types/GenerateInsightSuccess.ts new file mode 100644 index 00000000000..ac099aa2295 --- /dev/null +++ b/packages/server/graphql/public/types/GenerateInsightSuccess.ts @@ -0,0 +1,21 @@ +import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {GenerateInsightSuccessResolvers} from '../resolverTypes' + +export type GenerateInsightSuccessSource = { + wins: string[] + challenges: string[] + meetingIds: string[] +} + +const GenerateInsightSuccess: GenerateInsightSuccessResolvers = { + wins: ({wins}) => wins, + challenges: ({challenges}) => challenges, + meetings: async ({meetingIds}, _args, {dataLoader}) => { + const meetings = (await dataLoader + .get('newMeetings') + .loadMany(meetingIds)) as MeetingRetrospective[] + return meetings + } +} + +export default GenerateInsightSuccess diff --git a/packages/server/package.json b/packages/server/package.json index 32707f4bef4..4b039768211 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -121,7 +121,7 @@ "node-pg-migrate": "^5.9.0", "nodemailer": "^6.9.9", "oauth-1.0a": "^2.2.6", - "openai": "^4.24.1", + "openai": "^4.53.0", "openapi-fetch": "^0.9.7", "oy-vey": "^0.12.1", "parabol-client": "7.40.0", diff --git a/packages/server/postgres/migrations/1722011287034_addInsight.ts b/packages/server/postgres/migrations/1722011287034_addInsight.ts new file mode 100644 index 00000000000..7127a688cb8 --- /dev/null +++ b/packages/server/postgres/migrations/1722011287034_addInsight.ts @@ -0,0 +1,32 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + CREATE TABLE "Insight" ( + "id" SERIAL PRIMARY KEY, + "teamId" VARCHAR(100) NOT NULL, + "startDateTime" TIMESTAMP WITH TIME ZONE NOT NULL, + "endDateTime" TIMESTAMP WITH TIME ZONE NOT NULL, + "wins" TEXT[] NOT NULL, + "challenges" TEXT[] NOT NULL, + "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL + ); + CREATE INDEX IF NOT EXISTS "idx_teamId" ON "Insight" ("teamId"); + CREATE INDEX IF NOT EXISTS "idx_startDateTime" ON "Insight" ("startDateTime"); + CREATE INDEX IF NOT EXISTS "idx_endDateTime" ON "Insight" ("endDateTime"); + `) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + DROP TABLE IF EXISTS "Insight"; + `) + await client.end() +} diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 25ae8526109..c076b5dbec5 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -10,6 +10,11 @@ type Prompt = { description: string } +type InsightResponse = { + wins: string[] + challenges: string[] +} + type Template = { templateId: string templateName: string @@ -339,6 +344,133 @@ class OpenAIServerManager { return null } } + + async generateInsight( + yamlData: string, + useSummaries: boolean, + userPrompt?: string | null + ): Promise { + if (!this.openAIApi) return null + const meetingURL = 'https://action.parabol.co/meet/' + const promptForMeetingData = ` + You work at a start-up and you need to discover behavioral trends for a given team. + Below is a list of reflection topics in YAML format from meetings over recent months. + You should describe the situation in two sections with no more than 3 bullet points each. + The first section should describe the team's positive behavior in bullet points. One bullet point should cite a direct quote from the meeting, attributing it to the person who wrote it. + The second section should pick out one or two examples of the team's negative behavior and you should cite a direct quote from the meeting, attributing it to the person who wrote it. + When citing the quote, include the meetingId in the format of https://action.parabol.co/meet/[meetingId]. + Prioritize topics with more votes. + Be sure that each author is only mentioned once. + Your tone should be kind and straight forward. Use plain English. No yapping. + Return the output as a JSON object with the following structure: + { + "wins": ["bullet point 1", "bullet point 2", "bullet point 3"], + "challenges": ["bullet point 1", "bullet point 2"] + } + ` + + const promptForSummaries = ` + You work at a start-up and you need to discover behavioral trends for a given team. + Below is a list of meeting summaries in YAML format from meetings over recent months. + You should describe the situation in two sections with exactly 3 bullet points each. + The first section should describe the team's positive behavior in bullet points. + The second section should pick out one or two examples of the team's negative behavior. + Cite direct quotes from the meeting, attributing them to the person who wrote it, if they're included in the summary. + Include discussion links included in the summaries. They must be in the markdown format of [link](${meetingURL}[meetingId]/discuss/[discussionId]). + Try to spot trends. If a topic comes up in several summaries, prioritize it. + The most important topics are usually at the beginning of each summary, so prioritize them. + Don't repeat the same points in both the wins and challenges. + Return the output as a JSON object with the following structure: + { + "wins": ["bullet point 1", "bullet point 2", "bullet point 3"], + "challenges": ["bullet point 1", "bullet point 2"] + } + Your tone should be kind and straight forward. Use plain English. No yapping. + ` + + const defaultPrompt = useSummaries ? promptForSummaries : promptForMeetingData + const prompt = userPrompt ? userPrompt : defaultPrompt + + try { + const response = await this.openAIApi.chat.completions.create({ + model: 'gpt-4o', + messages: [ + { + role: 'user', + content: `${prompt}\n\n${yamlData}` + } + ], + response_format: { + type: 'json_object' + }, + temperature: 0.7, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0 + }) + + const completionContent = response.choices[0]?.message.content as string + + let data: InsightResponse + try { + data = JSON.parse(completionContent) + } catch (e) { + const error = e instanceof Error ? e : new Error('Error parsing JSON in generateInsight') + sendToSentry(error) + return null + } + + return data + } catch (e) { + const error = e instanceof Error ? e : new Error('Error in generateInsight') + sendToSentry(error) + return null + } + } + + // if we keep generateSummary, we'll need to merge it with getSummary. This will require a UI change as we're returning links in markdown format here + async generateSummary(yamlData: string): Promise { + if (!this.openAIApi) return null + const meetingURL = 'https://action.parabol.co/meet/' + const prompt = ` + You need to summarize the content of a meeting. Your summary must be one paragraph with no more than a two or three sentences. + Below is a list of reflection topics and comments in YAML format from the meeting. + Include quotes from the meeting, and mention the author. + Link directly to the discussion in the markdown format of [link](${meetingURL}[meetingId]/discuss/[discussionId]). + Don't mention the name of the meeting. + Prioritise the topics that got the most votes. + Be sure that each author is only mentioned once. + Your output must be a string. + The most important topics are the ones that got the most votes. + Start the summary with the most important topic. + You do not need to mention everything. Just mention the most important points, and ensure the summary is concise. + Your tone should be kind. Write in plain English. No jargon. + ` + + try { + const response = await this.openAIApi.chat.completions.create({ + model: 'gpt-4o', + messages: [ + { + role: 'user', + content: `${prompt}\n\n${yamlData}` + } + ], + + temperature: 0.7, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0 + }) + + const content = response.choices[0]?.message.content as string + return content + } catch (e) { + const error = e instanceof Error ? e : new Error('Error in generateInsight') + sendToSentry(error) + return null + } + } } export default OpenAIServerManager diff --git a/yarn.lock b/yarn.lock index 07f1a8e8861..252953cb10c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9613,11 +9613,6 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== -base-64@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/base-64/-/base-64-0.1.0.tgz#780a99c84e7d600260361511c4877613bf24f6bb" - integrity sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA== - base-64@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/base-64/-/base-64-1.0.0.tgz#09d0f2084e32a3fd08c2475b973788eee6ae8f4a" @@ -11430,14 +11425,6 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -digest-fetch@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/digest-fetch/-/digest-fetch-1.3.0.tgz#898e69264d00012a23cf26e8a3e40320143fc661" - integrity sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA== - dependencies: - base-64 "^0.1.0" - md5 "^2.3.0" - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -15989,7 +15976,7 @@ marked@^4.3.0: resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== -md5@^2.2.1, md5@^2.3.0: +md5@^2.2.1: version "2.3.0" resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== @@ -17183,16 +17170,15 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" -openai@^4.24.1: - version "4.24.1" - resolved "https://registry.yarnpkg.com/openai/-/openai-4.24.1.tgz#3759001eca835228289fcf18c1bd8d35dae538ba" - integrity sha512-ezm/O3eiZMnyBqirUnWm9N6INJU1WhNtz+nK/Zj/2oyKvRz9pgpViDxa5wYOtyGYXPn1sIKBV0I/S4BDhtydqw== +openai@^4.53.0: + version "4.53.0" + resolved "https://registry.yarnpkg.com/openai/-/openai-4.53.0.tgz#5ac6fc2ba1bba239a31c910bd57d793814bea61d" + integrity sha512-XoMaJsSLuedW5eoMEMmZbdNoXgML3ujcU5KfwRnC6rnbmZkHE2Q4J/SArwhqCxQRqJwHnQUj1LpiROmKPExZJA== dependencies: "@types/node" "^18.11.18" "@types/node-fetch" "^2.6.4" abort-controller "^3.0.0" agentkeepalive "^4.2.1" - digest-fetch "^1.3.0" form-data-encoder "1.7.2" formdata-node "^4.3.2" node-fetch "^2.6.7"