Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate a summary of meeting summaries #10017

Merged
merged 30 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
05250cf
add generateInsight mutation
nickoferrall Jul 1, 2024
c4ef355
replace shortUrls with real urls
nickoferrall Jul 1, 2024
450b9c2
handle teamId arg and replace short meeting ids
nickoferrall Jul 2, 2024
c806eca
add orgId arg to generateInsight
nickoferrall Jul 2, 2024
e262f9f
filter meetings more efficiently
nickoferrall Jul 3, 2024
3e4aff4
return wins and challenges from generateInsight
nickoferrall Jul 3, 2024
b0b042e
Merge branch 'feat/generate-insight' into feat/generate-team-insight
nickoferrall Jul 16, 2024
14a34dc
generate insight
nickoferrall Jul 17, 2024
7792681
implement addInsight migration
nickoferrall Jul 17, 2024
7c02ca4
check for existingInsight
nickoferrall Jul 17, 2024
91e56b0
start summary of summaries
nickoferrall Jul 18, 2024
a0f2607
include links to discussions
nickoferrall Jul 18, 2024
adc15ac
update prompt
nickoferrall Jul 22, 2024
3b939b1
return summary if exists
nickoferrall Jul 23, 2024
cebc7b3
update prompt and clean up processing getTopics meetingId
nickoferrall Jul 23, 2024
e15d1e7
remove generated files
nickoferrall Jul 23, 2024
23448b1
remove meetingSummary yaml file
nickoferrall Jul 23, 2024
fde4584
update short meeting date
nickoferrall Jul 23, 2024
555ec41
Merge branch 'master' into feat/summary-of-summaries
nickoferrall Jul 23, 2024
ca5c6fc
move addInsight migration after merging master
nickoferrall Jul 23, 2024
6e5aeeb
fix insight start end date insert
nickoferrall Jul 23, 2024
5b1964e
return prev insight if dates and teamid exist
nickoferrall Jul 24, 2024
bc7b351
update generate insight prompt to reduce jargon
nickoferrall Jul 24, 2024
f9e3449
update migration to make wins and challenges non null
nickoferrall Jul 24, 2024
aeb8fa8
accept prompt as arg in generateInsight
nickoferrall Jul 26, 2024
cc9768c
update migration order
nickoferrall Jul 26, 2024
a3ba8d0
remove meetings from generateInsight query
nickoferrall Jul 26, 2024
5c191ef
use number.isNaN instead
nickoferrall Jul 26, 2024
9920f55
update userPrompt type
nickoferrall Jul 31, 2024
7c858cd
Merge branch 'master' into feat/summary-of-summaries
nickoferrall Jul 31, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codegen.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"File": "./types/File#TFile",
"GcalIntegration": "./types/GcalIntegration#GcalIntegrationSource",
"GenerateGroupsSuccess": "./types/GenerateGroupsSuccess#GenerateGroupsSuccessSource",
"GenerateInsightSuccess": "./types/GenerateInsightSuccess#GenerateInsightSuccessSource",
"GetTemplateSuggestionSuccess": "./types/GetTemplateSuggestionSuccess#GetTemplateSuggestionSuccessSource",
"IntegrationProviderWebhook": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider",
"IntegrationProviderOAuth1": "../../postgres/queries/getIntegrationProvidersByIds#TIntegrationProvider",
Expand Down
51 changes: 51 additions & 0 deletions packages/client/mutations/GenerateInsightMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import graphql from 'babel-plugin-relay/macro'
import {commitMutation} from 'react-relay'
import {GenerateInsightMutation as TGenerateInsightMutation} from '../__generated__/GenerateInsightMutation.graphql'
import {StandardMutation} from '../types/relayMutations'

graphql`
fragment GenerateInsightMutation_team on GenerateInsightSuccess {
wins
challenges
}
`

const mutation = graphql`
mutation GenerateInsightMutation(
$teamId: ID!
$startDate: DateTime!
$endDate: DateTime!
$useSummaries: Boolean
$prompt: String
) {
generateInsight(
teamId: $teamId
startDate: $startDate
endDate: $endDate
useSummaries: $useSummaries
prompt: $prompt
) {
... on ErrorPayload {
error {
message
}
}
...GenerateInsightMutation_team @relay(mask: false)
}
}
`

