blob: de5d0498b26a5805a49f95fd8affa9ff8e46bde6 [file] [log] [blame]
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io';
import 'package:github/github.dart';
import 'package:google_generative_ai/google_generative_ai.dart';
import 'src/common.dart';
import 'src/gemini.dart';
import 'src/github.dart';
import 'src/prompts.dart';
final sdkSlug = RepositorySlug('dart-lang', 'sdk');
Future<void> triage(
int issueNumber, {
bool dryRun = false,
bool forceTriage = false,
required GithubService githubService,
required GeminiService geminiService,
required Logger logger,
}) async {
logger.log('Triaging $sdkSlug...');
logger.log('');
// retrieve the issue
final issue = await githubService.fetchIssue(sdkSlug, issueNumber);
logger.log('## issue ${issue.htmlUrl}');
logger.log('');
final labels = issue.labels.map((l) => l.name).toList();
if (labels.isNotEmpty) {
logger.log('labels: ${labels.join(', ')}');
logger.log('');
}
logger.log('"${issue.title}"');
logger.log('');
final bodyLines =
issue.body.split('\n').where((l) => l.trim().isNotEmpty).toList();
for (final line in bodyLines.take(4)) {
logger.log(line);
}
if (bodyLines.length > 4) {
logger.log('...');
}
logger.log('');
// If the issue has any comments, retrieve and include the last comment in the
// prompt.
String? lastComment;
if (issue.hasComments) {
final comments = await githubService.fetchIssueComments(sdkSlug, issue);
final comment = comments.last;
lastComment = '''
---
Here is the last comment on the issue (by user @${comment.user?.login}):
${trimmedBody(comment.body ?? '')}
''';
}
// decide if we should triage
if (!forceTriage) {
if (issue.alreadyTriaged) {
logger.log('Exiting (issue is already triaged).');
return;
}
}
var bodyTrimmed = trimmedBody(issue.body);
// ask for the 'area-' classification
List<String> newLabels;
try {
newLabels = await geminiService.classify(
assignAreaPrompt(
title: issue.title,
body: bodyTrimmed,
lastComment: lastComment,
),
);
} on GenerativeAIException catch (e) {
// Failures here can include things like gemini safety issues, ...
stderr.writeln('gemini: $e');
exit(1);
}
// ask for the summary
String summary;
try {
summary = await geminiService.summarize(
summarizeIssuePrompt(
title: issue.title,
body: bodyTrimmed,
needsInfo: newLabels.contains('needs-info'),
),
);
} on GenerativeAIException catch (e) {
// Failures here can include things like gemini safety issues, ...
stderr.writeln('gemini: $e');
exit(1);
}
logger.log('## gemini summary');
logger.log('');
logger.log(summary);
logger.log('');
logger.log('## gemini classification');
logger.log('');
logger.log(newLabels.toString());
logger.log('');
if (dryRun) {
logger.log('Exiting (dry run mode - not applying changes).');
return;
}
// perform changes
logger.log('## github comment');
logger.log('');
logger.log('labels: $newLabels');
logger.log('');
logger.log(summary);
final comment = '**Summary:** $summary\n';
// create github comment
await githubService.createComment(sdkSlug, issueNumber, comment);
final allRepoLabels = await githubService.getAllLabels(sdkSlug);
final labelAdditions =
filterLegalLabels(newLabels, allRepoLabels: allRepoLabels);
if (labelAdditions.isNotEmpty) {
labelAdditions.add('triage-automation');
}
// apply github labels
if (newLabels.isNotEmpty) {
await githubService.addLabelsToIssue(sdkSlug, issueNumber, labelAdditions);
}
logger.log('');
logger.log('---');
logger.log('');
logger.log('Triaged ${issue.htmlUrl}');
}
List<String> filterLegalLabels(
List<String> labels, {
required List<String> allRepoLabels,
}) {
final validLabels = allRepoLabels.toSet();
return [
for (var label in labels)
if (validLabels.contains(label)) label,
]..sort();
}