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
8 changes: 2 additions & 6 deletions contrib/samples/pom.xml
Original file line number Diff line number Diff line change
@@ -1,25 +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.8.0</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>a2a_server</module>
<module>configagent</module>
<module>helloworld</module>
<module>mcpfilesystem</module>
<module>stale-agent</module>
</modules>
</project>
83 changes: 83 additions & 0 deletions contrib/samples/stale-agent/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?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.8.0</version>
</parent>
<groupId>com.google.adk.samples</groupId>
<artifactId>stale-agent</artifactId>
<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>${project.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,202 @@
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.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class StaleBotApp {

private static final Logger logger = LoggerFactory.getLogger(StaleAgent.class);
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.error(String.format("Unexpected fatal error", e));
}
}

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

GitHubUtils.resetApiCallCount();

double filterDays = StaleBotSettings.STALE_HOURS_THRESHOLD / 24.0;
logger.info(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.error(String.format("Failed to fetch issue list", e));
return;
}

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

if (totalCount == 0) {
logger.info(String.format("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, runner))
.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.error(String.format("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.info(
String.format(
"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.warn(String.format("Sleep interrupted."));
}
}
}

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

logger.info(String.format("Successfully processed " + processedCount + " issues."));
logger.info(String.format("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, InMemoryRunner localRunner) {
return CompletableFuture.supplyAsync(
() -> {
long startNano = System.nanoTime();
int startApiCalls = GitHubUtils.getApiCallCount();

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

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

try {

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

logger.info(String.format("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 e) {
logger.warn(String.format("Failed to process Issue #" + issueNumber, e));
}
},
error -> {
logger.error(
String.format(
"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.error(String.format("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