-
Notifications
You must be signed in to change notification settings - Fork 281
Stale Issue Auditor agent implementation #845
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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> |
| 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> --> | ||
| <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()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This class uses |
||
| 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()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A new |
||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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); | ||
| }); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ADK version is hardcoded to
0.5.0, while the parent POM is using0.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.