-
Notifications
You must be signed in to change notification settings - Fork 334
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
Changes from 39 commits
Commits
Show all changes
44 commits
Select commit
Hold shift + click to select a range
05250cf
add generateInsight mutation
nickoferrall c4ef355
replace shortUrls with real urls
nickoferrall 450b9c2
handle teamId arg and replace short meeting ids
nickoferrall c806eca
add orgId arg to generateInsight
nickoferrall e262f9f
filter meetings more efficiently
nickoferrall 3e4aff4
return wins and challenges from generateInsight
nickoferrall b0b042e
Merge branch 'feat/generate-insight' into feat/generate-team-insight
nickoferrall 14a34dc
generate insight
nickoferrall 7792681
implement addInsight migration
nickoferrall 7c02ca4
check for existingInsight
nickoferrall 91e56b0
start summary of summaries
nickoferrall a0f2607
include links to discussions
nickoferrall adc15ac
update prompt
nickoferrall 3b939b1
return summary if exists
nickoferrall cebc7b3
update prompt and clean up processing getTopics meetingId
nickoferrall e15d1e7
remove generated files
nickoferrall 23448b1
remove meetingSummary yaml file
nickoferrall fde4584
update short meeting date
nickoferrall 555ec41
Merge branch 'master' into feat/summary-of-summaries
nickoferrall ca5c6fc
move addInsight migration after merging master
nickoferrall 6e5aeeb
fix insight start end date insert
nickoferrall 5b1964e
return prev insight if dates and teamid exist
nickoferrall bc7b351
update generate insight prompt to reduce jargon
nickoferrall f9e3449
update migration to make wins and challenges non null
nickoferrall aeb8fa8
accept prompt as arg in generateInsight
nickoferrall cc9768c
update migration order
nickoferrall a3ba8d0
remove meetings from generateInsight query
nickoferrall 5c191ef
use number.isNaN instead
nickoferrall a5c6a61
generate new meeting summary
nickoferrall e082197
create new summaries for old meetings
nickoferrall 5cbcb1a
clean up
nickoferrall 568a560
move generateMeetingSummary to private schema
nickoferrall 6de0b50
remove generateMeetingSummary from permissions
nickoferrall 13ef865
update userPrompt type
nickoferrall 5e4f68c
update generateInsight userPrompt type
nickoferrall 9920f55
update userPrompt type
nickoferrall 7c858cd
Merge branch 'master' into feat/summary-of-summaries
nickoferrall 76b5883
fix conflict
nickoferrall 80683fc
Merge branch 'feat/summary-of-summaries' into feat/10038/new-meeting-…
nickoferrall 4d5e81e
render link in summary
nickoferrall d854293
Merge branch 'master' of github.com:ParabolInc/parabol
nickoferrall c0702d2
fix merge conflict
nickoferrall b64ed8c
remove meeting summary changes following merge conflict
nickoferrall 344de95
fix yarn lock
nickoferrall File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
185
packages/server/graphql/private/mutations/generateMeetingSummary.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
.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}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
28 changes: 28 additions & 0 deletions
28
packages/server/graphql/private/typeDefs/generateMeetingSummary.graphql
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!]! | ||
} |
17 changes: 17 additions & 0 deletions
17
packages/server/graphql/private/types/GenerateMeetingSummarySuccess.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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