const GenerateInsightMutation: StandardMutation<TGenerateInsightMutation> = (
atmosphere,
variables,
{onError, onCompleted}
) => {
return commitMutation<TGenerateInsightMutation>(atmosphere, {
mutation,
variables,
onCompleted,
onError
})
}

export default GenerateInsightMutation
46 changes: 46 additions & 0 deletions packages/server/graphql/public/mutations/generateInsight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import getKysely from '../../../postgres/getKysely'
import standardError from '../../../utils/standardError'
import {MutationResolvers} from '../resolverTypes'
import {getSummaries} from './helpers/getSummaries'
import {getTopics} from './helpers/getTopics'

const generateInsight: MutationResolvers['generateInsight'] = async (
_source,
{teamId, startDate, endDate, useSummaries = true, prompt},
{dataLoader}
) => {
if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
return standardError(
new Error('Invalid date format. Please use ISO 8601 format (e.g., 2024-01-01T00:00:00Z).')
)
}
const oneWeekInMs = 7 * 24 * 60 * 60 * 1000
if (endDate.getTime() - startDate.getTime() < oneWeekInMs) {
return standardError(new Error('The end date must be at least one week after the start date.'))
}

const response = useSummaries
? await getSummaries(teamId, startDate, endDate, prompt)
: await getTopics(teamId, startDate, endDate, dataLoader, prompt)

if ('error' in response) {
return response
}
const {wins, challenges} = response
const pg = getKysely()

await pg
.insertInto('Insight')
.values({
teamId,
wins,
challenges,
startDateTime: startDate,
endDateTime: endDate
})
.execute()

return response
}

export default generateInsight
57 changes: 57 additions & 0 deletions packages/server/graphql/public/mutations/helpers/getSummaries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import yaml from 'js-yaml'
import getRethink from '../../../../database/rethinkDriver'
import MeetingRetrospective from '../../../../database/types/MeetingRetrospective'
import OpenAIServerManager from '../../../../utils/OpenAIServerManager'
import standardError from '../../../../utils/standardError'

export const getSummaries = async (
teamId: string,
startDate: Date,
endDate: Date,
prompt?: string | null
) => {
const r = await getRethink()
const MIN_MILLISECONDS = 60 * 1000 // 1 minute
const MIN_REFLECTION_COUNT = 3

const rawMeetings = (await r
.table('NewMeeting')
.getAll(teamId, {index: 'teamId'})
.filter((row: any) =>
row('meetingType')
.eq('retrospective')
.and(row('createdAt').ge(startDate))
.and(row('createdAt').le(endDate))
.and(row('reflectionCount').gt(MIN_REFLECTION_COUNT))
.and(r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'}).count().gt(1))
.and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS))
.and(row.hasFields('summary'))
)
.run()) as MeetingRetrospective[]

if (!rawMeetings.length) {
return standardError(new Error('No meetings found'))
}

const summaries = rawMeetings.map((meeting) => ({
meetingName: meeting.name,
date: meeting.createdAt,
summary: meeting.summary
}))

const yamlData = yaml.dump(summaries, {
noCompatMode: true
})

const openAI = new OpenAIServerManager()
const rawInsight = await openAI.generateInsight(yamlData, true, prompt)
if (!rawInsight) {
return standardError(new Error('No insights generated'))
}

return {
wins: rawInsight.wins,
challenges: rawInsight.challenges,
meetingIds: rawMeetings.map((meeting) => meeting.id)
}
}
229 changes: 229 additions & 0 deletions packages/server/graphql/public/mutations/helpers/getTopics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import yaml from 'js-yaml'
import getRethink from '../../../../database/rethinkDriver'
import MeetingRetrospective from '../../../../database/types/MeetingRetrospective'
import getKysely from '../../../../postgres/getKysely'
import OpenAIServerManager from '../../../../utils/OpenAIServerManager'
import sendToSentry from '../../../../utils/sendToSentry'
import standardError from '../../../../utils/standardError'
import {DataLoaderWorker} from '../../../graphql'

const getComments = async (reflectionGroupId: string, dataLoader: DataLoaderWorker) => {
const pg = getKysely()
const IGNORE_COMMENT_USER_IDS = ['parabolAIUser']
const discussion = await pg
.selectFrom('Discussion')
.selectAll()
.where('discussionTopicId', '=', reflectionGroupId)
.limit(1)
.executeTakeFirst()
if (!discussion) return null
const {id: discussionId} = discussion
const rawComments = await dataLoader.get('commentsByDiscussionId').load(discussionId)
const humanComments = rawComments.filter((c) => !IGNORE_COMMENT_USER_IDS.includes(c.createdBy))
const rootComments = humanComments.filter((c) => !c.threadParentId)
rootComments.sort((a, b) => {
return a.createdAt.getTime() < b.createdAt.getTime() ? -1 : 1
})
const comments = await Promise.all(
rootComments.map(async (comment) => {
const {createdBy, isAnonymous, plaintextContent} = comment
const creator = await dataLoader.get('users').loadNonNull(createdBy)
const commentAuthor = isAnonymous ? 'Anonymous' : creator.preferredName
const commentReplies = await Promise.all(
humanComments
.filter((c) => c.threadParentId === comment.id)
.sort((a, b) => {
return a.createdAt.getTime() < b.createdAt.getTime() ? -1 : 1
})
.map(async (reply) => {
const {createdBy, isAnonymous, plaintextContent} = reply
const creator = await dataLoader.get('users').loadNonNull(createdBy)
const replyAuthor = isAnonymous ? 'Anonymous' : creator.preferredName
return {
text: plaintextContent,
author: replyAuthor
}
})
)
return commentReplies.length === 0
? {
text: plaintextContent,
author: commentAuthor
}
: {
text: plaintextContent,
author: commentAuthor,
replies: commentReplies
}
})
)
return comments
}
Comment on lines +10 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimise comment retrieval

The function is well-structured but can be optimised by reducing redundant data loading and improving readability.

- const rootComments = humanComments.filter((c) => !c.threadParentId)
- rootComments.sort((a, b) => {
-   return a.createdAt.getTime() < b.createdAt.getTime() ? -1 : 1
- })
+ const rootComments = humanComments
+   .filter((c) => !c.threadParentId)
+   .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())

- 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 commentReplies = await Promise.all(
+   humanComments
+     .filter((c) => c.threadParentId === comment.id)
+     .sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
+     .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
+       }
+     })
+ )
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getComments = async (reflectionGroupId: string, dataLoader: DataLoaderWorker) => {
const pg = getKysely()
const IGNORE_COMMENT_USER_IDS = ['parabolAIUser']
const discussion = await pg
.selectFrom('Discussion')
.selectAll()
.where('discussionTopicId', '=', reflectionGroupId)
.limit(1)
.executeTakeFirst()
if (!discussion) return null
const {id: discussionId} = discussion
const rawComments = await dataLoader.get('commentsByDiscussionId').load(discussionId)
const humanComments = rawComments.filter((c) => !IGNORE_COMMENT_USER_IDS.includes(c.createdBy))
const rootComments = humanComments.filter((c) => !c.threadParentId)
rootComments.sort((a, b) => {
return a.createdAt.getTime() < b.createdAt.getTime() ? -1 : 1
})
const comments = await Promise.all(
rootComments.map(async (comment) => {
const {createdBy, isAnonymous, plaintextContent} = comment
const creator = await dataLoader.get('users').loadNonNull(createdBy)
const commentAuthor = isAnonymous ? 'Anonymous' : creator.preferredName
const commentReplies = await Promise.all(
humanComments
.filter((c) => c.threadParentId === comment.id)
.sort((a, b) => {
return a.createdAt.getTime() < b.createdAt.getTime() ? -1 : 1
})
.map(async (reply) => {
const {createdBy, isAnonymous, plaintextContent} = reply
const creator = await dataLoader.get('users').loadNonNull(createdBy)
const replyAuthor = isAnonymous ? 'Anonymous' : creator.preferredName
return {
text: plaintextContent,
author: replyAuthor
}
})
)
return commentReplies.length === 0
? {
text: plaintextContent,
author: commentAuthor
}
: {
text: plaintextContent,
author: commentAuthor,
replies: commentReplies
}
})
)
return comments
}
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)
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
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) => a.createdAt.getTime() - b.createdAt.getTime())
.map(async (reply) => {
const {createdBy, isAnonymous, plaintextContent} = reply
const creator = await dataLoader.get('users').loadNonNull(createdBy)
const replyAuthor = isAnonymous ? 'Anonymous' : creator.preferredName
return {
text: plaintextContent,
author: replyAuthor
}
})
)
return commentReplies.length === 0
? {
text: plaintextContent,
author: commentAuthor
}
: {
text: plaintextContent,
author: commentAuthor,
replies: commentReplies
}
})
)
return comments
}


type MeetingLookup = Record<string, string | Date>
const meetingLookup: MeetingLookup = {}

const processLines = (lines: string[]): string[] => {
const meetingURL = 'https://action.parabol.co/meet/'
return lines
.map((line) => {
if (line.includes(meetingURL)) {
let processedLine = line
const regex = new RegExp(`${meetingURL}\\S+`, 'g')
const matches = processedLine.match(regex) || []

let isValid = true
matches.forEach((match) => {
const shortMeetingId = match.split(meetingURL)[1]?.split(/[),\s]/)[0] // Split by closing parenthesis, comma, or space
const actualMeetingId = shortMeetingId && (meetingLookup[shortMeetingId] as string)

if (shortMeetingId && actualMeetingId) {
processedLine = processedLine.replace(shortMeetingId, actualMeetingId)
} else {
const error = new Error(
`AI hallucinated. Unable to find meetingId for ${shortMeetingId}. Line: ${line}`
)
sendToSentry(error)
isValid = false
}
})
return isValid ? processedLine : ''
}
return line
})
.filter((line) => line.trim() !== '')
}
Comment on lines +66 to +95
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improve error handling in processLines

The function is well-structured but can be improved by handling errors more gracefully.

- matches.forEach((match) => {
-   const shortMeetingId = match.split(meetingURL)[1]?.split(/[),\s]/)[0] // Split by closing parenthesis, comma, or space
-   const actualMeetingId = shortMeetingId && (meetingLookup[shortMeetingId] as string)
-   if (shortMeetingId && actualMeetingId) {
-     processedLine = processedLine.replace(shortMeetingId, actualMeetingId)
-   } else {
-     const error = new Error(
-       `AI hallucinated. Unable to find meetingId for ${shortMeetingId}. Line: ${line}`
-     )
-     sendToSentry(error)
-     isValid = false
-   }
- })
+ for (const match of matches) {
+   const shortMeetingId = match.split(meetingURL)[1]?.split(/[),\s]/)[0]
+   const actualMeetingId = shortMeetingId && (meetingLookup[shortMeetingId] as string)
+   if (shortMeetingId && actualMeetingId) {
+     processedLine = processedLine.replace(shortMeetingId, actualMeetingId)
+   } else {
+     sendToSentry(new Error(
+       `AI hallucinated. Unable to find meetingId for ${shortMeetingId}. Line: ${line}`
+     ))
+     isValid = false
+   }
+ }
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const processLines = (lines: string[]): string[] => {
const meetingURL = 'https://action.parabol.co/meet/'
return lines
.map((line) => {
if (line.includes(meetingURL)) {
let processedLine = line
const regex = new RegExp(`${meetingURL}\\S+`, 'g')
const matches = processedLine.match(regex) || []
let isValid = true
matches.forEach((match) => {
const shortMeetingId = match.split(meetingURL)[1]?.split(/[),\s]/)[0] // Split by closing parenthesis, comma, or space
const actualMeetingId = shortMeetingId && (meetingLookup[shortMeetingId] as string)
if (shortMeetingId && actualMeetingId) {
processedLine = processedLine.replace(shortMeetingId, actualMeetingId)
} else {
const error = new Error(
`AI hallucinated. Unable to find meetingId for ${shortMeetingId}. Line: ${line}`
)
sendToSentry(error)
isValid = false
}
})
return isValid ? processedLine : ''
}
return line
})
.filter((line) => line.trim() !== '')
}
const 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
for (const match of matches) {
const shortMeetingId = match.split(meetingURL)[1]?.split(/[),\s]/)[0] // Split by closing parenthesis, comma, or space
const actualMeetingId = shortMeetingId && (meetingLookup[shortMeetingId] as string)
if (shortMeetingId && actualMeetingId) {
processedLine = processedLine.replace(shortMeetingId, actualMeetingId)
} else {
sendToSentry(new Error(
`AI hallucinated. Unable to find meetingId for ${shortMeetingId}. Line: ${line}`
))
isValid = false
}
}
return isValid ? processedLine : ''
}
return line
})
.filter((line) => line.trim() !== '')
}


