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

Conversation

nickoferrall
Copy link
Contributor

@nickoferrall nickoferrall commented Jul 23, 2024

View more in depth update in Slack with example output here.

Fix: #10037

This PR generates an insight for a team. I'm comparing two approaches that you can test by toggling the useSummaries argument in the mutation:

  1. Generate an insight by passing a large YAML file with data from meetings.
  2. Summarise each meeting and then create a summary of those summaries.

The second option is significantly faster: one insight took 32 seconds with the first approach and 5 seconds with the second (see here).

There has been some feedback that the quality of the insight isn't quite as good with the second approach - see Drew & Jordan's comments here.

I prefer the second approach. The speed means that users can generate Insights for the time periods they want (e.g. since I became the boss). I think the quality is similar, and a better prompt will satisfy the concerns.

Rather than just me playing with the prompt, I'd like to open it up to anyone at Parabol, so I've added prompt as an optional argument.

I've added a Notion doc here as a guide to generate insights.

Next Steps

  • Create a separate PR with a mutation that generates new summarys for each retro meeting for a team. We need to do this as the old summaries don't include links or quotes.
  • Once this PR a that separate PR are in prod, let everyone at Parabol play with the prompts
  • Choose one of the two approaches and refactor the code to remove the redundant one

To test

  • Connect to staging: cloud-sql-proxy --address 0.0.0.0 --port 5433 parabol-saas-staging:us-central1:saas-staging-postgresql-clone-for-upgrade
  • Run the generateInsight mutation, e.g. generateInsight(teamId: [teamId], startDate: "2024-01-01T00:00:00.000Z", endDate: "2024-04-01T00:00:00.000Z", useSummaries: true) {

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • Introduced a GraphQL mutation for generating insights based on team data, including parameters for date ranges and summaries.
    • Added functionality to retrieve and process meeting summaries and topics for improved insight generation.
  • Bug Fixes

    • Enhanced permission checks for generating insights, restricting access to superusers to improve security.
  • Documentation

    • Updated GraphQL schema to include new types and mutation for generating insights.
  • Chores

    • Upgraded the OpenAI library to a newer version, potentially providing enhanced functionality for insight generation.

@nickoferrall nickoferrall marked this pull request as ready for review July 26, 2024 16:49
Copy link
Contributor

coderabbitai bot commented Jul 26, 2024

Walkthrough

The changes introduce a new GraphQL mutation for generating insights based on team performance across specified date ranges. This feature encompasses robust error handling, data retrieval from multiple sources, and structured response types. Additionally, it strengthens the permissions model by limiting access to superusers. New helper functions and types were created to facilitate effective insight processing, thereby enhancing the overall functionality of the application.

Changes

Files Change Summary
codegen.json Added a type declaration for GenerateInsightSuccess to enhance type definitions.
packages/client/mutations/GenerateInsightMutation.ts Introduced a new GraphQL mutation for generating insights, managing parameters and the mutation lifecycle, including error handling.
packages/server/graphql/public/mutations/generateInsight.ts Implemented a mutation resolver for generating insights, encompassing input validation, data retrieval, and database interactions.
packages/server/graphql/public/mutations/helpers/getSummaries.ts Added a function to retrieve and process meeting summaries, returning structured insights based on retrospective meetings.
packages/server/graphql/public/mutations/helpers/getTopics.ts Implemented a function to retrieve meeting topics and generate insights, including data processing and hierarchical comment organisation.
packages/server/graphql/public/permissions.ts Updated permission checks to restrict the generateInsight operation to superusers.
packages/server/graphql/public/typeDefs/generateInsight.graphql Extended the GraphQL schema with a new mutation, including input parameters and response types for insights.
packages/server/graphql/public/types/GenerateInsightSuccess.ts Defined a TypeScript type and resolver for GenerateInsightSuccess, enabling structured responses for insights.
packages/server/package.json Updated the openai package version from ^4.24.1 to ^4.53.0, incorporating enhancements and bug fixes.
packages/server/postgres/migrations/1722011287034_addInsight.ts Implemented database migration functions to create and manage the new Insight table for storing generated insights.
packages/server/utils/OpenAIServerManager.ts Added a new type and method for generating insights from meeting data, enhancing the OpenAIServerManager class with structured insight generation capabilities.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server
    participant Database
    participant OpenAI

    Client->>Server: Call generateInsight(teamId, startDate, endDate)
    Server->>Database: Validate and retrieve data
    Database-->>Server: Return meeting data
    Server->>OpenAI: Generate insights from data
    OpenAI-->>Server: Return insights
    Server-->>Client: Return GenerateInsightSuccess
Loading

Assessment against linked issues

Objective Addressed Explanation
Create a mutation that generates an insight for a team (#10037)

Possibly related issues


Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL

Commits

Files that changed from the base of the PR and between a3ba8d0 and 7c858cd.

Files selected for processing (4)
  • codegen.json (1 hunks)
  • packages/server/graphql/public/mutations/generateInsight.ts (1 hunks)
  • packages/server/graphql/public/mutations/helpers/getSummaries.ts (1 hunks)
  • packages/server/graphql/public/mutations/helpers/getTopics.ts (1 hunks)
Files skipped from review as they are similar to previous changes (3)
  • codegen.json
  • packages/server/graphql/public/mutations/generateInsight.ts
  • packages/server/graphql/public/mutations/helpers/getSummaries.ts
Additional context used
Biome
packages/server/graphql/public/mutations/helpers/getTopics.ts

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

Unsafe fix: Use an undefined assignment instead.

(lint/performance/noDelete)

Additional comments not posted (4)
packages/server/graphql/public/mutations/helpers/getTopics.ts (4)

1-8: Imports Review

The imports look appropriate and necessary for the functionality provided in the file. Ensure that all imported modules are used in the code.


192-194: LGTM!

The idGenerator object is straightforward and does not require changes.


63-65: LGTM!

The meetingLookup object is straightforward and does not require changes.


114-115: LGTM!

The constants MIN_REFLECTION_COUNT and MIN_MILLISECONDS are appropriately defined and used.


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

Share
Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai generate interesting stats about this repository and render them as a table.
    • @coderabbitai show all the console.log statements in this repository.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (invoked as PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Additionally, you can add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

return null
}

return data
Copy link
Contributor

Choose a reason for hiding this comment

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

Log the error before returning null.

Logging the error can help in debugging.

+ console.error('Error in generateInsight:', e)

Committable suggestion was skipped due to low confidence.

Comment on lines 348 to 352
async generateInsight(
yamlData: string,
useSummaries: boolean,
userPrompt?: string
): Promise<InsightResponse | null> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider making the meeting URL a configurable parameter.

Hardcoding the URL might limit flexibility. Consider passing it as a parameter or using a configuration file.

- const meetingURL = 'https://action.parabol.co/meet/'
+ const meetingURL = process.env.MEETING_URL || 'https://action.parabol.co/meet/'

Committable suggestion was skipped due to low confidence.

Comment on lines +394 to +410
try {
const response = await this.openAIApi.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'user',
content: `${prompt}\n\n${yamlData}`
}
],
response_format: {
type: 'json_object'
},
temperature: 0.7,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Enhance error handling by providing more context in error messages.

Providing more context in error messages can help in debugging.

- const error = e instanceof Error ? e : new Error('Error in generateInsight')
+ const error = e instanceof Error ? e : new Error(`Error in generateInsight: ${e}`)

Committable suggestion was skipped due to low confidence.

}

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)

