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 new meeting summaries for a team #10050

Merged
merged 44 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
44 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
a5c6a61
generate new meeting summary
nickoferrall Jul 29, 2024
e082197
create new summaries for old meetings
nickoferrall Jul 29, 2024
5cbcb1a
clean up
nickoferrall Jul 30, 2024
568a560
move generateMeetingSummary to private schema
nickoferrall Jul 30, 2024
6de0b50
remove generateMeetingSummary from permissions
nickoferrall Jul 30, 2024
13ef865
update userPrompt type
nickoferrall Jul 31, 2024
5e4f68c
update generateInsight userPrompt type
nickoferrall Jul 31, 2024
9920f55
update userPrompt type
nickoferrall Jul 31, 2024
7c858cd
Merge branch 'master' into feat/summary-of-summaries
nickoferrall Jul 31, 2024
76b5883
fix conflict
nickoferrall Jul 31, 2024
80683fc
Merge branch 'feat/summary-of-summaries' into feat/10038/new-meeting-…
nickoferrall Jul 31, 2024
4d5e81e
render link in summary
nickoferrall Aug 5, 2024
d854293
Merge branch 'master' of github.com:ParabolInc/parabol
nickoferrall Aug 7, 2024
c0702d2
fix merge conflict
nickoferrall Aug 7, 2024
b64ed8c
remove meeting summary changes following merge conflict
nickoferrall Aug 7, 2024
344de95
fix yarn lock
nickoferrall Aug 7, 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
41 changes: 22 additions & 19 deletions codegen.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
"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",
"Organization": "../public/types/Organization#OrganizationSource",
"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",
Expand All @@ -43,19 +45,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",
Expand All @@ -70,9 +60,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",
Expand All @@ -85,15 +77,22 @@
"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",
"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",
Expand Down Expand Up @@ -125,8 +124,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",
Expand All @@ -139,6 +138,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",
Expand Down Expand Up @@ -177,8 +178,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"
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
185 changes: 185 additions & 0 deletions packages/server/graphql/private/mutations/generateMeetingSummary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
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 {MutationResolvers} from '../resolverTypes'

const generateMeetingSummary: MutationResolvers['generateMeetingSummary'] = async (
_source,
{teamIds, prompt},
{dataLoader}
) => {
const r = await getRethink()
const pg = getKysely()
const MIN_MILLISECONDS = 60 * 1000 // 1 minute
const MIN_REFLECTION_COUNT = 3

const endDate = new Date()
const twoYearsAgo = new Date()
twoYearsAgo.setFullYear(endDate.getFullYear() - 2)

const rawMeetings = (await r
.table('NewMeeting')
.getAll(r.args(teamIds), {index: 'teamId'})
.filter((row: any) =>
row('meetingType')
.eq('retrospective')
.and(row('createdAt').ge(twoYearsAgo))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have this two year limit so that the mutation doesn't take too long, but we could remove it and just accept that it might take several minutes to complete? Otherwise, when building the Insights UI, we'll need to restrict summaries to the past two years

.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[]

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
const rawReflectionGroups = await dataLoader
.get('retroReflectionGroupsByMeetingId')
.load(meetingId)
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
})
)

return reflectionGroups
}
const manager = new OpenAIServerManager()

const updatedMeetingIds = await Promise.all(
rawMeetings.map(async (meeting) => {
const meetingsContent = await getMeetingsContent(meeting)
if (!meetingsContent || meetingsContent.length === 0) {
return null
}
const yamlData = yaml.dump(meetingsContent, {
noCompatMode: true
})
const newSummary = await manager.generateSummary(yamlData, prompt)
if (!newSummary) return null

const now = new Date()
await r
.table('NewMeeting')
.get(meeting.id)
.update({summary: newSummary, updatedAt: now})
Copy link
Member

Choose a reason for hiding this comment

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

+1 overwriting a user-facing value on an ad-hoc basis feels a little dirty. It means the experiments we run will be immediately visible to the user. Probably OK for now if we assume the prompt isn't going to change!

.run()
meeting.summary = newSummary
return meeting.id
})
)
const filteredMeetingIds = updatedMeetingIds.filter(
(meetingId): meetingId is string => meetingId !== null
)
const data = {meetingIds: filteredMeetingIds}
return data
}

export default generateMeetingSummary
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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!]!

"""
The optional user prompt that will be used to generate the meeting summary
"""
prompt: String
): 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!]!
}
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading