Skip to content

Conversation

@PierrickVoulet
Copy link
Contributor

No description provided.

@gemini-code-assist
Copy link

Summary of Changes

Hello @PierrickVoulet, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request delivers a new quickstart sample designed to illustrate the integration of a Google Chat bot with an Agent-to-Agent (A2A) AI agent, leveraging the Vertex AI Agent Engine. The sample provides a comprehensive framework for processing user interactions from Google Chat, orchestrating communication with the AI agent, and rendering rich, interactive responses back to the user, including support for file attachments and structured UI elements.

Highlights

  • New A2A AI Agent Quickstart Sample: This pull request introduces a complete quickstart sample for a Google Chat bot that integrates with an A2A-wrapped ADK AI agent hosted in Vertex AI Agent Engine. It provides a functional example of how to connect Google Chat with advanced AI capabilities.
  • Google Chat Integration: The sample includes robust handling for Google Chat messages, covering incoming event processing, attachment downloading, and structured message creation and updates within the Chat interface. It demonstrates how to build dynamic responses using CardService widgets.
  • AI Agent Interaction Logic: Core logic is implemented for sending user input to the AI agent, polling for task completion, and processing responses from multiple sub-agents (e.g., 'critic_agent', 'reviser_agent'). This showcases a common pattern for interacting with multi-agent systems.
  • Credential and Environment Management: Dedicated modules are provided for securely managing service account credentials required for authenticating with Google Cloud APIs (Vertex AI and Google Chat) and for configuring environment-specific variables such as the reasoning engine resource name and debug mode.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@PierrickVoulet PierrickVoulet merged commit 1772b95 into googleworkspace:main Dec 18, 2025
5 of 7 checks passed
Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Google Apps Script project that integrates a Google Chat application with an A2A-wrapped AI agent hosted in Vertex AI Agent Engine. The changes include core logic for sending messages to the AI agent, polling its task status, and processing its responses, along with functions for handling Google Chat events, managing service account credentials, and configuring environment variables. Review comments highlight several critical areas for improvement, including adding robust error handling for all external API calls (e.g., UrlFetchApp.fetch, Chat API calls) by checking HTTP response codes and using try...catch blocks, addressing potential script timeouts by making the agent processing asynchronous and implementing a timeout mechanism for the task polling loop, correcting a bug in the debug mode parsing logic, improving the robustness of location extraction from resource names, and refining a regular expression used for removing references from agent responses to prevent unintended truncation.

Comment on lines +22 to +40
const sendResponseContentText = UrlFetchApp.fetch(
`https://${getLocation()}-aiplatform.googleapis.com/v1beta1/${getReasoningEngine()}/a2a/v1/message:send`,
{
method: 'post',
headers: { 'Authorization': `Bearer ${getCredentials().getAccessToken()}` },
contentType: 'application/json',
payload: JSON.stringify({
"message": {
"messageId": Utilities.getUuid(),
"role": "1",
"content": extractContentFromInput(input)
}
}),
muteHttpExceptions: true
}
).getContentText();
if (isInDebugMode()) {
console.log("Send response: " + sendResponseContentText);
}

Choose a reason for hiding this comment

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

critical

The UrlFetchApp.fetch call has muteHttpExceptions: true, but there is no subsequent check on the HTTP response code. If the API call fails, getContentText() will return an error payload, and JSON.parse() on line 43 will likely throw an unhandled exception, crashing the script. You should check the response code and handle errors gracefully before parsing the response.

  const sendHttpResponse = UrlFetchApp.fetch(
    `https://${getLocation()}-aiplatform.googleapis.com/v1beta1/${getReasoningEngine()}/a2a/v1/message:send`,
    {
      method: 'post',
      headers: { 'Authorization': `Bearer ${getCredentials().getAccessToken()}` },
      contentType: 'application/json',
      payload: JSON.stringify({
        "message": {
          "messageId": Utilities.getUuid(),
          "role": "ROLE_USER",
          "content": extractContentFromInput(input)
        }
      }),
      muteHttpExceptions: true
    }
  );

  const sendResponseContentText = sendHttpResponse.getContentText();
  if (sendHttpResponse.getResponseCode() >= 400) {
    console.error(`Error sending message to agent: ${sendResponseContentText}`);
    // TODO: Inform the user about the error.
    return;
  }

  if (isInDebugMode()) {
    console.log("Send response: " + sendResponseContentText);
  }


