Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions .github/workflows/stale-bot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@

name: ADK Stale Issue Auditor (Java)

on:
workflow_dispatch:
schedule:
# This runs at 6:00 AM UTC (10 PM PST)
- cron: '0 6 * * *'

jobs:
audit-stale-issues:

if: github.repository == 'google/adk-java'

runs-on: ubuntu-latest
timeout-minutes: 60

permissions:
issues: write
contents: read

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven

- name: Build with Maven
run: mvn clean compile

- name: Run Auditor Agent
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
STALE_HOURS_THRESHOLD: ${{ secrets.STALE_HOURS_THRESHOLD }}
CLOSE_HOURS_AFTER_STALE_THRESHOLD: ${{ secrets.CLOSE_HOURS_AFTER_STALE_THRESHOLD }}

GRAPHQL_COMMENT_LIMIT: ${{ secrets.GRAPHQL_COMMENT_LIMIT }}
GRAPHQL_EDIT_LIMIT: ${{ secrets.GRAPHQL_EDIT_LIMIT }}
GRAPHQL_TIMELINE_LIMIT: ${{ secrets.GRAPHQL_TIMELINE_LIMIT }}

SLEEP_BETWEEN_CHUNKS: ${{ secrets.SLEEP_BETWEEN_CHUNKS }}

OWNER: ${{ github.repository_owner }}
REPO: adk-java
CONCURRENCY_LIMIT: 3
LLM_MODEL_NAME: "gemini-2.5-flash"

JAVA_TOOL_OPTIONS: "-Djava.util.logging.SimpleFormatter.format='%1$tF %1$tT %4$s %2$s %5$s%6$s%n'"

run: mvn compile exec:java@run-stale-bot -pl :stale-agent
11 changes: 4 additions & 7 deletions contrib/samples/pom.xml
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
<?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">
<?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">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-parent</artifactId>
<version>0.5.1-SNAPSHOT</version><!-- {x-version-update:google-adk:current} -->
<version>0.5.1-SNAPSHOT</version>
<!-- {x-version-update:google-adk:current} -->
<relativePath>../..</relativePath>
</parent>

<artifactId>google-adk-samples</artifactId>
<packaging>pom</packaging>

<name>Google ADK Samples</name>
<description>Aggregator for sample applications.</description>

<modules>
<module>a2a_basic</module>
<module>configagent</module>
<module>helloworld</module>
<module>mcpfilesystem</module>
<module>stale-agent</module>
</modules>
</project>
84 changes: 84 additions & 0 deletions contrib/samples/stale-agent/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?xml version="1.0"?>
<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"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.google.adk</groupId>
<artifactId>google-adk-samples</artifactId>
<version>0.5.1-SNAPSHOT</version>
</parent>
<groupId>com.google.adk</groupId>
<artifactId>stale-agent</artifactId>
<version>0.5.1-SNAPSHOT</version>
<name>stale-agent</name>
<url>http://maven.apache.org</url>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<adk.version>0.5.0</adk.version> <!--${project.version}</adk.version> -->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The ADK version is hardcoded to 0.5.0, while the parent POM is using 0.5.1-SNAPSHOT. This can lead to build failures or runtime issues if the sample relies on features or fixes introduced in the newer version, which is likely given the concurrent changes to the core framework in this PR. It's best practice to inherit the version from the parent POM to ensure consistency.

Suggested change
<adk.version>0.5.0</adk.version> <!--${project.version}</adk.version> -->
<adk.version>${project.version}</adk.version>

<slf4j.version>2.0.9</slf4j.version>
</properties>
<dependencies>
<dependency>
<groupId>com.google.adk</groupId>
<artifactId>google-adk</artifactId>
<version>${adk.version}</version>
</dependency>

<dependency>
<groupId>org.kohsuke</groupId>
<artifactId>github-api</artifactId>
<version>1.318</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>

</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>run-stale-bot</id>
<goals>
<goal>java</goal>
</goals>
<configuration>
<mainClass>com.google.adk.samples.stale.StaleBotApp</mainClass>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package com.google.adk.samples.stale;

import com.google.adk.runner.InMemoryRunner;
import com.google.adk.samples.stale.agent.StaleAgent;
import com.google.adk.samples.stale.config.StaleBotSettings;
import com.google.adk.samples.stale.utils.GitHubUtils;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;

