From 05250cff0200a99a0a8d9a68e2c2d5b96f0ea22f Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 1 Jul 2024 16:43:56 +0100 Subject: [PATCH 01/43] add generateInsight mutation --- codegen.json | 1 + .../mutations/GenerateInsightMutation.ts | 41 +++ .../public/mutations/generateInsight.ts | 249 ++++++++++++++++++ .../public/typeDefs/generateInsight.graphql | 18 ++ .../public/types/GenerateInsightSuccess.ts | 13 + packages/server/utils/OpenAIServerManager.ts | 30 +++ 6 files changed, 352 insertions(+) create mode 100644 packages/client/mutations/GenerateInsightMutation.ts create mode 100644 packages/server/graphql/public/mutations/generateInsight.ts create mode 100644 packages/server/graphql/public/typeDefs/generateInsight.graphql create mode 100644 packages/server/graphql/public/types/GenerateInsightSuccess.ts diff --git a/codegen.json b/codegen.json index fa006c77adb..6d88cd8fdf5 100644 --- a/codegen.json +++ b/codegen.json @@ -70,6 +70,7 @@ "File": "./types/File#TFile", "GcalIntegration": "./types/GcalIntegration#GcalIntegrationSource", "GenerateGroupsSuccess": "./types/GenerateGroupsSuccess#GenerateGroupsSuccessSource", + "GenerateInsightSuccess": "./types/GenerateInsightSuccess#GenerateInsightSuccessSource", "GetTemplateSuggestionSuccess": "./types/GetTemplateSuggestionSuccess#GetTemplateSuggestionSuccessSource", "IntegrationProviderOAuth2": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", "InviteToTeamPayload": "./types/InviteToTeamPayload#InviteToTeamPayloadSource", diff --git a/packages/client/mutations/GenerateInsightMutation.ts b/packages/client/mutations/GenerateInsightMutation.ts new file mode 100644 index 00000000000..8420a720434 --- /dev/null +++ b/packages/client/mutations/GenerateInsightMutation.ts @@ -0,0 +1,41 @@ +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 { + successField + } +` + +const mutation = graphql` + mutation GenerateInsightMutation($teamId: ID!) { + generateInsight(teamId: $teamId) { + ... on ErrorPayload { + error { + message + } + } + ...GenerateInsightMutation_team @relay(mask: false) + } + } +` + +const GenerateInsightMutation: StandardMutation = ( + atmosphere, + variables, + {onError, onCompleted} +) => { + return commitMutation(atmosphere, { + mutation, + variables, + optimisticUpdater: (store) => { + const {} = 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..572fa0d0d21 --- /dev/null +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -0,0 +1,249 @@ +import yaml from 'js-yaml' +import fs from 'node:fs' +import getRethink from '../../../database/rethinkDriver' +import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import getKysely from '../../../postgres/getKysely' +import OpenAIServerManager from '../../../utils/OpenAIServerManager' +import {getUserId} from '../../../utils/authorization' +import {MutationResolvers} from '../resolverTypes' + +const generateInsight: MutationResolvers['generateInsight'] = async ( + _source, + {teamId}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const viewerId = getUserId(authToken) + console.log('šŸš€ ~ generateInsight_____:', viewerId) + const now = new Date() + + const getComments = async (reflectionGroupId: string) => { + const IGNORE_COMMENT_USER_IDS = ['parabolAIUser'] + const pg = getKysely() + const discussion = await pg + .selectFrom('Discussion') + .selectAll() + .where('discussionTopicId', '=', reflectionGroupId) + .limit(1) + .executeTakeFirst() + if (!discussion) { + console.log('no discuss', reflectionGroupId) + 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 + } + }) + ) + const res = { + text: plaintextContent, + author: commentAuthor, + replies: commentReplies + } + if (res.replies.length === 0) { + delete (res as any).commentReplies + } + return res + }) + ) + return comments + } + + const getTopicJSON = async (orgId: string, startDate: Date, endDate: Date) => { + const teams = await dataLoader.get('teamsByOrgIds').load(orgId) + const teamIds = teams.map((team) => team.id) + const r = await getRethink() + const rawMeetings = await r + .table('NewMeeting') + .getAll(r.args(teamIds), {index: 'teamId'}) + .filter((row: any) => row('createdAt').ge(startDate).and(row('createdAt').le(endDate))) + .filter({meetingType: 'retrospective'}) + .run() + + const meetings = await Promise.all( + rawMeetings.map(async (meeting) => { + const { + id: meetingId, + disableAnonymity, + teamId, + // templateId, + name: meetingName, + createdAt: meetingDate + } = meeting as MeetingRetrospective + const [team, rawReflectionGroups] = await Promise.all([ + // dataLoader.get('meetingTemplates').loadNonNull(templateId), + dataLoader.get('teams').loadNonNull(teamId), + dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId) + ]) + // const {name: meetingTemplateName} = template + const {name: teamName} = team + const reflectionGroups = Promise.all( + rawReflectionGroups + // for performance since it's really slow! + // .filter((g) => g.voterIds.length > 0) + .map(async (group) => { + const {id: reflectionGroupId, voterIds, title} = group + const [comments, rawReflections] = await Promise.all([ + getComments(reflectionGroupId), + 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), + dataLoader.get('users').loadNonNull(creatorId) + ]) + console.log('šŸš€ ~ creator:', creator) + const {question} = prompt + const creatorName = disableAnonymity ? creator.preferredName : 'Anonymous' + return { + prompt: question, + author: creatorName, + text: plaintextContent + } + }) + ) + console.log('šŸš€ ~ reflections:', reflections) + const res = { + // topicId: reflectionGroupId, + voteCount: voterIds.length, + title: title, + comments, + reflections, + meetingName, + date: meetingDate, + meetingId, + // meetingTemplateName, + teamName + // teamId + } + console.log('šŸš€ ~ res:', res) + if (!res.comments || !res.comments.length) { + delete (res as any).comments + } + return res + }) + ) + return reflectionGroups + }) + ) + console.log('šŸš€ ~ meetings:', {meetings: meetings.flat()}) + return meetings.flat() + } + + const doWork = async () => { + const openAI = new OpenAIServerManager() + const org = 'parabol' + const startDate = new Date('2024-01-01') + // const endDate = new Date('2024-04-01') + const endDate = new Date() + + const orgLookup = { + parabol: 'y3ZJgMy6hq' + } + const orgId = orgLookup[org] + const inTopics = await getTopicJSON(orgId, startDate, endDate) + fs.writeFileSync(`./topics_${org}.json`, JSON.stringify(inTopics)) + console.log('wrote topics!') + // return + + const rawTopics = JSON.parse(fs.readFileSync(`./topics_${org}.json`, 'utf-8')) as Awaited< + ReturnType + > + console.log('šŸš€ ~ rawTopics:', rawTopics) + const hotTopics = rawTopics + // .filter((t) => t.voteCount > 2) + // .sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1)) + type IDLookup = Record + const idLookup = { + team: {} as IDLookup, + topic: {} as IDLookup, + meeting: {} as IDLookup + } + const idGenerator = { + team: 1, + topic: 1, + meeting: 1 + } + + const shortTokenedTopics = hotTopics.map((t) => { + const {date, meetingId} = t + // const shortTeamId = `t${idGenerator.team++}` + // const shortTopicId = `to${idGenerator.topic++}` + const shortMeetingId = `m${idGenerator.meeting++}` + const shortMeetingDate = new Date(date).toISOString().split('T')[0] + // idLookup.team[shortTeamId] = teamId + // idLookup.topic[shortTopicId] = topicId + idLookup.meeting[shortMeetingId] = meetingId + return { + ...t, + // teamId: shortTeamId, + // topicId: shortTopicId, + meetingDate: shortMeetingDate, + meetingId: shortMeetingId + } + }) + console.log('šŸš€ ~ hotTopics:', hotTopics) + console.log('šŸš€ ~ shortTokenedTopics:', shortTokenedTopics) + // fs.writeFileSync('./topics_target_short.json', JSON.stringify(shortTokenedTopics)) + const yamlData = yaml.dump(shortTokenedTopics, { + noCompatMode: true // This option ensures compatibility mode is off + }) + fs.writeFileSync(`./topics_${org}_short.yml`, yamlData) + // return + + const summarizingPrompt = ` + You are a management consultant who needs to discover behavioral trends for a given team. + Below is a list of reflection topics in YAML format from meetings over the last 3 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, inlcude the meetingId in the format of https://action.parabol.co/meet/[meetingId]. + For each topic, mention how many votes it has. + Be sure that each author is only mentioned once. + Above the two sections, include a short subject line that mentions the team name and summarizes the negative behavior mentioned in the second paragraph. + The subject must not be generic sounding. The person who reads the subject should instantly know that the person who wrote it has deep understanding of the team's problems. + The format of the subject line should be the following: Subject: [Team Name] [Short description of the negative behavior] + Your tone should be kind and professional. No yapping.` + + const batch = await openAI.batchChatCompletion(summarizingPrompt, yamlData) + console.log('šŸš€ ~ batch:', batch) + + // const meetingIdRegex = /\/meet\/([m|t|to]\d+)/gm + // const fixedUrls = summaryEmail!.replace(meetingIdRegex, (_, meetingId) => { + // return `/meet/${idLookup.meeting[meetingId]}` + // }) + process.exit() + } + + doWork() + + // RESOLUTION + const data = {} + return data +} + +export default generateInsight diff --git a/packages/server/graphql/public/typeDefs/generateInsight.graphql b/packages/server/graphql/public/typeDefs/generateInsight.graphql new file mode 100644 index 00000000000..8fb647f050a --- /dev/null +++ b/packages/server/graphql/public/typeDefs/generateInsight.graphql @@ -0,0 +1,18 @@ +extend type Mutation { + """ + Describe the mutation here + """ + generateInsight(teamId: ID!): GenerateInsightPayload! +} + +""" +Return value for generateInsight, which could be an error +""" +union GenerateInsightPayload = ErrorPayload | GenerateInsightSuccess + +type GenerateInsightSuccess { + """ + Describe the first return field here + """ + successField: ID! +} diff --git a/packages/server/graphql/public/types/GenerateInsightSuccess.ts b/packages/server/graphql/public/types/GenerateInsightSuccess.ts new file mode 100644 index 00000000000..f54b83f3b3f --- /dev/null +++ b/packages/server/graphql/public/types/GenerateInsightSuccess.ts @@ -0,0 +1,13 @@ +// import {GenerateInsightSuccessResolvers} from '../resolverTypes' + +export type GenerateInsightSuccessSource = { + id: string +} + +const GenerateInsightSuccess = { + successField: async ({id}, _args, {dataLoader}) => { + return null + } +} + +export default GenerateInsightSuccess diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index b39615807af..82eccd597df 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -339,6 +339,36 @@ class OpenAIServerManager { return null } } + + // TODO: actually batch the completions + async batchChatCompletion(prompt: string, yamlData: string) { + if (!this.openAIApi) return null + + try { + const response = await this.openAIApi.chat.completions.create({ + model: 'gpt-4', + messages: [ + { + role: 'user', + content: `${prompt}\n\n${yamlData}` + } + ], + temperature: 0.7, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0 + }) + + const completionText = (response.choices[0]?.message?.content?.trim() as string) ?? null + return completionText + } catch (e) { + const error = + e instanceof Error ? e : new Error('OpenAI failed to generate the batch completion') + Logger.error(error.message) + sendToSentry(error) + return null + } + } } export default OpenAIServerManager From c4ef355e9cfde4e655ce143f104e8eaa4d240209 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 1 Jul 2024 18:09:58 +0100 Subject: [PATCH 02/43] replace shortUrls with real urls --- .../public/mutations/generateInsight.ts | 150 ++++---- packages/server/graphql/public/permissions.ts | 1 + packages/server/package.json | 2 +- packages/server/utils/OpenAIServerManager.ts | 2 +- topics_parabol.json | 1 + topics_parabol_short.yml | 330 ++++++++++++++++++ yarn.lock | 55 +-- 7 files changed, 412 insertions(+), 129 deletions(-) create mode 100644 topics_parabol.json create mode 100644 topics_parabol_short.yml diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 572fa0d0d21..e6c2122aa8d 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -4,7 +4,6 @@ import getRethink from '../../../database/rethinkDriver' import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import getKysely from '../../../postgres/getKysely' import OpenAIServerManager from '../../../utils/OpenAIServerManager' -import {getUserId} from '../../../utils/authorization' import {MutationResolvers} from '../resolverTypes' const generateInsight: MutationResolvers['generateInsight'] = async ( @@ -12,10 +11,6 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( {teamId}, {authToken, dataLoader, socketId: mutatorId} ) => { - const viewerId = getUserId(authToken) - console.log('šŸš€ ~ generateInsight_____:', viewerId) - const now = new Date() - const getComments = async (reflectionGroupId: string) => { const IGNORE_COMMENT_USER_IDS = ['parabolAIUser'] const pg = getKysely() @@ -25,10 +20,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( .where('discussionTopicId', '=', reflectionGroupId) .limit(1) .executeTakeFirst() - if (!discussion) { - console.log('no discuss', reflectionGroupId) - return null - } + 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)) @@ -75,11 +67,13 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const teams = await dataLoader.get('teamsByOrgIds').load(orgId) const teamIds = teams.map((team) => team.id) const r = await getRethink() + const MIN_REFLECTION_COUNT = 3 const rawMeetings = await r .table('NewMeeting') .getAll(r.args(teamIds), {index: 'teamId'}) - .filter((row: any) => row('createdAt').ge(startDate).and(row('createdAt').le(endDate))) .filter({meetingType: 'retrospective'}) + .filter((row: any) => row('createdAt').ge(startDate).and(row('createdAt').le(endDate))) + .filter((row: any) => row('reflectionCount').gt(MIN_REFLECTION_COUNT)) .run() const meetings = await Promise.all( @@ -116,7 +110,6 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( dataLoader.get('reflectPrompts').load(promptId), dataLoader.get('users').loadNonNull(creatorId) ]) - console.log('šŸš€ ~ creator:', creator) const {question} = prompt const creatorName = disableAnonymity ? creator.preferredName : 'Anonymous' return { @@ -126,7 +119,6 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( } }) ) - console.log('šŸš€ ~ reflections:', reflections) const res = { // topicId: reflectionGroupId, voteCount: voterIds.length, @@ -140,7 +132,6 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( teamName // teamId } - console.log('šŸš€ ~ res:', res) if (!res.comments || !res.comments.length) { delete (res as any).comments } @@ -150,72 +141,65 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( return reflectionGroups }) ) - console.log('šŸš€ ~ meetings:', {meetings: meetings.flat()}) return meetings.flat() } - const doWork = async () => { - const openAI = new OpenAIServerManager() - const org = 'parabol' - const startDate = new Date('2024-01-01') - // const endDate = new Date('2024-04-01') - const endDate = new Date() + const openAI = new OpenAIServerManager() + const org = 'parabol' + const startDate = new Date('2024-01-01') + // const endDate = new Date('2024-04-01') + const endDate = new Date() - const orgLookup = { - parabol: 'y3ZJgMy6hq' - } - const orgId = orgLookup[org] - const inTopics = await getTopicJSON(orgId, startDate, endDate) - fs.writeFileSync(`./topics_${org}.json`, JSON.stringify(inTopics)) - console.log('wrote topics!') - // return + const orgLookup = { + parabol: 'y3ZJgMy6hq' + } + const orgId = orgLookup[org] + const inTopics = await getTopicJSON(orgId, startDate, endDate) + fs.writeFileSync(`./topics_${org}.json`, JSON.stringify(inTopics)) - const rawTopics = JSON.parse(fs.readFileSync(`./topics_${org}.json`, 'utf-8')) as Awaited< - ReturnType - > - console.log('šŸš€ ~ rawTopics:', rawTopics) - const hotTopics = rawTopics - // .filter((t) => t.voteCount > 2) - // .sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1)) - type IDLookup = Record - const idLookup = { - team: {} as IDLookup, - topic: {} as IDLookup, - meeting: {} as IDLookup - } - const idGenerator = { - team: 1, - topic: 1, - meeting: 1 - } + const rawTopics = JSON.parse(fs.readFileSync(`./topics_${org}.json`, 'utf-8')) as Awaited< + ReturnType + > + const hotTopics = rawTopics + // .filter((t) => t.voteCount > 2) + // .sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1)) + type IDLookup = Record + const idLookup = { + team: {} as IDLookup, + topic: {} as IDLookup, + meeting: {} as IDLookup + } + const idGenerator = { + team: 1, + topic: 1, + meeting: 1 + } - const shortTokenedTopics = hotTopics.map((t) => { - const {date, meetingId} = t - // const shortTeamId = `t${idGenerator.team++}` - // const shortTopicId = `to${idGenerator.topic++}` - const shortMeetingId = `m${idGenerator.meeting++}` - const shortMeetingDate = new Date(date).toISOString().split('T')[0] - // idLookup.team[shortTeamId] = teamId - // idLookup.topic[shortTopicId] = topicId - idLookup.meeting[shortMeetingId] = meetingId - return { - ...t, - // teamId: shortTeamId, - // topicId: shortTopicId, - meetingDate: shortMeetingDate, - meetingId: shortMeetingId - } - }) - console.log('šŸš€ ~ hotTopics:', hotTopics) - console.log('šŸš€ ~ shortTokenedTopics:', shortTokenedTopics) - // fs.writeFileSync('./topics_target_short.json', JSON.stringify(shortTokenedTopics)) - const yamlData = yaml.dump(shortTokenedTopics, { - noCompatMode: true // This option ensures compatibility mode is off - }) - fs.writeFileSync(`./topics_${org}_short.yml`, yamlData) - // return + const shortTokenedTopics = hotTopics.map((t) => { + const {date, meetingId} = t + // const shortTeamId = `t${idGenerator.team++}` + // const shortTopicId = `to${idGenerator.topic++}` + const shortMeetingId = `m${idGenerator.meeting++}` + const shortMeetingDate = new Date(date).toISOString().split('T')[0] + // idLookup.team[shortTeamId] = teamId + // idLookup.topic[shortTopicId] = topicId + idLookup.meeting[shortMeetingId] = meetingId + return { + ...t, + // teamId: shortTeamId, + // topicId: shortTopicId, + date: shortMeetingDate, + meetingId: shortMeetingId + } + }) + // fs.writeFileSync('./topics_target_short.json', JSON.stringify(shortTokenedTopics)) + const yamlData = yaml.dump(shortTokenedTopics, { + noCompatMode: true // This option ensures compatibility mode is off + }) + fs.writeFileSync(`./topics_${org}_short.yml`, yamlData) + // return - const summarizingPrompt = ` + const summarizingPrompt = ` You are a management consultant who needs to discover behavioral trends for a given team. Below is a list of reflection topics in YAML format from meetings over the last 3 months. You should describe the situation in two sections with no more than 3 bullet points each. @@ -229,17 +213,23 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( The format of the subject line should be the following: Subject: [Team Name] [Short description of the negative behavior] Your tone should be kind and professional. No yapping.` - const batch = await openAI.batchChatCompletion(summarizingPrompt, yamlData) - console.log('šŸš€ ~ batch:', batch) + const batch = await openAI.batchChatCompletion(summarizingPrompt, yamlData) - // const meetingIdRegex = /\/meet\/([m|t|to]\d+)/gm - // const fixedUrls = summaryEmail!.replace(meetingIdRegex, (_, meetingId) => { - // return `/meet/${idLookup.meeting[meetingId]}` - // }) - process.exit() + const replaceShortTokensWithUrls = (text: string, lookup: IDLookup) => { + return text.replace(/https:\/\/action\.parabol\.co\/meet\/(m\d+)/g, (_, shortMeetingId) => { + const actualMeetingId = lookup.meeting[shortMeetingId] + return `https://action.parabol.co/meet/${actualMeetingId}` + }) } - doWork() + if (!batch) return null + + const insight = replaceShortTokensWithUrls(batch, idLookup) + + // const meetingIdRegex = /\/meet\/([m|t|to]\d+)/gm + // const fixedUrls = summaryEmail!.replace(meetingIdRegex, (_, meetingId) => { + // return `/meet/${idLookup.meeting[meetingId]}` + // }) // RESOLUTION const data = {} 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/package.json b/packages/server/package.json index 04b87f3a2a6..75eecac4c10 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.52.2", "openapi-fetch": "^0.9.7", "oy-vey": "^0.12.1", "parabol-client": "7.37.8", diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 82eccd597df..72d89781545 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -346,7 +346,7 @@ class OpenAIServerManager { try { const response = await this.openAIApi.chat.completions.create({ - model: 'gpt-4', + model: 'gpt-4o', messages: [ { role: 'user', diff --git a/topics_parabol.json b/topics_parabol.json new file mode 100644 index 00000000000..2b00422f009 --- /dev/null +++ b/topics_parabol.json @@ -0,0 +1 @@ +[{"voteCount":0,"title":"Work team","reflections":[{"prompt":"Start","author":"Anonymous","text":"amazing work team"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Team","reflections":[{"prompt":"Start","author":"Anonymous","text":"love it team"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Communication struggles","reflections":[{"prompt":"Stop","author":"Anonymous","text":"communication struggles"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"we should communicate","reflections":[{"prompt":"Stop","author":"Anonymous","text":"we should communicate better"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Communication","comments":[{"text":"ds","author":"Nick O'Ferrall","replies":[]},{"text":"ds","author":"Nick O'Ferrall","replies":[]},{"text":"d","author":"Nick O'Ferrall","replies":[]}],"reflections":[{"prompt":"Stop","author":"Anonymous","text":"we must improve communication"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Stuff","reflections":[{"prompt":"Start","author":"Anonymous","text":"fantastic stuff"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"ds"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"ds"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"d"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"sd"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"ds"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"d"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"ds"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"ds"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"}] \ No newline at end of file diff --git a/topics_parabol_short.yml b/topics_parabol_short.yml new file mode 100644 index 00000000000..1133d9b12a1 --- /dev/null +++ b/topics_parabol_short.yml @@ -0,0 +1,330 @@ +- voteCount: 0 + title: Work team + reflections: + - prompt: Start + author: Anonymous + text: amazing work team + meetingName: Retro 6 + date: '2024-07-01' + meetingId: m1 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Team + reflections: + - prompt: Start + author: Anonymous + text: love it team + meetingName: Retro 6 + date: '2024-07-01' + meetingId: m2 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Communication struggles + reflections: + - prompt: Stop + author: Anonymous + text: communication struggles + meetingName: Retro 6 + date: '2024-07-01' + meetingId: m3 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: we should communicate + reflections: + - prompt: Stop + author: Anonymous + text: we should communicate better + meetingName: Retro 6 + date: '2024-07-01' + meetingId: m4 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Communication + comments: + - text: ds + author: Nick O'Ferrall + replies: [] + - text: ds + author: Nick O'Ferrall + replies: [] + - text: d + author: Nick O'Ferrall + replies: [] + reflections: + - prompt: Stop + author: Anonymous + text: we must improve communication + meetingName: Retro 6 + date: '2024-07-01' + meetingId: m5 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Stuff + reflections: + - prompt: Start + author: Anonymous + text: fantastic stuff + meetingName: Retro 6 + date: '2024-07-01' + meetingId: m6 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: What went well šŸ˜„ + author: Anonymous + text: ds + meetingName: Retro 1 + date: '2024-06-26' + meetingId: m7 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: What went well šŸ˜„ + author: Anonymous + text: ds + meetingName: Retro 1 + date: '2024-06-26' + meetingId: m8 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: What went well šŸ˜„ + author: Anonymous + text: d + meetingName: Retro 1 + date: '2024-06-26' + meetingId: m9 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Sd + reflections: + - prompt: What went well šŸ˜„ + author: Anonymous + text: sd + meetingName: Retro 1 + date: '2024-06-26' + meetingId: m10 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Wins šŸ† + author: Anonymous + text: ds + meetingName: Retro 4 + date: '2024-06-27' + meetingId: m11 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: Wins šŸ† + author: Anonymous + text: d + meetingName: Retro 4 + date: '2024-06-27' + meetingId: m12 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Wins šŸ† + author: Anonymous + text: ds + meetingName: Retro 4 + date: '2024-06-27' + meetingId: m13 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Wins šŸ† + author: Anonymous + text: ds + meetingName: Retro 4 + date: '2024-06-27' + meetingId: m14 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: Start + author: Anonymous + text: d + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m15 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Sd + reflections: + - prompt: Start + author: Anonymous + text: sd + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m16 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Sd + reflections: + - prompt: Start + author: Anonymous + text: sd + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m17 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: Start + author: Anonymous + text: d + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m18 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Sd + reflections: + - prompt: Start + author: Anonymous + text: sd + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m19 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: Start + author: Anonymous + text: d + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m20 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: s + reflections: + - prompt: Start + author: Anonymous + text: s + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m21 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Start + author: Anonymous + text: ds + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m22 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: s + reflections: + - prompt: Start + author: Anonymous + text: s + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m23 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Sd + reflections: + - prompt: Start + author: Anonymous + text: sd + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m24 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Sd + reflections: + - prompt: Start + author: Anonymous + text: sd + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m25 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: s + reflections: + - prompt: Start + author: Anonymous + text: s + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m26 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: s + reflections: + - prompt: Start + author: Anonymous + text: s + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m27 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Start + author: Anonymous + text: ds + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m28 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Start + author: Anonymous + text: ds + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m29 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Start + author: Anonymous + text: ds + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m30 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: Start + author: Anonymous + text: d + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m31 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: Start + author: Anonymous + text: d + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m32 + teamName: Nick O'Ferrallā€™s Team diff --git a/yarn.lock b/yarn.lock index 8d7b138ae27..cbb31d3f8dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9541,11 +9541,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" @@ -11384,14 +11379,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" @@ -15988,7 +15975,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== @@ -17182,16 +17169,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.52.2: + version "4.52.2" + resolved "https://registry.yarnpkg.com/openai/-/openai-4.52.2.tgz#5d67271f3df84c0b54676b08990eaa9402151759" + integrity sha512-mMc0XgFuVSkcm0lRIi8zaw++otC82ZlfkCur1qguXYWPETr/+ZwL9A/vvp3YahX+shpaT6j03dwsmUyLAfmEfg== 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" @@ -20357,7 +20343,7 @@ string-similarity@^3.0.0: resolved "https://registry.yarnpkg.com/string-similarity/-/string-similarity-3.0.0.tgz#07b0bc69fae200ad88ceef4983878d03793847c7" integrity sha512-7kS7LyTp56OqOI2BDWQNVnLX/rCxIQn+/5M0op1WV6P8Xx6TZNdajpuqQdiJ7Xx+p1C5CsWMvdiBp9ApMhxzEQ== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -20375,15 +20361,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.0.tgz#5ab00980cfb29f43e736b113a120a73a0fb569d3" @@ -20460,7 +20437,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -20474,13 +20451,6 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -22328,7 +22298,7 @@ workbox-window@6.5.4: "@types/trusted-types" "^2.0.2" workbox-core "6.5.4" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -22346,15 +22316,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 450b9c245b019dc6cd50b274d4cfb447001626ad Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 2 Jul 2024 15:58:11 +0100 Subject: [PATCH 03/43] handle teamId arg and replace short meeting ids --- .../public/mutations/generateInsight.ts | 92 ++--- topics_y3ZJgMy6hq.json | 1 + topics_y3ZJgMy6hq_short.yml | 1 + topics_y3ZJgMy6hr.json | 1 + topics_y3ZJgMy6hr_short.yml | 330 ++++++++++++++++++ 5 files changed, 384 insertions(+), 41 deletions(-) create mode 100644 topics_y3ZJgMy6hq.json create mode 100644 topics_y3ZJgMy6hq_short.yml create mode 100644 topics_y3ZJgMy6hr.json create mode 100644 topics_y3ZJgMy6hr_short.yml diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index e6c2122aa8d..9547fd16ec9 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -4,11 +4,13 @@ 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 {MutationResolvers} from '../resolverTypes' const generateInsight: MutationResolvers['generateInsight'] = async ( _source, - {teamId}, + // {teamId, orgId, startDate, endDate}, + {teamId, orgId}, {authToken, dataLoader, socketId: mutatorId} ) => { const getComments = async (reflectionGroupId: string) => { @@ -63,9 +65,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( return comments } - const getTopicJSON = async (orgId: string, startDate: Date, endDate: Date) => { - const teams = await dataLoader.get('teamsByOrgIds').load(orgId) - const teamIds = teams.map((team) => team.id) + const getTopicJSON = async (teamIds: string[], startDate: Date, endDate: Date) => { const r = await getRethink() const MIN_REFLECTION_COUNT = 3 const rawMeetings = await r @@ -82,12 +82,10 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( id: meetingId, disableAnonymity, teamId, - // templateId, name: meetingName, createdAt: meetingDate } = meeting as MeetingRetrospective const [team, rawReflectionGroups] = await Promise.all([ - // dataLoader.get('meetingTemplates').loadNonNull(templateId), dataLoader.get('teams').loadNonNull(teamId), dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId) ]) @@ -128,7 +126,6 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( meetingName, date: meetingDate, meetingId, - // meetingTemplateName, teamName // teamId } @@ -145,19 +142,24 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( } const openAI = new OpenAIServerManager() - const org = 'parabol' + + const getTeamIds = async (orgId?: string, teamId?: string) => { + if (teamId) return [teamId] + const teams = await dataLoader.get('teamsByOrgIds').load(orgId!) + return teams.map((team) => team.id) + } + + const teamIds = await getTeamIds(orgId, teamId) + const startDate = new Date('2024-01-01') // const endDate = new Date('2024-04-01') const endDate = new Date() - const orgLookup = { - parabol: 'y3ZJgMy6hq' - } - const orgId = orgLookup[org] - const inTopics = await getTopicJSON(orgId, startDate, endDate) - fs.writeFileSync(`./topics_${org}.json`, JSON.stringify(inTopics)) + const identifier = teamId ?? orgId + const inTopics = await getTopicJSON(teamIds, startDate, endDate) + fs.writeFileSync(`./topics_${identifier}.json`, JSON.stringify(inTopics)) - const rawTopics = JSON.parse(fs.readFileSync(`./topics_${org}.json`, 'utf-8')) as Awaited< + const rawTopics = JSON.parse(fs.readFileSync(`./topics_${identifier}.json`, 'utf-8')) as Awaited< ReturnType > const hotTopics = rawTopics @@ -165,29 +167,20 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( // .sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1)) type IDLookup = Record const idLookup = { - team: {} as IDLookup, - topic: {} as IDLookup, meeting: {} as IDLookup } + const idGenerator = { - team: 1, - topic: 1, meeting: 1 } - const shortTokenedTopics = hotTopics.map((t) => { const {date, meetingId} = t - // const shortTeamId = `t${idGenerator.team++}` - // const shortTopicId = `to${idGenerator.topic++}` const shortMeetingId = `m${idGenerator.meeting++}` const shortMeetingDate = new Date(date).toISOString().split('T')[0] - // idLookup.team[shortTeamId] = teamId - // idLookup.topic[shortTopicId] = topicId + console.log('šŸš€ ~ shortMeetingId:', shortMeetingId) idLookup.meeting[shortMeetingId] = meetingId return { ...t, - // teamId: shortTeamId, - // topicId: shortTopicId, date: shortMeetingDate, meetingId: shortMeetingId } @@ -196,8 +189,9 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const yamlData = yaml.dump(shortTokenedTopics, { noCompatMode: true // This option ensures compatibility mode is off }) - fs.writeFileSync(`./topics_${org}_short.yml`, yamlData) - // return + fs.writeFileSync(`./topics_${identifier}_short.yml`, yamlData) + + const meetingURL = 'https://action.parabol.co/meet/' const summarizingPrompt = ` You are a management consultant who needs to discover behavioral trends for a given team. @@ -205,7 +199,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( 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, inlcude the meetingId in the format of https://action.parabol.co/meet/[meetingId]. + When citing the quote, inlcude the meetingId in the format of ${meetingURL}[meetingId]. For each topic, mention how many votes it has. Be sure that each author is only mentioned once. Above the two sections, include a short subject line that mentions the team name and summarizes the negative behavior mentioned in the second paragraph. @@ -214,22 +208,38 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( Your tone should be kind and professional. No yapping.` const batch = await openAI.batchChatCompletion(summarizingPrompt, yamlData) - - const replaceShortTokensWithUrls = (text: string, lookup: IDLookup) => { - return text.replace(/https:\/\/action\.parabol\.co\/meet\/(m\d+)/g, (_, shortMeetingId) => { - const actualMeetingId = lookup.meeting[shortMeetingId] - return `https://action.parabol.co/meet/${actualMeetingId}` - }) + if (!batch) { + const error = new Error('Unable to generate insight.') + sendToSentry(error) + return null } - if (!batch) return null + const lines = batch.split('\n') - const insight = replaceShortTokensWithUrls(batch, idLookup) + const processedLines = lines.map((line) => { + const hasMeetingId = line.includes(meetingURL) + if (hasMeetingId) { + let shortMeetingId = line.split('https://action.parabol.co/meet/')[1] + if (shortMeetingId) { + shortMeetingId = shortMeetingId.split(/[),]/)[0] // Split by closing parenthesis or comma + } + console.log('šŸš€ ~ shortMeetingId:', shortMeetingId) + const actualMeetingId = shortMeetingId && idLookup.meeting[shortMeetingId] + if (shortMeetingId && actualMeetingId) { + return line.replace(shortMeetingId, actualMeetingId) + } else { + const error = new Error( + `AI hallucinated. Unable to find meetingId for ${shortMeetingId}. Line: ${line}` + ) + sendToSentry(error) + return '' + } + } + return line + }) - // const meetingIdRegex = /\/meet\/([m|t|to]\d+)/gm - // const fixedUrls = summaryEmail!.replace(meetingIdRegex, (_, meetingId) => { - // return `/meet/${idLookup.meeting[meetingId]}` - // }) + const insight = processedLines.filter((line) => line.trim() !== '').join('\n') + console.log('šŸš€ ~ insight:', insight) // RESOLUTION const data = {} diff --git a/topics_y3ZJgMy6hq.json b/topics_y3ZJgMy6hq.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/topics_y3ZJgMy6hq.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/topics_y3ZJgMy6hq_short.yml b/topics_y3ZJgMy6hq_short.yml new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/topics_y3ZJgMy6hq_short.yml @@ -0,0 +1 @@ +[] diff --git a/topics_y3ZJgMy6hr.json b/topics_y3ZJgMy6hr.json new file mode 100644 index 00000000000..2b00422f009 --- /dev/null +++ b/topics_y3ZJgMy6hr.json @@ -0,0 +1 @@ +[{"voteCount":0,"title":"Work team","reflections":[{"prompt":"Start","author":"Anonymous","text":"amazing work team"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Team","reflections":[{"prompt":"Start","author":"Anonymous","text":"love it team"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Communication struggles","reflections":[{"prompt":"Stop","author":"Anonymous","text":"communication struggles"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"we should communicate","reflections":[{"prompt":"Stop","author":"Anonymous","text":"we should communicate better"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Communication","comments":[{"text":"ds","author":"Nick O'Ferrall","replies":[]},{"text":"ds","author":"Nick O'Ferrall","replies":[]},{"text":"d","author":"Nick O'Ferrall","replies":[]}],"reflections":[{"prompt":"Stop","author":"Anonymous","text":"we must improve communication"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Stuff","reflections":[{"prompt":"Start","author":"Anonymous","text":"fantastic stuff"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"ds"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"ds"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"d"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"sd"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"ds"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"d"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"ds"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"ds"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"}] \ No newline at end of file diff --git a/topics_y3ZJgMy6hr_short.yml b/topics_y3ZJgMy6hr_short.yml new file mode 100644 index 00000000000..1133d9b12a1 --- /dev/null +++ b/topics_y3ZJgMy6hr_short.yml @@ -0,0 +1,330 @@ +- voteCount: 0 + title: Work team + reflections: + - prompt: Start + author: Anonymous + text: amazing work team + meetingName: Retro 6 + date: '2024-07-01' + meetingId: m1 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Team + reflections: + - prompt: Start + author: Anonymous + text: love it team + meetingName: Retro 6 + date: '2024-07-01' + meetingId: m2 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Communication struggles + reflections: + - prompt: Stop + author: Anonymous + text: communication struggles + meetingName: Retro 6 + date: '2024-07-01' + meetingId: m3 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: we should communicate + reflections: + - prompt: Stop + author: Anonymous + text: we should communicate better + meetingName: Retro 6 + date: '2024-07-01' + meetingId: m4 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Communication + comments: + - text: ds + author: Nick O'Ferrall + replies: [] + - text: ds + author: Nick O'Ferrall + replies: [] + - text: d + author: Nick O'Ferrall + replies: [] + reflections: + - prompt: Stop + author: Anonymous + text: we must improve communication + meetingName: Retro 6 + date: '2024-07-01' + meetingId: m5 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Stuff + reflections: + - prompt: Start + author: Anonymous + text: fantastic stuff + meetingName: Retro 6 + date: '2024-07-01' + meetingId: m6 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: What went well šŸ˜„ + author: Anonymous + text: ds + meetingName: Retro 1 + date: '2024-06-26' + meetingId: m7 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: What went well šŸ˜„ + author: Anonymous + text: ds + meetingName: Retro 1 + date: '2024-06-26' + meetingId: m8 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: What went well šŸ˜„ + author: Anonymous + text: d + meetingName: Retro 1 + date: '2024-06-26' + meetingId: m9 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Sd + reflections: + - prompt: What went well šŸ˜„ + author: Anonymous + text: sd + meetingName: Retro 1 + date: '2024-06-26' + meetingId: m10 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Wins šŸ† + author: Anonymous + text: ds + meetingName: Retro 4 + date: '2024-06-27' + meetingId: m11 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: Wins šŸ† + author: Anonymous + text: d + meetingName: Retro 4 + date: '2024-06-27' + meetingId: m12 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Wins šŸ† + author: Anonymous + text: ds + meetingName: Retro 4 + date: '2024-06-27' + meetingId: m13 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Wins šŸ† + author: Anonymous + text: ds + meetingName: Retro 4 + date: '2024-06-27' + meetingId: m14 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: Start + author: Anonymous + text: d + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m15 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Sd + reflections: + - prompt: Start + author: Anonymous + text: sd + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m16 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Sd + reflections: + - prompt: Start + author: Anonymous + text: sd + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m17 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: Start + author: Anonymous + text: d + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m18 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Sd + reflections: + - prompt: Start + author: Anonymous + text: sd + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m19 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: Start + author: Anonymous + text: d + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m20 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: s + reflections: + - prompt: Start + author: Anonymous + text: s + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m21 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Start + author: Anonymous + text: ds + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m22 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: s + reflections: + - prompt: Start + author: Anonymous + text: s + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m23 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Sd + reflections: + - prompt: Start + author: Anonymous + text: sd + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m24 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Sd + reflections: + - prompt: Start + author: Anonymous + text: sd + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m25 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: s + reflections: + - prompt: Start + author: Anonymous + text: s + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m26 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: s + reflections: + - prompt: Start + author: Anonymous + text: s + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m27 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Start + author: Anonymous + text: ds + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m28 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Start + author: Anonymous + text: ds + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m29 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: Ds + reflections: + - prompt: Start + author: Anonymous + text: ds + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m30 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: Start + author: Anonymous + text: d + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m31 + teamName: Nick O'Ferrallā€™s Team +- voteCount: 0 + title: D + reflections: + - prompt: Start + author: Anonymous + text: d + meetingName: Retro 3 + date: '2024-06-27' + meetingId: m32 + teamName: Nick O'Ferrallā€™s Team From c806eca4a092d1e7a9cee1d75294e36d97abcadb Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 2 Jul 2024 15:59:33 +0100 Subject: [PATCH 04/43] add orgId arg to generateInsight --- .../client/mutations/GenerateInsightMutation.ts | 13 +++++-------- .../graphql/public/typeDefs/generateInsight.graphql | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/client/mutations/GenerateInsightMutation.ts b/packages/client/mutations/GenerateInsightMutation.ts index 8420a720434..1a625a55e52 100644 --- a/packages/client/mutations/GenerateInsightMutation.ts +++ b/packages/client/mutations/GenerateInsightMutation.ts @@ -1,6 +1,6 @@ import graphql from 'babel-plugin-relay/macro' import {commitMutation} from 'react-relay' -// import {GenerateInsightMutation as TGenerateInsightMutation} from '../__generated__/GenerateInsightMutation.graphql' +import {GenerateInsightMutation as TGenerateInsightMutation} from '../__generated__/GenerateInsightMutation.graphql' import {StandardMutation} from '../types/relayMutations' graphql` @@ -10,8 +10,8 @@ graphql` ` const mutation = graphql` - mutation GenerateInsightMutation($teamId: ID!) { - generateInsight(teamId: $teamId) { + mutation GenerateInsightMutation($teamId: ID, $orgId: ID) { + generateInsight(teamId: $teamId, orgId: $orgId) { ... on ErrorPayload { error { message @@ -22,17 +22,14 @@ const mutation = graphql` } ` -const GenerateInsightMutation: StandardMutation = ( +const GenerateInsightMutation: StandardMutation = ( atmosphere, variables, {onError, onCompleted} ) => { - return commitMutation(atmosphere, { + return commitMutation(atmosphere, { mutation, variables, - optimisticUpdater: (store) => { - const {} = variables - }, onCompleted, onError }) diff --git a/packages/server/graphql/public/typeDefs/generateInsight.graphql b/packages/server/graphql/public/typeDefs/generateInsight.graphql index 8fb647f050a..e219dc89c4d 100644 --- a/packages/server/graphql/public/typeDefs/generateInsight.graphql +++ b/packages/server/graphql/public/typeDefs/generateInsight.graphql @@ -2,7 +2,7 @@ extend type Mutation { """ Describe the mutation here """ - generateInsight(teamId: ID!): GenerateInsightPayload! + generateInsight(teamId: ID, orgId: ID): GenerateInsightPayload! } """ From e262f9f29b8faf0b9a0793e6f0f68a572ec0da4f Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 3 Jul 2024 16:44:33 +0100 Subject: [PATCH 05/43] filter meetings more efficiently --- .../public/mutations/generateInsight.ts | 56 +++++++++++-------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 9547fd16ec9..5ad02e82053 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -5,12 +5,13 @@ 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 {MutationResolvers} from '../resolverTypes' const generateInsight: MutationResolvers['generateInsight'] = async ( _source, // {teamId, orgId, startDate, endDate}, - {teamId, orgId}, + {teamId, orgId}: {teamId?: string; orgId?: string}, {authToken, dataLoader, socketId: mutatorId} ) => { const getComments = async (reflectionGroupId: string) => { @@ -68,12 +69,19 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const getTopicJSON = async (teamIds: string[], startDate: Date, endDate: Date) => { const r = await getRethink() const MIN_REFLECTION_COUNT = 3 + const MIN_MILLISECONDS = 60 * 1000 // 1 minute const rawMeetings = await r .table('NewMeeting') .getAll(r.args(teamIds), {index: 'teamId'}) - .filter({meetingType: 'retrospective'}) - .filter((row: any) => row('createdAt').ge(startDate).and(row('createdAt').le(endDate))) - .filter((row: any) => row('reflectionCount').gt(MIN_REFLECTION_COUNT)) + .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( @@ -86,11 +94,10 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( createdAt: meetingDate } = meeting as MeetingRetrospective const [team, rawReflectionGroups] = await Promise.all([ - dataLoader.get('teams').loadNonNull(teamId), + orgId ? dataLoader.get('teams').loadNonNull(teamId) : null, dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId) ]) - // const {name: meetingTemplateName} = template - const {name: teamName} = team + const {name: teamName} = team ?? {} const reflectionGroups = Promise.all( rawReflectionGroups // for performance since it's really slow! @@ -106,10 +113,11 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const {promptId, creatorId, plaintextContent} = reflection const [prompt, creator] = await Promise.all([ dataLoader.get('reflectPrompts').load(promptId), - dataLoader.get('users').loadNonNull(creatorId) + creatorId ? dataLoader.get('users').loadNonNull(creatorId) : null ]) const {question} = prompt - const creatorName = disableAnonymity ? creator.preferredName : 'Anonymous' + const creatorName = + disableAnonymity && creator ? creator.preferredName : 'Anonymous' return { prompt: question, author: creatorName, @@ -118,7 +126,6 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( }) ) const res = { - // topicId: reflectionGroupId, voteCount: voterIds.length, title: title, comments, @@ -126,9 +133,9 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( meetingName, date: meetingDate, meetingId, - teamName - // teamId + ...(teamName ? {teamName} : {}) } + if (!res.comments || !res.comments.length) { delete (res as any).comments } @@ -141,8 +148,6 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( return meetings.flat() } - const openAI = new OpenAIServerManager() - const getTeamIds = async (orgId?: string, teamId?: string) => { if (teamId) return [teamId] const teams = await dataLoader.get('teamsByOrgIds').load(orgId!) @@ -157,6 +162,9 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const identifier = teamId ?? orgId const inTopics = await getTopicJSON(teamIds, startDate, endDate) + if (!inTopics.length) { + return standardError(new Error('Not enough data to generate insight.')) + } fs.writeFileSync(`./topics_${identifier}.json`, JSON.stringify(inTopics)) const rawTopics = JSON.parse(fs.readFileSync(`./topics_${identifier}.json`, 'utf-8')) as Awaited< @@ -165,20 +173,25 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const hotTopics = rawTopics // .filter((t) => t.voteCount > 2) // .sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1)) - type IDLookup = Record + + type IDLookup = Record const idLookup = { - meeting: {} as IDLookup + meeting: {} as IDLookup, + date: {} as IDLookup } const idGenerator = { meeting: 1 } + console.log('šŸš€ ~ hotTopics:', hotTopics) + const shortTokenedTopics = hotTopics.map((t) => { const {date, meetingId} = t const shortMeetingId = `m${idGenerator.meeting++}` const shortMeetingDate = new Date(date).toISOString().split('T')[0] console.log('šŸš€ ~ shortMeetingId:', shortMeetingId) idLookup.meeting[shortMeetingId] = meetingId + idLookup.date[shortMeetingId] = date return { ...t, date: shortMeetingDate, @@ -207,11 +220,10 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( The format of the subject line should be the following: Subject: [Team Name] [Short description of the negative behavior] Your tone should be kind and professional. No yapping.` + const openAI = new OpenAIServerManager() const batch = await openAI.batchChatCompletion(summarizingPrompt, yamlData) if (!batch) { - const error = new Error('Unable to generate insight.') - sendToSentry(error) - return null + return standardError(new Error('Unable to generate insight.')) } const lines = batch.split('\n') @@ -219,12 +231,12 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const processedLines = lines.map((line) => { const hasMeetingId = line.includes(meetingURL) if (hasMeetingId) { - let shortMeetingId = line.split('https://action.parabol.co/meet/')[1] + let shortMeetingId = line.split(meetingURL)[1] if (shortMeetingId) { shortMeetingId = shortMeetingId.split(/[),]/)[0] // Split by closing parenthesis or comma } - console.log('šŸš€ ~ shortMeetingId:', shortMeetingId) - const actualMeetingId = shortMeetingId && idLookup.meeting[shortMeetingId] + const actualMeetingId = shortMeetingId && (idLookup.meeting[shortMeetingId] as string) + if (shortMeetingId && actualMeetingId) { return line.replace(shortMeetingId, actualMeetingId) } else { From 3e4aff47f682999079f8ad739b2f9193736f0e58 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 3 Jul 2024 17:45:34 +0100 Subject: [PATCH 06/43] return wins and challenges from generateInsight --- .../mutations/GenerateInsightMutation.ts | 7 +- .../public/mutations/generateInsight.ts | 154 ++++++++++-------- .../public/typeDefs/generateInsight.graphql | 13 +- .../public/types/GenerateInsightSuccess.ts | 13 +- packages/server/utils/OpenAIServerManager.ts | 28 +++- 5 files changed, 118 insertions(+), 97 deletions(-) diff --git a/packages/client/mutations/GenerateInsightMutation.ts b/packages/client/mutations/GenerateInsightMutation.ts index 1a625a55e52..54e3f206c76 100644 --- a/packages/client/mutations/GenerateInsightMutation.ts +++ b/packages/client/mutations/GenerateInsightMutation.ts @@ -5,13 +5,14 @@ import {StandardMutation} from '../types/relayMutations' graphql` fragment GenerateInsightMutation_team on GenerateInsightSuccess { - successField + wins + challenges } ` const mutation = graphql` - mutation GenerateInsightMutation($teamId: ID, $orgId: ID) { - generateInsight(teamId: $teamId, orgId: $orgId) { + mutation GenerateInsightMutation($teamId: ID!) { + generateInsight(teamId: $teamId) { ... on ErrorPayload { error { message diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 5ad02e82053..bcfe76bbbf0 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -10,10 +10,10 @@ import {MutationResolvers} from '../resolverTypes' const generateInsight: MutationResolvers['generateInsight'] = async ( _source, - // {teamId, orgId, startDate, endDate}, - {teamId, orgId}: {teamId?: string; orgId?: string}, + {teamId}, {authToken, dataLoader, socketId: mutatorId} ) => { + console.log('šŸš€ ~ teamId:', teamId) const getComments = async (reflectionGroupId: string) => { const IGNORE_COMMENT_USER_IDS = ['parabolAIUser'] const pg = getKysely() @@ -66,38 +66,37 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( return comments } - const getTopicJSON = async (teamIds: string[], startDate: Date, endDate: Date) => { + const getTopicJSON = async (teamId: string, startDate: Date, endDate: Date) => { const r = await getRethink() const MIN_REFLECTION_COUNT = 3 const MIN_MILLISECONDS = 60 * 1000 // 1 minute const rawMeetings = await r .table('NewMeeting') - .getAll(r.args(teamIds), {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)) + .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() + console.log('šŸš€ ~ rawMeetings:', rawMeetings) const meetings = await Promise.all( rawMeetings.map(async (meeting) => { const { id: meetingId, disableAnonymity, - teamId, name: meetingName, createdAt: meetingDate } = meeting as MeetingRetrospective - const [team, rawReflectionGroups] = await Promise.all([ - orgId ? dataLoader.get('teams').loadNonNull(teamId) : null, - dataLoader.get('retroReflectionGroupsByMeetingId').load(meetingId) - ]) - const {name: teamName} = team ?? {} + const rawReflectionGroups = await dataLoader + .get('retroReflectionGroupsByMeetingId') + .load(meetingId) const reflectionGroups = Promise.all( rawReflectionGroups // for performance since it's really slow! @@ -132,8 +131,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( reflections, meetingName, date: meetingDate, - meetingId, - ...(teamName ? {teamName} : {}) + meetingId } if (!res.comments || !res.comments.length) { @@ -148,26 +146,17 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( return meetings.flat() } - const getTeamIds = async (orgId?: string, teamId?: string) => { - if (teamId) return [teamId] - const teams = await dataLoader.get('teamsByOrgIds').load(orgId!) - return teams.map((team) => team.id) - } - - const teamIds = await getTeamIds(orgId, teamId) - const startDate = new Date('2024-01-01') // const endDate = new Date('2024-04-01') const endDate = new Date() - const identifier = teamId ?? orgId - const inTopics = await getTopicJSON(teamIds, startDate, endDate) + const inTopics = await getTopicJSON(teamId, startDate, endDate) if (!inTopics.length) { return standardError(new Error('Not enough data to generate insight.')) } - fs.writeFileSync(`./topics_${identifier}.json`, JSON.stringify(inTopics)) + fs.writeFileSync(`./topics_${teamId}.json`, JSON.stringify(inTopics)) - const rawTopics = JSON.parse(fs.readFileSync(`./topics_${identifier}.json`, 'utf-8')) as Awaited< + const rawTopics = JSON.parse(fs.readFileSync(`./topics_${teamId}.json`, 'utf-8')) as Awaited< ReturnType > const hotTopics = rawTopics @@ -183,13 +172,11 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const idGenerator = { meeting: 1 } - console.log('šŸš€ ~ hotTopics:', hotTopics) const shortTokenedTopics = hotTopics.map((t) => { const {date, meetingId} = t const shortMeetingId = `m${idGenerator.meeting++}` const shortMeetingDate = new Date(date).toISOString().split('T')[0] - console.log('šŸš€ ~ shortMeetingId:', shortMeetingId) idLookup.meeting[shortMeetingId] = meetingId idLookup.date[shortMeetingId] = date return { @@ -202,59 +189,82 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const yamlData = yaml.dump(shortTokenedTopics, { noCompatMode: true // This option ensures compatibility mode is off }) - fs.writeFileSync(`./topics_${identifier}_short.yml`, yamlData) + fs.writeFileSync(`./topics_${teamId}_short.yml`, yamlData) const meetingURL = 'https://action.parabol.co/meet/' const summarizingPrompt = ` - You are a management consultant who needs to discover behavioral trends for a given team. - Below is a list of reflection topics in YAML format from meetings over the last 3 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, inlcude the meetingId in the format of ${meetingURL}[meetingId]. - For each topic, mention how many votes it has. - Be sure that each author is only mentioned once. - Above the two sections, include a short subject line that mentions the team name and summarizes the negative behavior mentioned in the second paragraph. - The subject must not be generic sounding. The person who reads the subject should instantly know that the person who wrote it has deep understanding of the team's problems. - The format of the subject line should be the following: Subject: [Team Name] [Short description of the negative behavior] - Your tone should be kind and professional. No yapping.` +You are a management consultant who needs to discover behavioral trends for a given team. +Below is a list of reflection topics in YAML format from meetings over the last 3 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. 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 ${meetingURL}[meetingId]. +For each topic, mention how many votes it has. +Be sure that each author is only mentioned once. +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", "bullet point 3"] +} +Your tone should be kind and professional. No yapping. +` const openAI = new OpenAIServerManager() const batch = await openAI.batchChatCompletion(summarizingPrompt, yamlData) + console.log('šŸš€ ~ batch:', batch) if (!batch) { return standardError(new Error('Unable to generate insight.')) } - const lines = batch.split('\n') + const processLines = (lines: string[], meetingURL: string): string => { + return lines + .map((line) => { + if (line.includes(meetingURL)) { + let processedLine = line + const regex = new RegExp(`${meetingURL}\\S+`, 'g') + const matches = processedLine.match(regex) || [] - const processedLines = lines.map((line) => { - const hasMeetingId = line.includes(meetingURL) - if (hasMeetingId) { - let shortMeetingId = line.split(meetingURL)[1] - if (shortMeetingId) { - shortMeetingId = shortMeetingId.split(/[),]/)[0] // Split by closing parenthesis or comma - } - const actualMeetingId = shortMeetingId && (idLookup.meeting[shortMeetingId] as string) + let isValid = true + matches.forEach((match) => { + let shortMeetingId = match.split(meetingURL)[1].split(/[),\s]/)[0] // Split by closing parenthesis, comma, or space + const actualMeetingId = shortMeetingId && (idLookup.meeting[shortMeetingId] as string) - if (shortMeetingId && actualMeetingId) { - return line.replace(shortMeetingId, actualMeetingId) - } else { - const error = new Error( - `AI hallucinated. Unable to find meetingId for ${shortMeetingId}. Line: ${line}` - ) - sendToSentry(error) - return '' - } - } - return line - }) + 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 empty string if invalid + } + return line + }) + .filter((line) => line.trim() !== '') + .join('\n') + } + + const processSection = (section: string[]): string => { + return section + .map((item) => { + const lines = item.split('\n') + return processLines(lines, meetingURL) + }) + .filter((processedItem) => processedItem.trim() !== '') + .join('\n') + } + + const wins = processSection(batch.wins) + const challenges = processSection(batch.challenges) - const insight = processedLines.filter((line) => line.trim() !== '').join('\n') - console.log('šŸš€ ~ insight:', insight) + console.log('šŸš€ ~ Wins:', wins) + console.log('šŸš€ ~ Challenges:', challenges) - // RESOLUTION - const data = {} + const data = {wins, challenges} return data } diff --git a/packages/server/graphql/public/typeDefs/generateInsight.graphql b/packages/server/graphql/public/typeDefs/generateInsight.graphql index e219dc89c4d..f89549d8f85 100644 --- a/packages/server/graphql/public/typeDefs/generateInsight.graphql +++ b/packages/server/graphql/public/typeDefs/generateInsight.graphql @@ -1,8 +1,8 @@ extend type Mutation { """ - Describe the mutation here + Generate an insight for a team """ - generateInsight(teamId: ID, orgId: ID): GenerateInsightPayload! + generateInsight(teamId: ID!): GenerateInsightPayload! } """ @@ -12,7 +12,12 @@ union GenerateInsightPayload = ErrorPayload | GenerateInsightSuccess type GenerateInsightSuccess { """ - Describe the first return field here + The insights generated focusing on the wins of the team """ - successField: ID! + wins: String! + + """ + The insights generated focusing on the challenges team are facing + """ + challenges: String! } diff --git a/packages/server/graphql/public/types/GenerateInsightSuccess.ts b/packages/server/graphql/public/types/GenerateInsightSuccess.ts index f54b83f3b3f..4fcf8a83230 100644 --- a/packages/server/graphql/public/types/GenerateInsightSuccess.ts +++ b/packages/server/graphql/public/types/GenerateInsightSuccess.ts @@ -1,13 +1,4 @@ -// import {GenerateInsightSuccessResolvers} from '../resolverTypes' - export type GenerateInsightSuccessSource = { - id: string + wins: string + challenges: string } - -const GenerateInsightSuccess = { - successField: async ({id}, _args, {dataLoader}) => { - return null - } -} - -export default GenerateInsightSuccess diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 72d89781545..3bb0a9315fb 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 @@ -341,12 +346,12 @@ class OpenAIServerManager { } // TODO: actually batch the completions - async batchChatCompletion(prompt: string, yamlData: string) { + async batchChatCompletion(prompt: string, yamlData: string): Promise { if (!this.openAIApi) return null try { const response = await this.openAIApi.chat.completions.create({ - model: 'gpt-4o', + model: 'gpt-4', messages: [ { role: 'user', @@ -359,12 +364,21 @@ class OpenAIServerManager { presence_penalty: 0 }) - const completionText = (response.choices[0]?.message?.content?.trim() as string) ?? null - return completionText + 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 batchChatCompletion') + sendToSentry(error) + return null + } + + return data } catch (e) { - const error = - e instanceof Error ? e : new Error('OpenAI failed to generate the batch completion') - Logger.error(error.message) + const error = e instanceof Error ? e : new Error('Error in batchChatCompletion') sendToSentry(error) return null } From 14a34dc5e939ac38ef9b86eb481d0b1bbb61fc86 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 17 Jul 2024 15:11:49 +0100 Subject: [PATCH 07/43] generate insight --- .../public/mutations/generateInsight.ts | 48 ++++++------------- packages/server/utils/OpenAIServerManager.ts | 20 +++++++- 2 files changed, 32 insertions(+), 36 deletions(-) diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index bcfe76bbbf0..99d750125f8 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -73,15 +73,14 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( 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)) + .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() console.log('šŸš€ ~ rawMeetings:', rawMeetings) @@ -100,7 +99,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const reflectionGroups = Promise.all( rawReflectionGroups // for performance since it's really slow! - // .filter((g) => g.voterIds.length > 0) + .filter((g) => g.voterIds.length > 0) .map(async (group) => { const {id: reflectionGroupId, voterIds, title} = group const [comments, rawReflections] = await Promise.all([ @@ -147,8 +146,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( } const startDate = new Date('2024-01-01') - // const endDate = new Date('2024-04-01') - const endDate = new Date() + const endDate = new Date('2024-02-01') const inTopics = await getTopicJSON(teamId, startDate, endDate) if (!inTopics.length) { @@ -160,8 +158,8 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( ReturnType > const hotTopics = rawTopics - // .filter((t) => t.voteCount > 2) - // .sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1)) + .filter((t) => t.voteCount > 2) + .sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1)) type IDLookup = Record const idLookup = { @@ -193,26 +191,8 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const meetingURL = 'https://action.parabol.co/meet/' - const summarizingPrompt = ` -You are a management consultant who needs to discover behavioral trends for a given team. -Below is a list of reflection topics in YAML format from meetings over the last 3 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. 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 ${meetingURL}[meetingId]. -For each topic, mention how many votes it has. -Be sure that each author is only mentioned once. -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", "bullet point 3"] -} -Your tone should be kind and professional. No yapping. -` - const openAI = new OpenAIServerManager() - const batch = await openAI.batchChatCompletion(summarizingPrompt, yamlData) - console.log('šŸš€ ~ batch:', batch) + const batch = await openAI.generateInsight(yamlData) if (!batch) { return standardError(new Error('Unable to generate insight.')) } @@ -227,7 +207,7 @@ Your tone should be kind and professional. No yapping. let isValid = true matches.forEach((match) => { - let shortMeetingId = match.split(meetingURL)[1].split(/[),\s]/)[0] // Split by closing parenthesis, comma, or space + let shortMeetingId = match.split(meetingURL)[1]?.split(/[),\s]/)[0] // Split by closing parenthesis, comma, or space const actualMeetingId = shortMeetingId && (idLookup.meeting[shortMeetingId] as string) if (shortMeetingId && actualMeetingId) { diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 3bb0a9315fb..a6494ccdb9d 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -345,9 +345,25 @@ class OpenAIServerManager { } } - // TODO: actually batch the completions - async batchChatCompletion(prompt: string, yamlData: string): Promise { + async generateInsight(yamlData: string): Promise { if (!this.openAIApi) return null + const meetingURL = 'https://action.parabol.co/meet/' + const prompt = ` + You are a management consultant who needs to discover behavioral trends for a given team. + Below is a list of reflection topics in YAML format from meetings over the last 3 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. 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 ${meetingURL}[meetingId]. + For each topic, mention how many votes it has. + Be sure that each author is only mentioned once. + 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", "bullet point 3"] + } + Your tone should be kind and professional. No yapping. + ` try { const response = await this.openAIApi.chat.completions.create({ From 779268119c7b1e821d9b0f53019746f19e047128 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 17 Jul 2024 18:33:01 +0100 Subject: [PATCH 08/43] implement addInsight migration --- .../public/mutations/generateInsight.ts | 54 ++++++++++--------- .../public/typeDefs/generateInsight.graphql | 2 +- .../migrations/1721225543186_addInsight.ts | 32 +++++++++++ 3 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 packages/server/postgres/migrations/1721225543186_addInsight.ts diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 99d750125f8..f3f7565f26a 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -1,5 +1,4 @@ import yaml from 'js-yaml' -import fs from 'node:fs' import getRethink from '../../../database/rethinkDriver' import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import getKysely from '../../../postgres/getKysely' @@ -10,13 +9,24 @@ import {MutationResolvers} from '../resolverTypes' const generateInsight: MutationResolvers['generateInsight'] = async ( _source, - {teamId}, - {authToken, dataLoader, socketId: mutatorId} + {teamId, startDate, endDate}, + {dataLoader} ) => { - console.log('šŸš€ ~ teamId:', teamId) + const start = new Date(startDate) + const end = new Date(endDate) + if (isNaN(start.getTime()) || isNaN(end.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 (end.getTime() - start.getTime() < oneWeekInMs) { + return standardError(new Error('The end date must be at least one week after the start date.')) + } + + const pg = getKysely() const getComments = async (reflectionGroupId: string) => { const IGNORE_COMMENT_USER_IDS = ['parabolAIUser'] - const pg = getKysely() const discussion = await pg .selectFrom('Discussion') .selectAll() @@ -83,7 +93,6 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) ) .run() - console.log('šŸš€ ~ rawMeetings:', rawMeetings) const meetings = await Promise.all( rawMeetings.map(async (meeting) => { @@ -98,7 +107,6 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( .load(meetingId) const reflectionGroups = Promise.all( rawReflectionGroups - // for performance since it's really slow! .filter((g) => g.voterIds.length > 0) .map(async (group) => { const {id: reflectionGroupId, voterIds, title} = group @@ -145,19 +153,11 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( return meetings.flat() } - const startDate = new Date('2024-01-01') - const endDate = new Date('2024-02-01') - const inTopics = await getTopicJSON(teamId, startDate, endDate) if (!inTopics.length) { return standardError(new Error('Not enough data to generate insight.')) } - fs.writeFileSync(`./topics_${teamId}.json`, JSON.stringify(inTopics)) - - const rawTopics = JSON.parse(fs.readFileSync(`./topics_${teamId}.json`, 'utf-8')) as Awaited< - ReturnType - > - const hotTopics = rawTopics + const hotTopics = inTopics .filter((t) => t.voteCount > 2) .sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1)) @@ -183,13 +183,9 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( meetingId: shortMeetingId } }) - // fs.writeFileSync('./topics_target_short.json', JSON.stringify(shortTokenedTopics)) const yamlData = yaml.dump(shortTokenedTopics, { noCompatMode: true // This option ensures compatibility mode is off }) - fs.writeFileSync(`./topics_${teamId}_short.yml`, yamlData) - - const meetingURL = 'https://action.parabol.co/meet/' const openAI = new OpenAIServerManager() const batch = await openAI.generateInsight(yamlData) @@ -197,7 +193,9 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( return standardError(new Error('Unable to generate insight.')) } - const processLines = (lines: string[], meetingURL: string): string => { + const meetingURL = 'https://action.parabol.co/meet/' + + const processLines = (lines: string[]): string => { return lines .map((line) => { if (line.includes(meetingURL)) { @@ -232,7 +230,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( return section .map((item) => { const lines = item.split('\n') - return processLines(lines, meetingURL) + return processLines(lines) }) .filter((processedItem) => processedItem.trim() !== '') .join('\n') @@ -241,8 +239,16 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const wins = processSection(batch.wins) const challenges = processSection(batch.challenges) - console.log('šŸš€ ~ Wins:', wins) - console.log('šŸš€ ~ Challenges:', challenges) + await pg + .insertInto('Insight') + .values({ + teamId, + wins, + challenges, + startDate, + endDate + }) + .execute() const data = {wins, challenges} return data diff --git a/packages/server/graphql/public/typeDefs/generateInsight.graphql b/packages/server/graphql/public/typeDefs/generateInsight.graphql index f89549d8f85..3d2fc6e8579 100644 --- a/packages/server/graphql/public/typeDefs/generateInsight.graphql +++ b/packages/server/graphql/public/typeDefs/generateInsight.graphql @@ -2,7 +2,7 @@ extend type Mutation { """ Generate an insight for a team """ - generateInsight(teamId: ID!): GenerateInsightPayload! + generateInsight(teamId: ID!, startDate: DateTime!, endDate: DateTime!): GenerateInsightPayload! } """ diff --git a/packages/server/postgres/migrations/1721225543186_addInsight.ts b/packages/server/postgres/migrations/1721225543186_addInsight.ts new file mode 100644 index 00000000000..0026d0a3ff5 --- /dev/null +++ b/packages/server/postgres/migrations/1721225543186_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" JSONB, + "challenges" JSONB, + "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() +} From 7c02ca49491234c10fefeafdef9c9fd696bed0a6 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 17 Jul 2024 19:06:42 +0100 Subject: [PATCH 09/43] check for existingInsight --- .../mutations/GenerateInsightMutation.ts | 4 ++-- .../public/mutations/generateInsight.ts | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/client/mutations/GenerateInsightMutation.ts b/packages/client/mutations/GenerateInsightMutation.ts index 54e3f206c76..432eb838dee 100644 --- a/packages/client/mutations/GenerateInsightMutation.ts +++ b/packages/client/mutations/GenerateInsightMutation.ts @@ -11,8 +11,8 @@ graphql` ` const mutation = graphql` - mutation GenerateInsightMutation($teamId: ID!) { - generateInsight(teamId: $teamId) { + mutation GenerateInsightMutation($teamId: ID!, $startDate: DateTime!, $endDate: DateTime!) { + generateInsight(teamId: $teamId, startDate: $startDate, endDate: $endDate) { ... on ErrorPayload { error { message diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index f3f7565f26a..22ad24f4d0d 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -23,8 +23,24 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( if (end.getTime() - start.getTime() < oneWeekInMs) { return standardError(new Error('The end date must be at least one week after the start date.')) } - const pg = getKysely() + + const existingInsight = await pg + .selectFrom('Insight') + .selectAll() + .where('teamId', '=', teamId) + .where('startDateTime', '=', start) + .where('endDateTime', '=', end) + .limit(1) + .executeTakeFirst() + + if (existingInsight) { + return { + wins: existingInsight.wins, + challenges: existingInsight.challenges + } + } + const getComments = async (reflectionGroupId: string) => { const IGNORE_COMMENT_USER_IDS = ['parabolAIUser'] const discussion = await pg From 91e56b00f58e7674344390d2647c375742a43603 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Thu, 18 Jul 2024 16:41:42 +0100 Subject: [PATCH 10/43] start summary of summaries --- .../mutations/GenerateInsightMutation.ts | 14 +- .../helpers/generateWholeMeetingSummary.ts | 27 +- .../public/mutations/generateInsight.ts | 240 ++++-------------- .../public/mutations/helpers/getSummaries.ts | 217 ++++++++++++++++ .../public/mutations/helpers/getTopics.ts | 166 ++++++++++++ .../public/typeDefs/generateInsight.graphql | 7 +- packages/server/utils/OpenAIServerManager.ts | 53 +++- 7 files changed, 511 insertions(+), 213 deletions(-) create mode 100644 packages/server/graphql/public/mutations/helpers/getSummaries.ts create mode 100644 packages/server/graphql/public/mutations/helpers/getTopics.ts diff --git a/packages/client/mutations/GenerateInsightMutation.ts b/packages/client/mutations/GenerateInsightMutation.ts index 432eb838dee..885d54b82c9 100644 --- a/packages/client/mutations/GenerateInsightMutation.ts +++ b/packages/client/mutations/GenerateInsightMutation.ts @@ -11,8 +11,18 @@ graphql` ` const mutation = graphql` - mutation GenerateInsightMutation($teamId: ID!, $startDate: DateTime!, $endDate: DateTime!) { - generateInsight(teamId: $teamId, startDate: $startDate, endDate: $endDate) { + mutation GenerateInsightMutation( + $teamId: ID! + $startDate: DateTime! + $endDate: DateTime! + $useSummaries: Boolean + ) { + generateInsight( + teamId: $teamId + startDate: $startDate + endDate: $endDate + useSummaries: $useSummaries + ) { ... on ErrorPayload { error { message diff --git a/packages/server/graphql/mutations/helpers/generateWholeMeetingSummary.ts b/packages/server/graphql/mutations/helpers/generateWholeMeetingSummary.ts index 037cc4135de..180feb3a29c 100644 --- a/packages/server/graphql/mutations/helpers/generateWholeMeetingSummary.ts +++ b/packages/server/graphql/mutations/helpers/generateWholeMeetingSummary.ts @@ -2,7 +2,6 @@ import {PARABOL_AI_USER_ID} from 'parabol-client/utils/constants' import OpenAIServerManager from '../../../utils/OpenAIServerManager' import {DataLoaderWorker} from '../../graphql' import isValid from '../../isValid' -import canAccessAISummary from './canAccessAISummary' const generateWholeMeetingSummary = async ( discussionIds: string[], @@ -11,22 +10,26 @@ const generateWholeMeetingSummary = async ( facilitatorUserId: string, dataLoader: DataLoaderWorker ) => { - const [facilitator, team] = await Promise.all([ - dataLoader.get('users').loadNonNull(facilitatorUserId), - dataLoader.get('teams').loadNonNull(teamId) - ]) - const isAISummaryAccessible = await canAccessAISummary( - team, - facilitator.featureFlags, - dataLoader, - 'retrospective' - ) - if (!isAISummaryAccessible) return + console.log('šŸš€ ~ discussionIds:', {dataLoader}) + // const [facilitator, team] = await Promise.all([ + // dataLoader.get('users').loadNonNull(facilitatorUserId), + // dataLoader.get('teams').loadNonNull(teamId) + // ]) + // console.log('heee', {facilitator, team}) + // const isAISummaryAccessible = await canAccessAISummary( + // team, + // facilitator.featureFlags, + // dataLoader, + // 'retrospective' + // ) + // console.log('šŸš€ ~ isAISummaryAccessible:', isAISummaryAccessible) + // if (!isAISummaryAccessible) return const [commentsByDiscussions, tasksByDiscussions, reflections] = await Promise.all([ dataLoader.get('commentsByDiscussionId').loadMany(discussionIds), dataLoader.get('tasksByDiscussionId').loadMany(discussionIds), dataLoader.get('retroReflectionsByMeetingId').load(meetingId) ]) + console.log('šŸš€ ~ commentsByDiscussions:', commentsByDiscussions) const manager = new OpenAIServerManager() const reflectionsContent = reflections.map((reflection) => reflection.plaintextContent) const commentsContent = commentsByDiscussions diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 22ad24f4d0d..844cdb7254f 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -1,15 +1,15 @@ 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 {MutationResolvers} from '../resolverTypes' +import {getSummaries} from './helpers/getSummaries' +import {getTopics} from './helpers/getTopics' const generateInsight: MutationResolvers['generateInsight'] = async ( _source, - {teamId, startDate, endDate}, + {teamId, startDate, endDate, useSummaries = true}, {dataLoader} ) => { const start = new Date(startDate) @@ -25,187 +25,37 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( } const pg = getKysely() - const existingInsight = await pg - .selectFrom('Insight') - .selectAll() - .where('teamId', '=', teamId) - .where('startDateTime', '=', start) - .where('endDateTime', '=', end) - .limit(1) - .executeTakeFirst() - - if (existingInsight) { - return { - wins: existingInsight.wins, - challenges: existingInsight.challenges - } - } - - const getComments = async (reflectionGroupId: string) => { - 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 - } - }) - ) - const res = { - text: plaintextContent, - author: commentAuthor, - replies: commentReplies - } - if (res.replies.length === 0) { - delete (res as any).commentReplies - } - return res - }) - ) - return comments + // const existingInsight = await pg + // .selectFrom('Insight') + // .selectAll() + // .where('teamId', '=', teamId) + // .where('startDateTime', '=', start) + // .where('endDateTime', '=', end) + // .limit(1) + // .executeTakeFirst() + + // if (existingInsight) { + // return { + // wins: existingInsight.wins, + // challenges: existingInsight.challenges + // } + // } + + const meetingsContent = useSummaries + ? await getSummaries(teamId, startDate, endDate, dataLoader) + : await getTopics(teamId, startDate, endDate, dataLoader) + + if (meetingsContent.length === 0) { + return standardError(new Error('No meeting content found for the specified date range.')) } - const getTopicJSON = async (teamId: string, startDate: Date, endDate: Date) => { - 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.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 - }) - ) - return meetings.flat() - } - - const inTopics = await getTopicJSON(teamId, startDate, endDate) - if (!inTopics.length) { - return standardError(new Error('Not enough data to generate insight.')) - } - const hotTopics = inTopics - .filter((t) => t.voteCount > 2) - .sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1)) - - type IDLookup = Record - const idLookup = { - meeting: {} as IDLookup, - date: {} as IDLookup - } - - 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] - idLookup.meeting[shortMeetingId] = meetingId - idLookup.date[shortMeetingId] = date - return { - ...t, - date: shortMeetingDate, - meetingId: shortMeetingId - } - }) - const yamlData = yaml.dump(shortTokenedTopics, { + const yamlData = yaml.dump(meetingsContent, { noCompatMode: true // This option ensures compatibility mode is off }) const openAI = new OpenAIServerManager() - const batch = await openAI.generateInsight(yamlData) - if (!batch) { + const rawInsight = await openAI.generateInsight(yamlData) + if (!rawInsight) { return standardError(new Error('Unable to generate insight.')) } @@ -252,21 +102,25 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( .join('\n') } - const wins = processSection(batch.wins) - const challenges = processSection(batch.challenges) - - await pg - .insertInto('Insight') - .values({ - teamId, - wins, - challenges, - startDate, - endDate - }) - .execute() - - const data = {wins, challenges} + console.log('šŸš€ ~ batch.wins:', rawInsight.wins) + // const wins = processSection(rawInsight.wins) + // console.log('šŸš€ ~ wins:', wins) + + // const challenges = processSection(rawInsight.challenges) + console.log('šŸš€ ~ rawInsight.challenges:', rawInsight.challenges) + // console.log('šŸš€ ~ challenges:', challenges) + // await pg + // .insertInto('Insight') + // .values({ + // teamId, + // wins, + // challenges, + // startDate, + // endDate + // }) + // .execute() + + const data = {wins: rawInsight.wins[0], challenges: rawInsight.challenges[0]} return data } 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..63554461dcb --- /dev/null +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -0,0 +1,217 @@ +import fs from 'fs' +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 getPhase from '../../../../utils/getPhase' +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 + } + }) + ) + const res = { + text: plaintextContent, + author: commentAuthor, + replies: commentReplies + } + if (res.replies.length === 0) { + delete (res as any).commentReplies + } + return res + }) + ) + return comments +} + +const generateMeetingSummary = async ( + meeting: MeetingRetrospective, + dataLoader: DataLoaderWorker +) => { + const {id: meetingId, disableAnonymity, name: meetingName, createdAt: meetingDate} = meeting + 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 + }) + ) + console.log('šŸš€ ~ reflectionGroups:', reflectionGroups) + + return reflectionGroups +} + +export const getSummaries = async ( + teamId: string, + startDate: Date, + endDate: Date, + dataLoader: DataLoaderWorker +) => { + 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)) + ) + .run()) as MeetingRetrospective[] + // console.log('šŸš€ ~ rawMeetings:', rawMeetings) + + const summaries = await Promise.all( + rawMeetings.map(async (meeting) => { + console.log('šŸš€ ~ meeting.summary____:', meeting.summary) + // if (!meeting.summary) { + const discussPhase = getPhase(meeting.phases, 'discuss') + const {stages} = discussPhase + const discussionIds = stages.map((stage) => stage.discussionId) + // const newSummary = await generateWholeMeetingSummary( + // discussionIds, + // meeting.id, + // teamId, + // meeting.facilitatorUserId, + // dataLoader + // ) + const res = await generateMeetingSummary(meeting, dataLoader) + const yamlData = yaml.dump(res, { + noCompatMode: true + }) + fs.writeFileSync('meetingSummary.yaml', yamlData, 'utf8') + + const manager = new OpenAIServerManager() + const mySummary = await manager.generateSummary(yamlData) + console.log('šŸš€ ~ mySummary:', mySummary) + + console.log('šŸš€ ~ res_____:', res) + const newSummary = null + console.log('šŸš€ ~ newSummary___:', newSummary) + + if (newSummary) { + // Update the meeting with the new summary + await r.table('NewMeeting').get(meeting.id).update({summary: newSummary}).run() + meeting.summary = newSummary + } + // } + return { + meetingId: meeting.id, + date: meeting.createdAt, + summary: meeting.summary + } + }) + ) + + return summaries +} + +// import getRethink from '../../../../database/rethinkDriver' + +// export const getSummaries = async (teamId: string, startDate: Date, endDate: Date) => { +// 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(row('summary').eq(null)) +// .and(r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'}).count().gt(1)) +// .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) +// ) +// .run() + +// console.log('šŸš€ ~ rawMeetings:', rawMeetings) +// const summaries = rawMeetings.map((meeting) => ({ +// meetingId: meeting.id, +// date: meeting.createdAt, +// summary: meeting.summary +// })) +// console.log('šŸš€ ~ summaries____:', summaries) + +// return summaries +// } 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..e4cf918bdc1 --- /dev/null +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -0,0 +1,166 @@ +import getRethink from '../../../../database/rethinkDriver' +import MeetingRetrospective from '../../../../database/types/MeetingRetrospective' +import getKysely from '../../../../postgres/getKysely' +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 + } + }) + ) + const res = { + text: plaintextContent, + author: commentAuthor, + replies: commentReplies + } + if (res.replies.length === 0) { + delete (res as any).commentReplies + } + return res + }) + ) + return comments +} + +export const getTopics = async ( + teamId: string, + startDate: Date, + endDate: Date, + dataLoader: DataLoaderWorker +) => { + 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)) + + type IDLookup = Record + const idLookup = { + meeting: {} as IDLookup, + date: {} as IDLookup + } + + 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] + idLookup.meeting[shortMeetingId] = meetingId + idLookup.date[shortMeetingId] = date + return { + ...t, + date: shortMeetingDate, + meetingId: shortMeetingId + } + }) + return shortTokenedTopics +} diff --git a/packages/server/graphql/public/typeDefs/generateInsight.graphql b/packages/server/graphql/public/typeDefs/generateInsight.graphql index 3d2fc6e8579..fed44360c7d 100644 --- a/packages/server/graphql/public/typeDefs/generateInsight.graphql +++ b/packages/server/graphql/public/typeDefs/generateInsight.graphql @@ -2,7 +2,12 @@ extend type Mutation { """ Generate an insight for a team """ - generateInsight(teamId: ID!, startDate: DateTime!, endDate: DateTime!): GenerateInsightPayload! + generateInsight( + teamId: ID! + startDate: DateTime! + endDate: DateTime! + useSummaries: Boolean + ): GenerateInsightPayload! } """ diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index a6494ccdb9d..10722eba58c 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -350,7 +350,7 @@ class OpenAIServerManager { const meetingURL = 'https://action.parabol.co/meet/' const prompt = ` You are a management consultant who needs to discover behavioral trends for a given team. - Below is a list of reflection topics in YAML format from meetings over the last 3 months. + Below is a list of reflection topics in YAML format from meetings over the past 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. 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. @@ -367,13 +367,16 @@ class OpenAIServerManager { try { const response = await this.openAIApi.chat.completions.create({ - model: 'gpt-4', + 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, @@ -386,15 +389,55 @@ class OpenAIServerManager { try { data = JSON.parse(completionContent) } catch (e) { - const error = - e instanceof Error ? e : new Error('Error parsing JSON in batchChatCompletion') + 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 batchChatCompletion') + const error = e instanceof Error ? e : new Error('Error in generateInsight') + sendToSentry(error) + return null + } + } + + 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 few sentences. + Below is a list of reflection topics and comments in YAML format from the meeting. + When citing the quote, link directly to the discussion in the format of ${meetingURL}[meetingId]/discuss/[discussionId]. + Mention how many votes a topic has. + Be sure that each author is only mentioned once. + Your output must be a string. + Your tone should be kind, professional, and concise. + ` + + 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 + }) + console.log('šŸš€ ~ response:', response) + + const completionContent = response.choices[0]?.message.content as string + console.log('šŸš€ ~ completionContent:', completionContent) + + return completionContent + } catch (e) { + const error = e instanceof Error ? e : new Error('Error in generateInsight') sendToSentry(error) return null } From a0f26078abc6bc87dd1973f23f75cd7d3c7afee3 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Thu, 18 Jul 2024 17:56:55 +0100 Subject: [PATCH 11/43] include links to discussions --- meetingSummary.yaml | 473 ++++++++++++++++++ .../public/mutations/generateInsight.ts | 2 +- .../public/mutations/helpers/getSummaries.ts | 30 +- packages/server/utils/OpenAIServerManager.ts | 4 +- 4 files changed, 499 insertions(+), 10 deletions(-) create mode 100644 meetingSummary.yaml diff --git a/meetingSummary.yaml b/meetingSummary.yaml new file mode 100644 index 00000000000..8a39dd5b18b --- /dev/null +++ b/meetingSummary.yaml @@ -0,0 +1,473 @@ +- voteCount: 1 + title: Ai icebreakers + reflections: + - prompt: āœˆļø In-Flight + author: Jordan + text: 'AI icebreakers: is this safe to release?' + discussionId: 16 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 16 +- voteCount: 7 + title: Public teams + comments: + - text: Would it be public within an org or a whole domain? + author: Grayson Crickman + replies: + - text: It's by org + author: Nick O'Ferrall + reflections: + - prompt: šŸ›‘ Halt + author: Jordan + text: >- + Defer: making teams public by default ā€“ let's get this released to GA + when we've got a feature that gives making teams public more value + (single team summarization or org summarization) + discussionId: 8 + - prompt: āœˆļø In-Flight + author: Nick O'Ferrall + text: >- + Public teams. This is still being tested: + https://eppo.cloud/experiments/13129 + discussionId: 8 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 8 +- voteCount: 8 + title: Team conversion throttle + comments: + - text: >- + interesting data on sales led vs organic Team Plan conversions in this + thread + + + https://parabol.slack.com/archives/C08FL5UE7/p1697653709789529?thread_ts=1697481390.194139&cid=C08FL5UE7 + author: Drew + replies: [] + - text: >- + TLDR: + + All time: 56.11% sales led vs 43.89% organic + + This is probably skewed from historical changes to our HubSpot + pipelines, meaning we aren't accurately identify "sales-led" deals in + the past + + Last 12 months: 60.9% sales led vs 39.1% organic + + Last 6 months: 61.7% sales led vs 38.3% organic + + Last 3 months: 61.3% sales led vs 38.6% organic + author: Drew + replies: [] + - text: 'safe to try getting rid of them all for a couple months? ' + author: Drew + replies: + - text: Not sure + author: Jordan + - text: >- + I got 68% sales-led all time from the excel sheet that Taylor shared + back in October + author: Grayson Crickman + replies: [] + - text: >- + Pricing + https://www.notion.so/parabol/84e7bd61edfc40aeb6331a4e48c8085c?v=9fc22914219f457982e9a3e36d31ecc1 + author: Lorena MartĆ­nez + replies: + - text: Here are the limits and special features per Tier + author: Lorena MartĆ­nez + - text: >- + Lorena adds that folks churn from template landing pages when they find + the template isnā€™t free + author: Terry + replies: [] + - text: >- + Maybe being able to use premium templates once to evaluate them? Or + like: you are a free team, but you can use premium templates 10 times + until you run out of trial templates. + + + That means 10 meetings and they can evaluate 10 different templates or + use the same 10 times + author: Rafael Romero + replies: [] + - text: >- + About half of templates are free right now, all in "test: free" column: + https://www.notion.so/parabol/Templates-ce5a1b9ee7df47a1a2142c6e9e2edd8b + author: Georg Bremer + replies: [] + reflections: + - prompt: āœØNew + author: Jordan + text: >- + Team conversion: relax constraints such as template limits (need list of + conversion levers) + discussionId: 5 + - prompt: āœØNew + author: Charles + text: >- + A no credit card trial when a P0 signs up - along with a series of + emails showing off the benefits of Team + discussionId: 5 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 5 +- voteCount: 19 + title: Feature Team insights Team management + comments: + - text: >- + https://www.notion.so/parabol/Feature-Flag-Lifecycle-35075fae1be84e2a99c84068b3475a6e + author: "matt\_šŸ™ˆ" + replies: [] + - text: I'm pretty sure current version of team insights is rolled out + author: Georg Bremer + replies: [] + - text: https://eppo.cloud/experiments?show=1-25 + author: Nick O'Ferrall + replies: [] + - text: >- + I feel like a better metaphor is, we are asking users: which pizza + sounds better? i.e., we are not always testing the right set of metrics + for statistical significance, e.g., the AI discussion prompt might not + be a boosting factor for org growth, yet we are comparing number of + users added between control vs. experiment groups. + author: Bruce Tian + replies: [] + - text: 'Management interface PR: https://github.com/ParabolInc/parabol/pull/9285' + author: Jordan + replies: [] + reflections: + - prompt: āœˆļø In-Flight + author: Jordan + text: >- + Team Management: what choices shall we make here? We have a Figma + design, milestone, and partially completed branch + discussionId: 1 + - prompt: āœØNew + author: Charles + text: Insights+Reporting for Leaders + discussionId: 1 + - prompt: āœˆļø In-Flight + author: Charles + text: >- + Team Insights - I think it is still being tested. I'd vote for this + being a Team and Enterprise feature + discussionId: 1 + - prompt: āœˆļø In-Flight + author: Charles + text: >- + Org Admin Role & Managing Users/Teams for Enterprise orgs only. These + two together will make a lot of our current paid users happy + discussionId: 1 + - prompt: āœØNew + author: Charles + text: Improved Org/Team view UI + discussionId: 1 + - prompt: āœˆļø In-Flight + author: Drew + text: >- + I would love for us to release some kind of "super user" feature that + allows an Org Admin to see all teams, adjust team leads, adjust billing + leaders, etc + discussionId: 1 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 1 +- voteCount: 7 + title: Retros Integration + reflections: + - prompt: āœˆļø In-Flight + author: Jordan + text: 'Recurring retros: what''s left to do?' + discussionId: 7 + - prompt: āœˆļø In-Flight + author: Charles + text: >- + MS Teams and Azure DevOps Integrations. They were released under a + feature flag a while ago and have been used by users upon request - + let's roll them out! + discussionId: 7 + - prompt: āœˆļø In-Flight + author: Georg Bremer + text: >- + Recurring retros, especially having a recurring Gcal event associated + with it. + discussionId: 7 + - prompt: āœˆļø In-Flight + author: Bruce Tian + text: GCal integration + discussionId: 7 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 7 +- voteCount: 1 + title: Onboarding Improvements + reflections: + - prompt: āœˆļø In-Flight + author: Nick O'Ferrall + text: >- + Alicia created onboarding improvement recommendations. We could + implement some of them, depending on what other work we're considering: + https://www.notion.so/parabol/Onboarding-Recommendations-e02cce5464fa42f88a31b0ac9955adc7 + discussionId: 15 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 15 +- voteCount: 4 + title: Add Activity Button + reflections: + - prompt: šŸ›‘ Halt + author: Jordan + text: >- + Halt: add an activity button; I'd like to back this change out ā€“ the UX + is very clunky + discussionId: 10 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 10 +- voteCount: 4 + title: Kudos + comments: + - text: |- + I feel like we could have a card in team insights: + * # all kudos given this week + * # kudos you've given + * # kudos you've received + author: Georg Bremer + replies: [] + - text: 'people do love their kudos retro columns, they want that button :) ' + author: Drew + replies: + - text: >- + I agree, I think this would be cool. You can give people sincere + gratitude with reflections + author: Nick O'Ferrall + - text: Yes, I really want to put this draft over the edge + author: Georg Bremer + reflections: + - prompt: āœˆļø In-Flight + author: Jordan + text: >- + Kudos: much effort has been poured here, what if anything should be + released and at what scale? + discussionId: 12 + - prompt: šŸ›‘ Halt + author: Nick O'Ferrall + text: >- + Kudos. We'd need to build a leaderboard or some sort of kudos page + before promoting this feature. Personally, I'm not a fan of gamifying + appreciation as it can cheapen gratitude, so I'd prefer to stop work on + this. + discussionId: 12 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 12 +- voteCount: 2 + title: Release process and config management + reflections: + - prompt: āœØNew + author: Rafael Romero + text: Improve configuration management + discussionId: 14 + - prompt: šŸ›‘ Halt + author: Rafael Romero + text: >- + The release process is simple enough. No need to improve it more. If we + are not able to fix the merge conflict when merging the PRs to the + branch production, just drop that last part and use Gitlab pipeline to + release to prod. Simple and no more work to do. + discussionId: 14 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 14 +- voteCount: 18 + title: Activity library + reflections: + - prompt: āœˆļø In-Flight + author: Bruce Tian + text: Activity Library full rollout + discussionId: 2 + - prompt: āœˆļø In-Flight + author: Nick O'Ferrall + text: >- + Activity library. Still being tested: + https://eppo.cloud/experiments/8430 + discussionId: 2 + - prompt: āœØNew + author: Jordan + text: 'Activity Library: simple search' + discussionId: 2 + - prompt: āœØNew + author: Georg Bremer + text: >- + Finish up the custom template logic in the activity library to be on par + with the legacy dialog and finally remove the legacy dialog + discussionId: 2 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 2 +- voteCount: 3 + title: 1:1 and Ad Hoc Meetings + reflections: + - prompt: šŸ›‘ Halt + author: Bruce Tian + text: AdHoc Team & 1:1 + discussionId: 13 + - prompt: šŸ›‘ Halt + author: Jordan + text: Halt near-term future work on 1-on-1 meetings + discussionId: 13 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 13 +- voteCount: 4 + title: Ai summary Stand-up summaries + reflections: + - prompt: āœˆļø In-Flight + author: Bruce Tian + text: AI summary for standup + discussionId: 11 + - prompt: āœˆļø In-Flight + author: Jordan + text: 'Stand-up summaries: any reason this can''t be released to GA?' + discussionId: 11 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 11 +- voteCount: 9 + title: Zoom transcription + reflections: + - prompt: šŸ›‘ Halt + author: Jordan + text: >- + Defer: zoom transcription, I'd like to return to this after we've got + infrastructure in place for embedding and summarization + discussionId: 3 + - prompt: āœØNew + author: Nick O'Ferrall + text: >- + Video call transcription. Data is the key to building an AI product with + a business moat. This would be a useful feature, and it would give us + access to a lot more data. + discussionId: 3 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 3 +- voteCount: 7 + title: RethinkDB + comments: + - text: >- + Even a partial migration would: + + - Reduce our risk. Backups and restorations would be faster, smaller + and everything in PG can be restored using Point In-Time Recovery. + + - Saving šŸ’ø in the cloud, being able to reduce the RethinkDB instance to + a smaller one. + + + A full migration out of RethinkDB should: + + - Reduce around 15% of GCP cost per environment ($376.07264 $/month per + env => 2 envs => 752 $/month) + + - Make our backups consistent and reduce our time to make them to + virtually nothing. + + - Probably helping PubSec projects, simplifying the stack and removing + the unmaintained component. + + - Allow us to automatically deploy PPMIs: running databases in container + would be possible in a safe enough way. That would allow us to build + something to sell cheap and easy to deploy and maintain PPMIs + "automatically". + author: Rafael Romero + replies: [] + reflections: + - prompt: āœØNew + author: "matt\_šŸ™ˆ" + text: >- + getting RetroReflection and RetroReflectionGruop tables into PG. This + will halve our RethinkDB size & unlock the ability to move reflections + to tiptap + discussionId: 6 + - prompt: āœØNew + author: Rafael Romero + text: >- + RethinkDB. We must get rid of RethinkDB, even more if we are going to + enter in a do not touch mode. And we should start by RetroReflection and + RetroReflectionGroup. Those two make for almost the 50% of size and more + than 40% of docs. + discussionId: 6 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 6 +- voteCount: 9 + title: Embeddings + comments: + - text: >- + future thing: it would be VERY cool if this ended in a place where the + sales team could generate the reports themselves + author: Drew + replies: + - text: I think itā€™d be cool if you could demo it in the product šŸ¤” + author: Terry + - text: 'Here''s a milestone: https://github.com/ParabolInc/parabol/milestone/193' + author: Jordan + replies: [] + reflections: + - prompt: āœˆļø In-Flight + author: Jordan + text: >- + Embedder service: we got a milestone's work of backend effort. How to + share the love here? + discussionId: 4 + - prompt: āœˆļø In-Flight + author: Jordan + text: >- + Related Conversations: I can take point on PR to allow us to experiment + with this feature, and get a feel for how to use the Embedder tables + (and their efficacy) + discussionId: 4 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 4 +- voteCount: 5 + title: Ai Prompt + reflections: + - prompt: āœˆļø In-Flight + author: Bruce Tian + text: AI generated discussion prompt + discussionId: 9 + - prompt: āœˆļø In-Flight + author: Jordan + text: >- + AI Discussion prompt: what next step shall we take? This feature seems + rather safe... + discussionId: 9 + - prompt: āœˆļø In-Flight + author: Charles + text: >- + AI Generated Discussion Prompt - is this still under test? We should + release it for Team and Enterprise users only + discussionId: 9 + meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ + date: 2024-02-19T23:50:06.859Z + meetingId: uEofN2TrmU + discussionId: 9 diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 844cdb7254f..dbbc699179b 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -50,7 +50,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( } const yamlData = yaml.dump(meetingsContent, { - noCompatMode: true // This option ensures compatibility mode is off + noCompatMode: true }) const openAI = new OpenAIServerManager() diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts index 63554461dcb..b41269f430a 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -63,6 +63,7 @@ const generateMeetingSummary = async ( meeting: MeetingRetrospective, dataLoader: DataLoaderWorker ) => { + const pg = getKysely() const {id: meetingId, disableAnonymity, name: meetingName, createdAt: meetingDate} = meeting const rawReflectionGroups = await dataLoader .get('retroReflectionGroupsByMeetingId') @@ -72,10 +73,23 @@ const generateMeetingSummary = async ( .filter((g) => g.voterIds.length > 0) .map(async (group) => { const {id: reflectionGroupId, voterIds, title} = group - const [comments, rawReflections] = await Promise.all([ + const [comments, rawReflections, discussion] = await Promise.all([ getComments(reflectionGroupId, dataLoader), - dataLoader.get('retroReflectionsByGroupId').load(group.id) + dataLoader.get('retroReflectionsByGroupId').load(group.id), + pg + .selectFrom('Discussion') + .selectAll() + .where('discussionTopicId', '=', reflectionGroupId) + .limit(1) + .executeTakeFirst() ]) + const discussPhase = getPhase(meeting.phases, 'discuss') + const {stages} = discussPhase + const stageIdx = stages + .sort((a, b) => (a.sortOrder < b.sortOrder ? -1 : 1)) + .findIndex((stage) => stage.discussionId === discussion?.id) + const discussionIdx = stageIdx + 1 + const reflections = await Promise.all( rawReflections.map(async (reflection) => { const {promptId, creatorId, plaintextContent} = reflection @@ -88,7 +102,8 @@ const generateMeetingSummary = async ( return { prompt: question, author: creatorName, - text: plaintextContent + text: plaintextContent, + discussionId: discussionIdx } }) ) @@ -99,7 +114,8 @@ const generateMeetingSummary = async ( reflections, meetingName, date: meetingDate, - meetingId + meetingId, + discussionId: discussionIdx } if (!res.comments || !res.comments.length) { @@ -159,12 +175,10 @@ export const getSummaries = async ( fs.writeFileSync('meetingSummary.yaml', yamlData, 'utf8') const manager = new OpenAIServerManager() - const mySummary = await manager.generateSummary(yamlData) - console.log('šŸš€ ~ mySummary:', mySummary) + const newSummary = await manager.generateSummary(yamlData) + console.log('šŸš€ ~ newSummary:', newSummary) console.log('šŸš€ ~ res_____:', res) - const newSummary = null - console.log('šŸš€ ~ newSummary___:', newSummary) if (newSummary) { // Update the meeting with the new summary diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 10722eba58c..4119d68b5ab 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -409,9 +409,11 @@ class OpenAIServerManager { You need to summarize the content of a meeting. Your summary must be one paragraph with no more than a few sentences. Below is a list of reflection topics and comments in YAML format from the meeting. When citing the quote, link directly to the discussion in the format of ${meetingURL}[meetingId]/discuss/[discussionId]. - Mention how many votes a topic has. + 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. + You do not need to mention everything. Just mention the most important points, and ensure the summary is concise. Your tone should be kind, professional, and concise. ` From adc15acd93367ac9bd57c5593f8d2c094f7510b0 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 22 Jul 2024 16:32:23 +0100 Subject: [PATCH 12/43] update prompt --- packages/server/graphql/public/mutations/generateInsight.ts | 2 ++ packages/server/utils/OpenAIServerManager.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index dbbc699179b..22f75b73021 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -1,3 +1,4 @@ +import fs from 'fs' import yaml from 'js-yaml' import getKysely from '../../../postgres/getKysely' import OpenAIServerManager from '../../../utils/OpenAIServerManager' @@ -52,6 +53,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const yamlData = yaml.dump(meetingsContent, { noCompatMode: true }) + fs.writeFileSync('summaryMeetingContent.yaml', yamlData, 'utf8') const openAI = new OpenAIServerManager() const rawInsight = await openAI.generateInsight(yamlData) diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 4119d68b5ab..2684e4f5c48 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -354,7 +354,7 @@ class OpenAIServerManager { 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. 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 ${meetingURL}[meetingId]. + When citing the quote, include the meetingId in the format of ${meetingURL}[meetingId] or if the meeting summary already includes a link, use that link. For each topic, mention how many votes it has. Be sure that each author is only mentioned once. Return the output as a JSON object with the following structure: From 3b939b14d2169a6e324f0fcef7a0f2a1636efb8d Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 23 Jul 2024 14:11:10 +0100 Subject: [PATCH 13/43] return summary if exists --- .../public/mutations/generateInsight.ts | 2 +- .../public/mutations/helpers/getSummaries.ts | 71 ++++++++++--------- .../public/typeDefs/generateInsight.graphql | 4 +- .../public/types/GenerateInsightSuccess.ts | 4 +- packages/server/utils/OpenAIServerManager.ts | 28 ++++---- 5 files changed, 58 insertions(+), 51 deletions(-) diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 22f75b73021..39504e3b2e4 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -122,7 +122,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( // }) // .execute() - const data = {wins: rawInsight.wins[0], challenges: rawInsight.challenges[0]} + const data = {wins: rawInsight.wins, challenges: rawInsight.challenges} return data } diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts index b41269f430a..cf1ea8a9f5a 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -59,10 +59,7 @@ const getComments = async (reflectionGroupId: string, dataLoader: DataLoaderWork return comments } -const generateMeetingSummary = async ( - meeting: MeetingRetrospective, - dataLoader: DataLoaderWorker -) => { +const getMeetingsContent = async (meeting: MeetingRetrospective, dataLoader: DataLoaderWorker) => { const pg = getKysely() const {id: meetingId, disableAnonymity, name: meetingName, createdAt: meetingDate} = meeting const rawReflectionGroups = await dataLoader @@ -107,13 +104,19 @@ const generateMeetingSummary = async ( } }) ) + const shortDate = meetingDate.toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }) + console.log('šŸš€ ~ shortDate:', shortDate) const res = { voteCount: voterIds.length, title: title, comments, reflections, meetingName, - date: meetingDate, + date: shortDate, meetingId, discussionId: discussionIdx } @@ -142,34 +145,37 @@ export const getSummaries = async ( 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)) + .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()) as MeetingRetrospective[] // console.log('šŸš€ ~ rawMeetings:', rawMeetings) + const meetingsCount = rawMeetings.length + console.log('šŸš€ ~ meetingsCount:', meetingsCount) const summaries = await Promise.all( rawMeetings.map(async (meeting) => { console.log('šŸš€ ~ meeting.summary____:', meeting.summary) - // if (!meeting.summary) { - const discussPhase = getPhase(meeting.phases, 'discuss') - const {stages} = discussPhase - const discussionIds = stages.map((stage) => stage.discussionId) - // const newSummary = await generateWholeMeetingSummary( - // discussionIds, - // meeting.id, - // teamId, - // meeting.facilitatorUserId, - // dataLoader - // ) - const res = await generateMeetingSummary(meeting, dataLoader) - const yamlData = yaml.dump(res, { + const newlyGeneratedSummariesDate = new Date('2024-07-22T00:00:00Z') + if (meeting.summary && meeting.updatedAt > newlyGeneratedSummariesDate) { + console.log('returnnining__') + return meeting.summary + } + const meetingsContent = await getMeetingsContent(meeting, dataLoader) + const now = new Date() + await r.table('NewMeeting').get(meeting.id).update({summary: undefined, updatedAt: now}).run() + if (!meetingsContent || meetingsContent.length === 0) { + console.log('šŸš€ ~ meetingsContent:', {meetingsContent, meeting}) + return null + } + const yamlData = yaml.dump(meetingsContent, { noCompatMode: true }) fs.writeFileSync('meetingSummary.yaml', yamlData, 'utf8') @@ -178,16 +184,17 @@ export const getSummaries = async ( const newSummary = await manager.generateSummary(yamlData) console.log('šŸš€ ~ newSummary:', newSummary) - console.log('šŸš€ ~ res_____:', res) - if (newSummary) { - // Update the meeting with the new summary - await r.table('NewMeeting').get(meeting.id).update({summary: newSummary}).run() + const now = new Date() + await r + .table('NewMeeting') + .get(meeting.id) + .update({summary: newSummary, updatedAt: now}) + .run() meeting.summary = newSummary } - // } return { - meetingId: meeting.id, + meetingName: meeting.name, date: meeting.createdAt, summary: meeting.summary } diff --git a/packages/server/graphql/public/typeDefs/generateInsight.graphql b/packages/server/graphql/public/typeDefs/generateInsight.graphql index fed44360c7d..4469ec0da39 100644 --- a/packages/server/graphql/public/typeDefs/generateInsight.graphql +++ b/packages/server/graphql/public/typeDefs/generateInsight.graphql @@ -19,10 +19,10 @@ type GenerateInsightSuccess { """ The insights generated focusing on the wins of the team """ - wins: String! + wins: [String] """ The insights generated focusing on the challenges team are facing """ - challenges: String! + challenges: [String] } diff --git a/packages/server/graphql/public/types/GenerateInsightSuccess.ts b/packages/server/graphql/public/types/GenerateInsightSuccess.ts index 4fcf8a83230..1d8419965f7 100644 --- a/packages/server/graphql/public/types/GenerateInsightSuccess.ts +++ b/packages/server/graphql/public/types/GenerateInsightSuccess.ts @@ -1,4 +1,4 @@ export type GenerateInsightSuccessSource = { - wins: string - challenges: string + wins: string[] + challenges: string[] } diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 2684e4f5c48..b3a761bf0a2 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -348,15 +348,16 @@ class OpenAIServerManager { async generateInsight(yamlData: string): Promise { if (!this.openAIApi) return null const meetingURL = 'https://action.parabol.co/meet/' + // Below is a list of reflection topics in YAML format from meetings over the past months. + // 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. const prompt = ` You are a management consultant who needs to discover behavioral trends for a given team. - Below is a list of reflection topics in YAML format from meetings over the past months. + Below is a list of meeting summaries in YAML format from meetings over the past 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. 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 ${meetingURL}[meetingId] or if the meeting summary already includes a link, use that link. - For each topic, mention how many votes it has. - Be sure that each author is only mentioned once. + 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. + Try to cite direct quotes from the meeting, attributing it to the person who wrote it, if they're included in the summary. Include the discussion link in the markdown format of [link](${meetingURL}[meetingId]/discuss/[discussionId]). + The most important topics are usually at the beginning of the summary, so prioritize them. If the link is ${meetingURL}[meetingId]/discuss/1, that means it was the first discussion in the meeting, got the most votes, and is the most important. Return the output as a JSON object with the following structure: { "wins": ["bullet point 1", "bullet point 2", "bullet point 3"], @@ -406,15 +407,17 @@ class OpenAIServerManager { 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 few sentences. + 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. - When citing the quote, link directly to the discussion in the format of ${meetingURL}[meetingId]/discuss/[discussionId]. + 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. You do not need to mention everything. Just mention the most important points, and ensure the summary is concise. - Your tone should be kind, professional, and concise. + Your tone should be kind and concise. Write in plain English. No jargon. ` try { @@ -432,12 +435,9 @@ class OpenAIServerManager { frequency_penalty: 0, presence_penalty: 0 }) - console.log('šŸš€ ~ response:', response) - - const completionContent = response.choices[0]?.message.content as string - console.log('šŸš€ ~ completionContent:', completionContent) - return completionContent + 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) From cebc7b36dc6153e5facb045c1cab2198dd228646 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 23 Jul 2024 17:43:42 +0100 Subject: [PATCH 14/43] update prompt and clean up processing getTopics meetingId --- .../public/mutations/generateInsight.ts | 116 +++-------------- .../public/mutations/helpers/getSummaries.ts | 106 ++++++---------- .../public/mutations/helpers/getTopics.ts | 117 ++++++++++++++---- packages/server/package.json | 2 +- packages/server/utils/OpenAIServerManager.ts | 39 ++++-- yarn.lock | 8 +- 6 files changed, 183 insertions(+), 205 deletions(-) diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 39504e3b2e4..1a2e3e5d886 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -1,8 +1,4 @@ -import fs from 'fs' -import yaml from 'js-yaml' import getKysely from '../../../postgres/getKysely' -import OpenAIServerManager from '../../../utils/OpenAIServerManager' -import sendToSentry from '../../../utils/sendToSentry' import standardError from '../../../utils/standardError' import {MutationResolvers} from '../resolverTypes' import {getSummaries} from './helpers/getSummaries' @@ -24,106 +20,28 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( if (end.getTime() - start.getTime() < oneWeekInMs) { return standardError(new Error('The end date must be at least one week after the start date.')) } - const pg = getKysely() - - // const existingInsight = await pg - // .selectFrom('Insight') - // .selectAll() - // .where('teamId', '=', teamId) - // .where('startDateTime', '=', start) - // .where('endDateTime', '=', end) - // .limit(1) - // .executeTakeFirst() - - // if (existingInsight) { - // return { - // wins: existingInsight.wins, - // challenges: existingInsight.challenges - // } - // } - const meetingsContent = useSummaries + const response = useSummaries ? await getSummaries(teamId, startDate, endDate, dataLoader) : await getTopics(teamId, startDate, endDate, dataLoader) - if (meetingsContent.length === 0) { - return standardError(new Error('No meeting content found for the specified date range.')) + if ('error' in response) { + return response } - - const yamlData = yaml.dump(meetingsContent, { - noCompatMode: true - }) - fs.writeFileSync('summaryMeetingContent.yaml', yamlData, 'utf8') - - const openAI = new OpenAIServerManager() - const rawInsight = await openAI.generateInsight(yamlData) - if (!rawInsight) { - return standardError(new Error('Unable to generate insight.')) - } - - const meetingURL = 'https://action.parabol.co/meet/' - - const processLines = (lines: string[]): string => { - 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) => { - let shortMeetingId = match.split(meetingURL)[1]?.split(/[),\s]/)[0] // Split by closing parenthesis, comma, or space - const actualMeetingId = shortMeetingId && (idLookup.meeting[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 empty string if invalid - } - return line - }) - .filter((line) => line.trim() !== '') - .join('\n') - } - - const processSection = (section: string[]): string => { - return section - .map((item) => { - const lines = item.split('\n') - return processLines(lines) - }) - .filter((processedItem) => processedItem.trim() !== '') - .join('\n') - } - - console.log('šŸš€ ~ batch.wins:', rawInsight.wins) - // const wins = processSection(rawInsight.wins) - // console.log('šŸš€ ~ wins:', wins) - - // const challenges = processSection(rawInsight.challenges) - console.log('šŸš€ ~ rawInsight.challenges:', rawInsight.challenges) - // console.log('šŸš€ ~ challenges:', challenges) - // await pg - // .insertInto('Insight') - // .values({ - // teamId, - // wins, - // challenges, - // startDate, - // endDate - // }) - // .execute() - - const data = {wins: rawInsight.wins, challenges: rawInsight.challenges} - return data + const {wins, challenges} = response + const pg = getKysely() + await pg + .insertInto('Insight') + .values({ + teamId, + wins, + challenges, + startDate, + 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 index cf1ea8a9f5a..02cb91bdcc2 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -1,10 +1,10 @@ -import fs from 'fs' 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 getPhase from '../../../../utils/getPhase' +import standardError from '../../../../utils/standardError' import {DataLoaderWorker} from '../../../graphql' const getComments = async (reflectionGroupId: string, dataLoader: DataLoaderWorker) => { @@ -127,7 +127,6 @@ const getMeetingsContent = async (meeting: MeetingRetrospective, dataLoader: Dat return res }) ) - console.log('šŸš€ ~ reflectionGroups:', reflectionGroups) return reflectionGroups } @@ -145,54 +144,49 @@ export const getSummaries = async ( 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)) + .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()) as MeetingRetrospective[] - // console.log('šŸš€ ~ rawMeetings:', rawMeetings) - const meetingsCount = rawMeetings.length - console.log('šŸš€ ~ meetingsCount:', meetingsCount) const summaries = await Promise.all( rawMeetings.map(async (meeting) => { - console.log('šŸš€ ~ meeting.summary____:', meeting.summary) + // this is temporary, just to see what it looks like when we create summaries on the fly + // if we go with a summary of summaries approach, remove this and create a separate mutation that generates new meeting summaries which include links to discussions const newlyGeneratedSummariesDate = new Date('2024-07-22T00:00:00Z') if (meeting.summary && meeting.updatedAt > newlyGeneratedSummariesDate) { - console.log('returnnining__') - return meeting.summary + return { + meetingName: meeting.name, + date: meeting.createdAt, + summary: meeting.summary + } } const meetingsContent = await getMeetingsContent(meeting, dataLoader) - const now = new Date() - await r.table('NewMeeting').get(meeting.id).update({summary: undefined, updatedAt: now}).run() if (!meetingsContent || meetingsContent.length === 0) { - console.log('šŸš€ ~ meetingsContent:', {meetingsContent, meeting}) return null } const yamlData = yaml.dump(meetingsContent, { noCompatMode: true }) - fs.writeFileSync('meetingSummary.yaml', yamlData, 'utf8') + // fs.writeFileSync('meetingSummary.yaml', yamlData, 'utf8') const manager = new OpenAIServerManager() const newSummary = await manager.generateSummary(yamlData) - console.log('šŸš€ ~ newSummary:', newSummary) + if (!newSummary) return null - if (newSummary) { - const now = new Date() - await r - .table('NewMeeting') - .get(meeting.id) - .update({summary: newSummary, updatedAt: now}) - .run() - meeting.summary = newSummary - } + const now = new Date() + await r + .table('NewMeeting') + .get(meeting.id) + .update({summary: newSummary, updatedAt: now}) + .run() + meeting.summary = newSummary return { meetingName: meeting.name, date: meeting.createdAt, @@ -201,38 +195,20 @@ export const getSummaries = async ( }) ) - return summaries + const meetingsContent = summaries.filter((summary) => summary) + const yamlData = yaml.dump(meetingsContent, { + noCompatMode: true + }) + // fs.writeFileSync('summaryMeetingContent.yaml', yamlData, 'utf8') + + const openAI = new OpenAIServerManager() + const rawInsight = await openAI.generateInsight(yamlData, true) + if (!rawInsight) { + return standardError(new Error('No insights generated')) + } + + return { + wins: rawInsight.wins, + challenges: rawInsight.challenges + } } - -// import getRethink from '../../../../database/rethinkDriver' - -// export const getSummaries = async (teamId: string, startDate: Date, endDate: Date) => { -// 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(row('summary').eq(null)) -// .and(r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'}).count().gt(1)) -// .and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS)) -// ) -// .run() - -// console.log('šŸš€ ~ rawMeetings:', rawMeetings) -// const summaries = rawMeetings.map((meeting) => ({ -// meetingId: meeting.id, -// date: meeting.createdAt, -// summary: meeting.summary -// })) -// console.log('šŸš€ ~ summaries____:', summaries) - -// return summaries -// } diff --git a/packages/server/graphql/public/mutations/helpers/getTopics.ts b/packages/server/graphql/public/mutations/helpers/getTopics.ts index e4cf918bdc1..163b320f1ba 100644 --- a/packages/server/graphql/public/mutations/helpers/getTopics.ts +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -1,6 +1,10 @@ +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) => { @@ -41,20 +45,65 @@ const getComments = async (reflectionGroupId: string, dataLoader: DataLoaderWork } }) ) - const res = { - text: plaintextContent, - author: commentAuthor, - replies: commentReplies - } - if (res.replies.length === 0) { - delete (res as any).commentReplies - } - return res + 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) => { + let shortMeetingId = match.split(meetingURL)[1]?.split(/[),\s]/)[0] // Split by closing parenthesis, comma, or space + const actualMeetingId = shortMeetingId && (meetingLookup[shortMeetingId] as string) + console.log('šŸš€ ~ ________:', {actualMeetingId, meetingLookup}) + + 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, @@ -140,27 +189,41 @@ export const getTopics = async ( .filter((t) => t.voteCount > 2) .sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1)) - type IDLookup = Record - const idLookup = { - meeting: {} as IDLookup, - date: {} as IDLookup - } - 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] - idLookup.meeting[shortMeetingId] = meetingId - idLookup.date[shortMeetingId] = date - return { - ...t, - date: shortMeetingDate, - meetingId: shortMeetingId - } + 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 }) - return shortTokenedTopics + // fs.writeFileSync('summaryMeetingContent.yaml', yamlData, 'utf8') + + const openAI = new OpenAIServerManager() + const rawInsight = await openAI.generateInsight(yamlData, false) + if (!rawInsight) { + return standardError(new Error('Unable to generate insight.')) + } + + const wins = processSection(rawInsight.wins) + const challenges = processSection(rawInsight.challenges) + + return {wins, challenges} } diff --git a/packages/server/package.json b/packages/server/package.json index 62e9d945d40..575982da0ec 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.52.2", + "openai": "^4.53.0", "openapi-fetch": "^0.9.7", "oy-vey": "^0.12.1", "parabol-client": "7.38.7", diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index b3a761bf0a2..233a8fdf03b 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -345,27 +345,46 @@ class OpenAIServerManager { } } - async generateInsight(yamlData: string): Promise { + async generateInsight(yamlData: string, useSummaries: boolean): Promise { if (!this.openAIApi) return null const meetingURL = 'https://action.parabol.co/meet/' - // Below is a list of reflection topics in YAML format from meetings over the past months. - // 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. - const prompt = ` + const defaultPrompt = ` You are a management consultant who needs to discover behavioral trends for a given team. - Below is a list of meeting summaries in YAML format from meetings over the past months. + 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 professional. 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 are a management consultant who needs 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. - Try to cite direct quotes from the meeting, attributing it to the person who wrote it, if they're included in the summary. Include the discussion link in the markdown format of [link](${meetingURL}[meetingId]/discuss/[discussionId]). - The most important topics are usually at the beginning of the summary, so prioritize them. If the link is ${meetingURL}[meetingId]/discuss/1, that means it was the first discussion in the meeting, got the most votes, and is the most important. + Try to cite direct quotes from the meeting, attributing it 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. 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", "bullet point 3"] + "challenges": ["bullet point 1", "bullet point 2"] } Your tone should be kind and professional. No yapping. ` + const prompt = useSummaries ? promptForSummaries : defaultPrompt + try { const response = await this.openAIApi.chat.completions.create({ model: 'gpt-4o', @@ -403,6 +422,7 @@ class OpenAIServerManager { } } + // 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/' @@ -416,8 +436,9 @@ class OpenAIServerManager { 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 and concise. Write in plain English. No jargon. + Your tone should be kind. Write in plain English. No jargon. ` try { diff --git a/yarn.lock b/yarn.lock index 56784c4104c..5744aa9e633 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17169,10 +17169,10 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" -openai@^4.52.2: - version "4.52.2" - resolved "https://registry.yarnpkg.com/openai/-/openai-4.52.2.tgz#5d67271f3df84c0b54676b08990eaa9402151759" - integrity sha512-mMc0XgFuVSkcm0lRIi8zaw++otC82ZlfkCur1qguXYWPETr/+ZwL9A/vvp3YahX+shpaT6j03dwsmUyLAfmEfg== +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" From e15d1e7414dcac53a8279f3d51889373b34d3e71 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 23 Jul 2024 17:45:29 +0100 Subject: [PATCH 15/43] remove generated files --- .../helpers/generateWholeMeetingSummary.ts | 27 +- topics_parabol.json | 1 - topics_parabol_short.yml | 330 ------------------ topics_y3ZJgMy6hq.json | 1 - topics_y3ZJgMy6hq_short.yml | 1 - topics_y3ZJgMy6hr.json | 1 - topics_y3ZJgMy6hr_short.yml | 330 ------------------ 7 files changed, 12 insertions(+), 679 deletions(-) delete mode 100644 topics_parabol.json delete mode 100644 topics_parabol_short.yml delete mode 100644 topics_y3ZJgMy6hq.json delete mode 100644 topics_y3ZJgMy6hq_short.yml delete mode 100644 topics_y3ZJgMy6hr.json delete mode 100644 topics_y3ZJgMy6hr_short.yml diff --git a/packages/server/graphql/mutations/helpers/generateWholeMeetingSummary.ts b/packages/server/graphql/mutations/helpers/generateWholeMeetingSummary.ts index 180feb3a29c..037cc4135de 100644 --- a/packages/server/graphql/mutations/helpers/generateWholeMeetingSummary.ts +++ b/packages/server/graphql/mutations/helpers/generateWholeMeetingSummary.ts @@ -2,6 +2,7 @@ import {PARABOL_AI_USER_ID} from 'parabol-client/utils/constants' import OpenAIServerManager from '../../../utils/OpenAIServerManager' import {DataLoaderWorker} from '../../graphql' import isValid from '../../isValid' +import canAccessAISummary from './canAccessAISummary' const generateWholeMeetingSummary = async ( discussionIds: string[], @@ -10,26 +11,22 @@ const generateWholeMeetingSummary = async ( facilitatorUserId: string, dataLoader: DataLoaderWorker ) => { - console.log('šŸš€ ~ discussionIds:', {dataLoader}) - // const [facilitator, team] = await Promise.all([ - // dataLoader.get('users').loadNonNull(facilitatorUserId), - // dataLoader.get('teams').loadNonNull(teamId) - // ]) - // console.log('heee', {facilitator, team}) - // const isAISummaryAccessible = await canAccessAISummary( - // team, - // facilitator.featureFlags, - // dataLoader, - // 'retrospective' - // ) - // console.log('šŸš€ ~ isAISummaryAccessible:', isAISummaryAccessible) - // if (!isAISummaryAccessible) return + const [facilitator, team] = await Promise.all([ + dataLoader.get('users').loadNonNull(facilitatorUserId), + dataLoader.get('teams').loadNonNull(teamId) + ]) + const isAISummaryAccessible = await canAccessAISummary( + team, + facilitator.featureFlags, + dataLoader, + 'retrospective' + ) + if (!isAISummaryAccessible) return const [commentsByDiscussions, tasksByDiscussions, reflections] = await Promise.all([ dataLoader.get('commentsByDiscussionId').loadMany(discussionIds), dataLoader.get('tasksByDiscussionId').loadMany(discussionIds), dataLoader.get('retroReflectionsByMeetingId').load(meetingId) ]) - console.log('šŸš€ ~ commentsByDiscussions:', commentsByDiscussions) const manager = new OpenAIServerManager() const reflectionsContent = reflections.map((reflection) => reflection.plaintextContent) const commentsContent = commentsByDiscussions diff --git a/topics_parabol.json b/topics_parabol.json deleted file mode 100644 index 2b00422f009..00000000000 --- a/topics_parabol.json +++ /dev/null @@ -1 +0,0 @@ -[{"voteCount":0,"title":"Work team","reflections":[{"prompt":"Start","author":"Anonymous","text":"amazing work team"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Team","reflections":[{"prompt":"Start","author":"Anonymous","text":"love it team"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Communication struggles","reflections":[{"prompt":"Stop","author":"Anonymous","text":"communication struggles"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"we should communicate","reflections":[{"prompt":"Stop","author":"Anonymous","text":"we should communicate better"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Communication","comments":[{"text":"ds","author":"Nick O'Ferrall","replies":[]},{"text":"ds","author":"Nick O'Ferrall","replies":[]},{"text":"d","author":"Nick O'Ferrall","replies":[]}],"reflections":[{"prompt":"Stop","author":"Anonymous","text":"we must improve communication"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Stuff","reflections":[{"prompt":"Start","author":"Anonymous","text":"fantastic stuff"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"ds"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"ds"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"d"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"sd"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"ds"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"d"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"ds"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"ds"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"}] \ No newline at end of file diff --git a/topics_parabol_short.yml b/topics_parabol_short.yml deleted file mode 100644 index 1133d9b12a1..00000000000 --- a/topics_parabol_short.yml +++ /dev/null @@ -1,330 +0,0 @@ -- voteCount: 0 - title: Work team - reflections: - - prompt: Start - author: Anonymous - text: amazing work team - meetingName: Retro 6 - date: '2024-07-01' - meetingId: m1 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Team - reflections: - - prompt: Start - author: Anonymous - text: love it team - meetingName: Retro 6 - date: '2024-07-01' - meetingId: m2 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Communication struggles - reflections: - - prompt: Stop - author: Anonymous - text: communication struggles - meetingName: Retro 6 - date: '2024-07-01' - meetingId: m3 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: we should communicate - reflections: - - prompt: Stop - author: Anonymous - text: we should communicate better - meetingName: Retro 6 - date: '2024-07-01' - meetingId: m4 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Communication - comments: - - text: ds - author: Nick O'Ferrall - replies: [] - - text: ds - author: Nick O'Ferrall - replies: [] - - text: d - author: Nick O'Ferrall - replies: [] - reflections: - - prompt: Stop - author: Anonymous - text: we must improve communication - meetingName: Retro 6 - date: '2024-07-01' - meetingId: m5 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Stuff - reflections: - - prompt: Start - author: Anonymous - text: fantastic stuff - meetingName: Retro 6 - date: '2024-07-01' - meetingId: m6 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: What went well šŸ˜„ - author: Anonymous - text: ds - meetingName: Retro 1 - date: '2024-06-26' - meetingId: m7 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: What went well šŸ˜„ - author: Anonymous - text: ds - meetingName: Retro 1 - date: '2024-06-26' - meetingId: m8 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: What went well šŸ˜„ - author: Anonymous - text: d - meetingName: Retro 1 - date: '2024-06-26' - meetingId: m9 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Sd - reflections: - - prompt: What went well šŸ˜„ - author: Anonymous - text: sd - meetingName: Retro 1 - date: '2024-06-26' - meetingId: m10 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Wins šŸ† - author: Anonymous - text: ds - meetingName: Retro 4 - date: '2024-06-27' - meetingId: m11 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: Wins šŸ† - author: Anonymous - text: d - meetingName: Retro 4 - date: '2024-06-27' - meetingId: m12 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Wins šŸ† - author: Anonymous - text: ds - meetingName: Retro 4 - date: '2024-06-27' - meetingId: m13 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Wins šŸ† - author: Anonymous - text: ds - meetingName: Retro 4 - date: '2024-06-27' - meetingId: m14 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: Start - author: Anonymous - text: d - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m15 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Sd - reflections: - - prompt: Start - author: Anonymous - text: sd - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m16 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Sd - reflections: - - prompt: Start - author: Anonymous - text: sd - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m17 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: Start - author: Anonymous - text: d - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m18 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Sd - reflections: - - prompt: Start - author: Anonymous - text: sd - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m19 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: Start - author: Anonymous - text: d - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m20 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: s - reflections: - - prompt: Start - author: Anonymous - text: s - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m21 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Start - author: Anonymous - text: ds - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m22 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: s - reflections: - - prompt: Start - author: Anonymous - text: s - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m23 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Sd - reflections: - - prompt: Start - author: Anonymous - text: sd - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m24 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Sd - reflections: - - prompt: Start - author: Anonymous - text: sd - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m25 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: s - reflections: - - prompt: Start - author: Anonymous - text: s - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m26 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: s - reflections: - - prompt: Start - author: Anonymous - text: s - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m27 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Start - author: Anonymous - text: ds - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m28 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Start - author: Anonymous - text: ds - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m29 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Start - author: Anonymous - text: ds - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m30 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: Start - author: Anonymous - text: d - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m31 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: Start - author: Anonymous - text: d - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m32 - teamName: Nick O'Ferrallā€™s Team diff --git a/topics_y3ZJgMy6hq.json b/topics_y3ZJgMy6hq.json deleted file mode 100644 index 0637a088a01..00000000000 --- a/topics_y3ZJgMy6hq.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/topics_y3ZJgMy6hq_short.yml b/topics_y3ZJgMy6hq_short.yml deleted file mode 100644 index fe51488c706..00000000000 --- a/topics_y3ZJgMy6hq_short.yml +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/topics_y3ZJgMy6hr.json b/topics_y3ZJgMy6hr.json deleted file mode 100644 index 2b00422f009..00000000000 --- a/topics_y3ZJgMy6hr.json +++ /dev/null @@ -1 +0,0 @@ -[{"voteCount":0,"title":"Work team","reflections":[{"prompt":"Start","author":"Anonymous","text":"amazing work team"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Team","reflections":[{"prompt":"Start","author":"Anonymous","text":"love it team"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Communication struggles","reflections":[{"prompt":"Stop","author":"Anonymous","text":"communication struggles"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"we should communicate","reflections":[{"prompt":"Stop","author":"Anonymous","text":"we should communicate better"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Communication","comments":[{"text":"ds","author":"Nick O'Ferrall","replies":[]},{"text":"ds","author":"Nick O'Ferrall","replies":[]},{"text":"d","author":"Nick O'Ferrall","replies":[]}],"reflections":[{"prompt":"Stop","author":"Anonymous","text":"we must improve communication"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Stuff","reflections":[{"prompt":"Start","author":"Anonymous","text":"fantastic stuff"}],"meetingName":"Retro 6","date":"2024-07-01T11:37:47.722Z","meetingId":"yci8KSTb3O","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"ds"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"ds"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"d"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"What went well šŸ˜„","author":"Anonymous","text":"sd"}],"meetingName":"Retro 1","date":"2024-06-26T11:39:26.547Z","meetingId":"y3ZJQtbjck","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"ds"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"d"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"ds"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Wins šŸ†","author":"Anonymous","text":"ds"}],"meetingName":"Retro 4","date":"2024-06-27T14:14:14.559Z","meetingId":"y5PHK6ctgI","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Sd","reflections":[{"prompt":"Start","author":"Anonymous","text":"sd"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"s","reflections":[{"prompt":"Start","author":"Anonymous","text":"s"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"Ds","reflections":[{"prompt":"Start","author":"Anonymous","text":"ds"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"},{"voteCount":0,"title":"D","reflections":[{"prompt":"Start","author":"Anonymous","text":"d"}],"meetingName":"Retro 3","date":"2024-06-27T09:08:45.342Z","meetingId":"y5tSfXE1ig","teamName":"Nick O'Ferrallā€™s Team"}] \ No newline at end of file diff --git a/topics_y3ZJgMy6hr_short.yml b/topics_y3ZJgMy6hr_short.yml deleted file mode 100644 index 1133d9b12a1..00000000000 --- a/topics_y3ZJgMy6hr_short.yml +++ /dev/null @@ -1,330 +0,0 @@ -- voteCount: 0 - title: Work team - reflections: - - prompt: Start - author: Anonymous - text: amazing work team - meetingName: Retro 6 - date: '2024-07-01' - meetingId: m1 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Team - reflections: - - prompt: Start - author: Anonymous - text: love it team - meetingName: Retro 6 - date: '2024-07-01' - meetingId: m2 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Communication struggles - reflections: - - prompt: Stop - author: Anonymous - text: communication struggles - meetingName: Retro 6 - date: '2024-07-01' - meetingId: m3 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: we should communicate - reflections: - - prompt: Stop - author: Anonymous - text: we should communicate better - meetingName: Retro 6 - date: '2024-07-01' - meetingId: m4 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Communication - comments: - - text: ds - author: Nick O'Ferrall - replies: [] - - text: ds - author: Nick O'Ferrall - replies: [] - - text: d - author: Nick O'Ferrall - replies: [] - reflections: - - prompt: Stop - author: Anonymous - text: we must improve communication - meetingName: Retro 6 - date: '2024-07-01' - meetingId: m5 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Stuff - reflections: - - prompt: Start - author: Anonymous - text: fantastic stuff - meetingName: Retro 6 - date: '2024-07-01' - meetingId: m6 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: What went well šŸ˜„ - author: Anonymous - text: ds - meetingName: Retro 1 - date: '2024-06-26' - meetingId: m7 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: What went well šŸ˜„ - author: Anonymous - text: ds - meetingName: Retro 1 - date: '2024-06-26' - meetingId: m8 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: What went well šŸ˜„ - author: Anonymous - text: d - meetingName: Retro 1 - date: '2024-06-26' - meetingId: m9 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Sd - reflections: - - prompt: What went well šŸ˜„ - author: Anonymous - text: sd - meetingName: Retro 1 - date: '2024-06-26' - meetingId: m10 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Wins šŸ† - author: Anonymous - text: ds - meetingName: Retro 4 - date: '2024-06-27' - meetingId: m11 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: Wins šŸ† - author: Anonymous - text: d - meetingName: Retro 4 - date: '2024-06-27' - meetingId: m12 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Wins šŸ† - author: Anonymous - text: ds - meetingName: Retro 4 - date: '2024-06-27' - meetingId: m13 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Wins šŸ† - author: Anonymous - text: ds - meetingName: Retro 4 - date: '2024-06-27' - meetingId: m14 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: Start - author: Anonymous - text: d - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m15 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Sd - reflections: - - prompt: Start - author: Anonymous - text: sd - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m16 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Sd - reflections: - - prompt: Start - author: Anonymous - text: sd - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m17 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: Start - author: Anonymous - text: d - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m18 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Sd - reflections: - - prompt: Start - author: Anonymous - text: sd - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m19 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: Start - author: Anonymous - text: d - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m20 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: s - reflections: - - prompt: Start - author: Anonymous - text: s - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m21 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Start - author: Anonymous - text: ds - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m22 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: s - reflections: - - prompt: Start - author: Anonymous - text: s - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m23 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Sd - reflections: - - prompt: Start - author: Anonymous - text: sd - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m24 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Sd - reflections: - - prompt: Start - author: Anonymous - text: sd - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m25 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: s - reflections: - - prompt: Start - author: Anonymous - text: s - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m26 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: s - reflections: - - prompt: Start - author: Anonymous - text: s - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m27 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Start - author: Anonymous - text: ds - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m28 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Start - author: Anonymous - text: ds - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m29 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: Ds - reflections: - - prompt: Start - author: Anonymous - text: ds - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m30 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: Start - author: Anonymous - text: d - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m31 - teamName: Nick O'Ferrallā€™s Team -- voteCount: 0 - title: D - reflections: - - prompt: Start - author: Anonymous - text: d - meetingName: Retro 3 - date: '2024-06-27' - meetingId: m32 - teamName: Nick O'Ferrallā€™s Team From 23448b1b814c8dad9c372a9e23c1244b9f104808 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 23 Jul 2024 17:46:32 +0100 Subject: [PATCH 16/43] remove meetingSummary yaml file --- meetingSummary.yaml | 473 -------------------------------------------- 1 file changed, 473 deletions(-) delete mode 100644 meetingSummary.yaml diff --git a/meetingSummary.yaml b/meetingSummary.yaml deleted file mode 100644 index 8a39dd5b18b..00000000000 --- a/meetingSummary.yaml +++ /dev/null @@ -1,473 +0,0 @@ -- voteCount: 1 - title: Ai icebreakers - reflections: - - prompt: āœˆļø In-Flight - author: Jordan - text: 'AI icebreakers: is this safe to release?' - discussionId: 16 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 16 -- voteCount: 7 - title: Public teams - comments: - - text: Would it be public within an org or a whole domain? - author: Grayson Crickman - replies: - - text: It's by org - author: Nick O'Ferrall - reflections: - - prompt: šŸ›‘ Halt - author: Jordan - text: >- - Defer: making teams public by default ā€“ let's get this released to GA - when we've got a feature that gives making teams public more value - (single team summarization or org summarization) - discussionId: 8 - - prompt: āœˆļø In-Flight - author: Nick O'Ferrall - text: >- - Public teams. This is still being tested: - https://eppo.cloud/experiments/13129 - discussionId: 8 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 8 -- voteCount: 8 - title: Team conversion throttle - comments: - - text: >- - interesting data on sales led vs organic Team Plan conversions in this - thread - - - https://parabol.slack.com/archives/C08FL5UE7/p1697653709789529?thread_ts=1697481390.194139&cid=C08FL5UE7 - author: Drew - replies: [] - - text: >- - TLDR: - - All time: 56.11% sales led vs 43.89% organic - - This is probably skewed from historical changes to our HubSpot - pipelines, meaning we aren't accurately identify "sales-led" deals in - the past - - Last 12 months: 60.9% sales led vs 39.1% organic - - Last 6 months: 61.7% sales led vs 38.3% organic - - Last 3 months: 61.3% sales led vs 38.6% organic - author: Drew - replies: [] - - text: 'safe to try getting rid of them all for a couple months? ' - author: Drew - replies: - - text: Not sure - author: Jordan - - text: >- - I got 68% sales-led all time from the excel sheet that Taylor shared - back in October - author: Grayson Crickman - replies: [] - - text: >- - Pricing - https://www.notion.so/parabol/84e7bd61edfc40aeb6331a4e48c8085c?v=9fc22914219f457982e9a3e36d31ecc1 - author: Lorena MartĆ­nez - replies: - - text: Here are the limits and special features per Tier - author: Lorena MartĆ­nez - - text: >- - Lorena adds that folks churn from template landing pages when they find - the template isnā€™t free - author: Terry - replies: [] - - text: >- - Maybe being able to use premium templates once to evaluate them? Or - like: you are a free team, but you can use premium templates 10 times - until you run out of trial templates. - - - That means 10 meetings and they can evaluate 10 different templates or - use the same 10 times - author: Rafael Romero - replies: [] - - text: >- - About half of templates are free right now, all in "test: free" column: - https://www.notion.so/parabol/Templates-ce5a1b9ee7df47a1a2142c6e9e2edd8b - author: Georg Bremer - replies: [] - reflections: - - prompt: āœØNew - author: Jordan - text: >- - Team conversion: relax constraints such as template limits (need list of - conversion levers) - discussionId: 5 - - prompt: āœØNew - author: Charles - text: >- - A no credit card trial when a P0 signs up - along with a series of - emails showing off the benefits of Team - discussionId: 5 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 5 -- voteCount: 19 - title: Feature Team insights Team management - comments: - - text: >- - https://www.notion.so/parabol/Feature-Flag-Lifecycle-35075fae1be84e2a99c84068b3475a6e - author: "matt\_šŸ™ˆ" - replies: [] - - text: I'm pretty sure current version of team insights is rolled out - author: Georg Bremer - replies: [] - - text: https://eppo.cloud/experiments?show=1-25 - author: Nick O'Ferrall - replies: [] - - text: >- - I feel like a better metaphor is, we are asking users: which pizza - sounds better? i.e., we are not always testing the right set of metrics - for statistical significance, e.g., the AI discussion prompt might not - be a boosting factor for org growth, yet we are comparing number of - users added between control vs. experiment groups. - author: Bruce Tian - replies: [] - - text: 'Management interface PR: https://github.com/ParabolInc/parabol/pull/9285' - author: Jordan - replies: [] - reflections: - - prompt: āœˆļø In-Flight - author: Jordan - text: >- - Team Management: what choices shall we make here? We have a Figma - design, milestone, and partially completed branch - discussionId: 1 - - prompt: āœØNew - author: Charles - text: Insights+Reporting for Leaders - discussionId: 1 - - prompt: āœˆļø In-Flight - author: Charles - text: >- - Team Insights - I think it is still being tested. I'd vote for this - being a Team and Enterprise feature - discussionId: 1 - - prompt: āœˆļø In-Flight - author: Charles - text: >- - Org Admin Role & Managing Users/Teams for Enterprise orgs only. These - two together will make a lot of our current paid users happy - discussionId: 1 - - prompt: āœØNew - author: Charles - text: Improved Org/Team view UI - discussionId: 1 - - prompt: āœˆļø In-Flight - author: Drew - text: >- - I would love for us to release some kind of "super user" feature that - allows an Org Admin to see all teams, adjust team leads, adjust billing - leaders, etc - discussionId: 1 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 1 -- voteCount: 7 - title: Retros Integration - reflections: - - prompt: āœˆļø In-Flight - author: Jordan - text: 'Recurring retros: what''s left to do?' - discussionId: 7 - - prompt: āœˆļø In-Flight - author: Charles - text: >- - MS Teams and Azure DevOps Integrations. They were released under a - feature flag a while ago and have been used by users upon request - - let's roll them out! - discussionId: 7 - - prompt: āœˆļø In-Flight - author: Georg Bremer - text: >- - Recurring retros, especially having a recurring Gcal event associated - with it. - discussionId: 7 - - prompt: āœˆļø In-Flight - author: Bruce Tian - text: GCal integration - discussionId: 7 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 7 -- voteCount: 1 - title: Onboarding Improvements - reflections: - - prompt: āœˆļø In-Flight - author: Nick O'Ferrall - text: >- - Alicia created onboarding improvement recommendations. We could - implement some of them, depending on what other work we're considering: - https://www.notion.so/parabol/Onboarding-Recommendations-e02cce5464fa42f88a31b0ac9955adc7 - discussionId: 15 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 15 -- voteCount: 4 - title: Add Activity Button - reflections: - - prompt: šŸ›‘ Halt - author: Jordan - text: >- - Halt: add an activity button; I'd like to back this change out ā€“ the UX - is very clunky - discussionId: 10 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 10 -- voteCount: 4 - title: Kudos - comments: - - text: |- - I feel like we could have a card in team insights: - * # all kudos given this week - * # kudos you've given - * # kudos you've received - author: Georg Bremer - replies: [] - - text: 'people do love their kudos retro columns, they want that button :) ' - author: Drew - replies: - - text: >- - I agree, I think this would be cool. You can give people sincere - gratitude with reflections - author: Nick O'Ferrall - - text: Yes, I really want to put this draft over the edge - author: Georg Bremer - reflections: - - prompt: āœˆļø In-Flight - author: Jordan - text: >- - Kudos: much effort has been poured here, what if anything should be - released and at what scale? - discussionId: 12 - - prompt: šŸ›‘ Halt - author: Nick O'Ferrall - text: >- - Kudos. We'd need to build a leaderboard or some sort of kudos page - before promoting this feature. Personally, I'm not a fan of gamifying - appreciation as it can cheapen gratitude, so I'd prefer to stop work on - this. - discussionId: 12 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 12 -- voteCount: 2 - title: Release process and config management - reflections: - - prompt: āœØNew - author: Rafael Romero - text: Improve configuration management - discussionId: 14 - - prompt: šŸ›‘ Halt - author: Rafael Romero - text: >- - The release process is simple enough. No need to improve it more. If we - are not able to fix the merge conflict when merging the PRs to the - branch production, just drop that last part and use Gitlab pipeline to - release to prod. Simple and no more work to do. - discussionId: 14 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 14 -- voteCount: 18 - title: Activity library - reflections: - - prompt: āœˆļø In-Flight - author: Bruce Tian - text: Activity Library full rollout - discussionId: 2 - - prompt: āœˆļø In-Flight - author: Nick O'Ferrall - text: >- - Activity library. Still being tested: - https://eppo.cloud/experiments/8430 - discussionId: 2 - - prompt: āœØNew - author: Jordan - text: 'Activity Library: simple search' - discussionId: 2 - - prompt: āœØNew - author: Georg Bremer - text: >- - Finish up the custom template logic in the activity library to be on par - with the legacy dialog and finally remove the legacy dialog - discussionId: 2 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 2 -- voteCount: 3 - title: 1:1 and Ad Hoc Meetings - reflections: - - prompt: šŸ›‘ Halt - author: Bruce Tian - text: AdHoc Team & 1:1 - discussionId: 13 - - prompt: šŸ›‘ Halt - author: Jordan - text: Halt near-term future work on 1-on-1 meetings - discussionId: 13 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 13 -- voteCount: 4 - title: Ai summary Stand-up summaries - reflections: - - prompt: āœˆļø In-Flight - author: Bruce Tian - text: AI summary for standup - discussionId: 11 - - prompt: āœˆļø In-Flight - author: Jordan - text: 'Stand-up summaries: any reason this can''t be released to GA?' - discussionId: 11 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 11 -- voteCount: 9 - title: Zoom transcription - reflections: - - prompt: šŸ›‘ Halt - author: Jordan - text: >- - Defer: zoom transcription, I'd like to return to this after we've got - infrastructure in place for embedding and summarization - discussionId: 3 - - prompt: āœØNew - author: Nick O'Ferrall - text: >- - Video call transcription. Data is the key to building an AI product with - a business moat. This would be a useful feature, and it would give us - access to a lot more data. - discussionId: 3 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 3 -- voteCount: 7 - title: RethinkDB - comments: - - text: >- - Even a partial migration would: - - - Reduce our risk. Backups and restorations would be faster, smaller - and everything in PG can be restored using Point In-Time Recovery. - - - Saving šŸ’ø in the cloud, being able to reduce the RethinkDB instance to - a smaller one. - - - A full migration out of RethinkDB should: - - - Reduce around 15% of GCP cost per environment ($376.07264 $/month per - env => 2 envs => 752 $/month) - - - Make our backups consistent and reduce our time to make them to - virtually nothing. - - - Probably helping PubSec projects, simplifying the stack and removing - the unmaintained component. - - - Allow us to automatically deploy PPMIs: running databases in container - would be possible in a safe enough way. That would allow us to build - something to sell cheap and easy to deploy and maintain PPMIs - "automatically". - author: Rafael Romero - replies: [] - reflections: - - prompt: āœØNew - author: "matt\_šŸ™ˆ" - text: >- - getting RetroReflection and RetroReflectionGruop tables into PG. This - will halve our RethinkDB size & unlock the ability to move reflections - to tiptap - discussionId: 6 - - prompt: āœØNew - author: Rafael Romero - text: >- - RethinkDB. We must get rid of RethinkDB, even more if we are going to - enter in a do not touch mode. And we should start by RetroReflection and - RetroReflectionGroup. Those two make for almost the 50% of size and more - than 40% of docs. - discussionId: 6 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 6 -- voteCount: 9 - title: Embeddings - comments: - - text: >- - future thing: it would be VERY cool if this ended in a place where the - sales team could generate the reports themselves - author: Drew - replies: - - text: I think itā€™d be cool if you could demo it in the product šŸ¤” - author: Terry - - text: 'Here''s a milestone: https://github.com/ParabolInc/parabol/milestone/193' - author: Jordan - replies: [] - reflections: - - prompt: āœˆļø In-Flight - author: Jordan - text: >- - Embedder service: we got a milestone's work of backend effort. How to - share the love here? - discussionId: 4 - - prompt: āœˆļø In-Flight - author: Jordan - text: >- - Related Conversations: I can take point on PR to allow us to experiment - with this feature, and get a feel for how to use the Embedder tables - (and their efficacy) - discussionId: 4 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 4 -- voteCount: 5 - title: Ai Prompt - reflections: - - prompt: āœˆļø In-Flight - author: Bruce Tian - text: AI generated discussion prompt - discussionId: 9 - - prompt: āœˆļø In-Flight - author: Jordan - text: >- - AI Discussion prompt: what next step shall we take? This feature seems - rather safe... - discussionId: 9 - - prompt: āœˆļø In-Flight - author: Charles - text: >- - AI Generated Discussion Prompt - is this still under test? We should - release it for Team and Enterprise users only - discussionId: 9 - meetingName: 2024T1 Spring Cleaning šŸŒ±šŸ§¹ - date: 2024-02-19T23:50:06.859Z - meetingId: uEofN2TrmU - discussionId: 9 From fde45849f91b44ccf3536a77194bacf0ff9f769a Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 23 Jul 2024 17:48:50 +0100 Subject: [PATCH 17/43] update short meeting date --- .../graphql/public/mutations/helpers/getSummaries.ts | 9 ++------- .../server/graphql/public/mutations/helpers/getTopics.ts | 3 +-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts index 02cb91bdcc2..a4e82f76c3d 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -104,19 +104,14 @@ const getMeetingsContent = async (meeting: MeetingRetrospective, dataLoader: Dat } }) ) - const shortDate = meetingDate.toLocaleDateString('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric' - }) - console.log('šŸš€ ~ shortDate:', shortDate) + const shortMeetingDate = new Date(meetingDate).toISOString().split('T')[0] const res = { voteCount: voterIds.length, title: title, comments, reflections, meetingName, - date: shortDate, + date: shortMeetingDate, meetingId, discussionId: discussionIdx } diff --git a/packages/server/graphql/public/mutations/helpers/getTopics.ts b/packages/server/graphql/public/mutations/helpers/getTopics.ts index 163b320f1ba..a874c4131c2 100644 --- a/packages/server/graphql/public/mutations/helpers/getTopics.ts +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -74,9 +74,8 @@ const processLines = (lines: string[]): string[] => { let isValid = true matches.forEach((match) => { - let shortMeetingId = match.split(meetingURL)[1]?.split(/[),\s]/)[0] // Split by closing parenthesis, comma, or space + const shortMeetingId = match.split(meetingURL)[1]?.split(/[),\s]/)[0] // Split by closing parenthesis, comma, or space const actualMeetingId = shortMeetingId && (meetingLookup[shortMeetingId] as string) - console.log('šŸš€ ~ ________:', {actualMeetingId, meetingLookup}) if (shortMeetingId && actualMeetingId) { processedLine = processedLine.replace(shortMeetingId, actualMeetingId) From ca5c6fcd3e27edfefd7b4db06f76444e8888fba6 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 23 Jul 2024 17:52:08 +0100 Subject: [PATCH 18/43] move addInsight migration after merging master --- .../{1721225543186_addInsight.ts => 1721753460715_addInsight.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/server/postgres/migrations/{1721225543186_addInsight.ts => 1721753460715_addInsight.ts} (100%) diff --git a/packages/server/postgres/migrations/1721225543186_addInsight.ts b/packages/server/postgres/migrations/1721753460715_addInsight.ts similarity index 100% rename from packages/server/postgres/migrations/1721225543186_addInsight.ts rename to packages/server/postgres/migrations/1721753460715_addInsight.ts From 6e5aeeb11c3a1441f672ddf11e92f03b8e5fbaad Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 23 Jul 2024 18:05:38 +0100 Subject: [PATCH 19/43] fix insight start end date insert --- .../server/graphql/public/mutations/generateInsight.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 1a2e3e5d886..b2e84d6208f 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -9,15 +9,13 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( {teamId, startDate, endDate, useSummaries = true}, {dataLoader} ) => { - const start = new Date(startDate) - const end = new Date(endDate) - if (isNaN(start.getTime()) || isNaN(end.getTime())) { + if (isNaN(startDate.getTime()) || 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 (end.getTime() - start.getTime() < oneWeekInMs) { + if (endDate.getTime() - startDate.getTime() < oneWeekInMs) { return standardError(new Error('The end date must be at least one week after the start date.')) } @@ -36,8 +34,8 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( teamId, wins, challenges, - startDate, - endDate + startDateTime: startDate, + endDateTime: endDate }) .execute() From 5b1964e6ebeecce3fe7e028e606f40c8fd4216ab Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 24 Jul 2024 11:06:35 +0100 Subject: [PATCH 20/43] return prev insight if dates and teamid exist --- .../graphql/public/mutations/generateInsight.ts | 17 ++++++++++++++++- .../public/mutations/helpers/getSummaries.ts | 3 ++- ...ddInsight.ts => 1721753460716_addInsight.ts} | 4 ++-- packages/server/utils/OpenAIServerManager.ts | 2 +- 4 files changed, 21 insertions(+), 5 deletions(-) rename packages/server/postgres/migrations/{1721753460715_addInsight.ts => 1721753460716_addInsight.ts} (95%) diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index b2e84d6208f..b4e931606c4 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -9,6 +9,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( {teamId, startDate, endDate, useSummaries = true}, {dataLoader} ) => { + const pg = getKysely() if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { return standardError( new Error('Invalid date format. Please use ISO 8601 format (e.g., 2024-01-01T00:00:00Z).') @@ -19,6 +20,21 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( return standardError(new Error('The end date must be at least one week after the start date.')) } + const existingInsight = await pg + .selectFrom('Insight') + .select(['teamId', 'startDateTime', 'endDateTime', 'wins', 'challenges']) + .where('teamId', '=', teamId) + .where('startDateTime', '=', startDate) + .where('endDateTime', '=', endDate) + .executeTakeFirst() + + if (existingInsight) { + return { + wins: existingInsight.wins, + challenges: existingInsight.challenges + } + } + const response = useSummaries ? await getSummaries(teamId, startDate, endDate, dataLoader) : await getTopics(teamId, startDate, endDate, dataLoader) @@ -27,7 +43,6 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( return response } const {wins, challenges} = response - const pg = getKysely() await pg .insertInto('Insight') .values({ diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts index a4e82f76c3d..f461f6ceec0 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -152,7 +152,7 @@ export const getSummaries = async ( const summaries = await Promise.all( rawMeetings.map(async (meeting) => { - // this is temporary, just to see what it looks like when we create summaries on the fly + // newlyGeneratedSummariesDate is temporary, just to see what it looks like when we create summaries on the fly // if we go with a summary of summaries approach, remove this and create a separate mutation that generates new meeting summaries which include links to discussions const newlyGeneratedSummariesDate = new Date('2024-07-22T00:00:00Z') if (meeting.summary && meeting.updatedAt > newlyGeneratedSummariesDate) { @@ -173,6 +173,7 @@ export const getSummaries = async ( const manager = new OpenAIServerManager() const newSummary = await manager.generateSummary(yamlData) + console.log('šŸš€ ~ newSummary:', newSummary) if (!newSummary) return null const now = new Date() diff --git a/packages/server/postgres/migrations/1721753460715_addInsight.ts b/packages/server/postgres/migrations/1721753460716_addInsight.ts similarity index 95% rename from packages/server/postgres/migrations/1721753460715_addInsight.ts rename to packages/server/postgres/migrations/1721753460716_addInsight.ts index 0026d0a3ff5..386b966ae75 100644 --- a/packages/server/postgres/migrations/1721753460715_addInsight.ts +++ b/packages/server/postgres/migrations/1721753460716_addInsight.ts @@ -10,8 +10,8 @@ export async function up() { "teamId" VARCHAR(100) NOT NULL, "startDateTime" TIMESTAMP WITH TIME ZONE NOT NULL, "endDateTime" TIMESTAMP WITH TIME ZONE NOT NULL, - "wins" JSONB, - "challenges" JSONB, + "wins" TEXT[], + "challenges" TEXT[], "createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL ); diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 6886ae36683..a81421c2bfa 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -371,7 +371,7 @@ class OpenAIServerManager { 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. - Try to cite direct quotes from the meeting, attributing it to the person who wrote it, if they're included in the summary. + Cite direct quotes from the meeting, attributing it 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. From bc7b351e291abed194003759b69c40427712e375 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 24 Jul 2024 11:53:34 +0100 Subject: [PATCH 21/43] update generate insight prompt to reduce jargon --- .../graphql/public/mutations/helpers/getSummaries.ts | 4 ++++ packages/server/utils/OpenAIServerManager.ts | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts index f461f6ceec0..366888b24ca 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -150,6 +150,10 @@ export const getSummaries = async ( ) .run()) as MeetingRetrospective[] + if (!rawMeetings.length) { + return standardError(new Error('No meetings found')) + } + const summaries = await Promise.all( rawMeetings.map(async (meeting) => { // newlyGeneratedSummariesDate is temporary, just to see what it looks like when we create summaries on the fly diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index a81421c2bfa..fde40d494d7 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -349,7 +349,7 @@ class OpenAIServerManager { if (!this.openAIApi) return null const meetingURL = 'https://action.parabol.co/meet/' const defaultPrompt = ` - You are a management consultant who needs to discover behavioral trends for a given team. + 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. @@ -357,7 +357,7 @@ class OpenAIServerManager { 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 professional. No yapping. + 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"], @@ -366,7 +366,7 @@ class OpenAIServerManager { ` const promptForSummaries = ` - You are a management consultant who needs to discover behavioral trends for a given team. + 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. @@ -380,7 +380,7 @@ class OpenAIServerManager { "wins": ["bullet point 1", "bullet point 2", "bullet point 3"], "challenges": ["bullet point 1", "bullet point 2"] } - Your tone should be kind and professional. No yapping. + Your tone should be kind and straight forward. Use plain English. No yapping. ` const prompt = useSummaries ? promptForSummaries : defaultPrompt From f9e34491e040ad58f6d98606ca4168c58402c983 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 24 Jul 2024 11:59:57 +0100 Subject: [PATCH 22/43] update migration to make wins and challenges non null --- .../server/postgres/migrations/1721753460716_addInsight.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/postgres/migrations/1721753460716_addInsight.ts b/packages/server/postgres/migrations/1721753460716_addInsight.ts index 386b966ae75..7127a688cb8 100644 --- a/packages/server/postgres/migrations/1721753460716_addInsight.ts +++ b/packages/server/postgres/migrations/1721753460716_addInsight.ts @@ -10,8 +10,8 @@ export async function up() { "teamId" VARCHAR(100) NOT NULL, "startDateTime" TIMESTAMP WITH TIME ZONE NOT NULL, "endDateTime" TIMESTAMP WITH TIME ZONE NOT NULL, - "wins" TEXT[], - "challenges" TEXT[], + "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 ); From aeb8fa86f78cc4bffab34fbb52111092cd444591 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Fri, 26 Jul 2024 17:25:49 +0100 Subject: [PATCH 23/43] accept prompt as arg in generateInsight --- .../mutations/GenerateInsightMutation.ts | 5 + .../public/mutations/generateInsight.ts | 44 ++--- .../public/mutations/helpers/getSummaries.ts | 179 ++---------------- .../public/mutations/helpers/getTopics.ts | 9 +- .../public/typeDefs/generateInsight.graphql | 10 +- .../public/types/GenerateInsightSuccess.ts | 17 ++ packages/server/utils/OpenAIServerManager.ts | 14 +- 7 files changed, 71 insertions(+), 207 deletions(-) diff --git a/packages/client/mutations/GenerateInsightMutation.ts b/packages/client/mutations/GenerateInsightMutation.ts index 885d54b82c9..4cb2364152f 100644 --- a/packages/client/mutations/GenerateInsightMutation.ts +++ b/packages/client/mutations/GenerateInsightMutation.ts @@ -7,6 +7,9 @@ graphql` fragment GenerateInsightMutation_team on GenerateInsightSuccess { wins challenges + meetings { + id + } } ` @@ -16,12 +19,14 @@ const mutation = graphql` $startDate: DateTime! $endDate: DateTime! $useSummaries: Boolean + $prompt: String ) { generateInsight( teamId: $teamId startDate: $startDate endDate: $endDate useSummaries: $useSummaries + prompt: $prompt ) { ... on ErrorPayload { error { diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index b4e931606c4..b4b9ffe11de 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -6,10 +6,9 @@ import {getTopics} from './helpers/getTopics' const generateInsight: MutationResolvers['generateInsight'] = async ( _source, - {teamId, startDate, endDate, useSummaries = true}, + {teamId, startDate, endDate, useSummaries = true, prompt}, {dataLoader} ) => { - const pg = getKysely() if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { return standardError( new Error('Invalid date format. Please use ISO 8601 format (e.g., 2024-01-01T00:00:00Z).') @@ -20,39 +19,26 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( return standardError(new Error('The end date must be at least one week after the start date.')) } - const existingInsight = await pg - .selectFrom('Insight') - .select(['teamId', 'startDateTime', 'endDateTime', 'wins', 'challenges']) - .where('teamId', '=', teamId) - .where('startDateTime', '=', startDate) - .where('endDateTime', '=', endDate) - .executeTakeFirst() - - if (existingInsight) { - return { - wins: existingInsight.wins, - challenges: existingInsight.challenges - } - } - const response = useSummaries - ? await getSummaries(teamId, startDate, endDate, dataLoader) - : await getTopics(teamId, startDate, endDate, dataLoader) + ? await getSummaries(teamId, startDate, endDate, prompt as string) + : await getTopics(teamId, startDate, endDate, dataLoader, prompt as string) if ('error' in response) { return response } const {wins, challenges} = response - await pg - .insertInto('Insight') - .values({ - teamId, - wins, - challenges, - startDateTime: startDate, - endDateTime: endDate - }) - .execute() + const pg = getKysely() + + // await pg + // .insertInto('Insight') + // .values({ + // teamId, + // wins, + // challenges, + // startDateTime: startDate, + // endDateTime: endDate + // }) + // .execute() return response } diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts index 366888b24ca..6e8ba4a7c3a 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -1,136 +1,14 @@ 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 getPhase from '../../../../utils/getPhase' 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 - } - }) - ) - const res = { - text: plaintextContent, - author: commentAuthor, - replies: commentReplies - } - if (res.replies.length === 0) { - delete (res as any).commentReplies - } - return res - }) - ) - return comments -} - -const getMeetingsContent = async (meeting: MeetingRetrospective, dataLoader: DataLoaderWorker) => { - const pg = getKysely() - const {id: meetingId, disableAnonymity, name: meetingName, createdAt: meetingDate} = meeting - 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, discussion] = await Promise.all([ - getComments(reflectionGroupId, dataLoader), - dataLoader.get('retroReflectionsByGroupId').load(group.id), - pg - .selectFrom('Discussion') - .selectAll() - .where('discussionTopicId', '=', reflectionGroupId) - .limit(1) - .executeTakeFirst() - ]) - const discussPhase = getPhase(meeting.phases, 'discuss') - const {stages} = discussPhase - const stageIdx = stages - .sort((a, b) => (a.sortOrder < b.sortOrder ? -1 : 1)) - .findIndex((stage) => stage.discussionId === discussion?.id) - const discussionIdx = stageIdx + 1 - - 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, - discussionId: discussionIdx - } - }) - ) - const shortMeetingDate = new Date(meetingDate).toISOString().split('T')[0] - const res = { - voteCount: voterIds.length, - title: title, - comments, - reflections, - meetingName, - date: shortMeetingDate, - meetingId, - discussionId: discussionIdx - } - - if (!res.comments || !res.comments.length) { - delete (res as any).comments - } - return res - }) - ) - - return reflectionGroups -} export const getSummaries = async ( teamId: string, startDate: Date, endDate: Date, - dataLoader: DataLoaderWorker + prompt?: string ) => { const r = await getRethink() const MIN_MILLISECONDS = 60 * 1000 // 1 minute @@ -147,6 +25,7 @@ export const getSummaries = async ( .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[] @@ -154,61 +33,25 @@ export const getSummaries = async ( return standardError(new Error('No meetings found')) } - const summaries = await Promise.all( - rawMeetings.map(async (meeting) => { - // newlyGeneratedSummariesDate is temporary, just to see what it looks like when we create summaries on the fly - // if we go with a summary of summaries approach, remove this and create a separate mutation that generates new meeting summaries which include links to discussions - const newlyGeneratedSummariesDate = new Date('2024-07-22T00:00:00Z') - if (meeting.summary && meeting.updatedAt > newlyGeneratedSummariesDate) { - return { - meetingName: meeting.name, - date: meeting.createdAt, - summary: meeting.summary - } - } - const meetingsContent = await getMeetingsContent(meeting, dataLoader) - if (!meetingsContent || meetingsContent.length === 0) { - return null - } - const yamlData = yaml.dump(meetingsContent, { - noCompatMode: true - }) - // fs.writeFileSync('meetingSummary.yaml', yamlData, 'utf8') - - const manager = new OpenAIServerManager() - const newSummary = await manager.generateSummary(yamlData) - console.log('šŸš€ ~ newSummary:', newSummary) - if (!newSummary) return null - - const now = new Date() - await r - .table('NewMeeting') - .get(meeting.id) - .update({summary: newSummary, updatedAt: now}) - .run() - meeting.summary = newSummary - return { - meetingName: meeting.name, - date: meeting.createdAt, - summary: meeting.summary - } - }) - ) + const summaries = rawMeetings.map((meeting) => ({ + meetingName: meeting.name, + date: meeting.createdAt, + summary: meeting.summary + })) - const meetingsContent = summaries.filter((summary) => summary) - const yamlData = yaml.dump(meetingsContent, { + const yamlData = yaml.dump(summaries, { noCompatMode: true }) - // fs.writeFileSync('summaryMeetingContent.yaml', yamlData, 'utf8') const openAI = new OpenAIServerManager() - const rawInsight = await openAI.generateInsight(yamlData, true) + const rawInsight = await openAI.generateInsight(yamlData, true, prompt) if (!rawInsight) { return standardError(new Error('No insights generated')) } return { wins: rawInsight.wins, - challenges: rawInsight.challenges + 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 index a874c4131c2..8567c3a8392 100644 --- a/packages/server/graphql/public/mutations/helpers/getTopics.ts +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -107,7 +107,8 @@ export const getTopics = async ( teamId: string, startDate: Date, endDate: Date, - dataLoader: DataLoaderWorker + dataLoader: DataLoaderWorker, + prompt?: string ) => { const r = await getRethink() const MIN_REFLECTION_COUNT = 3 @@ -213,16 +214,16 @@ export const getTopics = async ( const yamlData = yaml.dump(shortTokenedTopics, { noCompatMode: true }) - // fs.writeFileSync('summaryMeetingContent.yaml', yamlData, 'utf8') const openAI = new OpenAIServerManager() - const rawInsight = await openAI.generateInsight(yamlData, false) + 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} + return {wins, challenges, meetingIds} } diff --git a/packages/server/graphql/public/typeDefs/generateInsight.graphql b/packages/server/graphql/public/typeDefs/generateInsight.graphql index 4469ec0da39..d07b656a8f0 100644 --- a/packages/server/graphql/public/typeDefs/generateInsight.graphql +++ b/packages/server/graphql/public/typeDefs/generateInsight.graphql @@ -7,6 +7,7 @@ extend type Mutation { startDate: DateTime! endDate: DateTime! useSummaries: Boolean + prompt: String ): GenerateInsightPayload! } @@ -19,10 +20,15 @@ type GenerateInsightSuccess { """ The insights generated focusing on the wins of the team """ - wins: [String] + wins: [String!]! """ The insights generated focusing on the challenges team are facing """ - challenges: [String] + 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 index 1d8419965f7..ac099aa2295 100644 --- a/packages/server/graphql/public/types/GenerateInsightSuccess.ts +++ b/packages/server/graphql/public/types/GenerateInsightSuccess.ts @@ -1,4 +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/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index fde40d494d7..ad3f12ac489 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -345,10 +345,14 @@ class OpenAIServerManager { } } - async generateInsight(yamlData: string, useSummaries: boolean): Promise { + async generateInsight( + yamlData: string, + useSummaries: boolean, + userPrompt?: string + ): Promise { if (!this.openAIApi) return null const meetingURL = 'https://action.parabol.co/meet/' - const defaultPrompt = ` + 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. @@ -371,10 +375,11 @@ class OpenAIServerManager { 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 it to the person who wrote it, if they're included in the summary. + 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"], @@ -383,7 +388,8 @@ class OpenAIServerManager { Your tone should be kind and straight forward. Use plain English. No yapping. ` - const prompt = useSummaries ? promptForSummaries : defaultPrompt + const defaultPrompt = useSummaries ? promptForSummaries : promptForMeetingData + const prompt = userPrompt ? userPrompt : defaultPrompt try { const response = await this.openAIApi.chat.completions.create({ From cc9768ce630b42ebfcd835817f5153acc9d4672a Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Fri, 26 Jul 2024 17:28:30 +0100 Subject: [PATCH 24/43] update migration order --- .../public/mutations/generateInsight.ts | 20 +++++++++---------- ...Insight.ts => 1722011287034_addInsight.ts} | 0 2 files changed, 10 insertions(+), 10 deletions(-) rename packages/server/postgres/migrations/{1721753460716_addInsight.ts => 1722011287034_addInsight.ts} (100%) diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index b4b9ffe11de..75f77093f82 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -29,16 +29,16 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( const {wins, challenges} = response const pg = getKysely() - // await pg - // .insertInto('Insight') - // .values({ - // teamId, - // wins, - // challenges, - // startDateTime: startDate, - // endDateTime: endDate - // }) - // .execute() + await pg + .insertInto('Insight') + .values({ + teamId, + wins, + challenges, + startDateTime: startDate, + endDateTime: endDate + }) + .execute() return response } diff --git a/packages/server/postgres/migrations/1721753460716_addInsight.ts b/packages/server/postgres/migrations/1722011287034_addInsight.ts similarity index 100% rename from packages/server/postgres/migrations/1721753460716_addInsight.ts rename to packages/server/postgres/migrations/1722011287034_addInsight.ts From a3ba8d0960fb4d893ca8a08ccff9e447f38fe71b Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Fri, 26 Jul 2024 17:52:26 +0100 Subject: [PATCH 25/43] remove meetings from generateInsight query --- packages/client/mutations/GenerateInsightMutation.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/client/mutations/GenerateInsightMutation.ts b/packages/client/mutations/GenerateInsightMutation.ts index 4cb2364152f..d7bbe485b4f 100644 --- a/packages/client/mutations/GenerateInsightMutation.ts +++ b/packages/client/mutations/GenerateInsightMutation.ts @@ -7,9 +7,6 @@ graphql` fragment GenerateInsightMutation_team on GenerateInsightSuccess { wins challenges - meetings { - id - } } ` From 5c191ef5776d3ff1f39eda67a7e01270d8486870 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Fri, 26 Jul 2024 18:05:09 +0100 Subject: [PATCH 26/43] use number.isNaN instead --- packages/server/graphql/public/mutations/generateInsight.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 75f77093f82..16c35eaae78 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -9,7 +9,7 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( {teamId, startDate, endDate, useSummaries = true, prompt}, {dataLoader} ) => { - if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) { + 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).') ) From a5c6a617f6f9673eb9b6148f305cce824d684737 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 29 Jul 2024 11:57:22 +0100 Subject: [PATCH 27/43] generate new meeting summary --- codegen.json | 41 ++-- .../GenerateMeetingSummaryMutation.ts | 43 ++++ .../mutations/generateMeetingSummary.ts | 218 ++++++++++++++++++ .../typeDefs/generateMeetingSummary.graphql | 23 ++ .../types/GenerateMeetingSummarySuccess.ts | 17 ++ packages/server/utils/OpenAIServerManager.ts | 1 + 6 files changed, 323 insertions(+), 20 deletions(-) create mode 100644 packages/client/mutations/GenerateMeetingSummaryMutation.ts create mode 100644 packages/server/graphql/public/mutations/generateMeetingSummary.ts create mode 100644 packages/server/graphql/public/typeDefs/generateMeetingSummary.graphql create mode 100644 packages/server/graphql/public/types/GenerateMeetingSummarySuccess.ts diff --git a/codegen.json b/codegen.json index a24c518013f..c40970799de 100644 --- a/codegen.json +++ b/codegen.json @@ -43,19 +43,7 @@ "packages/server/graphql/public/resolverTypes.ts": { "config": { "contextType": "../graphql#GQLContext", - "showUnusedMappers": false, "mappers": { - "_xGitLabProject": "./types/_xGitLabProject#_xGitLabProjectSource as _xGitLabProject", - "JiraServerIntegration": "./types/JiraServerIntegration#JiraServerIntegrationSource", - "GitHubIntegration": "../../postgres/queries/getGitHubAuthByUserIdTeamId#GitHubAuth", - "GitLabIntegration": "./types/GitLabIntegration#GitLabIntegrationSource", - "MattermostIntegration": "./types/MattermostIntegration#MattermostIntegrationSource", - "MSTeamsIntegration": "./types/MSTeamsIntegration#MSTeamsIntegrationSource", - "SlackIntegration": "../../database/types/SlackAuth#default as SlackAuthDB", - "SlackNotification": "../../database/types/SlackNotification#default as SlackNotificationDB", - "AzureDevOpsIntegration": ".types/AzureDevOpsIntegration#AzureDevOpsIntegrationSource", - "AzureDevOpsWorkItem": "../../dataloader/azureDevOpsLoaders#AzureDevOpsWorkItem", - "AzureDevOpsRemoteProject": "./types/AzureDevOpsRemoteProject#AzureDevOpsRemoteProjectSource", "AcceptRequestToJoinDomainSuccess": "./types/AcceptRequestToJoinDomainSuccess#AcceptRequestToJoinDomainSuccessSource", "AcceptTeamInvitationPayload": "./types/AcceptTeamInvitationPayload#AcceptTeamInvitationPayloadSource", "ActionMeeting": "../../database/types/MeetingAction#default", @@ -70,9 +58,11 @@ "AgendaItem": "../../database/types/AgendaItem#default as AgendaItemDB", "ArchiveTeamPayload": "./types/ArchiveTeamPayload#ArchiveTeamPayloadSource", "AtlassianIntegration": "../../postgres/queries/getAtlassianAuthByUserIdTeamId#AtlassianAuth as AtlassianAuthDB", - "JiraSearchQuery": "../../database/types/JiraSearchQuery#default as JiraSearchQueryDB", "AuthTokenPayload": "./types/AuthTokenPayload#AuthTokenPayloadSource", "AutogroupSuccess": "./types/AutogroupSuccess#AutogroupSuccessSource", + "AzureDevOpsIntegration": ".types/AzureDevOpsIntegration#AzureDevOpsIntegrationSource", + "AzureDevOpsRemoteProject": "./types/AzureDevOpsRemoteProject#AzureDevOpsRemoteProjectSource", + "AzureDevOpsWorkItem": "../../dataloader/azureDevOpsLoaders#AzureDevOpsWorkItem", "BatchArchiveTasksSuccess": "./types/BatchArchiveTasksSuccess#BatchArchiveTasksSuccessSource", "Comment": "../../database/types/Comment#default as CommentDB", "Company": "./types/Company#CompanySource", @@ -86,15 +76,22 @@ "GcalIntegration": "./types/GcalIntegration#GcalIntegrationSource", "GenerateGroupsSuccess": "./types/GenerateGroupsSuccess#GenerateGroupsSuccessSource", "GenerateInsightSuccess": "./types/GenerateInsightSuccess#GenerateInsightSuccessSource", + "GenerateMeetingSummarySuccess": "./types/GenerateMeetingSummarySuccess#GenerateMeetingSummarySuccessSource", "GetTemplateSuggestionSuccess": "./types/GetTemplateSuggestionSuccess#GetTemplateSuggestionSuccessSource", - "IntegrationProviderWebhook": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", + "GitHubIntegration": "../../postgres/queries/getGitHubAuthByUserIdTeamId#GitHubAuth", + "GitLabIntegration": "./types/GitLabIntegration#GitLabIntegrationSource", "IntegrationProviderOAuth1": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", "IntegrationProviderOAuth2": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", + "IntegrationProviderWebhook": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider", "InviteToTeamPayload": "./types/InviteToTeamPayload#InviteToTeamPayloadSource", - "JiraServerIssue": "./types/JiraServerIssue#JiraServerIssueSource", - "JiraServerRemoteProject": "../../dataloader/jiraServerLoaders#JiraServerProject", "JiraIssue": "./types/JiraIssue#JiraIssueSource", "JiraRemoteProject": "./types/JiraRemoteProject#JiraRemoteProjectSource", + "JiraSearchQuery": "../../database/types/JiraSearchQuery#default as JiraSearchQueryDB", + "JiraServerIntegration": "./types/JiraServerIntegration#JiraServerIntegrationSource", + "JiraServerIssue": "./types/JiraServerIssue#JiraServerIssueSource", + "JiraServerRemoteProject": "../../dataloader/jiraServerLoaders#JiraServerProject", + "MSTeamsIntegration": "./types/MSTeamsIntegration#MSTeamsIntegrationSource", + "MattermostIntegration": "./types/MattermostIntegration#MattermostIntegrationSource", "MeetingSeries": "../../postgres/types/MeetingSeries#MeetingSeries", "MeetingTemplate": "../../database/types/MeetingTemplate#default", "NewMeeting": "../../postgres/types/Meeting#AnyMeeting", @@ -123,8 +120,8 @@ "ReflectTemplate": "../../database/types/ReflectTemplate#default", "RemoveApprovedOrganizationDomainsSuccess": "./types/RemoveApprovedOrganizationDomainsSuccess#RemoveApprovedOrganizationDomainsSuccessSource", "RemoveIntegrationSearchQuerySuccess": "./types/RemoveIntegrationSearchQuerySuccess#RemoveIntegrationSearchQuerySuccessSource", - "RemoveTeamMemberPayload": "./types/RemoveTeamMemberPayload#RemoveTeamMemberPayloadSource", "RemoveTeamMemberIntegrationAuthSuccess": "./types/RemoveTeamMemberIntegrationAuthPayload#RemoveTeamMemberIntegrationAuthSuccessSource", + "RemoveTeamMemberPayload": "./types/RemoveTeamMemberPayload#RemoveTeamMemberPayloadSource", "RequestToJoinDomainSuccess": "./types/RequestToJoinDomainSuccess#RequestToJoinDomainSuccessSource", "ResetReflectionGroupsSuccess": "./types/ResetReflectionGroupsSuccess#ResetReflectionGroupsSuccessSource", "RetroReflection": "./types/RetroReflection#RetroReflectionSource", @@ -137,6 +134,8 @@ "SetNotificationStatusPayload": "./types/SetNotificationStatusPayload#SetNotificationStatusPayloadSource", "SetOrgUserRoleSuccess": "./types/SetOrgUserRoleSuccess#SetOrgUserRoleSuccessSource", "ShareTopicSuccess": "./types/ShareTopicSuccess#ShareTopicSuccessSource", + "SlackIntegration": "../../database/types/SlackAuth#default as SlackAuthDB", + "SlackNotification": "../../database/types/SlackNotification#default as SlackNotificationDB", "StartCheckInSuccess": "./types/StartCheckInSuccess#StartCheckInSuccessSource", "StartRetrospectiveSuccess": "./types/StartRetrospectiveSuccess#StartRetrospectiveSuccessSource", "StartTeamPromptSuccess": "./types/StartTeamPromptSuccess#StartTeamPromptSuccessSource", @@ -147,9 +146,9 @@ "TeamHealthStage": "./types/TeamHealthStage#TeamHealthStageSource", "TeamInvitation": "../../database/types/TeamInvitation#default", "TeamMember": "../../database/types/TeamMember#default as TeamMemberDB", - "TeamMemberIntegrationAuthWebhook": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", "TeamMemberIntegrationAuthOAuth1": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", "TeamMemberIntegrationAuthOAuth2": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", + "TeamMemberIntegrationAuthWebhook": "../../postgres/queries/getTeamMemberIntegrationAuth#TeamMemberIntegrationAuth", "TeamMemberIntegrations": "./types/TeamMemberIntegrations#TeamMemberIntegrationsSource", "TeamPromptMeeting": "../../database/types/MeetingTeamPrompt#default as MeetingTeamPromptDB", "TeamPromptMeetingMember": "../../database/types/TeamPromptMeetingMember#default as TeamPromptMeetingMemberDB", @@ -175,8 +174,10 @@ "UpgradeToTeamTierSuccess": "./types/UpgradeToTeamTierSuccess#UpgradeToTeamTierSuccessSource", "UpsertTeamPromptResponseSuccess": "./types/UpsertTeamPromptResponseSuccess#UpsertTeamPromptResponseSuccessSource", "User": "../../postgres/types/IUser#default as IUser", - "UserLogInPayload": "./types/UserLogInPayload#UserLogInPayloadSource" - } + "UserLogInPayload": "./types/UserLogInPayload#UserLogInPayloadSource", + "_xGitLabProject": "./types/_xGitLabProject#_xGitLabProjectSource as _xGitLabProject" + }, + "showUnusedMappers": false }, "plugins": ["typescript", "typescript-resolvers", "add"], "schema": "packages/server/graphql/public/schema.graphql" diff --git a/packages/client/mutations/GenerateMeetingSummaryMutation.ts b/packages/client/mutations/GenerateMeetingSummaryMutation.ts new file mode 100644 index 00000000000..3508ada5c0c --- /dev/null +++ b/packages/client/mutations/GenerateMeetingSummaryMutation.ts @@ -0,0 +1,43 @@ +import graphql from 'babel-plugin-relay/macro' +import {commitMutation} from 'react-relay' +import {GenerateMeetingSummaryMutation as TGenerateMeetingSummaryMutation} from '../__generated__/GenerateMeetingSummaryMutation.graphql' +import {StandardMutation} from '../types/relayMutations' + +graphql` + fragment GenerateMeetingSummaryMutation_meeting on GenerateMeetingSummarySuccess { + meetings { + summary + } + } +` + +const mutation = graphql` + mutation GenerateMeetingSummaryMutation($teamIds: [ID!]!) { + generateMeetingSummary(teamIds: $teamIds) { + ... on ErrorPayload { + error { + message + } + } + ...GenerateMeetingSummaryMutation_meeting @relay(mask: false) + } + } +` + +const GenerateMeetingSummaryMutation: StandardMutation = ( + atmosphere, + variables, + {onError, onCompleted} +) => { + return commitMutation(atmosphere, { + mutation, + variables, + optimisticUpdater: (store) => { + const {} = variables + }, + onCompleted, + onError + }) +} + +export default GenerateMeetingSummaryMutation diff --git a/packages/server/graphql/public/mutations/generateMeetingSummary.ts b/packages/server/graphql/public/mutations/generateMeetingSummary.ts new file mode 100644 index 00000000000..0106bae2349 --- /dev/null +++ b/packages/server/graphql/public/mutations/generateMeetingSummary.ts @@ -0,0 +1,218 @@ +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 {getUserId} from '../../../utils/authorization' +import getPhase from '../../../utils/getPhase' +import {MutationResolvers} from '../resolverTypes' + +const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = async ( + _source, + {teamIds}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const viewerId = getUserId(authToken) + const now = new Date() + const r = await getRethink() + const pg = getKysely() + const MIN_MILLISECONDS = 60 * 1000 // 1 minute + const MIN_REFLECTION_COUNT = 3 + + const endDate = new Date() + const startDate = new Date() + startDate.setFullYear(endDate.getFullYear() - 1) + + const rawMeetings = (await r + .table('NewMeeting') + .getAll(r.args(teamIds), {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[] + + // const summaries = rawMeetings.map((meeting) => ({ + // meetingName: meeting.name, + // date: meeting.createdAt, + // summary: meeting.summary + // })) + console.log('šŸš€ ~ summaries:', rawMeetings.length) + + const getComments = async (reflectionGroupId: string) => { + 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 + } + }) + ) + const res = { + text: plaintextContent, + author: commentAuthor, + replies: commentReplies + } + if (res.replies.length === 0) { + delete (res as any).commentReplies + } + return res + }) + ) + return comments + } + + const getMeetingsContent = async (meeting: MeetingRetrospective) => { + const pg = getKysely() + const {id: meetingId, disableAnonymity, name: meetingName, createdAt: meetingDate} = meeting + console.log('šŸš€ ~ getMeetingsContent:', meetingId) + const rawReflectionGroups = await dataLoader + .get('retroReflectionGroupsByMeetingId') + .load(meetingId) + console.log('šŸš€ ~ rawReflectionGroups:', rawReflectionGroups.length) + const reflectionGroups = Promise.all( + rawReflectionGroups + .filter((g) => g.voterIds.length > 1) + .map(async (group) => { + const {id: reflectionGroupId, voterIds, title} = group + const [comments, rawReflections, discussion] = await Promise.all([ + getComments(reflectionGroupId), + dataLoader.get('retroReflectionsByGroupId').load(group.id), + pg + .selectFrom('Discussion') + .selectAll() + .where('discussionTopicId', '=', reflectionGroupId) + .limit(1) + .executeTakeFirst() + ]) + const discussPhase = getPhase(meeting.phases, 'discuss') + const {stages} = discussPhase + const stageIdx = stages + .sort((a, b) => (a.sortOrder < b.sortOrder ? -1 : 1)) + .findIndex((stage) => stage.discussionId === discussion?.id) + const discussionIdx = stageIdx + 1 + + 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, + discussionId: discussionIdx + } + }) + ) + const shortMeetingDate = new Date(meetingDate).toISOString().split('T')[0] + const res = { + voteCount: voterIds.length, + title: title, + comments, + reflections, + meetingName, + date: shortMeetingDate, + meetingId, + discussionId: discussionIdx + } + + if (!res.comments || !res.comments.length) { + delete (res as any).comments + } + return res + }) + ) + console.log('šŸš€ ~ reflectionGroups:', reflectionGroups) + + return reflectionGroups + } + + const summaries = await Promise.all( + rawMeetings.map(async (meeting) => { + // newlyGeneratedSummariesDate is temporary, just to see what it looks like when we create summaries on the fly + // if we go with a summary of summaries approach, remove this and create a separate mutation that generates new meeting summaries which include links to discussions + // const newlyGeneratedSummariesDate = new Date('2024-07-22T00:00:00Z') + // if (meeting.summary && meeting.updatedAt > newlyGeneratedSummariesDate) { + // return { + // meetingName: meeting.name, + // date: meeting.createdAt, + // summary: meeting.summary + // } + // } + const meetingsContent = await getMeetingsContent(meeting) + console.log('šŸš€ ~ meetingsContent:', meetingsContent) + if (!meetingsContent || meetingsContent.length === 0) { + return null + } + const yamlData = yaml.dump(meetingsContent, { + noCompatMode: true + }) + + const manager = new OpenAIServerManager() + console.log('gen sum', meeting.id) + const newSummary = await manager.generateSummary(yamlData) + console.log('šŸš€ ~ newSummary:', newSummary) + if (!newSummary) return null + + const now = new Date() + // await r + // .table('NewMeeting') + // .get(meeting.id) + // .update({summary: newSummary, updatedAt: now}) + // .run() + // meeting.summary = newSummary + return { + meetingName: meeting.name, + date: meeting.createdAt, + summary: newSummary + // summary: meeting.summary + } + }) + ) + console.log('šŸš€ ~ summaries:', summaries) + + // RESOLUTION + const data = {meetingIds: rawMeetings.map((meeting) => meeting.id)} + return data +} + +export default generateMeetingSummary diff --git a/packages/server/graphql/public/typeDefs/generateMeetingSummary.graphql b/packages/server/graphql/public/typeDefs/generateMeetingSummary.graphql new file mode 100644 index 00000000000..93fcd19a333 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/generateMeetingSummary.graphql @@ -0,0 +1,23 @@ +extend type Mutation { + """ + Generate new meeting summaries for every meeting in the given teams + """ + generateMeetingSummary( + """ + The ids of the teams to generate the meeting summary for + """ + teamIds: [ID!]! + ): GenerateMeetingSummaryPayload! +} + +""" +Return value for generateMeetingSummary, which could be an error +""" +union GenerateMeetingSummaryPayload = ErrorPayload | GenerateMeetingSummarySuccess + +type GenerateMeetingSummarySuccess { + """ + The meetings that were updated with new summaries + """ + meetings: [RetrospectiveMeeting!]! +} diff --git a/packages/server/graphql/public/types/GenerateMeetingSummarySuccess.ts b/packages/server/graphql/public/types/GenerateMeetingSummarySuccess.ts new file mode 100644 index 00000000000..8a572dec1e0 --- /dev/null +++ b/packages/server/graphql/public/types/GenerateMeetingSummarySuccess.ts @@ -0,0 +1,17 @@ +import MeetingRetrospective from '../../../database/types/MeetingRetrospective' +import {GenerateMeetingSummarySuccessResolvers} from '../resolverTypes' + +export type GenerateMeetingSummarySuccessSource = { + meetingIds: string[] +} + +const GenerateMeetingSummarySuccess: GenerateMeetingSummarySuccessResolvers = { + meetings: async ({meetingIds}, _args, {dataLoader}) => { + const meetings = (await dataLoader + .get('newMeetings') + .loadMany(meetingIds)) as MeetingRetrospective[] + return meetings + } +} + +export default GenerateMeetingSummarySuccess diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index ad3f12ac489..5110c9f217f 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -431,6 +431,7 @@ class OpenAIServerManager { // 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 + console.log('in generateSummary') 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. From e08219721498e5743810f6edb11b68c71f97e372 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 29 Jul 2024 17:44:07 +0100 Subject: [PATCH 28/43] create new summaries for old meetings --- .../mutations/generateMeetingSummary.ts | 67 +++++-------------- packages/server/graphql/public/permissions.ts | 1 + 2 files changed, 18 insertions(+), 50 deletions(-) diff --git a/packages/server/graphql/public/mutations/generateMeetingSummary.ts b/packages/server/graphql/public/mutations/generateMeetingSummary.ts index 0106bae2349..0fb340211f2 100644 --- a/packages/server/graphql/public/mutations/generateMeetingSummary.ts +++ b/packages/server/graphql/public/mutations/generateMeetingSummary.ts @@ -3,25 +3,22 @@ import getRethink from '../../../database/rethinkDriver' import MeetingRetrospective from '../../../database/types/MeetingRetrospective' import getKysely from '../../../postgres/getKysely' import OpenAIServerManager from '../../../utils/OpenAIServerManager' -import {getUserId} from '../../../utils/authorization' import getPhase from '../../../utils/getPhase' import {MutationResolvers} from '../resolverTypes' const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = async ( _source, {teamIds}, - {authToken, dataLoader, socketId: mutatorId} + {dataLoader} ) => { - const viewerId = getUserId(authToken) - const now = new Date() const r = await getRethink() const pg = getKysely() const MIN_MILLISECONDS = 60 * 1000 // 1 minute const MIN_REFLECTION_COUNT = 3 const endDate = new Date() - const startDate = new Date() - startDate.setFullYear(endDate.getFullYear() - 1) + const twoYearsAgo = new Date() + twoYearsAgo.setFullYear(endDate.getFullYear() - 2) const rawMeetings = (await r .table('NewMeeting') @@ -29,22 +26,14 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn .filter((row: any) => row('meetingType') .eq('retrospective') - .and(row('createdAt').ge(startDate)) + .and(row('createdAt').ge(twoYearsAgo)) .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[] - // const summaries = rawMeetings.map((meeting) => ({ - // meetingName: meeting.name, - // date: meeting.createdAt, - // summary: meeting.summary - // })) - console.log('šŸš€ ~ summaries:', rawMeetings.length) - const getComments = async (reflectionGroupId: string) => { const IGNORE_COMMENT_USER_IDS = ['parabolAIUser'] const discussion = await pg @@ -99,11 +88,9 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn const getMeetingsContent = async (meeting: MeetingRetrospective) => { const pg = getKysely() const {id: meetingId, disableAnonymity, name: meetingName, createdAt: meetingDate} = meeting - console.log('šŸš€ ~ getMeetingsContent:', meetingId) const rawReflectionGroups = await dataLoader .get('retroReflectionGroupsByMeetingId') .load(meetingId) - console.log('šŸš€ ~ rawReflectionGroups:', rawReflectionGroups.length) const reflectionGroups = Promise.all( rawReflectionGroups .filter((g) => g.voterIds.length > 1) @@ -161,57 +148,37 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn return res }) ) - console.log('šŸš€ ~ reflectionGroups:', reflectionGroups) return reflectionGroups } + const manager = new OpenAIServerManager() - const summaries = await Promise.all( + const updatedMeetingIds = await Promise.all( rawMeetings.map(async (meeting) => { - // newlyGeneratedSummariesDate is temporary, just to see what it looks like when we create summaries on the fly - // if we go with a summary of summaries approach, remove this and create a separate mutation that generates new meeting summaries which include links to discussions - // const newlyGeneratedSummariesDate = new Date('2024-07-22T00:00:00Z') - // if (meeting.summary && meeting.updatedAt > newlyGeneratedSummariesDate) { - // return { - // meetingName: meeting.name, - // date: meeting.createdAt, - // summary: meeting.summary - // } - // } const meetingsContent = await getMeetingsContent(meeting) - console.log('šŸš€ ~ meetingsContent:', meetingsContent) if (!meetingsContent || meetingsContent.length === 0) { return null } const yamlData = yaml.dump(meetingsContent, { noCompatMode: true }) - - const manager = new OpenAIServerManager() - console.log('gen sum', meeting.id) const newSummary = await manager.generateSummary(yamlData) - console.log('šŸš€ ~ newSummary:', newSummary) if (!newSummary) return null const now = new Date() - // await r - // .table('NewMeeting') - // .get(meeting.id) - // .update({summary: newSummary, updatedAt: now}) - // .run() - // meeting.summary = newSummary - return { - meetingName: meeting.name, - date: meeting.createdAt, - summary: newSummary - // summary: meeting.summary - } + await r + .table('NewMeeting') + .get(meeting.id) + .update({summary: newSummary, updatedAt: now}) + .run() + meeting.summary = newSummary + return meeting.id }) ) - console.log('šŸš€ ~ summaries:', summaries) - - // RESOLUTION - const data = {meetingIds: rawMeetings.map((meeting) => meeting.id)} + const filteredMeetingIds = updatedMeetingIds.filter( + (meetingId): meetingId is string => meetingId !== null + ) + const data = {meetingIds: filteredMeetingIds} return data } diff --git a/packages/server/graphql/public/permissions.ts b/packages/server/graphql/public/permissions.ts index a74143e3064..577011aaec5 100644 --- a/packages/server/graphql/public/permissions.ts +++ b/packages/server/graphql/public/permissions.ts @@ -33,6 +33,7 @@ const permissionMap: PermissionMap = { acceptTeamInvitation: rateLimit({perMinute: 50, perHour: 100}), createImposterToken: isSuperUser, generateInsight: isSuperUser, + generateMeetingSummary: isSuperUser, loginWithGoogle: and( not(isEnvVarTrue('AUTH_GOOGLE_DISABLED')), rateLimit({perMinute: 50, perHour: 500}) From 5cbcb1abdd73bfe762d911e95a12e129137780ae Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 30 Jul 2024 11:17:51 +0100 Subject: [PATCH 29/43] clean up --- packages/client/mutations/GenerateMeetingSummaryMutation.ts | 3 --- packages/server/utils/OpenAIServerManager.ts | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/client/mutations/GenerateMeetingSummaryMutation.ts b/packages/client/mutations/GenerateMeetingSummaryMutation.ts index 3508ada5c0c..d1b7619e272 100644 --- a/packages/client/mutations/GenerateMeetingSummaryMutation.ts +++ b/packages/client/mutations/GenerateMeetingSummaryMutation.ts @@ -32,9 +32,6 @@ const GenerateMeetingSummaryMutation: StandardMutation(atmosphere, { mutation, variables, - optimisticUpdater: (store) => { - const {} = variables - }, onCompleted, onError }) diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 5110c9f217f..8e9ea6bbf3e 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -77,6 +77,7 @@ class OpenAIServerManager { } } + // replace getSummary with generateSummary: https://github.com/ParabolInc/parabol/issues/10049 async getSummary(text: string | string[], summaryLocation?: 'discussion thread') { if (!this.openAIApi) return null const textStr = Array.isArray(text) ? text.join('\n') : text @@ -428,10 +429,9 @@ class OpenAIServerManager { } } - // 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 + // replace getSummary with generateSummary: https://github.com/ParabolInc/parabol/issues/10049 async generateSummary(yamlData: string): Promise { if (!this.openAIApi) return null - console.log('in generateSummary') 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. From 568a5608838046b8bd5874a14264fbd2c78aaea8 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 30 Jul 2024 12:29:11 +0100 Subject: [PATCH 30/43] move generateMeetingSummary to private schema --- codegen.json | 3 +- .../GenerateMeetingSummaryMutation.ts | 40 ------------------- .../mutations/generateMeetingSummary.ts | 4 +- .../typeDefs/generateMeetingSummary.graphql | 5 +++ .../types/GenerateMeetingSummarySuccess.ts | 0 packages/server/utils/OpenAIServerManager.ts | 5 ++- 6 files changed, 12 insertions(+), 45 deletions(-) delete mode 100644 packages/client/mutations/GenerateMeetingSummaryMutation.ts rename packages/server/graphql/{public => private}/mutations/generateMeetingSummary.ts (98%) rename packages/server/graphql/{public => private}/typeDefs/generateMeetingSummary.graphql (83%) rename packages/server/graphql/{public => private}/types/GenerateMeetingSummarySuccess.ts (100%) diff --git a/codegen.json b/codegen.json index c40970799de..4b2ba4f5e8a 100644 --- a/codegen.json +++ b/codegen.json @@ -18,6 +18,7 @@ "File": "../public/types/File#TFile", "FlagConversionModalPayload": "./types/FlagConversionModalPayload#FlagConversionModalPayloadSource", "FlagOverLimitPayload": "./types/FlagOverLimitPayload#FlagOverLimitPayloadSource", + "GenerateMeetingSummarySuccess": "./types/GenerateMeetingSummarySuccess#GenerateMeetingSummarySuccessSource", "LoginsPayload": "./types/LoginsPayload#LoginsPayloadSource", "MeetingTemplate": "../../database/types/MeetingTemplate#default as IMeetingTemplate", "NewMeeting": "../../postgres/types/Meeting#AnyMeeting", @@ -25,6 +26,7 @@ "PingableServices": "./types/PingableServices#PingableServicesSource", "ProcessRecurrenceSuccess": "./types/ProcessRecurrenceSuccess#ProcessRecurrenceSuccessSource", "RemoveAuthIdentitySuccess": "./types/RemoveAuthIdentitySuccess#RemoveAuthIdentitySuccessSource", + "RetrospectiveMeeting": "../../database/types/MeetingRetrospective#default", "SAML": "./types/SAML#SAMLSource", "SetIsFreeMeetingTemplateSuccess": "./types/SetIsFreeMeetingTemplateSuccess#SetIsFreeMeetingTemplateSuccessSource", "SignupsPayload": "./types/SignupsPayload#SignupsPayloadSource", @@ -76,7 +78,6 @@ "GcalIntegration": "./types/GcalIntegration#GcalIntegrationSource", "GenerateGroupsSuccess": "./types/GenerateGroupsSuccess#GenerateGroupsSuccessSource", "GenerateInsightSuccess": "./types/GenerateInsightSuccess#GenerateInsightSuccessSource", - "GenerateMeetingSummarySuccess": "./types/GenerateMeetingSummarySuccess#GenerateMeetingSummarySuccessSource", "GetTemplateSuggestionSuccess": "./types/GetTemplateSuggestionSuccess#GetTemplateSuggestionSuccessSource", "GitHubIntegration": "../../postgres/queries/getGitHubAuthByUserIdTeamId#GitHubAuth", "GitLabIntegration": "./types/GitLabIntegration#GitLabIntegrationSource", diff --git a/packages/client/mutations/GenerateMeetingSummaryMutation.ts b/packages/client/mutations/GenerateMeetingSummaryMutation.ts deleted file mode 100644 index d1b7619e272..00000000000 --- a/packages/client/mutations/GenerateMeetingSummaryMutation.ts +++ /dev/null @@ -1,40 +0,0 @@ -import graphql from 'babel-plugin-relay/macro' -import {commitMutation} from 'react-relay' -import {GenerateMeetingSummaryMutation as TGenerateMeetingSummaryMutation} from '../__generated__/GenerateMeetingSummaryMutation.graphql' -import {StandardMutation} from '../types/relayMutations' - -graphql` - fragment GenerateMeetingSummaryMutation_meeting on GenerateMeetingSummarySuccess { - meetings { - summary - } - } -` - -const mutation = graphql` - mutation GenerateMeetingSummaryMutation($teamIds: [ID!]!) { - generateMeetingSummary(teamIds: $teamIds) { - ... on ErrorPayload { - error { - message - } - } - ...GenerateMeetingSummaryMutation_meeting @relay(mask: false) - } - } -` - -const GenerateMeetingSummaryMutation: StandardMutation = ( - atmosphere, - variables, - {onError, onCompleted} -) => { - return commitMutation(atmosphere, { - mutation, - variables, - onCompleted, - onError - }) -} - -export default GenerateMeetingSummaryMutation diff --git a/packages/server/graphql/public/mutations/generateMeetingSummary.ts b/packages/server/graphql/private/mutations/generateMeetingSummary.ts similarity index 98% rename from packages/server/graphql/public/mutations/generateMeetingSummary.ts rename to packages/server/graphql/private/mutations/generateMeetingSummary.ts index 0fb340211f2..670f6332b6d 100644 --- a/packages/server/graphql/public/mutations/generateMeetingSummary.ts +++ b/packages/server/graphql/private/mutations/generateMeetingSummary.ts @@ -8,7 +8,7 @@ import {MutationResolvers} from '../resolverTypes' const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = async ( _source, - {teamIds}, + {teamIds, prompt}, {dataLoader} ) => { const r = await getRethink() @@ -162,7 +162,7 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn const yamlData = yaml.dump(meetingsContent, { noCompatMode: true }) - const newSummary = await manager.generateSummary(yamlData) + const newSummary = await manager.generateSummary(yamlData, prompt as string) if (!newSummary) return null const now = new Date() diff --git a/packages/server/graphql/public/typeDefs/generateMeetingSummary.graphql b/packages/server/graphql/private/typeDefs/generateMeetingSummary.graphql similarity index 83% rename from packages/server/graphql/public/typeDefs/generateMeetingSummary.graphql rename to packages/server/graphql/private/typeDefs/generateMeetingSummary.graphql index 93fcd19a333..beed39720c2 100644 --- a/packages/server/graphql/public/typeDefs/generateMeetingSummary.graphql +++ b/packages/server/graphql/private/typeDefs/generateMeetingSummary.graphql @@ -7,6 +7,11 @@ extend type Mutation { The ids of the teams to generate the meeting summary for """ teamIds: [ID!]! + + """ + The optional user prompt that will be used to generate the meeting summary + """ + prompt: String ): GenerateMeetingSummaryPayload! } diff --git a/packages/server/graphql/public/types/GenerateMeetingSummarySuccess.ts b/packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts similarity index 100% rename from packages/server/graphql/public/types/GenerateMeetingSummarySuccess.ts rename to packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 8e9ea6bbf3e..2b851da07eb 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -430,10 +430,10 @@ class OpenAIServerManager { } // replace getSummary with generateSummary: https://github.com/ParabolInc/parabol/issues/10049 - async generateSummary(yamlData: string): Promise { + async generateSummary(yamlData: string, userPrompt?: string): Promise { if (!this.openAIApi) return null const meetingURL = 'https://action.parabol.co/meet/' - const prompt = ` + const defaultPrompt = ` 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. @@ -447,6 +447,7 @@ class OpenAIServerManager { 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. ` + const prompt = userPrompt ? userPrompt : defaultPrompt try { const response = await this.openAIApi.chat.completions.create({ From 6de0b505cd3aba9af3538e9cd542fb597aa88e83 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 30 Jul 2024 12:57:12 +0100 Subject: [PATCH 31/43] remove generateMeetingSummary from permissions --- packages/server/graphql/public/permissions.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/server/graphql/public/permissions.ts b/packages/server/graphql/public/permissions.ts index 577011aaec5..a74143e3064 100644 --- a/packages/server/graphql/public/permissions.ts +++ b/packages/server/graphql/public/permissions.ts @@ -33,7 +33,6 @@ const permissionMap: PermissionMap = { acceptTeamInvitation: rateLimit({perMinute: 50, perHour: 100}), createImposterToken: isSuperUser, generateInsight: isSuperUser, - generateMeetingSummary: isSuperUser, loginWithGoogle: and( not(isEnvVarTrue('AUTH_GOOGLE_DISABLED')), rateLimit({perMinute: 50, perHour: 500}) From 13ef86505573d2f8c69630a2714a8870293d6a83 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 31 Jul 2024 14:45:18 +0100 Subject: [PATCH 32/43] update userPrompt type --- .../graphql/private/mutations/generateMeetingSummary.ts | 2 +- packages/server/graphql/public/mutations/generateInsight.ts | 4 ++-- .../server/graphql/public/mutations/helpers/getSummaries.ts | 2 +- packages/server/graphql/public/mutations/helpers/getTopics.ts | 2 +- packages/server/utils/OpenAIServerManager.ts | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/server/graphql/private/mutations/generateMeetingSummary.ts b/packages/server/graphql/private/mutations/generateMeetingSummary.ts index 670f6332b6d..6c82273ef1a 100644 --- a/packages/server/graphql/private/mutations/generateMeetingSummary.ts +++ b/packages/server/graphql/private/mutations/generateMeetingSummary.ts @@ -162,7 +162,7 @@ const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = asyn const yamlData = yaml.dump(meetingsContent, { noCompatMode: true }) - const newSummary = await manager.generateSummary(yamlData, prompt as string) + const newSummary = await manager.generateSummary(yamlData, prompt) if (!newSummary) return null const now = new Date() diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 16c35eaae78..903383135f0 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -20,8 +20,8 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( } const response = useSummaries - ? await getSummaries(teamId, startDate, endDate, prompt as string) - : await getTopics(teamId, startDate, endDate, dataLoader, prompt as string) + ? await getSummaries(teamId, startDate, endDate, prompt) + : await getTopics(teamId, startDate, endDate, dataLoader, prompt) if ('error' in response) { return response diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts index 6e8ba4a7c3a..c80339a30f9 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -8,7 +8,7 @@ export const getSummaries = async ( teamId: string, startDate: Date, endDate: Date, - prompt?: string + prompt?: string | null ) => { const r = await getRethink() const MIN_MILLISECONDS = 60 * 1000 // 1 minute diff --git a/packages/server/graphql/public/mutations/helpers/getTopics.ts b/packages/server/graphql/public/mutations/helpers/getTopics.ts index 8567c3a8392..bca0f2a4cec 100644 --- a/packages/server/graphql/public/mutations/helpers/getTopics.ts +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -108,7 +108,7 @@ export const getTopics = async ( startDate: Date, endDate: Date, dataLoader: DataLoaderWorker, - prompt?: string + prompt?: string | null ) => { const r = await getRethink() const MIN_REFLECTION_COUNT = 3 diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 2b851da07eb..bfa49a4ea2f 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -430,7 +430,7 @@ class OpenAIServerManager { } // replace getSummary with generateSummary: https://github.com/ParabolInc/parabol/issues/10049 - async generateSummary(yamlData: string, userPrompt?: string): Promise { + async generateSummary(yamlData: string, userPrompt?: string | null): Promise { if (!this.openAIApi) return null const meetingURL = 'https://action.parabol.co/meet/' const defaultPrompt = ` From 5e4f68c43604f714f18cb246e26ebf5b730f364c Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 31 Jul 2024 15:26:57 +0100 Subject: [PATCH 33/43] update generateInsight userPrompt type --- packages/server/utils/OpenAIServerManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index bfa49a4ea2f..9c616b9175b 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -349,7 +349,7 @@ class OpenAIServerManager { async generateInsight( yamlData: string, useSummaries: boolean, - userPrompt?: string + userPrompt?: string | null ): Promise { if (!this.openAIApi) return null const meetingURL = 'https://action.parabol.co/meet/' From 9920f551dd55d10a78a96a7d8678de18f4cb64c0 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 31 Jul 2024 15:34:32 +0100 Subject: [PATCH 34/43] update userPrompt type --- packages/server/graphql/public/mutations/generateInsight.ts | 4 ++-- .../server/graphql/public/mutations/helpers/getSummaries.ts | 2 +- packages/server/graphql/public/mutations/helpers/getTopics.ts | 2 +- packages/server/utils/OpenAIServerManager.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index 16c35eaae78..903383135f0 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -20,8 +20,8 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( } const response = useSummaries - ? await getSummaries(teamId, startDate, endDate, prompt as string) - : await getTopics(teamId, startDate, endDate, dataLoader, prompt as string) + ? await getSummaries(teamId, startDate, endDate, prompt) + : await getTopics(teamId, startDate, endDate, dataLoader, prompt) if ('error' in response) { return response diff --git a/packages/server/graphql/public/mutations/helpers/getSummaries.ts b/packages/server/graphql/public/mutations/helpers/getSummaries.ts index 6e8ba4a7c3a..c80339a30f9 100644 --- a/packages/server/graphql/public/mutations/helpers/getSummaries.ts +++ b/packages/server/graphql/public/mutations/helpers/getSummaries.ts @@ -8,7 +8,7 @@ export const getSummaries = async ( teamId: string, startDate: Date, endDate: Date, - prompt?: string + prompt?: string | null ) => { const r = await getRethink() const MIN_MILLISECONDS = 60 * 1000 // 1 minute diff --git a/packages/server/graphql/public/mutations/helpers/getTopics.ts b/packages/server/graphql/public/mutations/helpers/getTopics.ts index 8567c3a8392..bca0f2a4cec 100644 --- a/packages/server/graphql/public/mutations/helpers/getTopics.ts +++ b/packages/server/graphql/public/mutations/helpers/getTopics.ts @@ -108,7 +108,7 @@ export const getTopics = async ( startDate: Date, endDate: Date, dataLoader: DataLoaderWorker, - prompt?: string + prompt?: string | null ) => { const r = await getRethink() const MIN_REFLECTION_COUNT = 3 diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index ad3f12ac489..c076b5dbec5 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -348,7 +348,7 @@ class OpenAIServerManager { async generateInsight( yamlData: string, useSummaries: boolean, - userPrompt?: string + userPrompt?: string | null ): Promise { if (!this.openAIApi) return null const meetingURL = 'https://action.parabol.co/meet/' From 4d5e81e59c1f2ddc76f71337d9053b621191ef83 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 5 Aug 2024 18:59:08 +0100 Subject: [PATCH 35/43] render link in summary --- .../WholeMeetingSummaryResult.tsx | 62 +++++++++++++------ packages/client/package.json | 1 + yarn.lock | 11 +++- 3 files changed, 52 insertions(+), 22 deletions(-) diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx index 56e30fea03b..71bdb49e938 100644 --- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx +++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx @@ -1,10 +1,10 @@ import graphql from 'babel-plugin-relay/macro' -import {WholeMeetingSummaryResult_meeting$key} from 'parabol-client/__generated__/WholeMeetingSummaryResult_meeting.graphql' -import {PALETTE} from 'parabol-client/styles/paletteV3' -import {FONT_FAMILY} from 'parabol-client/styles/typographyV2' +import {marked} from 'marked' import React, {useEffect} from 'react' import {useFragment} from 'react-relay' import useAtmosphere from '../../../../../hooks/useAtmosphere' +import {PALETTE} from '../../../../../styles/paletteV3' +import {FONT_FAMILY} from '../../../../../styles/typographyV2' import {AIExplainer} from '../../../../../types/constEnums' import SendClientSideEvent from '../../../../../utils/SendClientSideEvent' import EmailBorderBottom from './EmailBorderBottom' @@ -35,12 +35,18 @@ const textStyle = { textAlign: 'left' } as const +const linkStyle = { + color: '#2563EB', + textDecoration: 'underline' +} + interface Props { - meetingRef: WholeMeetingSummaryResult_meeting$key + meetingRef: any // WholeMeetingSummaryResult_meeting$key } -const WholeMeetingSummaryResult = (props: Props) => { - const {meetingRef} = props +const WholeMeetingSummaryResult = ({meetingRef}: Props) => { + const atmosphere = useAtmosphere() + const meeting = useFragment( graphql` fragment WholeMeetingSummaryResult_meeting on NewMeeting { @@ -55,31 +61,49 @@ const WholeMeetingSummaryResult = (props: Props) => { `, meetingRef ) - const atmosphere = useAtmosphere() + const {summary: wholeMeetingSummary, team} = meeting + const test = + 'The budget work is recognized for its future value in improving focus and communication within the team ([link](https://action.parabol.co/meet/twbP2qPXNK/discuss/3)).' + + const renderedTest = marked(test, { + gfm: true, + breaks: true, + smartypants: true + }).replace( + /(.*?)<\/a>/g, + `$2` + ) + const explainerText = team?.tier === 'starter' ? AIExplainer.STARTER : AIExplainer.PREMIUM_MEETING + useEffect(() => { SendClientSideEvent(atmosphere, 'AI Summary Viewed', { source: 'Meeting Summary', tier: meeting.team.billingTier, meetingId: meeting.id }) - }, []) + }, [atmosphere, meeting.id, meeting.team.billingTier]) + return ( <> - - {explainerText} - - - - {'šŸ¤– Meeting Summary'} - - - - {wholeMeetingSummary} - + + + + + + + + + + + +
{explainerText}
+ {'šŸ¤– Meeting Summary'} +
+
diff --git a/packages/client/package.json b/packages/client/package.json index 98d5d5cd9e5..aa7d4dacb08 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -116,6 +116,7 @@ "json2csv": "5.0.7", "jwt-decode": "^2.1.0", "linkify-it": "^2.0.3", + "marked": "^13.0.3", "mousetrap": "^1.6.3", "ms": "^2.0.0", "react": "^17.0.2", diff --git a/yarn.lock b/yarn.lock index 90977ed9054..1265fd9402c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10929,9 +10929,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001580, caniuse-lite@^1.0.30001640, caniuse-lite@~1.0.0: - version "1.0.30001639" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz#972b3a6adeacdd8f46af5fc7f771e9639f6c1521" - integrity sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg== + version "1.0.30001649" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz#3ec700309ca0da2b0d3d5fb03c411b191761c992" + integrity sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ== capital-case@^1.0.4: version "1.0.4" @@ -16929,6 +16929,11 @@ markdown-it@^13.0.1: mdurl "^1.0.1" uc.micro "^1.0.5" +marked@^13.0.3: + version "13.0.3" + resolved "https://registry.yarnpkg.com/marked/-/marked-13.0.3.tgz#5c5b4a5d0198060c7c9bc6ef9420a7fed30f822d" + integrity sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA== + marked@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" From b39190b96049ee598799ac6f5cd5ed57e398b24a Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 6 Aug 2024 17:38:24 +0100 Subject: [PATCH 36/43] format links using replace --- .../WholeMeetingSummaryResult.tsx | 62 +++++++++++++------ 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx index 56e30fea03b..a2b36eaccee 100644 --- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx +++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx @@ -1,10 +1,11 @@ import graphql from 'babel-plugin-relay/macro' -import {WholeMeetingSummaryResult_meeting$key} from 'parabol-client/__generated__/WholeMeetingSummaryResult_meeting.graphql' -import {PALETTE} from 'parabol-client/styles/paletteV3' -import {FONT_FAMILY} from 'parabol-client/styles/typographyV2' +import {marked} from 'marked' import React, {useEffect} from 'react' import {useFragment} from 'react-relay' +import {WholeMeetingSummaryResult_meeting$key} from '../../../../../__generated__/WholeMeetingSummaryResult_meeting.graphql' import useAtmosphere from '../../../../../hooks/useAtmosphere' +import {PALETTE} from '../../../../../styles/paletteV3' +import {FONT_FAMILY} from '../../../../../styles/typographyV2' import {AIExplainer} from '../../../../../types/constEnums' import SendClientSideEvent from '../../../../../utils/SendClientSideEvent' import EmailBorderBottom from './EmailBorderBottom' @@ -35,12 +36,18 @@ const textStyle = { textAlign: 'left' } as const +const linkStyle = { + color: '#2563EB', + textDecoration: 'underline' +} + interface Props { meetingRef: WholeMeetingSummaryResult_meeting$key } -const WholeMeetingSummaryResult = (props: Props) => { - const {meetingRef} = props +const WholeMeetingSummaryResult = ({meetingRef}: Props) => { + const atmosphere = useAtmosphere() + const meeting = useFragment( graphql` fragment WholeMeetingSummaryResult_meeting on NewMeeting { @@ -55,31 +62,50 @@ const WholeMeetingSummaryResult = (props: Props) => { `, meetingRef ) - const atmosphere = useAtmosphere() + const {summary: wholeMeetingSummary, team} = meeting + const renderedSummary = + wholeMeetingSummary && + (marked(wholeMeetingSummary, { + gfm: true, + breaks: true + }) as string | null) + // replace links with styled links + const formattedSummary = renderedSummary?.replace( + /(.*?)<\/a>/g, + `$2` + ) + const explainerText = team?.tier === 'starter' ? AIExplainer.STARTER : AIExplainer.PREMIUM_MEETING + useEffect(() => { SendClientSideEvent(atmosphere, 'AI Summary Viewed', { source: 'Meeting Summary', tier: meeting.team.billingTier, meetingId: meeting.id }) - }, []) + }, [atmosphere, meeting.id, meeting.team.billingTier]) + + if (!formattedSummary) return null return ( <> - - {explainerText} - - - - {'šŸ¤– Meeting Summary'} - - - - {wholeMeetingSummary} - + + + + + + + + + + + +
{explainerText}
+ {'šŸ¤– Meeting Summary'} +
+
From 0f98d26c304e749181adae271b99f6bfc567843f Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 6 Aug 2024 17:49:12 +0100 Subject: [PATCH 37/43] re-add marked --- .../WholeMeetingSummaryResult.tsx | 36 ++++++++----------- packages/client/package.json | 1 + yarn.lock | 11 ++++-- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx index a2b36eaccee..3c13180c796 100644 --- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx +++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx @@ -36,11 +36,6 @@ const textStyle = { textAlign: 'left' } as const -const linkStyle = { - color: '#2563EB', - textDecoration: 'underline' -} - interface Props { meetingRef: WholeMeetingSummaryResult_meeting$key } @@ -62,31 +57,30 @@ const WholeMeetingSummaryResult = ({meetingRef}: Props) => { `, meetingRef ) + useEffect(() => { + SendClientSideEvent(atmosphere, 'AI Summary Viewed', { + source: 'Meeting Summary', + tier: meeting.team.billingTier, + meetingId: meeting.id + }) + }, [atmosphere, meeting.id, meeting.team.billingTier]) const {summary: wholeMeetingSummary, team} = meeting - const renderedSummary = - wholeMeetingSummary && - (marked(wholeMeetingSummary, { - gfm: true, - breaks: true - }) as string | null) + + if (!wholeMeetingSummary) return null + + const renderedSummary = marked(wholeMeetingSummary, { + gfm: true, + breaks: true + }) as string // replace links with styled links const formattedSummary = renderedSummary?.replace( /(.*?)<\/a>/g, - `$2` + `$2` ) const explainerText = team?.tier === 'starter' ? AIExplainer.STARTER : AIExplainer.PREMIUM_MEETING - useEffect(() => { - SendClientSideEvent(atmosphere, 'AI Summary Viewed', { - source: 'Meeting Summary', - tier: meeting.team.billingTier, - meetingId: meeting.id - }) - }, [atmosphere, meeting.id, meeting.team.billingTier]) - - if (!formattedSummary) return null return ( <> diff --git a/packages/client/package.json b/packages/client/package.json index 34921d3aeff..5221c8e3fbe 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -116,6 +116,7 @@ "json2csv": "5.0.7", "jwt-decode": "^2.1.0", "linkify-it": "^2.0.3", + "marked": "^13.0.3", "mousetrap": "^1.6.3", "ms": "^2.0.0", "react": "^17.0.2", diff --git a/yarn.lock b/yarn.lock index 252953cb10c..b4be6c9c3f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10008,9 +10008,9 @@ camelcase@^6.2.0: integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001517, caniuse-lite@^1.0.30001580, caniuse-lite@~1.0.0: - version "1.0.30001639" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001639.tgz#972b3a6adeacdd8f46af5fc7f771e9639f6c1521" - integrity sha512-eFHflNTBIlFwP2AIKaYuBQN/apnUoKNhBdza8ZnW/h2di4LCZ4xFqYlxUxo+LQ76KFI1PGcC1QDxMbxTZpSCAg== + version "1.0.30001649" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz#3ec700309ca0da2b0d3d5fb03c411b191761c992" + integrity sha512-fJegqZZ0ZX8HOWr6rcafGr72+xcgJKI9oWfDW5DrD7ExUtgZC7a7R7ZYmZqplh7XDocFdGeIFn7roAxhOeYrPQ== capital-case@^1.0.4: version "1.0.4" @@ -15971,6 +15971,11 @@ markdown-it@^13.0.1: mdurl "^1.0.1" uc.micro "^1.0.5" +marked@^13.0.3: + version "13.0.3" + resolved "https://registry.yarnpkg.com/marked/-/marked-13.0.3.tgz#5c5b4a5d0198060c7c9bc6ef9420a7fed30f822d" + integrity sha512-rqRix3/TWzE9rIoFGIn8JmsVfhiuC8VIQ8IdX5TfzmeBucdY05/0UlzKaw0eVtpcN/OdVFpBk7CjKGo9iHJ/zA== + marked@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" From 2ef6ea71bd8e04f18019abd6527d4d9c44c4793e Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Thu, 8 Aug 2024 16:36:02 +0100 Subject: [PATCH 38/43] fix merge conflict issues --- codegen.json | 3 -- .../migrations/1722011287034_addInsight.ts | 32 ------------------- 2 files changed, 35 deletions(-) delete mode 100644 packages/server/postgres/migrations/1722011287034_addInsight.ts diff --git a/codegen.json b/codegen.json index 574a9c7d0cb..c1edd8f890f 100644 --- a/codegen.json +++ b/codegen.json @@ -46,13 +46,10 @@ "config": { "contextType": "../graphql#GQLContext", "mappers": { -<<<<<<< HEAD -======= "JiraRemoteAvatarUrls": "./types/JiraRemoteAvatarUrls#JiraRemoteAvatarUrlsSource", "TemplateDimensionRef": "./types/TemplateDimensionRef#TemplateDimensionRefSource", "UpdateIntegrationProviderSuccess": "./types/UpdateIntegrationProviderSuccess#UpdateIntegrationProviderSuccessSource", "EndTeamPromptSuccess": "./types/EndTeamPromptSuccess#EndTeamPromptSuccessSource", ->>>>>>> master "AcceptRequestToJoinDomainSuccess": "./types/AcceptRequestToJoinDomainSuccess#AcceptRequestToJoinDomainSuccessSource", "AcceptTeamInvitationPayload": "./types/AcceptTeamInvitationPayload#AcceptTeamInvitationPayloadSource", "ActionMeeting": "../../database/types/MeetingAction#default", diff --git a/packages/server/postgres/migrations/1722011287034_addInsight.ts b/packages/server/postgres/migrations/1722011287034_addInsight.ts deleted file mode 100644 index 7127a688cb8..00000000000 --- a/packages/server/postgres/migrations/1722011287034_addInsight.ts +++ /dev/null @@ -1,32 +0,0 @@ -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() -} From 97e2eb54d29e309c366ac35c2ae960d855c3e9b2 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Thu, 8 Aug 2024 16:47:50 +0100 Subject: [PATCH 39/43] remove redundant table body elements --- .../WholeMeetingSummaryResult.tsx | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx index 3c13180c796..be9b93091ba 100644 --- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx +++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx @@ -85,21 +85,17 @@ const WholeMeetingSummaryResult = ({meetingRef}: Props) => { <> - - - - - - - - - - - -
{explainerText}
- {'šŸ¤– Meeting Summary'} -
-
+ + {explainerText} + + + + {'šŸ¤– Meeting Summary'} + + + + + From 57d7028cf20e735fee76cd91ad7c01a661cf5b67 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 12 Aug 2024 15:43:34 +0100 Subject: [PATCH 40/43] santize summary --- .../WholeMeetingSummaryResult.css | 4 ++++ .../WholeMeetingSummaryResult.tsx | 15 +++++++++------ 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.css diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.css b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.css new file mode 100644 index 00000000000..c87c41afe00 --- /dev/null +++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.css @@ -0,0 +1,4 @@ +.link-style a { + color: #329ae5; /* Equivalent to PALETTE.SKY_500 */ + text-decoration: underline; +} diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx index be9b93091ba..17eb82ad42f 100644 --- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx +++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx @@ -1,4 +1,5 @@ import graphql from 'babel-plugin-relay/macro' +import DOMPurify from 'dompurify' import {marked} from 'marked' import React, {useEffect} from 'react' import {useFragment} from 'react-relay' @@ -9,6 +10,7 @@ import {FONT_FAMILY} from '../../../../../styles/typographyV2' import {AIExplainer} from '../../../../../types/constEnums' import SendClientSideEvent from '../../../../../utils/SendClientSideEvent' import EmailBorderBottom from './EmailBorderBottom' +import './WholeMeetingSummaryResult.css' const topicTitleStyle = { color: PALETTE.SLATE_700, @@ -73,11 +75,7 @@ const WholeMeetingSummaryResult = ({meetingRef}: Props) => { gfm: true, breaks: true }) as string - // replace links with styled links - const formattedSummary = renderedSummary?.replace( - /(.*?)<\/a>/g, - `$2` - ) + const sanitizedSummary = DOMPurify.sanitize(renderedSummary) const explainerText = team?.tier === 'starter' ? AIExplainer.STARTER : AIExplainer.PREMIUM_MEETING @@ -94,7 +92,12 @@ const WholeMeetingSummaryResult = ({meetingRef}: Props) => { - + From 56344dd870e3093739de369876d264cf798e10dd Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 12 Aug 2024 15:53:04 +0100 Subject: [PATCH 41/43] add css file to static folder --- static/css/WholeMeetingSummaryResult.css | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 static/css/WholeMeetingSummaryResult.css diff --git a/static/css/WholeMeetingSummaryResult.css b/static/css/WholeMeetingSummaryResult.css new file mode 100644 index 00000000000..c87c41afe00 --- /dev/null +++ b/static/css/WholeMeetingSummaryResult.css @@ -0,0 +1,4 @@ +.link-style a { + color: #329ae5; /* Equivalent to PALETTE.SKY_500 */ + text-decoration: underline; +} From a2a3339ecc83ad53f14fdf9d7729d6f57455711a Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 12 Aug 2024 16:17:35 +0100 Subject: [PATCH 42/43] remove css files and move to global css --- .../MeetingSummaryEmail/WholeMeetingSummaryResult.css | 4 ---- .../MeetingSummaryEmail/WholeMeetingSummaryResult.tsx | 7 +++---- packages/client/styles/theme/global.css | 7 ++++++- static/css/WholeMeetingSummaryResult.css | 4 ---- 4 files changed, 9 insertions(+), 13 deletions(-) delete mode 100644 packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.css delete mode 100644 static/css/WholeMeetingSummaryResult.css diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.css b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.css deleted file mode 100644 index c87c41afe00..00000000000 --- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.css +++ /dev/null @@ -1,4 +0,0 @@ -.link-style a { - color: #329ae5; /* Equivalent to PALETTE.SKY_500 */ - text-decoration: underline; -} diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx index 17eb82ad42f..d62d653d1ce 100644 --- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx +++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx @@ -10,7 +10,6 @@ import {FONT_FAMILY} from '../../../../../styles/typographyV2' import {AIExplainer} from '../../../../../types/constEnums' import SendClientSideEvent from '../../../../../utils/SendClientSideEvent' import EmailBorderBottom from './EmailBorderBottom' -import './WholeMeetingSummaryResult.css' const topicTitleStyle = { color: PALETTE.SLATE_700, @@ -70,8 +69,8 @@ const WholeMeetingSummaryResult = ({meetingRef}: Props) => { const {summary: wholeMeetingSummary, team} = meeting if (!wholeMeetingSummary) return null - - const renderedSummary = marked(wholeMeetingSummary, { + const test = `The budget work is recognized for its future value in improving focus and communication within the team ([link](https://action.parabol.co/meet/twbP2qPXNK/discuss/3](https://action.parabol.co/meet/twbP2qPXNK/discuss/3) )).` + const renderedSummary = marked(test, { gfm: true, breaks: true }) as string @@ -95,7 +94,7 @@ const WholeMeetingSummaryResult = ({meetingRef}: Props) => { diff --git a/packages/client/styles/theme/global.css b/packages/client/styles/theme/global.css index 8bfbfb9f72e..b1a76203c76 100644 --- a/packages/client/styles/theme/global.css +++ b/packages/client/styles/theme/global.css @@ -65,7 +65,7 @@ 2) prevent a horizontal scrollbar from causing a vertical scrollbar due to the 100vh */ #root { - @apply w-full h-screen p-0 m-0 bg-slate-200; + @apply m-0 h-screen w-full bg-slate-200 p-0; } *, @@ -187,3 +187,8 @@ .draft-codeblock { @apply m-0 rounded-[1px] border-l-2 border-solid border-l-slate-500 bg-slate-200 py-0 px-[8px] font-mono font-[13px] leading-normal; } + +.summary-link-style a { + @apply text-sky-500; + text-decoration: underline; +} diff --git a/static/css/WholeMeetingSummaryResult.css b/static/css/WholeMeetingSummaryResult.css deleted file mode 100644 index c87c41afe00..00000000000 --- a/static/css/WholeMeetingSummaryResult.css +++ /dev/null @@ -1,4 +0,0 @@ -.link-style a { - color: #329ae5; /* Equivalent to PALETTE.SKY_500 */ - text-decoration: underline; -} From d2654c0ea1cf533636e7f93f7ff02e784a73b9a8 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 12 Aug 2024 16:36:52 +0100 Subject: [PATCH 43/43] remove example summary --- .../MeetingSummaryEmail/WholeMeetingSummaryResult.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx index d62d653d1ce..c9e45cad738 100644 --- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx +++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx @@ -69,8 +69,7 @@ const WholeMeetingSummaryResult = ({meetingRef}: Props) => { const {summary: wholeMeetingSummary, team} = meeting if (!wholeMeetingSummary) return null - const test = `The budget work is recognized for its future value in improving focus and communication within the team ([link](https://action.parabol.co/meet/twbP2qPXNK/discuss/3](https://action.parabol.co/meet/twbP2qPXNK/discuss/3) )).` - const renderedSummary = marked(test, { + const renderedSummary = marked(wholeMeetingSummary, { gfm: true, breaks: true }) as string