Skip to content

Commit cb9cc4a

Browse files
committed
feat: Add Stale Issue Auditor agent
1 parent 968a9a8 commit cb9cc4a

File tree

8 files changed

+1210
-7
lines changed

8 files changed

+1210
-7
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: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +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>
8-
<version>0.5.1-SNAPSHOT</version><!-- {x-version-update:google-adk:current} -->
6+
<version>0.5.1-SNAPSHOT</version>
7+
<!-- {x-version-update:google-adk:current} -->
98
<relativePath>../..</relativePath>
109
</parent>
11-
1210
<artifactId>google-adk-samples</artifactId>
1311
<packaging>pom</packaging>
14-
1512
<name>Google ADK Samples</name>
1613
<description>Aggregator for sample applications.</description>
17-
1814
<modules>
1915
<module>a2a_basic</module>
2016
<module>configagent</module>
2117
<module>helloworld</module>
2218
<module>mcpfilesystem</module>
19+
<module>stale-agent</module>
2320
</modules>
2421
</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.5.1-SNAPSHOT</version>
9+
</parent>
10+
<groupId>com.google.adk</groupId>
11+
<artifactId>stale-agent</artifactId>
12+
<version>0.5.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>0.5.0</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: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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.logging.Level;
13+
import java.util.logging.Logger;
14+
import java.util.stream.Collectors;
15+
16+
public class StaleBotApp {
17+
18+
private static final Logger logger = Logger.getLogger(StaleBotApp.class.getName());
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.log(Level.SEVERE, "Unexpected fatal error", e);
29+
}
30+
}
31+
32+
public static void runBot() {
33+
logger.info(" Starting Stale Bot for " + StaleBotSettings.OWNER + "/" + StaleBotSettings.REPO);
34+
logger.info("Concurrency level set to " + StaleBotSettings.CONCURRENCY_LIMIT);
35+
36+
GitHubUtils.resetApiCallCount();
37+
38+
double filterDays = StaleBotSettings.STALE_HOURS_THRESHOLD / 24.0;
39+
logger.fine(String.format("Fetching issues older than %.2f days...", filterDays));
40+
41+
List<Integer> allIssues;
42+
try {
43+
allIssues =
44+
GitHubUtils.getOldOpenIssueNumbers(
45+
StaleBotSettings.OWNER, StaleBotSettings.REPO, filterDays);
46+
} catch (Exception e) {
47+
logger.log(Level.SEVERE, "Failed to fetch issue list", e);
48+
return;
49+
}
50+
51+
int totalCount = allIssues.size();
52+
int searchApiCalls = GitHubUtils.getApiCallCount();
53+
54+
if (totalCount == 0) {
55+
logger.info("No issues matched the criteria. Run finished.");
56+
return;
57+
}
58+
59+
logger.info(
60+
String.format(
61+
"Found %d issues to process. (Initial search used %d API calls).",
62+
totalCount, searchApiCalls));
63+
64+
double totalProcessingTime = 0.0;
65+
int totalIssueApiCalls = 0;
66+
int processedCount = 0;
67+
68+
InMemoryRunner runner = new InMemoryRunner(StaleAgent.create());
69+
70+
for (int i = 0; i < totalCount; i += StaleBotSettings.CONCURRENCY_LIMIT) {
71+
int end = Math.min(i + StaleBotSettings.CONCURRENCY_LIMIT, totalCount);
72+
List<Integer> chunk = allIssues.subList(i, end);
73+
int currentChunkNum = (i / StaleBotSettings.CONCURRENCY_LIMIT) + 1;
74+
75+
logger.info(
76+
String.format("Starting chunk %d: Processing issues %s ", currentChunkNum, chunk));
77+
78+
// Create a list of Futures (Async Tasks)
79+
List<CompletableFuture<IssueResult>> futures =
80+
chunk.stream().map(issueNum -> processSingleIssue(issueNum)).collect(Collectors.toList());
81+
82+
// Wait for all tasks in this chunk to complete
83+
CompletableFuture<Void> allFutures =
84+
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
85+
86+
try {
87+
allFutures.join();
88+
89+
// Aggregate results
90+
for (CompletableFuture<IssueResult> f : futures) {
91+
IssueResult result = f.get();
92+
if (result != null) {
93+
totalProcessingTime += result.durationSeconds();
94+
totalIssueApiCalls += result.apiCalls();
95+
}
96+
}
97+
} catch (Exception e) {
98+
logger.log(Level.SEVERE, "Error gathering chunk results", e);
99+
}
100+
101+
processedCount += chunk.size();
102+
logger.info(
103+
String.format(
104+
"Finished chunk %d. Progress: %d/%d ", currentChunkNum, processedCount, totalCount));
105+
106+
// Sleep between chunks if not finished
107+
if (end < totalCount) {
108+
logger.fine(
109+
"Sleeping for "
110+
+ StaleBotSettings.SLEEP_BETWEEN_CHUNKS
111+
+ "s to respect rate limits...");
112+
try {
113+
Thread.sleep((long) (StaleBotSettings.SLEEP_BETWEEN_CHUNKS * 1000));
114+
} catch (InterruptedException e) {
115+
Thread.currentThread().interrupt();
116+
logger.warning("Sleep interrupted.");
117+
}
118+
}
119+
}
120+
121+
int totalApiCallsForRun = searchApiCalls + totalIssueApiCalls;
122+
double avgTimePerIssue = totalCount > 0 ? totalProcessingTime / totalCount : 0;
123+
124+
logger.info("Successfully processed " + processedCount + " issues.");
125+
logger.info("Total API calls made this run: " + totalApiCallsForRun);
126+
logger.info(String.format("Average processing time per issue: %.2f seconds.", avgTimePerIssue));
127+
}
128+
129+
private static CompletableFuture<IssueResult> processSingleIssue(int issueNumber) {
130+
return CompletableFuture.supplyAsync(
131+
() -> {
132+
long startNano = System.nanoTime();
133+
int startApiCalls = GitHubUtils.getApiCallCount();
134+
135+
logger.info("Processing Issue #" + issueNumber + "...");
136+
137+
InMemoryRunner localRunner = new InMemoryRunner(StaleAgent.create());
138+
139+
String sessionId = "session-" + issueNumber + "-" + UUID.randomUUID().toString();
140+
141+
try {
142+
143+
localRunner
144+
.sessionService()
145+
.createSession(localRunner.appName(), USER_ID, null, sessionId)
146+
.blockingGet();
147+
148+
logger.fine("Session created successfully: " + sessionId);
149+
150+
String promptText = "Audit Issue #" + issueNumber + ".";
151+
Content promptMessage = Content.fromParts(Part.fromText(promptText));
152+
StringBuilder fullResponse = new StringBuilder();
153+
154+
localRunner
155+
.runAsync(USER_ID, sessionId, promptMessage)
156+
.blockingSubscribe(
157+
event -> {
158+
try {
159+
if (event.content() != null && event.content().isPresent()) {
160+
event
161+
.content()
162+
.get()
163+
.parts()
164+
.get()
165+
.forEach(
166+
p -> {
167+
p.text().ifPresent(text -> fullResponse.append(text));
168+
});
169+
}
170+
} catch (Exception ignored) {
171+
}
172+
},
173+
error -> {
174+
logger.severe(
175+
"Stream failed for Issue #" + issueNumber + ": " + error.getMessage());
176+
});
177+
178+
String decision = fullResponse.toString().replace("\n", " ");
179+
if (decision.length() > 150) decision = decision.substring(0, 150);
180+
181+
logger.info("#" + issueNumber + " Decision: " + decision + "...");
182+
183+
} catch (Exception e) {
184+
logger.log(Level.SEVERE, "Error processing issue #" + issueNumber, e);
185+
}
186+
187+
double durationSeconds = (System.nanoTime() - startNano) / 1_000_000_000.0;
188+
int issueApiCalls = Math.max(0, GitHubUtils.getApiCallCount() - startApiCalls);
189+
190+
return new IssueResult(issueNumber, durationSeconds, issueApiCalls);
191+
});
192+
}
193+
}

0 commit comments

Comments
 (0)