Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
AmosHuKe committed Nov 1, 2024
0 parents commit 079d170
Show file tree
Hide file tree
Showing 13 changed files with 851 additions and 0 deletions.
37 changes: 37 additions & 0 deletions .github/workflows/test_issue_triage_bot.yml
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
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* @AmosHuKe
10 changes: 10 additions & 0 deletions pkgs/issue_triage_bot/.gitignore
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
27 changes: 27 additions & 0 deletions pkgs/issue_triage_bot/LICENSE
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.
27 changes: 27 additions & 0 deletions pkgs/issue_triage_bot/README.md
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
1 change: 1 addition & 0 deletions pkgs/issue_triage_bot/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include: package:flutter_lints/flutter.yaml
98 changes: 98 additions & 0 deletions pkgs/issue_triage_bot/bin/triage.dart
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>''';
53 changes: 53 additions & 0 deletions pkgs/issue_triage_bot/lib/src/common.dart
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);
}
}
51 changes: 51 additions & 0 deletions pkgs/issue_triage_bot/lib/src/gemini.dart
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();
}
}
Loading

0 comments on commit 079d170

Please sign in to comment.