public class StaleBotApp {

private static final Logger logger = Logger.getLogger(StaleBotApp.class.getName());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This class uses java.util.logging.Logger, but the project dependencies include SLF4J, which is used in other classes like StaleAgent. For consistency across the project, it's recommended to use SLF4J here as well. This would involve changing the logger initialization and updating the logging calls (e.g., logger.info(...) instead of logger.log(Level.INFO, ...)).

private static final String USER_ID = "stale_bot_user";

record IssueResult(long issueNumber, double durationSeconds, int apiCalls) {}

public static void main(String[] args) {

try {
runBot();
} catch (Exception e) {
logger.log(Level.SEVERE, "Unexpected fatal error", e);
}
}

public static void runBot() {
logger.info(" Starting Stale Bot for " + StaleBotSettings.OWNER + "/" + StaleBotSettings.REPO);
logger.info("Concurrency level set to " + StaleBotSettings.CONCURRENCY_LIMIT);

GitHubUtils.resetApiCallCount();

double filterDays = StaleBotSettings.STALE_HOURS_THRESHOLD / 24.0;
logger.fine(String.format("Fetching issues older than %.2f days...", filterDays));

List<Integer> allIssues;
try {
allIssues =
GitHubUtils.getOldOpenIssueNumbers(
StaleBotSettings.OWNER, StaleBotSettings.REPO, filterDays);
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to fetch issue list", e);
return;
}

int totalCount = allIssues.size();
int searchApiCalls = GitHubUtils.getApiCallCount();

if (totalCount == 0) {
logger.info("No issues matched the criteria. Run finished.");
return;
}

logger.info(
String.format(
"Found %d issues to process. (Initial search used %d API calls).",
totalCount, searchApiCalls));

double totalProcessingTime = 0.0;
int totalIssueApiCalls = 0;
int processedCount = 0;

InMemoryRunner runner = new InMemoryRunner(StaleAgent.create());

for (int i = 0; i < totalCount; i += StaleBotSettings.CONCURRENCY_LIMIT) {
int end = Math.min(i + StaleBotSettings.CONCURRENCY_LIMIT, totalCount);
List<Integer> chunk = allIssues.subList(i, end);
int currentChunkNum = (i / StaleBotSettings.CONCURRENCY_LIMIT) + 1;

logger.info(
String.format("Starting chunk %d: Processing issues %s ", currentChunkNum, chunk));

// Create a list of Futures (Async Tasks)
List<CompletableFuture<IssueResult>> futures =
chunk.stream().map(issueNum -> processSingleIssue(issueNum)).collect(Collectors.toList());

// Wait for all tasks in this chunk to complete
CompletableFuture<Void> allFutures =
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));

try {
allFutures.join();

// Aggregate results
for (CompletableFuture<IssueResult> f : futures) {
IssueResult result = f.get();
if (result != null) {
totalProcessingTime += result.durationSeconds();
totalIssueApiCalls += result.apiCalls();
}
}
} catch (Exception e) {
logger.log(Level.SEVERE, "Error gathering chunk results", e);
}

processedCount += chunk.size();
logger.info(
String.format(
"Finished chunk %d. Progress: %d/%d ", currentChunkNum, processedCount, totalCount));

// Sleep between chunks if not finished
if (end < totalCount) {
logger.fine(
"Sleeping for "
+ StaleBotSettings.SLEEP_BETWEEN_CHUNKS
+ "s to respect rate limits...");
try {
Thread.sleep((long) (StaleBotSettings.SLEEP_BETWEEN_CHUNKS * 1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
logger.warning("Sleep interrupted.");
}
}
}

int totalApiCallsForRun = searchApiCalls + totalIssueApiCalls;
double avgTimePerIssue = totalCount > 0 ? totalProcessingTime / totalCount : 0;

logger.info("Successfully processed " + processedCount + " issues.");
logger.info("Total API calls made this run: " + totalApiCallsForRun);
logger.info(String.format("Average processing time per issue: %.2f seconds.", avgTimePerIssue));
}

private static CompletableFuture<IssueResult> processSingleIssue(int issueNumber) {
return CompletableFuture.supplyAsync(
() -> {
long startNano = System.nanoTime();
int startApiCalls = GitHubUtils.getApiCallCount();

logger.info("Processing Issue #" + issueNumber + "...");

InMemoryRunner localRunner = new InMemoryRunner(StaleAgent.create());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

A new InMemoryRunner is created for each issue being processed within the loop. This is inefficient as it re-initializes the StaleAgent on every call. It would be more performant to create a single InMemoryRunner instance in the runBot method and reuse it for all issues. Note that there is already an unused InMemoryRunner instance created on line 68 which could be leveraged.


String sessionId = "session-" + issueNumber + "-" + UUID.randomUUID().toString();

try {

localRunner
.sessionService()
.createSession(localRunner.appName(), USER_ID, null, sessionId)
.blockingGet();

logger.fine("Session created successfully: " + sessionId);

String promptText = "Audit Issue #" + issueNumber + ".";
Content promptMessage = Content.fromParts(Part.fromText(promptText));
StringBuilder fullResponse = new StringBuilder();

localRunner
.runAsync(USER_ID, sessionId, promptMessage)
.blockingSubscribe(
event -> {
try {
if (event.content() != null && event.content().isPresent()) {
event
.content()
.get()
.parts()
.get()
.forEach(
p -> {
p.text().ifPresent(text -> fullResponse.append(text));
});
}
} catch (Exception ignored) {
}
Comment on lines +170 to +171
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The exception in this catch block is ignored. This can hide potential issues during event processing, making debugging difficult. It's better to at least log the exception at a warning or error level.

                      } catch (Exception e) {
                        logger.log(Level.WARNING, "Error processing event for issue #" + issueNumber, e);
                      }

},
error -> {
logger.severe(
"Stream failed for Issue #" + issueNumber + ": " + error.getMessage());
});

String decision = fullResponse.toString().replace("\n", " ");
if (decision.length() > 150) decision = decision.substring(0, 150);

logger.info("#" + issueNumber + " Decision: " + decision + "...");

} catch (Exception e) {
logger.log(Level.SEVERE, "Error processing issue #" + issueNumber, e);
}

double durationSeconds = (System.nanoTime() - startNano) / 1_000_000_000.0;
int issueApiCalls = Math.max(0, GitHubUtils.getApiCallCount() - startApiCalls);

return new IssueResult(issueNumber, durationSeconds, issueApiCalls);
});
}
}
Loading