Add nightly Moonbeam argument leaderboard extraction, API, and muzzle.lol review UI#258
Add nightly Moonbeam argument leaderboard extraction, API, and muzzle.lol review UI#258Copilot wants to merge 22 commits into
Conversation
|
@copilot refactor this to instead behave in such a way that as part of it's nightly job, this service will analyze all channels for arguments between users and determine who won. it should be intolerant of false positives, IE very selective in the conversations it believes are arguments. We should not be weakly checking if a tagged message is asking who won etc. This should largely be handled by the AI |
Addressed in a9a2285. Argument extraction now runs in a selective nightly analysis job across channels, and the tagged-message winner check was removed from Moonbeam participation. |
| - any conversation where the winner is ambiguous | ||
| - weak or low-substance disagreements that do not merit leaderboard tracking | ||
|
|
||
| If you find a qualifying argument, return ONLY a single JSON object in this exact shape: |
|
@copilot fix the failing checks come on COME ON |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
packages/backend/src/argument/argument.persistence.service.ts:88
- Participants are resolved without filtering out bot SlackUser records. That can allow bot accounts to be stored in
participants/participantViewpointsand appear in the UI. Consider addingisBot: falseto the participant lookup (and keeping the "at least 2 participants" check as "at least 2 human participants").
const participantUsers = await slackUserRepo.find({
where: participants.map((participant) => ({ slackId: participant.slackId, teamId: input.teamId })),
});
packages/backend/src/argument/argument.persistence.service.ts:156
- The leaderboard SQL does not filter out bot winners. Other leaderboards in the codebase exclude bots (e.g.
u.isBot = 0in DashboardPersistenceService), so adding the same filter here will prevent bot users from ranking on the argument leaderboard.
`SELECT u.name AS name,
u.slackId AS slackId,
CAST(COUNT(*) AS SIGNED) AS wins,
CAST(COALESCE(SUM(a.pointValue), 0) AS SIGNED) AS points
FROM argument_leaderboard a
INNER JOIN slack_user u ON u.id = a.winnerId
WHERE a.teamId = ?
GROUP BY u.id, u.name, u.slackId
ORDER BY wins DESC, points DESC, u.name ASC`,
| const slackUserRepo = getRepository(SlackUser); | ||
| const winner = await slackUserRepo.findOne({ | ||
| where: { slackId: input.winnerSlackId, teamId: input.teamId }, | ||
| }); |
There was a problem hiding this comment.
Fixed in 40a5c51. Winners and participant lookups now require isBot: false, and the leaderboard aggregate query also excludes bot winners.
| const participantSlackIds = extractParticipantSlackIds(historyMessages, { | ||
| excludeSlackIds: [MOONBEAM_SLACK_ID], | ||
| }); | ||
| if (participantSlackIds.length < 2) return; | ||
|
|
||
| await this.extractArgument(teamId, channelId, historyMessages); |
There was a problem hiding this comment.
Fixed in ce9c4b4. The job now checks the extracted participant Slack IDs against known non-bot SlackUser records and skips the OpenAI call unless at least two human participants remain.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (4)
packages/backend/src/argument/argument.persistence.service.ts:92
slackUserRepo.find({ where: participants.map(...) })can be called withparticipants.length === 0, which (depending on TypeORM behavior) may result in an unfiltered query. Add an early return when fewer than 2 participants remain after normalization, and includeisBot: falsein the participant lookup to prevent bot records from being attached.
const participants = Array.from(
new Map(
input.participants
.map(normalizeParticipant)
.filter((participant): participant is ArgumentParticipant => participant !== null)
.map((participant) => [participant.slackId, participant]),
).values(),
);
const participantUsers = await slackUserRepo.find({
where: participants.map((participant) => ({ slackId: participant.slackId, teamId: input.teamId, isBot: false })),
});
const extractedParticipantViewpoints = buildParticipantViewpoints(participants);
const resolvedParticipants = buildArgumentParticipants(participantUsers, extractedParticipantViewpoints);
const participantViewpoints = buildParticipantViewpoints(resolvedParticipants);
packages/backend/src/argument/argument.persistence.service.ts:99
- After resolving participants, there is no validation that the resolved participant set still contains
winnerSlackId. If the winner exists but is not among the resolved participants (e.g., participant resolution failed), you can persist an outcome where the winner wasn’t a participant. Consider skipping the save unless the winner is included in the resolved participants list.
const extractedParticipantViewpoints = buildParticipantViewpoints(participants);
const resolvedParticipants = buildArgumentParticipants(participantUsers, extractedParticipantViewpoints);
const participantViewpoints = buildParticipantViewpoints(resolvedParticipants);
if (resolvedParticipants.length < 2) {
this.logger.warn('Skipping argument outcome save because fewer than two participants were extracted', {
teamId: input.teamId,
channelId: input.channelId,
});
return null;
}
packages/backend/src/argument/argument.persistence.service.ts:156
- The aggregate leaderboard SQL does not exclude bot winners (missing
u.isBot = 0). If a bot is ever stored as a winner, it will be ranked here. Add the same bot filter used elsewhere in dashboard queries.
repo.query<LeaderboardRow[]>(
`SELECT u.name AS name,
u.slackId AS slackId,
CAST(COUNT(*) AS SIGNED) AS wins,
CAST(COALESCE(SUM(a.pointValue), 0) AS SIGNED) AS points
FROM argument_leaderboard a
INNER JOIN slack_user u ON u.id = a.winnerId
WHERE a.teamId = ? AND u.isBot = 0
GROUP BY u.id, u.name, u.slackId
ORDER BY wins DESC, points DESC, u.name ASC`,
packages/backend/src/argument/argument.persistence.service.spec.ts:174
- The
getArgumentLeaderboardtest only asserts the COUNT cast is present in the SQL. Once the query is updated to exclude bots, add an assertion that the SQL containsu.isBot = 0so bot filtering can’t regress silently.
winner: { id: 2, slackId: 'U2', name: 'Bob' },
pointValue: '4',
createdAt: '2026-05-21T00:00:00.000Z',
},
| const slackUserRepo = getRepository(SlackUser); | ||
| const winner = await slackUserRepo.findOne({ | ||
| where: { slackId: input.winnerSlackId, teamId: input.teamId, isBot: false }, | ||
| }); |
| repo.find({ | ||
| where: { teamId }, | ||
| relations: ['participants', 'winner'], | ||
| order: { createdAt: 'DESC' }, | ||
| }), |
| it('saves an argument outcome with a resolved winner', async () => { | ||
| findOne.mockResolvedValue({ id: 7, slackId: 'U2', name: 'Bob' }); | ||
| findUsers.mockResolvedValue([ | ||
| { id: 6, slackId: 'U1', name: 'Alice' }, | ||
| { id: 7, slackId: 'U2', name: 'Bob' }, | ||
| ]); | ||
| save.mockImplementation(async (entry: { createdAt?: Date }) => ({ | ||
| ...entry, | ||
| id: 11, | ||
| createdAt: entry.createdAt ?? new Date('2026-05-21T00:00:00.000Z'), | ||
| })); | ||
|
|
||
| const result = await service.saveArgumentOutcome({ | ||
| teamId: 'T1', | ||
| channelId: 'C1', | ||
| argumentSummary: 'Tabs versus spaces', | ||
| participants: [ | ||
| { slackId: 'U1', name: 'Alice', viewpoint: 'tabs are faster' }, | ||
| { slackId: 'U2', name: 'Bob', viewpoint: 'spaces are clearer' }, | ||
| ], | ||
| winnerSlackId: 'U2', | ||
| pointValue: 6, | ||
| }); | ||
|
|
||
| expect(findOne).toHaveBeenCalledWith({ where: { slackId: 'U2', teamId: 'T1', isBot: false } }); | ||
| expect(findUsers).toHaveBeenCalledWith({ | ||
| where: [ |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
| repo.find({ | ||
| where: { teamId }, | ||
| relations: ['participants', 'winner'], | ||
| order: { createdAt: 'DESC' }, | ||
| take: ARGUMENT_HISTORY_LIMIT, | ||
| }), |
| const participantViewpoints = buildParticipantViewpoints(resolvedParticipants); | ||
|
|
Original prompt
This pull request was created from Copilot chat.