// Retrieve the ID of the resulting task
const sendResponse = JSON.parse(sendResponseContentText);
taskId = sendResponse.task.id;

Choose a reason for hiding this comment

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

critical

The variable taskId is assigned without being declared using let, const, or var. This creates an implicit global variable, which is a dangerous practice in JavaScript and Apps Script as it can lead to unpredictable behavior and makes the code harder to debug. Please declare taskId with const.

  const taskId = sendResponse.task.id;

Comment on lines +52 to +63
const taskResponseContentText = UrlFetchApp.fetch(
`https://${getLocation()}-aiplatform.googleapis.com/v1beta1/${getReasoningEngine()}/a2a/v1/tasks/${taskId}?history_length=1`,
{
method: 'get',
headers: { 'Authorization': `Bearer ${getCredentials().getAccessToken()}` },
contentType: 'application/json',
muteHttpExceptions: true
}
).getContentText();
if (isInDebugMode()) {
console.log("Get task response: " + taskResponseContentText);
}

Choose a reason for hiding this comment

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

critical

This UrlFetchApp.fetch call to get the task status has muteHttpExceptions: true but lacks error handling. If this API call fails, JSON.parse on line 64 will likely throw an unhandled exception. You should check the response code and handle any potential errors, for instance by breaking the polling loop.

      const taskHttpResponse = UrlFetchApp.fetch(
        `https://${getLocation()}-aiplatform.googleapis.com/v1beta1/${getReasoningEngine()}/a2a/v1/tasks/${taskId}?history_length=1`,
        {
          method: 'get',
          headers: { 'Authorization': `Bearer ${getCredentials().getAccessToken()}` },
          contentType: 'application/json',
          muteHttpExceptions: true
        }
      );
      const taskResponseContentText = taskHttpResponse.getContentText();
      if (taskHttpResponse.getResponseCode() >= 400) {
        console.error(`Error fetching task status: ${taskResponseContentText}`);
        break;
      }
      if (isInDebugMode()) {
        console.log("Get task response: " + taskResponseContentText);
      }