Comment on lines 7 to 45
const generateInsight: MutationResolvers['generateInsight'] = async (
_source,
{teamId, startDate, endDate, useSummaries = true, prompt},
{dataLoader}
) => {
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 (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 as string)
: await getTopics(teamId, startDate, endDate, dataLoader, prompt as string)

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
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Use Number.isNaN instead of isNaN.

isNaN is unsafe as it attempts type coercion. Use Number.isNaN for better type safety.

-  if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
+  if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
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 generateInsight: MutationResolvers['generateInsight'] = async (
_source,
{teamId, startDate, endDate, useSummaries = true, prompt},
{dataLoader}
) => {
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 (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 as string)
: await getTopics(teamId, startDate, endDate, dataLoader, prompt as string)
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
}
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 as string)
: await getTopics(teamId, startDate, endDate, dataLoader, prompt as string)
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
}
Tools
Biome

[error] 12-12: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)


[error] 12-12: isNaN is unsafe. It attempts a type coercion. Use Number.isNaN instead.

See the MDN documentation for more details.
Unsafe fix: Use Number.isNaN instead.

(lint/suspicious/noGlobalIsNan)

@nickoferrall nickoferrall requested a review from mattkrick July 31, 2024 14:37
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Outside diff range, codebase verification and nitpick comments (1)
packages/server/graphql/public/mutations/helpers/getTopics.ts (1)

97-104: Improve readability in processSection

The function is straightforward but can be improved for readability by using descriptive variable names.

- return section
-   .flatMap((item) => {
-     const lines = item.split('\n')
-     return processLines(lines)
-   })
-   .filter((processedItem) => processedItem.trim() !== '')
+ return section
+   .flatMap((paragraph) => processLines(paragraph.split('\n')))
+   .filter((processedLine) => processedLine.trim() !== '')

Comment on lines +106 to +229
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}
}
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)

Comment on lines +66 to +95
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() !== '')
}
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() !== '')
}

Comment on lines +10 to +61
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
}
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
}

@nickoferrall nickoferrall merged commit dbb3497 into master Aug 7, 2024
8 checks passed
@nickoferrall nickoferrall deleted the feat/summary-of-summaries branch August 7, 2024 09:58
@github-actions github-actions bot mentioned this pull request Aug 8, 2024
24 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Insights: generate insight for team
1 participant