const processSection = (section: string[]): string[] => {
return section
.flatMap((item) => {
const lines = item.split('\n')
return processLines(lines)
})
.filter((processedItem) => processedItem.trim() !== '')
}

export const getTopics = async (
teamId: string,
startDate: Date,
endDate: Date,
dataLoader: DataLoaderWorker,
prompt?: string | null
) => {
const r = await getRethink()
const MIN_REFLECTION_COUNT = 3
const MIN_MILLISECONDS = 60 * 1000 // 1 minute
const rawMeetings = await r
.table('NewMeeting')
.getAll(teamId, {index: 'teamId'})
.filter((row: any) =>
row('meetingType')
.eq('retrospective')
.and(row('createdAt').ge(startDate))
.and(row('createdAt').le(endDate))
.and(row('reflectionCount').gt(MIN_REFLECTION_COUNT))
.and(r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'}).count().gt(1))
.and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS))
)
.run()

const meetings = await Promise.all(
rawMeetings.map(async (meeting) => {
const {
id: meetingId,
disableAnonymity,
name: meetingName,
createdAt: meetingDate
} = meeting as MeetingRetrospective
const rawReflectionGroups = await dataLoader
.get('retroReflectionGroupsByMeetingId')
.load(meetingId)
const reflectionGroups = Promise.all(
rawReflectionGroups
.filter((g) => g.voterIds.length > 0)
.map(async (group) => {
const {id: reflectionGroupId, voterIds, title} = group
const [comments, rawReflections] = await Promise.all([
getComments(reflectionGroupId, dataLoader),
dataLoader.get('retroReflectionsByGroupId').load(group.id)
])
const reflections = await Promise.all(
rawReflections.map(async (reflection) => {
const {promptId, creatorId, plaintextContent} = reflection
const [prompt, creator] = await Promise.all([
dataLoader.get('reflectPrompts').load(promptId),
creatorId ? dataLoader.get('users').loadNonNull(creatorId) : null
])
const {question} = prompt
const creatorName =
disableAnonymity && creator ? creator.preferredName : 'Anonymous'
return {
prompt: question,
author: creatorName,
text: plaintextContent
}
})
)
const res = {
voteCount: voterIds.length,
title: title,
comments,
reflections,
meetingName,
date: meetingDate,
meetingId
}

if (!res.comments || !res.comments.length) {
delete (res as any).comments
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace delete operator with assignment to undefined.

The delete operator can impact performance. Replace it with an assignment to undefined.

- delete (res as any).comments
+ (res as any).comments = undefined
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
delete (res as any).comments
(res as any).comments = undefined
Tools
Biome

[error] 178-178: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

}
return res
})
)
return reflectionGroups
})
)

const hotTopics = meetings
.flat()
.filter((t) => t.voteCount > 2)
.sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1))

const idGenerator = {
meeting: 1
}

const shortTokenedTopics = hotTopics
.map((t) => {
const {date, meetingId} = t
const shortMeetingId = `m${idGenerator.meeting++}`
const shortMeetingDate = new Date(date).toISOString().split('T')[0]
meetingLookup[shortMeetingId] = meetingId
return {
...t,
date: shortMeetingDate,
meetingId: shortMeetingId
}
})
.filter((t) => t)

if (shortTokenedTopics.length === 0) {
return standardError(new Error('No meeting content found for the specified date range.'))
}

const yamlData = yaml.dump(shortTokenedTopics, {
noCompatMode: true
})

const openAI = new OpenAIServerManager()
const rawInsight = await openAI.generateInsight(yamlData, false, prompt)
if (!rawInsight) {
return standardError(new Error('Unable to generate insight.'))
}

