Skip to content

Commit b23cf63

Browse files
committed
feat: Stale Issue Auditor agent implementation
1 parent 0a40557 commit b23cf63

8 files changed

Lines changed: 1216 additions & 6 deletions

File tree

.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>1.4.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: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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>1.4.1-SNAPSHOT</version>
9+
</parent>
10+
<groupId>com.google.adk.samples</groupId>
11+
<artifactId>stale-agent</artifactId>
12+
<name>stale-agent</name>
13+
<url>http://maven.apache.org</url>
14+
<properties>
15+
<java.version>17</java.version>
16+
<maven.compiler.source>17</maven.compiler.source>
17+
<maven.compiler.target>17</maven.compiler.target>
18+
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
19+
<adk.version>${project.version}</adk.version> <!--${project.version}</adk.version> -->
20+
<slf4j.version>2.0.9</slf4j.version>
21+
</properties>
22+
<dependencies>
23+
<dependency>
24+
<groupId>com.google.adk</groupId>
25+
<artifactId>google-adk</artifactId>
26+
<version>${adk.version}</version>
27+
</dependency>
28+
29+
<dependency>
30+
<groupId>org.kohsuke</groupId>
31+
<artifactId>github-api</artifactId>
32+
<version>1.318</version>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.apache.httpcomponents.client5</groupId>
36+
<artifactId>httpclient5</artifactId>
37+
<version>5.2.1</version>
38+
</dependency>
39+
40+
<dependency>
41+
<groupId>org.slf4j</groupId>
42+
<artifactId>slf4j-api</artifactId>
43+
<version>${slf4j.version}</version>
44+
</dependency>
45+
46+
<dependency>
47+
<groupId>org.slf4j</groupId>
48+
<artifactId>slf4j-simple</artifactId>
49+
<version>${slf4j.version}</version>
50+
</dependency>
51+
52+
<dependency>
53+
<groupId>com.fasterxml.jackson.core</groupId>
54+
<artifactId>jackson-databind</artifactId>
55+
<version>2.16.1</version>
56+
</dependency>
57+
58+
</dependencies>
59+
<build>
60+
<plugins>
61+
<plugin>
62+
<groupId>org.springframework.boot</groupId>
63+
<artifactId>spring-boot-maven-plugin</artifactId>
64+
</plugin>
65+
<plugin>
66+
<groupId>org.codehaus.mojo</groupId>
67+
<artifactId>exec-maven-plugin</artifactId>
68+
<version>3.1.0</version>
69+
<executions>
70+
<execution>
71+
<id>run-stale-bot</id>
72+
<goals>
73+
<goal>java</goal>
74+
</goals>
75+
<configuration>
76+
<mainClass>com.google.adk.samples.stale.StaleBotApp</mainClass>
77+
</configuration>
78+
</execution>
79+
</executions>
80+
</plugin>
81+
</plugins>
82+
</build>
83+
</project>
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
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("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.info("Fetching issues older than {} days...", String.format("%.2f", 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.error("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+
"Found {} issues to process. (Initial search used {} API calls).",
61+
totalCount,
62+
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("Starting chunk {}: Processing issues {} ", currentChunkNum, chunk);
76+
77+
// Create a list of Futures (Async Tasks)
78+
List<CompletableFuture<IssueResult>> futures =
79+
chunk.stream()
80+
.map(issueNum -> processSingleIssue(issueNum, runner))
81+
.collect(Collectors.toList());
82+
83+
// Wait for all tasks in this chunk to complete
84+
CompletableFuture<Void> allFutures =
85+
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
86+
87+
try {
88+
allFutures.join();
89+
90+
// Aggregate results
91+
for (CompletableFuture<IssueResult> f : futures) {
92+
IssueResult result = f.get();
93+
if (result != null) {
94+
totalProcessingTime += result.durationSeconds();
95+
totalIssueApiCalls += result.apiCalls();
96+
}
97+
}
98+
} catch (Exception e) {
99+
logger.error("Error gathering chunk results", e);
100+
}
101+
102+
processedCount += chunk.size();
103+
logger.info(
104+
"Finished chunk {}. Progress: {}/{} ", currentChunkNum, processedCount, totalCount);
105+
106+
// Sleep between chunks if not finished
107+
if (end < totalCount) {
108+
logger.info(
109+
"Sleeping for {}s to respect rate limits...", StaleBotSettings.SLEEP_BETWEEN_CHUNKS);
110+
try {
111+
Thread.sleep((long) (StaleBotSettings.SLEEP_BETWEEN_CHUNKS * 1000));
112+
} catch (InterruptedException e) {
113+
Thread.currentThread().interrupt();
114+
logger.warn("Sleep interrupted.");
115+
}
116+
}
117+
}
118+
119+
int totalApiCallsForRun = searchApiCalls + totalIssueApiCalls;
120+
double avgTimePerIssue = totalCount > 0 ? totalProcessingTime / totalCount : 0;
121+
122+
logger.info("Successfully processed {} issues.", processedCount);
123+
logger.info("Total API calls made this run: {}", totalApiCallsForRun);
124+
logger.info(
125+
"Average processing time per issue: {} seconds.", String.format("%.2f", avgTimePerIssue));
126+
}
127+
128+
private static CompletableFuture<IssueResult> processSingleIssue(
129+
int issueNumber, InMemoryRunner localRunner) {
130+
return CompletableFuture.supplyAsync(
131+
() -> {
132+
long startNano = System.nanoTime();
133+
int startApiCalls = GitHubUtils.getApiCallCount();
134+
135+
logger.info("Processing Issue #{}", issueNumber);
136+
137+
String sessionId = "session-" + issueNumber + "-" + UUID.randomUUID().toString();
138+
139+
try {
140+
141+
localRunner
142+
.sessionService()
143+
.createSession(localRunner.appName(), USER_ID, null, sessionId)
144+
.blockingGet();
145+
146+
logger.info("Session created successfully: {} ", sessionId);
147+
148+
String promptText = "Audit Issue #" + issueNumber + ".";
149+
Content promptMessage = Content.fromParts(Part.fromText(promptText));
150+
StringBuilder fullResponse = new StringBuilder();
151+
152+
localRunner
153+
.runAsync(USER_ID, sessionId, promptMessage)
154+
.blockingSubscribe(
155+
event -> {
156+
try {
157+
if (event.content() != null && event.content().isPresent()) {
158+
event
159+
.content()
160+
.get()
161+
.parts()
162+
.get()
163+
.forEach(
164+
p -> {
165+
p.text().ifPresent(text -> fullResponse.append(text));
166+
});
167+
}
168+
} catch (Exception e) {
169+
logger.warn("Failed to process Issue #{}", issueNumber, e);
170+
}
171+
},
172+
error -> {
173+
logger.error("Stream failed for Issue #{}", issueNumber, error);
174+
});
175+
176+
String decision = fullResponse.toString().replace("\n", " ");
177+
if (decision.length() > 150) decision = decision.substring(0, 150);
178+
179+
logger.info("#{} Decision: {}", issueNumber, decision);
180+
181+
} catch (Exception e) {
182+
logger.error("Error processing issue #{}", issueNumber, e);
183+
}
184+
185+
double durationSeconds = (System.nanoTime() - startNano) / 1_000_000_000.0;
186+
int issueApiCalls = Math.max(0, GitHubUtils.getApiCallCount() - startApiCalls);
187+
188+
return new IssueResult(issueNumber, durationSeconds, issueApiCalls);
189+
});
190+
}
191+
}

0 commit comments

Comments
 (0)