-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 079d170
Showing
13 changed files
with
851 additions
and
0 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
name: Test Issue Triage Bot | ||
|
||
# Run when an issue is created. | ||
on: | ||
issues: | ||
types: | ||
- opened | ||
|
||
# All permissions not specified are set to 'none'. | ||
permissions: | ||
issues: write | ||
|
||
jobs: | ||
triage_issues: | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions/checkout@v4 | ||
with: | ||
repository: fluttercandies/triage_bot_for_flutter_photo_manager | ||
|
||
- uses: dart-lang/setup-dart@v1 | ||
|
||
- run: dart pub get | ||
working-directory: pkgs/issue_triage_bot | ||
|
||
# Delay 1 minutes to allow a grace period between opening or transferring | ||
# an issue and assigning a label. | ||
- name: sleep 1m | ||
run: sleep 60 | ||
|
||
- name: triage issue | ||
working-directory: pkgs/issue_triage_bot | ||
env: | ||
ISSUE_URL: ${{ github.event.issue.html_url }} | ||
GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN }} | ||
GOOGLE_API_KEY: ${{ secrets.GEMINI_API_KEY }} | ||
run: dart bin/triage.dart $ISSUE_URL |
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 @@ | ||
* @AmosHuKe |
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,10 @@ | ||
# Files and directories created by pub. | ||
.dart_tool/ | ||
.packages | ||
|
||
# Conventional directory for build outputs. | ||
build/ | ||
|
||
# Omit committing pubspec.lock for library packages; see | ||
# https://dart.dev/guides/libraries/private-files#pubspeclock. | ||
pubspec.lock |
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,27 @@ | ||
Copyright 2022, the Dart project authors. | ||
|
||
Redistribution and use in source and binary forms, with or without | ||
modification, are permitted provided that the following conditions are | ||
met: | ||
|
||
* Redistributions of source code must retain the above copyright | ||
notice, this list of conditions and the following disclaimer. | ||
* Redistributions in binary form must reproduce the above | ||
copyright notice, this list of conditions and the following | ||
disclaimer in the documentation and/or other materials provided | ||
with the distribution. | ||
* Neither the name of Google LLC nor the names of its | ||
contributors may be used to endorse or promote products derived | ||
from this software without specific prior written permission. | ||
|
||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | ||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | ||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | ||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | ||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | ||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | ||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | ||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | ||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
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,27 @@ | ||
> Source: https://github.com/dart-lang/ecosystem | ||
## What's this? | ||
|
||
A LLM based triage automation system for the `fluttercandies/flutter_photo_manager` repo. It processes | ||
new issues filed against the repo and triages them in the same manner that a | ||
human would. This includes: | ||
|
||
- re-summarizing the issue for clarity | ||
- assigning the issues to the label | ||
|
||
## Bot trigger and entry-point | ||
|
||
This bot is generally triggered by a GitHub workflow listening for new issues | ||
on the `fluttercandies/flutter_photo_manager` repo. | ||
|
||
See https://github.comfluttercandies/flutter_photo_manager/blob/main/.github/workflows/issue-triage.yml. | ||
|
||
## Overview | ||
|
||
The general workflow of the tool is: | ||
|
||
- download the issue information (existing labels, title, first comment) | ||
- ask Gemini to summarize the issue (see [prompts](lib/src/prompts.dart)) | ||
- ask Gemini to classify the issue (see [prompts](lib/src/prompts.dart)) | ||
- create a comment on the issue (`@github-bot`) with the summary; | ||
apply any labels produced as part of the classification |
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 @@ | ||
include: package:flutter_lints/flutter.yaml |
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,98 @@ | ||
// ignore_for_file: avoid_print | ||
|
||
import 'dart:io' as io; | ||
|
||
import 'package:args/args.dart'; | ||
import 'package:github/github.dart'; | ||
import 'package:http/http.dart' as http; | ||
import 'package:issue_triage_bot/src/common.dart'; | ||
import 'package:issue_triage_bot/src/gemini.dart'; | ||
import 'package:issue_triage_bot/src/github.dart'; | ||
import 'package:issue_triage_bot/triage.dart'; | ||
|
||
void main(List<String> arguments) async { | ||
final argParser = ArgParser(); | ||
argParser.addFlag( | ||
'dry-run', | ||
negatable: false, | ||
help: 'Perform triage but don\'t make any actual changes to the issue.', | ||
); | ||
argParser.addFlag( | ||
'force', | ||
negatable: false, | ||
help: 'Make changes to the issue even if it already looks triaged.', | ||
); | ||
argParser.addFlag( | ||
'production', | ||
negatable: false, | ||
help: | ||
'true: fluttercandies/flutter_photo_manager, false: fluttercandies/triage_bot_for_flutter_photo_manager', | ||
); | ||
argParser.addFlag( | ||
'help', | ||
abbr: 'h', | ||
negatable: false, | ||
help: 'Print this usage information.', | ||
); | ||
|
||
final ArgResults results; | ||
try { | ||
results = argParser.parse(arguments); | ||
} on ArgParserException catch (e) { | ||
print(e.message); | ||
print(''); | ||
print(usage); | ||
print(''); | ||
print(argParser.usage); | ||
io.exit(64); | ||
} | ||
|
||
if (results.flag('help') || results.rest.isEmpty) { | ||
print(usage); | ||
print(''); | ||
print(argParser.usage); | ||
io.exit(results.flag('help') ? 0 : 64); | ||
} | ||
|
||
String issue = results.rest.first; | ||
final bool dryRun = results.flag('dry-run'); | ||
final bool forceTriage = results.flag('force'); | ||
final bool production = results.flag('production'); | ||
|
||
// Accept either an issue number or a url (i.e., | ||
// https://github.com/fluttercandies/flutter_photo_manager/issues/1215). | ||
final String issueToken = | ||
'${getRepositorySlug(production).toString()}/issues/'; | ||
if (issue.contains(issueToken)) { | ||
issue = issue.substring(issue.indexOf(issueToken) + issueToken.length); | ||
} | ||
|
||
final client = http.Client(); | ||
|
||
final github = GitHub( | ||
auth: Authentication.withToken(githubToken), | ||
client: client, | ||
); | ||
final githubService = GithubService(github: github); | ||
|
||
final geminiService = GeminiService( | ||
apiKey: geminiKey, | ||
httpClient: client, | ||
); | ||
|
||
await triage( | ||
int.parse(issue), | ||
dryRun: dryRun, | ||
forceTriage: forceTriage, | ||
githubService: githubService, | ||
geminiService: geminiService, | ||
logger: Logger(), | ||
); | ||
|
||
client.close(); | ||
} | ||
|
||
const String usage = ''' | ||
A tool to triage issues from https://github.com/fluttercandies/flutter_photo_manager. | ||
usage: dart bin/triage.dart [options] <issue>'''; |
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,53 @@ | ||
// ignore_for_file: avoid_print | ||
|
||
import 'dart:io'; | ||
|
||
String? _envFileTokenOrEnvironment({required String key}) { | ||
final envFile = File('.env'); | ||
if (envFile.existsSync()) { | ||
final env = <String, String>{}; | ||
for (final String line | ||
in envFile.readAsLinesSync().map((line) => line.trim())) { | ||
if (line.isEmpty || line.startsWith('#')) continue; | ||
final int split = line.indexOf('='); | ||
env[line.substring(0, split).trim()] = line.substring(split + 1).trim(); | ||
} | ||
return env[key]; | ||
} else { | ||
return Platform.environment[key]; | ||
} | ||
} | ||
|
||
String get githubToken { | ||
final String? token = _envFileTokenOrEnvironment(key: 'GITHUB_TOKEN'); | ||
if (token == null) { | ||
throw StateError('This tool expects a github access token in the ' | ||
'GITHUB_TOKEN environment variable.'); | ||
} | ||
return token; | ||
} | ||
|
||
String get geminiKey { | ||
final String? token = _envFileTokenOrEnvironment(key: 'GOOGLE_API_KEY'); | ||
if (token == null) { | ||
throw StateError('This tool expects a gemini api key in the ' | ||
'GOOGLE_API_KEY environment variable.'); | ||
} | ||
return token; | ||
} | ||
|
||
/// Maximal length of body used for querying. | ||
const bodyLengthLimit = 10 * 1024; | ||
|
||
/// The [body], truncated if larger than [bodyLengthLimit]. | ||
String trimmedBody(String body) { | ||
return body.length > bodyLengthLimit | ||
? body = body.substring(0, bodyLengthLimit) | ||
: body; | ||
} | ||
|
||
class Logger { | ||
void log(String message) { | ||
print(message); | ||
} | ||
} |
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 'package:google_generative_ai/google_generative_ai.dart'; | ||
import 'package:http/http.dart' as http; | ||
|
||
class GeminiService { | ||
// Possible values for models: gemini-1.5-pro-latest, gemini-1.5-flash-latest, | ||
// gemini-1.0-pro-latest, gemini-1.5-flash-exp-0827. | ||
static const String classificationModel = 'models/gemini-1.5-flash-latest'; | ||
static const String summarizationModel = 'models/gemini-1.5-flash-latest'; | ||
|
||
final GenerativeModel _summarizeModel; | ||
final GenerativeModel _classifyModel; | ||
|
||
GeminiService({ | ||
required String apiKey, | ||
required http.Client httpClient, | ||
}) : _summarizeModel = GenerativeModel( | ||
model: summarizationModel, | ||
apiKey: apiKey, | ||
generationConfig: GenerationConfig(temperature: 0.2), | ||
httpClient: httpClient, | ||
), | ||
_classifyModel = GenerativeModel( | ||
// TODO(Amos): 之后有必要的话可以换成微调的模型(指定仓库的历史) | ||
// model: 'tunedModels/autotune-triage-tuned-prompt-xxx', | ||
model: classificationModel, | ||
apiKey: apiKey, | ||
generationConfig: GenerationConfig(temperature: 0.2), | ||
httpClient: httpClient, | ||
); | ||
|
||
/// Call the summarize model with the given prompt. | ||
/// | ||
/// On failures, this will throw a [GenerativeAIException]. | ||
Future<String> summarize(String prompt) { | ||
return _query(_summarizeModel, prompt); | ||
} | ||
|
||
/// Call the classify model with the given prompt. | ||
/// | ||
/// On failures, this will throw a [GenerativeAIException]. | ||
Future<List<String>> classify(String prompt) async { | ||
final result = await _query(_classifyModel, prompt); | ||
final labels = result.split(',').map((l) => l.trim()).toList()..sort(); | ||
return labels; | ||
} | ||
|
||
Future<String> _query(GenerativeModel model, String prompt) async { | ||
final response = await model.generateContent([Content.text(prompt)]); | ||
return (response.text ?? '').trim(); | ||
} | ||
} |
Oops, something went wrong.