const wins = processSection(rawInsight.wins)
const challenges = processSection(rawInsight.challenges)
const meetingIds = rawMeetings.map((meeting) => meeting.id)

return {wins, challenges, meetingIds}
}
Comment on lines +106 to +229
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid using the delete operator

The delete operator can impact performance. Replace it with an assignment to undefined.

- delete (res as any).comments
+ (res as any).comments = undefined
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const getTopics = async (
teamId: string,
startDate: Date,
endDate: Date,
dataLoader: DataLoaderWorker,
prompt?: string | null
) => {
const r = await getRethink()
const MIN_REFLECTION_COUNT = 3
const MIN_MILLISECONDS = 60 * 1000 // 1 minute
const rawMeetings = await r
.table('NewMeeting')
.getAll(teamId, {index: 'teamId'})
.filter((row: any) =>
row('meetingType')
.eq('retrospective')
.and(row('createdAt').ge(startDate))
.and(row('createdAt').le(endDate))
.and(row('reflectionCount').gt(MIN_REFLECTION_COUNT))
.and(r.table('MeetingMember').getAll(row('id'), {index: 'meetingId'}).count().gt(1))
.and(row('endedAt').sub(row('createdAt')).gt(MIN_MILLISECONDS))
)
.run()
const meetings = await Promise.all(
rawMeetings.map(async (meeting) => {
const {
id: meetingId,
disableAnonymity,
name: meetingName,
createdAt: meetingDate
} = meeting as MeetingRetrospective
const rawReflectionGroups = await dataLoader
.get('retroReflectionGroupsByMeetingId')
.load(meetingId)
const reflectionGroups = Promise.all(
rawReflectionGroups
.filter((g) => g.voterIds.length > 0)
.map(async (group) => {
const {id: reflectionGroupId, voterIds, title} = group
const [comments, rawReflections] = await Promise.all([
getComments(reflectionGroupId, dataLoader),
dataLoader.get('retroReflectionsByGroupId').load(group.id)
])
const reflections = await Promise.all(
rawReflections.map(async (reflection) => {
const {promptId, creatorId, plaintextContent} = reflection
const [prompt, creator] = await Promise.all([
dataLoader.get('reflectPrompts').load(promptId),
creatorId ? dataLoader.get('users').loadNonNull(creatorId) : null
])
const {question} = prompt
const creatorName =
disableAnonymity && creator ? creator.preferredName : 'Anonymous'
return {
prompt: question,
author: creatorName,
text: plaintextContent
}
})
)
const res = {
voteCount: voterIds.length,
title: title,
comments,
reflections,
meetingName,
date: meetingDate,
meetingId
}
if (!res.comments || !res.comments.length) {
delete (res as any).comments
}
return res
})
)
return reflectionGroups
})
)
const hotTopics = meetings
.flat()
.filter((t) => t.voteCount > 2)
.sort((a, b) => (a.voteCount > b.voteCount ? -1 : 1))
const idGenerator = {
meeting: 1
}
const shortTokenedTopics = hotTopics
.map((t) => {
const {date, meetingId} = t
const shortMeetingId = `m${idGenerator.meeting++}`
const shortMeetingDate = new Date(date).toISOString().split('T')[0]
meetingLookup[shortMeetingId] = meetingId
return {
...t,
date: shortMeetingDate,
meetingId: shortMeetingId
}
})
.filter((t) => t)
if (shortTokenedTopics.length === 0) {
return standardError(new Error('No meeting content found for the specified date range.'))
}
const yamlData = yaml.dump(shortTokenedTopics, {
noCompatMode: true
})
const openAI = new OpenAIServerManager()
const rawInsight = await openAI.generateInsight(yamlData, false, prompt)
if (!rawInsight) {
return standardError(new Error('Unable to generate insight.'))
}
const wins = processSection(rawInsight.wins)
const challenges = processSection(rawInsight.challenges)
const meetingIds = rawMeetings.map((meeting) => meeting.id)
return {wins, challenges, meetingIds}
}
if (!res.comments || !res.comments.length) {
(res as any).comments = undefined
}
Tools
Biome

[error] 178-178: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

Loading
Loading