do {
Utilities.sleep(1000); // Wait a bit before polling
const taskResponseContentText = UrlFetchApp.fetch(
`https://${getLocation()}-aiplatform.googleapis.com/v1beta1/${getReasoningEngine()}/a2a/v1/tasks/${taskId}?history_length=1`,

Choose a reason for hiding this comment

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

critical

The history_length=1 query parameter will cause the API to return at most one message from the task's history. However, the logic in this loop is designed to process multiple new messages from sub-agents. If the agent produces multiple messages between polling intervals, this implementation will only ever see the most recent one, causing previous messages to be missed. This will lead to incomplete or incorrect agent responses. To fetch all new messages, you should remove this parameter to use the API's default.

        `https://${getLocation()}-aiplatform.googleapis.com/v1beta1/${getReasoningEngine()}/a2a/v1/tasks/${taskId}`,

setChatConfig(chatEvent.messagePayload.space.name);

// Request AI agent to answer the message
requestAgent(chatEvent.user.name, chatEvent.messagePayload.message)

Choose a reason for hiding this comment

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

critical

The onMessage function directly calls requestAgent, which is a long-running synchronous operation due to its polling mechanism. Google Chat webhooks require a response within 30 seconds. If requestAgent takes longer, the request will time out, and the user will see an "App is not responding" error. To fix this, you should make the agent processing asynchronous. The onMessage function should return immediately after triggering the asynchronous task, for example by using ScriptApp.newTrigger(). The triggered function can then perform the long-running work and post messages back to the chat space.

// Creates widgets for the markdown text and grounding response.
function createMarkdownAndGroundingWidgets(text, groundingMetadata) {
// Remove the references from text
let widgets = createMarkdownWidgets(text.replace(/^\s*```(json)?[\s\S]*?```\s*/i, '').replace(/Reference(s)?:[\s\S]*/i, ''))

Choose a reason for hiding this comment

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

high

The regular expression .replace(/Reference(s)?:[\s\S]*/i, '') is too greedy. It will remove everything from the first occurrence of "Reference:" or "References:" to the end of the string. If this pattern appears unexpectedly in the middle of the agent's response, it will cause the message to be truncated. To make this safer, consider anchoring the pattern to only match if it appears at the end of the text, for example, as its own paragraph.

  let widgets = createMarkdownWidgets(text.replace(/^\s*```(json)?[\s\S]*?```\s*/i, '').replace(/\n\nReference(s)?:[\s\S]*$/i, ''))

Comment on lines +60 to +67
const response = UrlFetchApp.fetch(
`https://chat.googleapis.com/v1/media/${attachmentName}?alt=media`,
{
method: 'get',
headers: { 'Authorization': `Bearer ${getCredentials().getAccessToken()}` },
muteHttpExceptions: true
}
);

Choose a reason for hiding this comment

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

high

The UrlFetchApp.fetch call to download an attachment has muteHttpExceptions: true but does not check the response code. If the media download fails, response.getContent() could return an error message or be empty, which would then be passed to Utilities.base64Encode(). This could lead to corrupted data or script errors. You should check the HTTP response code and handle failures appropriately.

  const response = UrlFetchApp.fetch(
    `https://chat.googleapis.com/v1/media/${attachmentName}?alt=media`,
    {
      method: 'get',
      headers: { 'Authorization': `Bearer ${getCredentials().getAccessToken()}` },
      muteHttpExceptions: true
    }
  );
  if (response.getResponseCode() >= 400) {
    console.error('Failed to download attachment %s: %s', attachmentName, response.getContentText());
    throw new Error('Attachment download failed.');
  }

Comment on lines +72 to +92
function createMessage(message) {
const spaceName = getConfiguredChat();
console.log(`Creating message in space ${spaceName}...`);
return Chat.Spaces.Messages.create(
message,
spaceName,
{},
{'Authorization': `Bearer ${getCredentials().getAccessToken()}`}
).name;
}

// Updates a Chat message in the configured space.
function updateMessage(name, message) {
console.log(`Updating message ${name}...`);
Chat.Spaces.Messages.patch(
message,
name,
{ updateMask: "*" },
{'Authorization': `Bearer ${getCredentials().getAccessToken()}`}
);
}

Choose a reason for hiding this comment

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

high

The calls to Chat.Spaces.Messages.create and Chat.Spaces.Messages.patch do not have any error handling. If an API call fails (e.g., due to permission issues or an invalid message format), it will throw an unhandled exception and terminate the script. It's important to wrap these calls in try...catch blocks to handle potential API errors gracefully and log them for debugging.

Comment on lines +27 to +31
function getLocation() {
const parts = REASONING_ENGINE_RESOURCE_NAME.split('/');
const locationIndex = parts.indexOf('locations') + 1;
return parts[locationIndex];
}

Choose a reason for hiding this comment

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

high

The logic to extract the location from REASONING_ENGINE_RESOURCE_NAME is brittle. It relies on splitting the string by / and assuming locations is always present and followed by the location string. If the resource name format is invalid, this could return an incorrect value (like the project ID) and cause API calls to fail. A regular expression would be a more robust way to extract the location.

Suggested change
function getLocation() {
const parts = REASONING_ENGINE_RESOURCE_NAME.split('/');
const locationIndex = parts.indexOf('locations') + 1;
return parts[locationIndex];
}
function getLocation() {
const match = REASONING_ENGINE_RESOURCE_NAME.match(/locations\/([^\/]+)/);
if (!match || !match[1]) {
throw new Error('Could not parse location from REASONING_ENGINE_RESOURCE_NAME: ' + REASONING_ENGINE_RESOURCE_NAME);
}
return match[1];
}

Comment on lines +33 to +38
const DEBUG = parseInt(PropertiesService.getScriptProperties().getProperty('DEBUG')) || 0;

// Returns whether the application is running in debug mode.
function isInDebugMode() {
return DEBUG == 1
}

Choose a reason for hiding this comment

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

high

The logic for parsing the DEBUG script property is buggy. parseInt() returns NaN for non-numeric strings like "true", and NaN || 0 evaluates to 0. This means isInDebugMode() will incorrectly return false if the DEBUG property is set to "true". To correctly handle boolean-like strings (e.g., "true") and the number 1, you should check for these values explicitly.

const DEBUG_PROPERTY = PropertiesService.getScriptProperties().getProperty('DEBUG');
const DEBUG = DEBUG_PROPERTY === '1' || (DEBUG_PROPERTY && DEBUG_PROPERTY.toLowerCase() === 'true');

// Returns whether the application is running in debug mode.
function isInDebugMode() {
  return DEBUG;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant