Skip to content

Commit 861360b

Browse files
committed
feat: Stale Issue Auditor agent implementation
1 parent 6a2669b commit 861360b

File tree

8 files changed

+1220
-6
lines changed

8 files changed

+1220
-6
lines changed

.github/workflows/stale-bot.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
2+
name: ADK Stale Issue Auditor (Java)
3+
4+
on:
5+
workflow_dispatch:
6+
schedule:
7+
# This runs at 6:00 AM UTC (10 PM PST)
8+
- cron: '0 6 * * *'
9+
10+
jobs:
11+
audit-stale-issues:
12+
13+
if: github.repository == 'google/adk-java'
14+
15+
runs-on: ubuntu-latest
16+
timeout-minutes: 60
17+
18+
permissions:
19+
issues: write
20+
contents: read
21+
22+
steps:
23+
- name: Checkout repository
24+
uses: actions/checkout@v4
25+
26+
- name: Set up JDK 17
27+
uses: actions/setup-java@v4
28+
with:
29+
java-version: '17'
30+
distribution: 'temurin'
31+
cache: maven
32+
33+
- name: Build with Maven
34+
run: mvn clean compile
35+
36+
- name: Run Auditor Agent
37+
env:
38+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
40+
STALE_HOURS_THRESHOLD: ${{ secrets.STALE_HOURS_THRESHOLD }}
41+
CLOSE_HOURS_AFTER_STALE_THRESHOLD: ${{ secrets.CLOSE_HOURS_AFTER_STALE_THRESHOLD }}
42+
43+
GRAPHQL_COMMENT_LIMIT: ${{ secrets.GRAPHQL_COMMENT_LIMIT }}
44+
GRAPHQL_EDIT_LIMIT: ${{ secrets.GRAPHQL_EDIT_LIMIT }}
45+
GRAPHQL_TIMELINE_LIMIT: ${{ secrets.GRAPHQL_TIMELINE_LIMIT }}
46+
47+
SLEEP_BETWEEN_CHUNKS: ${{ secrets.SLEEP_BETWEEN_CHUNKS }}
48+
49+
OWNER: ${{ github.repository_owner }}
50+
REPO: adk-java
51+
CONCURRENCY_LIMIT: 3
52+
LLM_MODEL_NAME: "gemini-2.5-flash"
53+
54+
JAVA_TOOL_OPTIONS: "-Djava.util.logging.SimpleFormatter.format='%1$tF %1$tT %4$s %2$s %5$s%6$s%n'"
55+
56+
run: mvn compile exec:java@run-stale-bot -pl :stale-agent

contrib/samples/pom.xml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
1-
<?xml version="1.0" encoding="UTF-8"?>
2-
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
1+
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
32
<modelVersion>4.0.0</modelVersion>
4-
53
<parent>
64
<groupId>com.google.adk</groupId>
75
<artifactId>google-adk-parent</artifactId>
86
<version>0.7.1-SNAPSHOT</version><!-- {x-version-update:google-adk:current} -->
97
<relativePath>../..</relativePath>
108
</parent>
11-
129
<artifactId>google-adk-samples</artifactId>
1310
<packaging>pom</packaging>
14-
1511
<name>Google ADK Samples</name>
1612
<description>Aggregator for sample applications.</description>
17-
1813
<modules>
1914
<module>a2a_basic</module>
2015
<module>a2a_server</module>
2116
<module>configagent</module>
2217
<module>helloworld</module>
2318
<module>mcpfilesystem</module>
19+
<module>stale-agent</module>
2420
</modules>
2521
</project>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?xml version="1.0"?>
2+
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
4+
<modelVersion>4.0.0</modelVersion>
5+
<parent>
6+
<groupId>com.google.adk</groupId>
7+
<artifactId>google-adk-samples</artifactId>
8+
<version>0.7.1-SNAPSHOT</version>
9+
</parent>
10+
<groupId>com.google.adk</groupId>
11+
<artifactId>stale-agent</artifactId>
12+
<version>0.7.1-SNAPSHOT</version>
13+
<name>stale-agent</name>
14+
<url>http://maven.apache.org</url>
15+
<properties>
16+
<java.version>17</java.version>
17+
<maven.compiler.source>17</maven.compiler.source>
18+
<maven.compiler.target>17</maven.compiler.target>
19+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
20+
<adk.version>${project.version}</adk.version> <!--${project.version}</adk.version> -->
21+
<slf4j.version>2.0.9</slf4j.version>
22+
</properties>
23+
<dependencies>
24+
<dependency>
25+
<groupId>com.google.adk</groupId>
26+
<artifactId>google-adk</artifactId>
27+
<version>${adk.version}</version>
28+
</dependency>
29+
30+
<dependency>
31+
<groupId>org.kohsuke</groupId>
32+
<artifactId>github-api</artifactId>
33+
<version>1.318</version>
34+
</dependency>
35+
<dependency>
36+
<groupId>org.apache.httpcomponents.client5</groupId>
37+
<artifactId>httpclient5</artifactId>
38+
<version>5.2.1</version>
39+
</dependency>
40+
41+
<dependency>
42+
<groupId>org.slf4j</groupId>
43+
<artifactId>slf4j-api</artifactId>
44+
<version>${slf4j.version}</version>
45+
</dependency>
46+
47+
<dependency>
48+
<groupId>org.slf4j</groupId>
49+
<artifactId>slf4j-simple</artifactId>
50+
<version>${slf4j.version}</version>
51+
</dependency>
52+
53+
<dependency>
54+
<groupId>com.fasterxml.jackson.core</groupId>
55+
<artifactId>jackson-databind</artifactId>
56+
<version>2.16.1</version>
57+
</dependency>
58+
59+
</dependencies>
60+
<build>
61+
<plugins>
62+
<plugin>
63+
<groupId>org.springframework.boot</groupId>
64+
<artifactId>spring-boot-maven-plugin</artifactId>
65+
</plugin>
66+
<plugin>
67+
<groupId>org.codehaus.mojo</groupId>
68+
<artifactId>exec-maven-plugin</artifactId>
69+
<version>3.1.0</version>
70+
<executions>
71+
<execution>
72+
<id>run-stale-bot</id>
73+
<goals>
74+
<goal>java</goal>
75+
</goals>
76+
<configuration>
77+
<mainClass>com.google.adk.samples.stale.StaleBotApp</mainClass>
78+
</configuration>
79+
</execution>
80+
</executions>
81+
</plugin>
82+
</plugins>
83+
</build>
84+
</project>
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package com.google.adk.samples.stale;
2+
3+
import com.google.adk.runner.InMemoryRunner;
4+
import com.google.adk.samples.stale.agent.StaleAgent;
5+
import com.google.adk.samples.stale.config.StaleBotSettings;
6+
import com.google.adk.samples.stale.utils.GitHubUtils;
7+
import com.google.genai.types.Content;
8+
import com.google.genai.types.Part;
9+
import java.util.List;
10+
import java.util.UUID;
11+
import java.util.concurrent.CompletableFuture;
12+
import java.util.stream.Collectors;
13+
import org.slf4j.Logger;
14+
import org.slf4j.LoggerFactory;
15+
16+
public class StaleBotApp {
17+
18+
private static final Logger logger = LoggerFactory.getLogger(StaleAgent.class);
19+
private static final String USER_ID = "stale_bot_user";
20+
21+
record IssueResult(long issueNumber, double durationSeconds, int apiCalls) {}
22+
23+
public static void main(String[] args) {
24+
25+
try {
26+
runBot();
27+
} catch (Exception e) {
28+
logger.error(String.format("Unexpected fatal error", e));
29+
}
30+
}
31+
32+
public static void runBot() {
33+
logger.info(
34+
String.format(
35+
" Starting Stale Bot for " + StaleBotSettings.OWNER + "/" + StaleBotSettings.REPO));
36+
logger.info(String.format("Concurrency level set to " + StaleBotSettings.CONCURRENCY_LIMIT));
37+
38+
GitHubUtils.resetApiCallCount();
39+
40+
double filterDays = StaleBotSettings.STALE_HOURS_THRESHOLD / 24.0;
41+
logger.info(String.format("Fetching issues older than %.2f days...", filterDays));
42+
43+
List<Integer> allIssues;
44+
try {
45+
allIssues =
46+
GitHubUtils.getOldOpenIssueNumbers(
47+
StaleBotSettings.OWNER, StaleBotSettings.REPO, filterDays);
48+
} catch (Exception e) {
49+
logger.error(String.format("Failed to fetch issue list", e));
50+
return;
51+
}
52+
53+
int totalCount = allIssues.size();
54+
int searchApiCalls = GitHubUtils.getApiCallCount();
55+
56+
if (totalCount == 0) {
57+
logger.info(String.format("No issues matched the criteria. Run finished."));
58+
return;
59+
}
60+
61+
logger.info(
62+
String.format(
63+
"Found %d issues to process. (Initial search used %d API calls).",
64+
totalCount, searchApiCalls));
65+
66+
double totalProcessingTime = 0.0;
67+
int totalIssueApiCalls = 0;
68+
int processedCount = 0;
69+
70+
InMemoryRunner runner = new InMemoryRunner(StaleAgent.create());
71+
72+
for (int i = 0; i < totalCount; i += StaleBotSettings.CONCURRENCY_LIMIT) {
73+
int end = Math.min(i + StaleBotSettings.CONCURRENCY_LIMIT, totalCount);
74+
List<Integer> chunk = allIssues.subList(i, end);
75+
int currentChunkNum = (i / StaleBotSettings.CONCURRENCY_LIMIT) + 1;
76+
77+
logger.info(
78+
String.format("Starting chunk %d: Processing issues %s ", currentChunkNum, chunk));
79+
80+
// Create a list of Futures (Async Tasks)
81+
List<CompletableFuture<IssueResult>> futures =
82+
chunk.stream()
83+
.map(issueNum -> processSingleIssue(issueNum, runner))
84+
.collect(Collectors.toList());
85+
86+
// Wait for all tasks in this chunk to complete
87+
CompletableFuture<Void> allFutures =
88+
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
89+
90+
try {
91+
allFutures.join();
92+
93+
// Aggregate results
94+
for (CompletableFuture<IssueResult> f : futures) {
95+
IssueResult result = f.get();
96+
if (result != null) {
97+
totalProcessingTime += result.durationSeconds();
98+
totalIssueApiCalls += result.apiCalls();
99+
}
100+
}
101+
} catch (Exception e) {
102+
logger.error(String.format("Error gathering chunk results", e));
103+
}
104+
105+
processedCount += chunk.size();
106+
logger.info(
107+
String.format(
108+
"Finished chunk %d. Progress: %d/%d ", currentChunkNum, processedCount, totalCount));
109+
110+
// Sleep between chunks if not finished
111+
if (end < totalCount) {
112+
logger.info(
113+
String.format(
114+
"Sleeping for "
115+
+ StaleBotSettings.SLEEP_BETWEEN_CHUNKS
116+
+ "s to respect rate limits..."));
117+
try {
118+
Thread.sleep((long) (StaleBotSettings.SLEEP_BETWEEN_CHUNKS * 1000));
119+
} catch (InterruptedException e) {
120+
Thread.currentThread().interrupt();
121+
logger.warn(String.format("Sleep interrupted."));
122+
}
123+
}
124+
}
125+
126+
int totalApiCallsForRun = searchApiCalls + totalIssueApiCalls;
127+
double avgTimePerIssue = totalCount > 0 ? totalProcessingTime / totalCount : 0;
128+
129+
logger.info(String.format("Successfully processed " + processedCount + " issues."));
130+
logger.info(String.format("Total API calls made this run: " + totalApiCallsForRun));
131+
logger.info(String.format("Average processing time per issue: %.2f seconds.", avgTimePerIssue));
132+
}
133+
134+
private static CompletableFuture<IssueResult> processSingleIssue(
135+
int issueNumber, InMemoryRunner localRunner) {
136+
return CompletableFuture.supplyAsync(
137+
() -> {
138+
long startNano = System.nanoTime();
139+
int startApiCalls = GitHubUtils.getApiCallCount();
140+
141+
logger.info(String.format("Processing Issue #" + issueNumber + "..."));
142+
143+
String sessionId = "session-" + issueNumber + "-" + UUID.randomUUID().toString();
144+
145+
try {
146+
147+
localRunner
148+
.sessionService()
149+
.createSession(localRunner.appName(), USER_ID, null, sessionId)
150+
.blockingGet();
151+
152+
logger.info(String.format("Session created successfully: " + sessionId));
153+
154+
String promptText = "Audit Issue #" + issueNumber + ".";
155+
Content promptMessage = Content.fromParts(Part.fromText(promptText));
156+
StringBuilder fullResponse = new StringBuilder();
157+
158+
localRunner
159+
.runAsync(USER_ID, sessionId, promptMessage)
160+
.blockingSubscribe(
161+
event -> {
162+
try {
163+
if (event.content() != null && event.content().isPresent()) {
164+
event
165+
.content()
166+
.get()
167+
.parts()
168+
.get()
169+
.forEach(
170+
p -> {
171+
p.text().ifPresent(text -> fullResponse.append(text));
172+
});
173+
}
174+
} catch (Exception e) {
175+
logger.warn(String.format("Failed to process Issue #" + issueNumber, e));
176+
}
177+
},
178+
error -> {
179+
logger.error(
180+
String.format(
181+
"Stream failed for Issue #"
182+
+ issueNumber
183+
+ ": "
184+
+ error.getMessage()));
185+
});
186+
187+
String decision = fullResponse.toString().replace("\n", " ");
188+
if (decision.length() > 150) decision = decision.substring(0, 150);
189+
190+
logger.info("#" + issueNumber + " Decision: " + decision + "...");
191+
192+
} catch (Exception e) {
193+
logger.error(String.format("Error processing issue #" + issueNumber, e));
194+
}
195+
196+
double durationSeconds = (System.nanoTime() - startNano) / 1_000_000_000.0;
197+
int issueApiCalls = Math.max(0, GitHubUtils.getApiCallCount() - startApiCalls);
198+
199+
return new IssueResult(issueNumber, durationSeconds, issueApiCalls);
200+
});
201+
}
202+
}

0 commit comments

Comments
 (0)